[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\ntrim_trailing_whitespace = true\ninsert_final_newline = true\ncharset = utf-8\nend_of_line = lf\n\n[*.{conf,html,ini,jinja,sql,py}]\nindent_style = space\nindent_size = 4\n\n[*.{json,schema,schema.example}]\nindent_style = space\nindent_size = 4\ninsert_final_newline = false\n\n[*.{css,feature,js,pu,sh,yaml,yml}]\nindent_style = space\nindent_size = 2\n\n[*.{inc,rst}]\nindent_style = space\nindent_size = 3\n\n[Makefile]\nindent_style = tab\n\n[*.mk]\nindent_style = tab\n\n[debian/*{preinst,postinst,prerm,postrm}]\nindent_style = space\nindent_size = 4\n\n[debian/changelog]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitignore",
    "content": "*.swp\n*.pyc\ngen_types.go\ngen_types_test.go\ngen_import.go\ngen_import_dev.go\n__pycache__\n.idea\n\n# ivxv-admin\n/build\n/dist\n/.pybuild\n/collector-admin/IVXVCollectorAdminDaemon.egg-info\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"Documentation/public/liidesed/VIS3-EHS\"]\n  path = Documentation/public/liidesed/VIS3-EHS\n  url = https://github.com/e-gov/VIS3-EHS.git\n  branch = feature/tervikdokument\n"
  },
  {
    "path": "Documentation/.gitignore",
    "content": "_build\n_master\n*.mo\n"
  },
  {
    "path": "Documentation/Makefile",
    "content": "# This Makefile supports two different invocation \"modes\".\n#\n# The default mode, invoked via \"all\", \"pdf\", or \"html\", builds all of the\n# documentation with a few exceptions, putting PDF files into $(DESTDIR)/pdf\n# (_build/pdf by default) and HTML files into $(DESTDIR)/html (_build/html by\n# default, \"all\" builds html only in development mode).\n#\n# Release mode, invoked via \"release\", builds only the release documents and\n# puts the PDF files, examples, and PDF diffs into a release hierarchy created\n# at $(DESTDIR) (_build/release by default).\n\n# Can be overridden from the command line.\nDESTDIR = _build\n\n# Build all Estonian and English documentation with a few exceptions.\nDOCS := $(wildcard internal/* public/* et/*)\nDOCS := $(filter-out et/example-config,$(DOCS)) # Does not contain reST.\nDOCS := $(filter-out internal/formal,$(DOCS))         # Does not contain reST.\nDOCS := $(filter-out internal/ivxv-pyapi,$(DOCS))     # Too many Python dependencies.\nDOCS := $(filter-out internal/entroopia,$(DOCS))      # Does not have a Makefile?\nDOCS := $(filter-out internal/stresstesting,$(DOCS))  # Does not have a Makefile?\n\nPDFDOCS   := $(DOCS:%=pdf-%)\nDIFFDOCS   := $(DOCS:%=diff-%)\nHTMLDOCS  := $(DOCS:%=html-%)\n\n.PHONY: all\nall: release\n\n.PHONY: pdf\npdf: $(PDFDOCS)\n\t$(MAKE) -C public/arhitektuur install-en-pdf DESTDIR=$(abspath $(DESTDIR)/pdf)\n\t$(MAKE) -C public/liidesed install-en-pdf DESTDIR=$(abspath $(DESTDIR)/pdf)\n\t$(MAKE) -C public/protokollid install-en-pdf DESTDIR=$(abspath $(DESTDIR)/pdf)\n\n.PHONY: $(PDFDOCS)\n$(PDFDOCS): pdf-%:\n\tmkdir -p $(DESTDIR)/pdf\n\t$(MAKE) -C $* install-pdf DESTDIR=$(abspath $(DESTDIR)/pdf)\n\n\n.PHONY: diff\ndiff: $(DIFFDOCS)\n\n.PHONY: $(DIFFDOCS)\n$(DIFFDOCS): diff-%:\n\tmkdir -p $(DESTDIR)/diff\n\t$(MAKE) -C $* install-diff DESTDIR=$(abspath $(DESTDIR)/diff)\n\n\n.PHONY: html\nhtml: $(HTMLDOCS)\n\n.PHONY: $(HTMLDOCS)\n$(HTMLDOCS): html-%:\n\tmkdir -p $(DESTDIR)/html/$(notdir $*) # Drop language prefix.\n\t$(MAKE) -C $* install-html DESTDIR=$(abspath $(DESTDIR)/html/$(notdir $*))\n\n\n# Rules for generating release documentation.\n\nRELEASE1 = \"$(abspath $(DESTDIR))/1. Ülddokumendid\"\nDOCS1ET := public/uldsisukord\n\nRELEASE2 = \"$(abspath $(DESTDIR))/2. Spetsifikatsioonid\"\nDOCS2ET := \\\n\tpublic/arhitektuur \\\n\tpublic/liidesed \\\n\tet/kasutusmall \\\n\tpublic/protokollid \\\n\tet/xteeteenus \\\n\tet/votmerakendus\nDOCS2EN := \\\n\tpublic/arhitektuur \\\n\tpublic/liidesed \\\n\tpublic/protokollid \\\n\ten/backendlogs\n\nRELEASE3 = \"$(abspath $(DESTDIR))/3. Juhendid\"\nDOCS3ET := \\\n\tet/haldusteenus \\\n\tet/kogumisteenuse_haldusjuhend \\\n\tet/seadistuste_koostejuhend \\\n\tet/audiitor\n\nRELEASE4 = \"$(abspath $(DESTDIR))/4. Näited\"\nDOCS4ET := \\\n\tet/example-config \\\n\tet/seadistuste_koostejuhend/config-examples/android-ios-config.json\n\nRELEASE5 = \"$(abspath $(DESTDIR))/5. Muudatused\"\n\nRELEASEDOCSET := $(patsubst %,release-et-%,$(DOCS1ET) $(DOCS2ET) $(DOCS3ET) $(DOCS4ET))\nRELEASEDOCSEN := $(patsubst %,release-en-%,$(DOCS1EN) $(DOCS2EN))\n\n# If releasing locally (i.e., no explicit DESTDIR is given), then use\n# _build/release. RELEASE[1-5] must be recursively expanded for this to work.\n$(RELEASEDOCSET): DESTDIR := $(DESTDIR)/release\n$(RELEASEDOCSEN): DESTDIR := $(DESTDIR)/release\n\n.PHONY: release\nrelease: $(RELEASEDOCSET) $(RELEASEDOCSEN)\n\n.PHONY: $(RELEASEDOCSET)\n$(DOCS1ET:%=release-et-%): release-et-%:\n\tmkdir -p $(RELEASE1)\n\t$(MAKE) -C $* install-pdf DESTDIR=$(RELEASE1)\n\tmkdir -p $(RELEASE5)\n\t$(MAKE) -C $* install-diff DESTDIR=$(RELEASE5)\n\n$(DOCS2ET:%=release-et-%): release-et-%:\n\tmkdir -p $(RELEASE2)\n\t$(MAKE) -C $* install-pdf DESTDIR=$(RELEASE2)\n\tmkdir -p $(RELEASE5)\n\t$(MAKE) -C $* install-diff DESTDIR=$(RELEASE5)\n\n$(DOCS3ET:%=release-et-%): release-et-%:\n\tmkdir -p $(RELEASE3)\n\t$(MAKE) -C $* install-pdf DESTDIR=$(RELEASE3)\n\tmkdir -p $(RELEASE5)\n\t$(MAKE) -C $* install-diff DESTDIR=$(RELEASE5)\n\n$(DOCS4ET:%=release-et-%): release-et-%:\n\tmkdir -p $(RELEASE4)\n\tcp --recursive --update $* $(RELEASE4)\n\n\n.PHONY: $(RELEASEDOCSEN)\n$(DOCS2EN:%=release-en-%): release-en-%:\n\tmkdir -p $(RELEASE2)\n\t$(MAKE) -C $* install-en-pdf DESTDIR=$(RELEASE2)\n\n\n# Cleanup.\n\n# DOCS already contains DOCS[1-3].\nCLEANDOCS := $(DOCS:%=clean-%)\n\n.PHONY: clean\nclean: $(CLEANDOCS)\n\t-git worktree remove --force _master\n\trm -rf $(DESTDIR) _master\n\tpy3clean .\n\n.PHONY: $(CLEANDOCS)\n$(CLEANDOCS): clean-%:\n\t$(MAKE) -C $* clean\n"
  },
  {
    "path": "Documentation/_static/custom.css",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Sphinx stylesheet for HTML documentation\n */\n\n.figure {\n  background-color: rgba(128, 128, 128, 0.1);\n  border: thin dotted gray;\n}\n\n.caption {\n  border-bottom: thin solid gray;\n}\n\n/* Highlight target section */\n\n.section:target {\n  background-color: rgba(220, 220, 255, 0.2);\n  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);\n  padding: 10px 5px 5px 5px;\n}\n\n/*\n * vim:foldmethod=syntax:\n */\n"
  },
  {
    "path": "Documentation/common/examples/id.rpc.vote.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Vote\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"tls\",\n            \"Choices\": \"0140.1\",\n            \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\",\n            \"OS\": \"Operating System,2,0\",\n            \"Type\": \"bdoc\",\n            \"Vote\": \"UEsDBAoABgAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlv\\nbi92bmQuZX...\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/id.rpc.vote.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Qualification\": {\n            \"ocsp\": \"MIIFTAoBAKCCBUUwggVBBgkrBgEFBQcwAQEEggUyMIIFLjCB5qFMME...\",\n            \"tspreg\": \"MIIDsAYJKoZIhvcNAQcCoIIDoTCCA50CAQMxCzAJBgUrDgMCGgQS...\"\n        },\n        \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\",\n        \"TestVote\": true,\n        \"VoteID\": \"VM/cUIU4n7VjxpUx1fC00Q==\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/id.rpc.voterchoices.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.VoterChoices\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"tls\",\n            \"OS\": \"Operating System,2,0\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/id.rpc.voterchoices.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Choices\": \"0140.1\",\n        \"List\": \"ew0KICAgICAgICAgICAgIkVyYWtvbmQgMSI6IHsNCiAgICAgICAgICAgIC...\",\n        \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\",\n        \"Voted\": true\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/json.rpc.method.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Method\",\n    \"params\": [\n        {\n            \"MethodParam\": \"value\",\n            \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/json.rpc.method.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"ResultParam\": \"value\",\n        \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticate.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Authenticate\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"IDCode\": \"60001019906\",\n            \"PhoneNo\": \"+37200000766\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticate.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Challenge\": \"EtsTur4XV7xEGS9LBjHSfF9Cc5PQxtYW+YAOysRIt2r...\",\n        \"SessionCode\": \"2127729011\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"DataToken\": \"G1GTSqBSBKrzqReuKYrmFUFXWFPkisjJjdiZi6zqAnaK3OvrT2Qu6...\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticatestatus.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.AuthenticateStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"2127729011\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticatestatus.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"AuthToken\": null,\n        \"GivenName\": \"\",\n        \"PersonalCode\": \"\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"POLL\",\n        \"Surname\": \"\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticatestatus2.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.AuthenticateStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"2127729011\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.authenticatestatus2.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT2Qu6...\",\n        \"GivenName\": \"MARY \\u00c4NN\",\n        \"PersonalCode\": \"60001019906\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"OK\",\n        \"Surname\": \"O\\u2019CONNE\\u017d-\\u0160USLIK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.getcertificate.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.GetCertificate\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"DataToken\": \"G1GTSqBSBKrzqReuKYrmFUFXWFPkisjJjdiZi6zqAnaK3OvrT2Qu6...\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.getcertificate.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Certificate\": \"MIIEVjCCAz6gAwIBAgIQRfmbsIcpkQ9UhxScCwG6VDANBgkqhki...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.sign.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Sign\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"DataToken\": \"G1GTSqBSBKrzqReuKYrmFUFXWFPkisjJjdiZi6zqAnaK3OvrT2Qu6...\",\n            \"Hash\": \"9IBrA05ylt2StdjxKkSTYMW/rQXY3Vub4upzShdfEzo=\",\n            \"HashType\": \"SHA256\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.sign.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.signstatus.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.SignStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.signstatus.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Signature\": null,\n        \"Status\": \"POLL\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.signstatus2.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.SignStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.signstatus2.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Signature\": \"MOj+8xQ9DmZPr/ItHlm0tHNMCuTgn6dT9jcXjPLf0+2sVjsS11jRI...\",\n        \"Algorithm\": \"SHA256WithECEncryption\",\n        \"Status\": \"OK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.vote.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Vote\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"Choices\": \"0919.1\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"OS\": \"Operating System,2,0\",\n            \"Type\": \"bdoc\",\n            \"Vote\": \"UEsDBAoAAAAAAAAAAACKIflFHwAAAB8AAAAIAAAAbWltZXR5cGVhcHB...\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.vote.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Qualification\": {\n            \"ocsp\": \"MIIG7QoBAKCCBuYwggbiBgkrBgEFBQcwAQEEggbTMIIGzzCCASGhgY...\",\n            \"tspreg\": \"MIIE0QYJKoZIhvcNAQcCoIIEwjCCBL4CAQMxDzANBglghkgBZQMEA...\"\n        },\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"TestVote\": true,\n        \"VoteID\": \"dWf/HUQVqXili7BeX2JAZQ==\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.voterchoices.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.VoterChoices\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/mid.rpc.voterchoices.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Choices\": \"0919.1\",\n        \"List\": \"ew0KICAgICAgICAgICAgIsOca3Npa2thbmRpZGFhZGlkIjogew0KICAgIC...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Voted\": true\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticate.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Authenticate\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"Identifier\": \"60001019906\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticate.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"SessionCode\": \"2127729011\",\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticatestatus.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.AuthenticateStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"2127729011\",\n            \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticatestatus.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"AuthToken\": null,\n        \"DataToken\": null,\n        \"GivenName\": \"\",\n        \"PersonalCode\": \"\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n        \"Status\": \"POLL\",\n        \"Surname\": \"\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticatestatus2.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.AuthenticateStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"2127729011\",\n            \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.authenticatestatus2.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT2Qu6...\",\n        \"DataToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWkajsdhaiuhdaksdj62vrT2Qu6...\",\n        \"GivenName\": \"MARY \\u00c4NN\",\n        \"PersonalCode\": \"60001019906\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"OK\",\n        \"Surname\": \"O\\u2019CONNE\\u017d-\\u0160USLIK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.challenge.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Challenge\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.challenge.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Challenge\": \"EtsTur4XV7xEGS9LBjHSfF9Cc5PQxtYW+YAOysRIt2r...\",\n        \"XSmartIDAuth\": \"4wesTurtYW+66BjHSfF9CcAQEB56It2rW+11112018Reji==...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.getcertificatechoice.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.GetCertificateChoice\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"DataToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWkajsdhaiuhdaksdj62vrT2Qu6...\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.getcertificatechoice.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"SessionCode\": \"2127729011\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.getcertificatechoicestatus.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.GetCertificateChoiceStatus\",\n    \"params\": [\n        {\n            \"SessionCode\": \"2127729011\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"OS\": \"Operating System,2,0\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.getcertificatechoicestatus.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Certificate\": null,\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"POLL\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.getcertificatechoicestatus2.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Certificate\": \"MIIEVjCCAz6gAwIBAgIQRfmbsIcpkQ9UhxScCwG6VDANBgkqhki...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"OK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.sign.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Sign\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"DataToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWkajsdhaiuhdaksdj62vrT2Qu6...\",\n            \"Hash\": \"9IBrA05ylt2StdjxKkSTYMW/rQXY3Vub4upzShdfEzo=\",\n            \"HashType\": \"SHA256\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.sign.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.signstatus.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.SignStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.signstatus.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Signature\": null,\n        \"Status\": \"POLL\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.signstatus2.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.SignStatus\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionCode\": \"E663A711BB9447EAD82491F9372F4CA\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.signstatus2.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Signature\": \"MOj+8xQ9DmZPr/ItHlm0tHNMCuTgn6dT9jcXjPLf0+2sVjsS11jRI...\",\n        \"Algorithm\": \"sha256WithRSAEncryption\",\n        \"Status\": \"OK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.vote.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Vote\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"Choices\": \"0919.1\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"OS\": \"Operating System,2,0\",\n            \"Type\": \"bdoc\",\n            \"Vote\": \"UEsDBAoAAAAAAAAAAACKIflFHwAAAB8AAAAIAAAAbWltZXR5cGVhcHB...\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.vote.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Qualification\": {\n            \"ocsp\": \"MIIG7QoBAKCCBuYwggbiBgkrBgEFBQcwAQEEggbTMIIGzzCCASGhgY...\",\n            \"tspreg\": \"MIIE0QYJKoZIhvcNAQcCoIIEwjCCBL4CAQMxDzANBglghkgBZQMEA...\"\n        },\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"TestVote\": true,\n        \"VoteID\": \"dWf/HUQVqXili7BeX2JAZQ==\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.voterchoices.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.VoterChoices\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/smartid.rpc.voterchoices.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Choices\": \"0919.1\",\n        \"List\": \"ew0KICAgICAgICAgICAgIsOca3Npa2thbmRpZGFhZGlkIjogew0KICAgIC...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Voted\": true\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/ver.rpc.verify.query.json",
    "content": "{\n    \"id\": 1,\n    \"method\": \"RPC.Verify\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"ec3a0cab353d552952289f2c7ad52e27\",\n            \"VoteID\": \"VM/cUIU4n7VjxpUx1fC00Q==\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/ver.rpc.verify.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 1,\n    \"result\": {\n        \"Qualification\": {\n            \"ocsp\": \"MIIG8woBAKCCBuwwggboBgkrBgEFBQcwAQEEggbZMIIG1TCCASehgY...\",\n            \"tspreg\": \"MIIE0QYJKoZIhvcNAQcCoIIEwjCCBL4CAQMxDzANBglghkgBDQEJE...\"\n        },\n        \"SessionID\": \"027ab451969d9d3f044ea2cb2675b503\",\n        \"Type\": \"bdoc\",\n        \"Vote\": \"UEsDBAoAAAAAAAAAAACKIflFHwAAAB8AAAAIAAAAbWltZXR5cGVhcHB...\",\n        \"ChoicesList\": \"ew0KICAgICAgICAgICAgIkVyYWtvbmQgMSI6IHsNCiAgICAgICAgICAgIC...\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/votesorder.rpc.votes.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Votes\",\n    \"params\": [\n        {\n            \"VotesFrom\": 1,\n            \"BatchMaxSize\": 1000\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/votesorder.rpc.votes.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"batchRecords\": [\n            {\n                \"seqNo\": 1,\n                \"idCode\": \"37123097845\",\n                \"voterName\": \"NIMI NIMESTE1\",\n                \"kovCode\": \"0123\",\n                \"electoralDistrictNo\": 1\n            },\n            {\n                \"seqNo\": 2,\n                \"idCode\": \"37123097653\",\n                \"voterName\": \"NIMI NIMESTE2\",\n                \"kovCode\": \"0444\",\n                \"electoralDistrictNo\": 1\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/votesorder.rpc.votesseqno.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.VotesSeqNo\",\n    \"params\": [\n        {\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/votesorder.rpc.votesseqno.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"SeqNo\": 10\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.challenge.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Challenge\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.challenge.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Challenge\": \"EtsTur4XV7xEGS9LBjHSfF9Cc5PQxtYW+YAOysRIt2r...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Bearer\": \"xtYW+YAOyXV7xEGS9YWxtYW+YAOtsTur4XV7xEGS9LB1...\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.token.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Token\",\n    \"params\": [\n        {\n            \"OS\": \"Operating System,2,0\",\n            \"Token\": \"{\\\"unverifiedCertificate\":\"MIICnDCCA...\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"Bearer\": \"xtYW+YAOyXV7xEGS9YWxtYW+YAOtsTur4XV7xEGS9LB1...\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.token.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT2Qu6...\",\n        \"GivenName\": \"MARY \\u00c4NN\",\n        \"PersonalCode\": \"60001019906\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Status\": \"OK\",\n        \"Surname\": \"O\\u2019CONNE\\u017d-\\u0160USLIK\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.vote.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.Vote\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"Choices\": \"0919.1\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n            \"OS\": \"Operating System,2,0\",\n            \"Type\": \"bdoc\",\n            \"Vote\": \"UEsDBAoAAAAAAAAAAACKIflFHwAAAB8AAAAIAAAAbWltZXR5cGVhcHB...\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.vote.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Qualification\": {\n            \"ocsp\": \"MIIG7QoBAKCCBuYwggbiBgkrBgEFBQcwAQEEggbTMIIGzzCCASGhgY...\",\n            \"tspreg\": \"MIIE0QYJKoZIhvcNAQcCoIIEwjCCBL4CAQMxDzANBglghkgBZQMEA...\"\n        },\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"TestVote\": true,\n        \"VoteID\": \"dWf/HUQVqXili7BeX2JAZQ==\"\n    }\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.voterchoices.query.json",
    "content": "{\n    \"id\": 0.0,\n    \"method\": \"RPC.VoterChoices\",\n    \"params\": [\n        {\n            \"AuthMethod\": \"ticket\",\n            \"AuthToken\": \"G1RTZqBSBKrzqReuKYrmFUFXWFPvaxhJjdiZi6zqAnaK3OvrT...\",\n            \"OS\": \"Operating System,2,0\",\n            \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\"\n        }\n    ]\n}\n"
  },
  {
    "path": "Documentation/common/examples/webeid.rpc.voterchoices.response.json",
    "content": "{\n    \"error\": null,\n    \"id\": 0.0,\n    \"result\": {\n        \"Choices\": \"0919.1\",\n        \"List\": \"ew0KICAgICAgICAgICAgIsOca3Npa2thbmRpZGFhZGlkIjogew0KICAgIC...\",\n        \"SessionID\": \"057229fdfa2df7d3c7f4ced81b02760b\",\n        \"Voted\": true\n    }\n}\n"
  },
  {
    "path": "Documentation/common/schema/Makefile",
    "content": ".PHONY: validate\nvalidate:\n\tpython3 validate_schema.py\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.anon-bb.schema",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"definitions\": {\n        \"results\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            },\n            \"additionalItems\": false\n        },\n        \"questions\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n                \"$ref\": \"#/definitions/results\"\n            },\n            \"minProperties\": 1\n        },\n        \"parish\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}$|^(FOREIGN)$\": {\n                    \"$ref\": \"#/definitions/questions\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        },\n        \"districts\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{1,2}$\": {\n                    \"$ref\": \"#/definitions/parish\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"election\": {\n            \"type\": \"string\"\n        },\n        \"districts\": {\n            \"$ref\": \"#/definitions/districts\"\n        }\n    },\n    \"required\": [\n        \"election\",\n        \"districts\"\n    ],\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.anon-bb.schema.example",
    "content": "{\n    \"election\": \"TESTKOV\",\n    \"districts\": {\n        \"0164.1\": {\n            \"0164\": {\n                \"TESTKOV.1\": [\n                    \"MDkxOS4xMDUK\",\n                    \"MDkxOS4xMDQK\",\n                    \"MDkxOS4xMDEK\",\n                    \"MDkxOS4xMDMK\"\n                ]\n            }\n        },\n        \"0296.1\": {\n            \"0296\": {\n                \"TESTKOV.1\": [\n                    \"MDkxOS4xMDQK\",\n                    \"MDkxOS4xMDQK\"\n                ]\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.choices.schema",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"definitions\": {\n        \"choice\": {\n            \"type\": \"string\",\n            \"description\": \"Candidate's name in case of RK, EP, KOV. Possible answer in case of RH\"\n        },\n        \"list_choices\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{3,4}$\": {\n                    \"$ref\": \"#/definitions/choice\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        },\n        \"district_choices\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n                \"$ref\": \"#/definitions/list_choices\"\n            },\n            \"minProperties\": 1,\n            \"description\": \"Political unions and independent candidates in district, in case of RH - question\"\n        },\n        \"district_dict\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{1,2}$\": {\n                    \"$ref\": \"#/definitions/district_choices\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"election\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[ \\\\-,\\\\+\\\\.:;=!?&%#<>_/\\\\'\\\\*()\\\\[\\\\]{}|^A-Za-z0-9]{1,28}$\"\n        },\n        \"choices\": {\n            \"$ref\": \"#/definitions/district_dict\"\n        }\n    },\n    \"required\": [\n        \"election\",\n        \"choices\"\n    ],\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.choices.schema.example",
    "content": "{\n    \"choices\": {\n        \"0164.1\": {\n            \"Nimi Valimisliit\": {\n                \"0164.126\": \"Nimi Kandidaat\",\n                \"0164.127\": \"Nimi Kandidaat\"\n            }\n        },\n        \"0296.1\": {\n            \"Nimi Erakond\": {\n                \"0296.198\": \"Nimi Kandidaat\",\n                \"0296.199\": \"Nimi Kandidaat\",\n                \"0296.200\": \"Nimi Kandidaat\"\n            },\n            \"Nimi Valimisliit\": {\n                \"0296.115\": \"Nimi Kandidaat\",\n                \"0296.116\": \"Nimi Kandidaat\",\n                \"0296.117\": \"Nimi Kandidaat\"\n            },\n            \"Üksikkandidaadid\": {\n                \"0296.101\": \"Nimi Kandidaat\",\n                \"0296.102\": \"Nimi Kandidaat\"\n            }\n        }\n    },\n    \"election\": \"TESTKOV\"\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.districts.schema",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"definitions\": {\n    \"region\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"state\": {\n          \"type\": \"string\"\n        },\n        \"county\": {\n          \"type\": \"string\"\n        },\n        \"parish\": {\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1,\n      \"description\": \"Value - parish - is absent in case of Tallinn and Tartu cities as they are marked as counties. Parish represents city district in case of Tallinn. Parish and county are absent in case of Välisriik and RK, EP and RH elections\"\n    },\n    \"region_dict\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[0-9]{4}$\": {\n          \"$ref\": \"#/definitions/region\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1,\n      \"description\": \"Parishes EHAK code; city district code in case of Tallinn; 0000 for foreign state in case of RK, EP and RH elections\"\n    },\n    \"parish\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[0-9]{4}$\",\n      \"description\": \"Parishes in election district of county. 0000 value in represents foreign state in case of RK, EP and RH elections for each election district in districts block\"\n    },\n    \"district\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"parish\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/parish\"\n          }\n        }\n      },\n      \"required\": [\n        \"parish\"\n      ],\n      \"description\": \"Object includes parishes or city districts that belong to this election district\"\n    },\n    \"district_dict\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[0-9]{4}.[0-9]{1,2}$\": {\n          \"$ref\": \"#/definitions/district\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"counties\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/parish\"\n      },\n      \"description\": \"Object includes parishes or city districts that belong into this election district\",\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"county_dict\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[0-9]{4}$\": {\n          \"$ref\": \"#/definitions/counties\"\n        }\n      },\n      \"description\": \"Object contains parishes or city districts inside county. Tallinn is defined as county and contains city districts, Tartu linn is both county and parish\",\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    }\n  },\n  \"type\": \"object\",\n  \"properties\": {\n    \"election\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[ \\\\-,\\\\+\\\\.:;=!?&%#<>_/\\\\'\\\\*()\\\\[\\\\]{}|^A-Za-z0-9]{1,28}$\"\n    },\n    \"districts\": {\n      \"$ref\": \"#/definitions/district_dict\"\n    },\n    \"regions\": {\n      \"$ref\": \"#/definitions/region_dict\"\n    },\n    \"counties\": {\n      \"$ref\": \"#/definitions/county_dict\"\n    }\n  },\n  \"required\": [\n    \"districts\",\n    \"regions\",\n    \"counties\",\n    \"election\"\n  ],\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.districts.schema.example",
    "content": "{\n  \"election\": \"KOV2034\",\n  \"regions\": {\n    \"0809\": {\n      \"parish\": \"Tori vald\",\n      \"county\": \"Pärnu maakond\",\n      \"state\": \"Eesti Vabariik\"\n    },\n    \"0624\": {\n      \"parish\": \"Pärnu linn\",\n      \"county\": \"Pärnu maakond\",\n      \"state\": \"Eesti Vabariik\"\n    },\n    \"0524\": {\n      \"parish\": \"Nõmme linnaosa\",\n      \"county\": \"Tallinn\",\n      \"state\": \"Eesti Vabariik\"\n    },\n    \"0784\": {\n      \"county\": \"Tallinn\",\n      \"state\": \"Eesti Vabariik\"\n    },\n    \"0796\": {\n      \"parish\": \"Tartu vald\",\n      \"county\": \"Tartu maakond\",\n      \"state\": \"Eesti Vabariik\"\n    },\n    \"0793\": {\n      \"county\": \"Tartu linn\",\n      \"state\": \"Eesti Vabariik\"\n    }\n  },\n  \"districts\": {\n    \"0809.1\": {\n      \"name\": \"Valimisringkond nr. 1\",\n      \"parish\": [\n        \"0809\"\n      ]\n    },\n    \"0809.2\": {\n      \"name\": \"Valimisringkond nr. 2\",\n      \"parish\": [\n        \"0809\"\n      ]\n    },\n    \"0624.1\": {\n      \"name\": \"Valimisringkond nr. 1\",\n      \"parish\": [\n        \"0624\"\n      ]\n    },\n    \"0784.6\": {\n      \"name\": \"Valimisringkond nr. 6\",\n      \"parish\": [\n        \"0524\"\n      ]\n    },\n    \"0796.1\": {\n      \"name\": \"Valimisringkond nr. 1\",\n      \"parish\": [\n        \"0796\"\n      ]\n    },\n    \"0793.1\": {\n      \"name\": \"Valimisringkond nr. 1\",\n      \"parish\": [\n        \"0793\"\n      ]\n    }\n  },\n  \"counties\": {\n    \"0068\": [\n      \"0809\",\n      \"0624\"\n    ],\n    \"0784\": [\n      \"0524\"\n    ],\n    \"0079\": [\n      \"0796\"\n    ],\n    \"0793\": [\n      \"0793\"\n    ]\n  }\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.result.schema",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"definitions\": {\n        \"results\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"invalid\": {\n                    \"type\": \"integer\",\n                    \"description\": \"Number of invalid votes\"\n                }\n            },\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{3,4}$\": {\n                    \"type\": \"integer\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"required\": [\n                \"invalid\"\n            ]\n        },\n        \"parish\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}$\": {\n                    \"$ref\": \"#/definitions/results\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        },\n        \"district_dict\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{1,2}$\": {\n                    \"$ref\": \"#/definitions/results\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        },\n        \"parish_dict\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{1,2}$\": {\n                    \"$ref\": \"#/definitions/parish\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"election\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[ \\\\-,\\\\+\\\\.:;=!?&%#<>_/\\\\'\\\\*()\\\\[\\\\]{}|^A-Za-z0-9]{1,28}$\"\n        },\n        \"bydistrict\": {\n            \"$ref\": \"#/definitions/district_dict\"\n        },\n        \"byparish\": {\n            \"$ref\": \"#/definitions/parish_dict\"\n        }\n    },\n    \"required\": [\"election\", \"bydistrict\", \"byparish\"],\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.result.schema.example",
    "content": "{\n    \"bydistrict\": {\n        \"0164.1\": {\n            \"0164.126\": 0,\n            \"0164.127\": 0,\n            \"invalid\": 0\n        },\n        \"0296.1\": {\n            \"0296.101\": 0,\n            \"0296.102\": 0,\n            \"0296.115\": 0,\n            \"0296.116\": 0,\n            \"0296.117\": 0,\n            \"0296.198\": 0,\n            \"0296.199\": 0,\n            \"0296.200\": 0,\n            \"invalid\": 0\n        }\n    },\n    \"byparish\": {\n        \"0164.1\": {\n            \"0164\": {\n                \"0164.126\": 0,\n                \"0164.127\": 0,\n                \"invalid\": 0\n            }\n        },\n        \"0296.1\": {\n            \"0296\": {\n                \"0296.101\": 0,\n                \"0296.102\": 0,\n                \"0296.115\": 0,\n                \"0296.116\": 0,\n                \"0296.117\": 0,\n                \"0296.198\": 0,\n                \"0296.199\": 0,\n                \"0296.200\": 0,\n                \"invalid\": 0\n            },\n            \"0296\": {\n                \"0296.101\": 0,\n                \"0296.102\": 0,\n                \"0296.115\": 0,\n                \"0296.116\": 0,\n                \"0296.117\": 0,\n                \"0296.198\": 0,\n                \"0296.199\": 0,\n                \"0296.200\": 0,\n                \"invalid\": 0\n            }\n        }\n    },\n    \"election\": \"TESTKOV\"\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.revoke.schema",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n    \"definitions\": {\n        \"rev_entry\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]{11}$\",\n            \"description\": \"Personal code of onlinevoter to be revoked\"\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"election\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[ \\\\-,\\\\+\\\\.:;=!?&%#<>_/\\\\'\\\\*()\\\\[\\\\]{}|^A-Za-z0-9]{1,28}$\"\n        },\n        \"type\": {\"enum\": [\"revoke\", \"restore\"]},\n        \"persons\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"#/definitions/rev_entry\"\n            }\n        }\n    },\n    \"required\": [\n        \"election\",\n        \"persons\",\n        \"type\"\n    ],\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.revoke.schema.example",
    "content": "{\n    \"election\": \"TESTKOV\",\n    \"persons\": [\n        \"11412090004\",\n        \"11412090005\",\n        \"11412090006\"\n    ],\n    \"type\": \"revoke\"\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.voterlist.schema",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"definitions\": {\n        \"onlinevoters_entry\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[0-9]{11}$\",\n            \"additionalItems\": false\n        },\n\n        \"onlinevoters\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"#/definitions/onlinevoters_entry\"\n            },\n            \"additionalItems\": false\n        },\n\n        \"parish\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}$\": {\n                    \"$ref\": \"#/definitions/onlinevoters\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        },\n\n        \"districts\": {\n            \"type\": \"object\",\n            \"patternProperties\": {\n                \"^[0-9]{4}\\\\.[0-9]{1,2}$\": {\n                    \"$ref\": \"#/definitions/parish\"\n                }\n            },\n            \"additionalProperties\": false,\n            \"minProperties\": 1\n        }\n    },\n    \"type\": \"object\",\n    \"properties\": {\n        \"election\": {\n            \"type\": \"string\",\n            \"pattern\": \"^[ \\\\-,\\\\+\\\\.:;=!?&%#<>_/\\\\'\\\\*()\\\\[\\\\]{}|^A-Za-z0-9]{1,28}$\"\n        },\n        \"onlinevoters\": {\n            \"$ref\": \"#/definitions/districts\"\n        }\n    },\n    \"required\": [\n        \"election\",\n        \"onlinevoters\"\n    ],\n    \"additionalProperties\": false\n}\n"
  },
  {
    "path": "Documentation/common/schema/ivxv.voterlist.schema.example",
    "content": "{\n  \"election\": \"RK2030\",\n  \"onlinevoters\": {\n    \"0000.1\": {\n      \"0176\": [\n        \"11412090001\"\n      ],\n      \"0339\": [\n        \"11412090002\",\n        \"11412090003\"\n      ],\n      \"0614\": [\n        \"11412090004\"\n      ],\n      \"0000\": [\n        \"11412090005\"\n      ]\n    },\n    \"0000.10\": {\n      \"0793\": [\n        \"11412090006\",\n        \"11412090007\",\n        \"11412090008\"\n      ],\n      \"0000\": [\n        \"11412090009\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "Documentation/common/schema/validate_schema.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Validate jsonschema example files.\"\"\"\n\nimport json\nimport logging\nimport os\nimport sys\n\nimport jsonschema\n\nlog = logging.getLogger()\nlogging.basicConfig(level=logging.INFO)\n\n\ndef main():\n    \"\"\"Main routine.\"\"\"\n    for schema_filename in sorted(os.listdir(\".\")):\n        if schema_filename.endswith(\".schema\"):\n            log.info(\"Loading schema %s\", schema_filename)\n            with open(schema_filename) as fd:\n                schema = json.load(fd)\n            log.info(\"Schema file %s is valid\", schema_filename)\n\n            example_filename = f\"{schema_filename}.example\"\n            log.info(\"Validating file %s\", example_filename)\n            with open(example_filename) as fd:\n                example = json.load(fd)\n\n            try:\n                jsonschema.validate(instance=example, schema=schema)\n            except jsonschema.exceptions.ValidationError as err:\n                log.error(\n                    \"Schema validation error while validating %r: %s\",\n                    example_filename,\n                    err,\n                )\n                return 1\n            log.info(\"File %s is valid\", example_filename)\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "Documentation/common/xmltemplates/si.template",
    "content": "<ds:SignedInfo xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\">\n  <ds:CanonicalizationMethod Algorithm=\"%CANON_ALG%\"></ds:CanonicalizationMethod>\n  <ds:SignatureMethod Algorithm=\"%SIG_ALG%\"></ds:SignatureMethod>\n  <ds:Reference Id=\"%VOTE_REF%\" URI=\"%VOTE_URI%\">\n    <ds:DigestMethod Algorithm=\"%DIGEST_ALG%\"></ds:DigestMethod>\n    <ds:DigestValue>%VOTE_DIGEST%</ds:DigestValue>\n  </ds:Reference>\n  <ds:Reference Id=\"%SP_REF%\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#%SP_URI%\">\n    <ds:Transforms>\n      <ds:Transform Algorithm=\"%CANON_ALG%\"></ds:Transform>\n    </ds:Transforms>\n    <ds:DigestMethod Algorithm=\"%DIGEST_ALG%\"></ds:DigestMethod>\n    <ds:DigestValue>%SP_DIGEST%</ds:DigestValue>\n  </ds:Reference>\n</ds:SignedInfo>\n"
  },
  {
    "path": "Documentation/common/xmltemplates/sig.template",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<asic:XAdESSignatures xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\">\n    <ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" Id=\"S0\">\n        %SI_XML%\n        %SV_XML%\n    <ds:KeyInfo>\n      <ds:X509Data>\n          <ds:X509Certificate>%X509_CERT%</ds:X509Certificate>\n      </ds:X509Data>\n    </ds:KeyInfo>\n    <ds:Object>\n        <xades:QualifyingProperties xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\" Target=\"#S0\">\n            %SP_XML%\n      </xades:QualifyingProperties>\n    </ds:Object>\n  </ds:Signature>\n</asic:XAdESSignatures>\n"
  },
  {
    "path": "Documentation/common/xmltemplates/sp.template",
    "content": "<xades:SignedProperties xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\" Id=\"%SP_URI%\">\n<xades:SignedSignatureProperties>\n  <xades:SigningTime>%SIGNING_TIME%</xades:SigningTime>\n  <xades:SigningCertificate>\n    <xades:Cert>\n      <xades:CertDigest>\n        <ds:DigestMethod Algorithm=\"%DIGEST_ALG%\"></ds:DigestMethod>\n        <ds:DigestValue>%CERT_DIGEST%</ds:DigestValue>\n      </xades:CertDigest>\n      <xades:IssuerSerial>\n        <ds:X509IssuerName>%ISSUER_NAME%</ds:X509IssuerName>\n        <ds:X509SerialNumber>%ISSUER_SERIAL%</ds:X509SerialNumber>\n      </xades:IssuerSerial>\n    </xades:Cert>\n  </xades:SigningCertificate>\n</xades:SignedSignatureProperties>\n<xades:SignedDataObjectProperties>\n  <xades:DataObjectFormat ObjectReference=\"#%VOTE_REF%\">\n    <xades:MimeType>application/octet-stream</xades:MimeType>\n  </xades:DataObjectFormat>\n</xades:SignedDataObjectProperties>\n</xades:SignedProperties>\n"
  },
  {
    "path": "Documentation/common/xmltemplates/sv.template",
    "content": "<ds:SignatureValue xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\" Id=\"%SV_URI%\">%SIG_VALUE%</ds:SignatureValue>\n"
  },
  {
    "path": "Documentation/common-model.mk",
    "content": "PLANTUML=env -u DISPLAY plantuml\nPLANTUML_EN=env -u DISPLAY plantuml -I./en/lang.pu\nPLANTUML_ET=env -u DISPLAY plantuml -I./et/lang.pu\n\nLANGUAGES=et en\n\nDIAGRAMS=$(SRC_DIAG) $(SUB_DIAG)\n\n# We process each diagram in all languages, so that the variables are set properly\nTARGETS=$(foreach DIAGRAM,$(DIAGRAMS),$(patsubst %, img/$(DIAGRAM).%.png, $(LANGUAGES)))\n\nall:\n\t@echo \"Dry run, make model to apply changes\"\n\t$(MAKE) -n model\n\nmodel: $(TARGETS)\n\n# Process manually translated diagrams\nimg/%.png: %.pu\n\t$(PLANTUML) $<\n\tmv $*.png $@\n\n\n# Process source diagrams\nimg/%.et.png: %.pu\n\t$(PLANTUML_ET) $<\n\tmv $*.png $@\n\nimg/%.en.png: %.pu\n\t$(PLANTUML_EN) $<\n\tmv $*.png $@\n\n\n# Process sub-diagrams of major diagram\nimg/%.et.png: $(PARENT).pu %.env\n\t$(PLANTUML_ET) $(DIAGRAM_DEF) $<\n\tmv $(PARENT).png $@\n\nimg/%.en.png: $(PARENT).pu %.env\n\t$(PLANTUML_EN) $(DIAGRAM_DEF) $<\n\tmv $(PARENT).png $@\n"
  },
  {
    "path": "Documentation/common.mk",
    "content": "# common.mk: Common recipes for Documentation.\n#\n# Defines a catch-all recipe which routes unknown targets to Sphinx in \"make\n# mode\". If $(DEPENDENCIES) is defined, then it will be set as a prerequisite\n# of the catch-all and removed on clean. In addition, if the source directory\n# contains a \"model\" subdirectory, then it invokes make in it to build and\n# clean that too.\n#\n# Defines a \"diff\" recipe which generates a PDF that highlights differences\n# between the current and latest released version of the document. If there are\n# no differences, then no PDF is produced.\n#\n# Defines \"install-pdf\", \"install-html\", and \"install-diff\" recipes which build\n# the specified type of documentation and install it to $(DESTDIR).\n\ncommon.mk := $(lastword $(MAKEFILE_LIST))\nMakefile  := $(lastword $(filter-out $(common.mk),$(MAKEFILE_LIST)))\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?= -c $(dir $(common.mk))\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = $(dir $(Makefile))\nBUILDDIR      = $(SOURCEDIR)_build\nSPHINXINTL    ?= sphinx-intl\n\n# Set IVXV_DOCUMENT to the name of the source directory. This will be used as\n# the key to look up configuration from documents.py.\nexport IVXV_DOCUMENT := $(notdir $(patsubst %/,%,$(abspath $(SOURCEDIR))))\n\n# Special case the help target: set as default and skip prerequisites.\n.DEFAULT_GOAL := help\n.PHONY: help\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\ntranslation-et:\n\t@$(SPHINXBUILD) -M gettext \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\t@$(SPHINXINTL) update -p _build/gettext -l et\n\ntranslation:\n\t@$(SPHINXBUILD) -M gettext \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\t@$(SPHINXINTL) update -p _build/gettext -l en\n\nspelling:\n\t@$(SPHINXBUILD) -M spelling \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\nenglish: clean $(DEPENDENCIES)\n\texport SPHINXOPTS=\"-D language='en'\" && $(SPHINXBUILD) -M latexpdf \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\nestonian: clean $(DEPENDENCIES)\n\t@$(SPHINXBUILD) -M latexpdf \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n\n# Special case the clean target: skip prerequisites and perform extra steps.\n.PHONY: clean\nclean:\n\t#if [ -d \"model\" ]; then $(MAKE) -C model clean; fi\n\t@$(SPHINXBUILD) -M clean \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\trm -rf $(BUILDDIR) $(DEPENDENCIES)\n\n# Do not regenerate the Makefiles.\n$(common.mk) $(Makefile): ;\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: $(DEPENDENCIES)\n\tif [ -d \"model\" ]; then $(MAKE) -C model; fi\n\t$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n# Variables and rules for diffing.\ndiffer.sh := $(dir $(common.mk))differ.sh\nmaster    := $(dir $(common.mk))_master/\ngitdir    := $(patsubst $(shell git rev-parse --show-toplevel)%,%,$(abspath $(SOURCEDIR)))\nolddir    := $(master)$(gitdir)\n\nifeq (\"$(olddir)\", \"../../_master//Documentation/public/protokollid\")\n\tolddir := \"../../_master//Documentation/et/protokollid\"\nendif\n\nifeq (\"$(olddir)\", \"../../_master//Documentation/public/uldsisukord\")\n\tolddir := \"../../_master//Documentation/et/uldsisukord\"\nendif\n\nifeq (\"$(olddir)\", \"../../_master//Documentation/public/arhitektuur\")\n\tolddir := \"../../_master//Documentation/et/arhitektuur\"\nendif\n\n.PHONY: diff\ndiff: latex master-latex\n\t# latexmk attempts to proceed even though errors may be present\n\t# pdflatex attempts to proceed with errors and is silent (batchmode)\n\tLATEXMKOPTS=\"-f -interaction=batchmode\" $(differ.sh) $(BUILDDIR)/master $(BUILDDIR)\n\n.PHONY: master-latex\nmaster-latex: $(master)\n\tif [ -d $(olddir) ]; then \\\n\t\t$(MAKE) -C $(olddir) BUILDDIR=$(abspath $(BUILDDIR))/master latex; \\\n\tfi\n\n$(master):\n\tgit worktree add $@ 1.9.10\n\n# Installation rules.\n.PHONY: install-pdf\ninstall-pdf: estonian\n\tcp --update $(filter-out %-diff.pdf,$(wildcard $(BUILDDIR)/latex/*.pdf)) \"$(DESTDIR)\"\n\n.PHONY: install-en-pdf\ninstall-en-pdf: english\n\tcp --update $(filter-out %-diff.pdf,$(wildcard $(BUILDDIR)/latex/*.pdf)) \"$(DESTDIR)\"\n\n.PHONY: install-html\ninstall-html: html\n\tcp --recursive --update $(BUILDDIR)/html/** \"$(DESTDIR)\"\n\n.PHONY: install-diff\ninstall-diff: diff\n\t$(eval diff.pdf = $(wildcard $(BUILDDIR)/latex/*-diff.pdf))\n\t$(if $(diff.pdf),cp --update $(diff.pdf) \"$(DESTDIR)\")\n"
  },
  {
    "path": "Documentation/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\nsys.path.insert(0, os.path.abspath('.'))\n\nimport documents\n\n# -- Project information -----------------------------------------------------\n\ndef detect_language():\n    opts = os.environ.get('SPHINXOPTS')\n    # expecting \"-D language='en'\"\n    if opts is not None and 'en' in opts:\n        return 'en'\n    return 'et'\n\n\nlanguage = detect_language()\n\nproject = documents.project\ncopyright = documents.copyright\nauthor = documents.author\nrelease = documents.release\n\ndocument = os.environ['IVXV_DOCUMENT']\n\nversion = documents.get(document, 'version', language)\ntoday = documents.get(document, 'changed', language)\ndocument_prefix = documents.get(document, 'document_prefix', language)\ndocument_type = documents.get(document, 'document_type', language)\ndocument_title = documents.get(document, 'document_title', language)\ndocument_target_name = documents.get(document, 'document_target_name', language)\n\ndocument_number = f'{document_prefix}-{version}'\n\ntoday_fmt = \"%d.%m.%Y\"\n\nnumfig = True\nnumfig_format = {\n    'section': '{name} (ptk. {number})'\n}\n\n# sphinx-intl\nlocale_dirs = ['locales/']\ngettext_compact = False\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosummary',\n    'sphinx.ext.imgmath',\n    'sphinx.ext.todo',\n    'sphinx_rtd_theme',\n    'sphinxcontrib.spelling',\n    'myst_parser',\n]\n\n\nspelling_lang='et_EE'\n\nspelling_word_list_filename = ['../../spelling_wordlist.txt', 'spelling_wordlist.txt']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = {\n    '.rst': 'restructuredtext',\n    '.md': 'markdown',\n}\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'sphinx_rtd_theme'\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n#\nhtml_title = f'{document_title} v{release}'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# List of CSS files.\nhtml_css_files = ['custom.css']\n\n# HTML logo image.\nhtml_logo = '_static/ivxv.png'\n\n# -- Options for LaTeX output ------------------------------------------------\n\nlatex_preamble_lines = [\n    '\\\\makeatletter',\n    '\\\\IVXVDefineDocumentVersion{%s}' % version,\n    '\\\\IVXVDefineDocumentNumber{%s}' % document_number,\n    '\\\\IVXVDefineDocumentType{%s}' % document_type,\n    '\\\\IVXVDefineDocumentSecurity{}',\n    '\\\\pagestyle{IVXVFancy}',\n    '\\\\makeatother',\n    '\\\\newcommand{\\\\tablecontinued}{\\\\sphinxtablecontinued}',\n]\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    'papersize': 'a4paper',\n    'preamble': ''.join(latex_preamble_lines),\n\n    # The font size ('10pt', '11pt' or '12pt').\n    'pointsize': '12pt',\n\n    # Additional stuff for the LaTeX preamble.\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc,\n     f'{document_target_name}.tex',\n     document_title,\n     author,\n     'ivxv-technical'),\n]\n\nlatex_additional_files = [\n    'ivxv-technical/ivxv-common.cls',\n    'ivxv-technical/ivxv-technical.cls'\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#\nlatex_logo = html_logo\n\n# If true, show page references after internal links.\n#\nlatex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#\n\nlatex_show_urls = 'footnote'\n\n\n# -- Options for Texinfo output ----------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc,\n     document_target_name,\n     document_title,\n     author,\n     document_target_name,\n     'One line description of project.',\n     'Miscellaneous'),\n]\n"
  },
  {
    "path": "Documentation/differ.sh",
    "content": "#!/bin/bash\n# differ.sh: Generate LaTeX differences between two Sphinx build directories.\n\nset -eu\n\nold=\"$1/latex/\"\nnew=\"$2/latex/\"\n\nshopt -s nullglob\nfor oldfile in ${old}*.tex; do\n  basename=\"$(basename \"${oldfile}\" .tex)\"\n  newfile=\"${new}${basename}.tex\"\n  difffile=\"${new}${basename}-diff.tex\"\n  if [[ \"${basename}\" != \"*-diff\" && -f \"${newfile}\" ]]; then\n    latexdiff --replace-context2cmd=\"none\" --config VERBATIMENV=sphinxVerbatim \"${oldfile}\" \"${newfile}\" > \"${difffile}\"\n\n    # Only generate the -diff.pdf if there are any actual differences.\n    if grep --invert-match \"%DIF PREAMBLE\" \"${difffile}\" | grep --quiet '\\\\DIF'; then\n      # latexdiff can mess up latex sometimes, but output is still visually fine\n      make --directory \"${new}\" \"${basename}-diff.pdf\" || true\n    else\n      # Remove any existing -diff.pdf to signal that there were no changes.\n      rm --force \"${new}${basename}-diff.pdf\"\n    fi\n  fi\ndone\n\nexit 0\n"
  },
  {
    "path": "Documentation/documents.py",
    "content": "#!/usr/bin/python3\n\"\"\"\nIVXV documents\n\"\"\"\n\nproject = 'Elektroonilise hääletamise infosüsteem'\ncopyright = '2016-2025, Cybernetica AS'\nauthor = 'Cybernetica AS'\n\nrelease = '1.10'\n\nMETA = {\n\n    'audiitor': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JAJ',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV audiitori juhend',\n            'document_target_name': 'IVXV-audiitor'\n         }\n    },\n\n    'arhitektuur': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-AR',\n            'document_type': 'Arhitektuuridokument',\n            'document_title': 'IVXV arhitektuur',\n            'document_target_name': 'IVXV-arhitektuur'\n         },\n        'en':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-AR-EN',\n            'document_type': 'Architecture document',\n            'document_title': 'IVXV architecture',\n            'document_target_name': 'IVXV-architecture'\n         }\n    },\n\n    'protokollid': {\n        'et':{\n            'lang': 'et',\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-PR',\n            'document_type': 'Spetsifikatsioon',\n            'document_title': 'IVXV protokollide kirjeldus',\n            'document_target_name': 'IVXV-protokollid'\n         },\n        'en':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-PR-EN',\n            'document_type': 'Specification',\n            'document_title': 'IVXV protocols',\n            'document_target_name': 'IVXV-protocols'\n         }\n    },\n\n    'haldusteenus': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JHT',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV haldusteenuse kasutusjuhend',\n            'document_target_name': 'IVXV-haldusteenuse-juhend'\n         }\n    },\n\n    'kasutusmall': {\n        'et':{\n            'version': '1.8.1',\n            'changed': \"16.12.2022\",\n            'document_prefix': 'IVXV-KM',\n            'document_type': 'Analüüsidokument',\n            'document_title': 'IVXV kasutusmallid',\n            'document_target_name': 'IVXV-kasutusmallid'\n         }\n    },\n\n    'votmerakendus': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-SVR',\n            'document_type': 'Spetsifikatsioon',\n            'document_title': 'IVXV võtmerakendus',\n            'document_target_name': 'IVXV-votmerakendus'\n         }\n    },\n\n    'ivxvapi': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-API',\n            'document_type': 'API-dokument',\n            'document_title': 'IVXV API',\n            'document_target_name': 'IVXV-API'\n         }\n    },\n\n    'seadistuste_koostejuhend': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JSK',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV seadistuste koostamise juhend',\n            'document_target_name': 'IVXV-seadistuste-koostejuhend'\n         }\n    },\n    'kogumisteenuse_haldusjuhend': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JSH',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV kogumisteenuse haldusjuhend',\n            'document_target_name': 'IVXV-kogumisteenuse-haldusjuhend'\n         }\n    },\n    'uldsisukord': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-YS',\n            'document_type': '',\n            'document_title': 'IVXV dokumentatsiooni üldsisukord',\n            'document_target_name': 'IVXV-dokumentatsiooni-uldsisukord'\n         },\n        'en':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-YS-EN',\n            'document_type': '',\n            'document_title': 'IVXV documentation overview',\n            'document_target_name': 'IVXV-documentation-overview'\n         }\n\n    },\n    'valijarakendus': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JVR',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV valijarakendus',\n            'document_target_name': 'IVXV-valijarakendus'\n         }\n    },\n    'valijarakenduse_pakendamine': {\n        'et':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-JVP',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV valijarakenduse pakendamine',\n            'document_target_name': 'IVXV-valijarakenduse-pakendamine'\n         }\n    },\n    'tarne': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-T',\n            'document_type': 'Juhend',\n            'document_title': 'IVXV tarnejuhend',\n            'document_target_name': 'IVXV-tarnejuhend'\n         }\n    },\n    'eriomadused': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-EO',\n            'document_type': 'Analüüsidokument',\n            'document_title': 'IVXV eriomadused',\n            'document_target_name': 'IVXV-eriomadused'\n         }\n    },\n    'testimisplaan': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-TP',\n            'document_type': 'Testimisplaan',\n            'document_title': 'IVXV testimisplaan',\n            'document_target_name': 'IVXV-testimisplaan'\n         }\n    },\n    'regteenus': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-SRT',\n            'document_type': 'Spetsifikatsioon',\n            'document_title': 'IVXV registreerimisteenuse kirjeldus',\n            'document_target_name': 'IVXV-registreerimisteenus'\n         }\n    },\n    'liidesed': {\n        'et': {\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-VIS-EHS',\n            'document_type': 'Spetsifikatsioon',\n            'document_title': 'VIS3-EHS liidesed',\n            'document_target_name': 'IVXV-liidesed'\n        },\n        'en': {\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-VIS-EHS-EN',\n            'document_type': 'Specification',\n            'document_title': 'VIS3-EHS interfaces',\n            'document_target_name': 'IVXV-interfaces'\n        }\n    },\n    'xteeteenus': {\n        'et':{\n             'version': '1.10',\n             'changed': \"25.09.2025\",\n             'document_prefix': 'IVXV-XTEE',\n             'document_type': 'Spetsifikatsioon',\n             'document_title': 'IVXV X-tee teenuse kirjeldus',\n             'document_target_name': 'IVXV-xteeteenus'\n         }\n     },\n    'ivxv-pyapi': {\n        'et':{\n            'version': '1.8.0',\n            'changed': \"01.12.2022\",\n            'document_prefix': 'IVXV-PYAPI',\n            'document_type': 'API-dokument',\n            'document_title': 'IVXV Python API',\n            'document_target_name': 'IVXV-python-api'\n         }\n    },\n    'backendlogs': {\n        'en':{\n            'version': '1.10',\n            'changed': \"25.09.2025\",\n            'document_prefix': 'IVXV-LOGS-EN',\n            'document_type': 'Technical documentation',\n            'document_title': 'IVXV Backend Log Messages',\n            'document_target_name': 'IVXV-backend-log-messages'\n         }\n    }\n}\n\n\ndef get(doc, meta, language):\n    return META[doc][language][meta]\n\n\ndef print_table_header():\n\n    print(\"\")\n    print(\".. list-table::\")\n    print(\"   :widths: 47 20 13 15\")\n    print(\"   :header-rows: 1\")\n    print(\"\")\n    print(\"   *  - Nimi\")\n    print(\"      - ID\")\n    print(\"      - Versioon\")\n    print(\"      - Kuupäev\")\n    print(\"\")\n\n\ndef print_document(doc, lang='et'):\n\n    title = get(doc, 'document_title', lang)\n    prefix = get(doc, 'document_prefix', lang)\n    version = get(doc, 'version', lang)\n    changed = get(doc, 'changed', lang)\n\n    print(f\"   *  - {title}\")\n    print(f\"      - {prefix}-{version}\")\n    print(f\"      - {version}\")\n    print(f\"      - {changed}\")\n    print(\"\")\n\n\nif __name__ == \"__main__\":\n\n    print(\"..  IVXV dokumentatsiooni üldsisukord\")\n    print(\"\")\n\n    print(\"Dokumendid\")\n    print(\"==========\")\n    print(\"\")\n\n    print(\"Ülddokumendid\")\n    print(\"-------------\")\n    print(\"\")\n\n    print_table_header()\n    print_document('uldsisukord')\n\n    print(\"Spetsifikatsioonid\")\n    print(\"------------------\")\n    print(\"\")\n\n    print_table_header()\n\n    print_document('kasutusmall')\n    print_document('protokollid')\n    print_document('arhitektuur')\n    print_document('votmerakendus')\n\n    print(\"Ingliskeelsed dokumendid\")\n    print(\"------------------------\")\n    print(\"\")\n\n    print_table_header()\n\n    print_document('protokollid', 'en')\n    print_document('arhitektuur', 'en')\n    print_document('liidesed', 'en')\n    print_document('backendlogs', 'en')\n\n    print(\"Juhendid\")\n    print(\"--------\")\n    print(\"\")\n\n    print_table_header()\n\n    print_document('seadistuste_koostejuhend')\n    print_document('kogumisteenuse_haldusjuhend')\n    print_document('haldusteenus')\n    print_document('ivxvapi')\n    print_document('audiitor')\n    print_document('valijarakendus')\n    print_document('valijarakenduse_pakendamine')\n"
  },
  {
    "path": "Documentation/en/backendlogs/.gitignore",
    "content": "index.rst\n"
  },
  {
    "path": "Documentation/en/backendlogs/Makefile",
    "content": "\nDEPENDENCIES := index.rst\ninclude ../../common.mk\n\nindex.rst:\n\t./create_rst.py\n"
  },
  {
    "path": "Documentation/en/backendlogs/create_rst.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nGenerate log-event reference doc from source code\n\"\"\"\n\n\nimport os\nimport re\n\nfrom rstcloth import RstCloth\n\n\ndef rstescape(txt):\n    \"\"\"\n    Escape RST formatting symbols from text\n    \"\"\"\n    return txt.replace(\"*\", r\"\\*\")\n\n\ndef parse_structs(file_content):\n    \"\"\"\n    Parse information about a particular struct in the generated file\n    \"\"\"\n    pattern = re.compile(\n        r\"// BEGIN (\\w+)\\n\\n\"  # Match typename\n        r\"// auto-generated.*\\n\"  # Match warning\n        r\"//\\s*([\\w./]+)\\.\\1\\s*\\n\"  # Match package\n        r\"((?:\\s*//.*\\n)+)\"  # Match comments\n        r\"(type\\s+\\1\\s+struct\\s*{(?:.|\\n)+?})\\s*\"  # Match typedef\n        r\"// END \\1\",  # Match end\n        re.MULTILINE,\n    )\n\n    structs = []\n\n    for match in pattern.finditer(file_content):\n        typename = match.group(1)\n        packagename = match.group(2)\n        comments = match.group(3).replace(\"//\", \"\").strip()\n        typedef = match.group(4).strip()\n\n        struct_info = {\n            \"typename\": typename,\n            \"packagename\": packagename,\n            \"fullcomment\": comments,\n            \"typedef\": typedef,\n        }\n\n        structs.append(struct_info)\n\n    return structs\n\n\ndef output_section_common(common_modules, rst, h2_common, h3_common, text):\n    \"\"\"\n    Output section with common modules\n    \"\"\"\n\n    rst.h2(h2_common)\n\n    sorted_modules = sorted(common_modules.keys())\n    for module in sorted_modules:\n        rst.h3(f\"{h3_common} {module.replace('ivxv.ee/', '')}\")\n\n        sorted_events = sorted(common_modules[module].keys())\n        for event in sorted_events:\n            data = common_modules[module][event]\n            rst.definition(event, text)\n            _, val = next(iter(data.items()))\n            rst.codeblock(val[\"typedef\"], indent=3)\n            rst.newline()\n            for mod in data:\n                rst.li(rstescape(f\"{mod}.{event}: {data[mod]['fullcomment']}\"))\n\n            rst.newline()\n\n\ndef output_section_unique(unique_modules, rst, h2_unique, h3_unique):\n    \"\"\"\n    Output section with unique models\n    \"\"\"\n\n    rst.h2(h2_unique)\n    sorted_modules = sorted(unique_modules.keys())\n    for module in sorted_modules:\n        rst.h3(f\"{h3_unique} {module.replace('ivxv.ee/', '')}\")\n\n        sorted_events = sorted(unique_modules[module].keys())\n        for event in sorted_events:\n            data = unique_modules[module][event]\n            rst.definition(\n                f\"{data['packagename']}.{event}\", rstescape(data[\"fullcomment\"])\n            )\n            rst.codeblock(data[\"typedef\"], indent=3)\n            rst.newline()\n\n\ndef output_intro(rst, h1, content_file):\n    \"\"\"\n    Output introduction\n    \"\"\"\n\n    rst.h1(h1)\n    with open(content_file, \"r\", encoding=\"ascii\") as inf:\n        for line in inf.readlines():\n            if line.startswith(\"*\"):\n                rst.li(line.replace(\"* \", \"\"))\n            else:\n                rst.content(line)\n        rst.newline()\n\n\ndef categorize_event(pkg, evt, item, exclusions, events_dicts):\n    \"\"\"\n    Analyze each event to a category\n    \"\"\"\n\n    if exclusions and pkg in exclusions:\n        return\n    if \"/cmd/\" in pkg:\n        events_dicts[\"cmd_events\"].setdefault(pkg, {})[evt] = item\n    elif \"/service/\" in pkg:\n        events_dicts[\"srv_events\"].setdefault(pkg, {})[evt] = item\n    elif \"/internal/\" in pkg:\n        events_dicts[\"int_events\"].setdefault(pkg, {})[evt] = item\n    else:\n        events_dicts[\"events\"].setdefault(pkg, {})[evt] = item\n\n\ndef analyze_src(src_dir, target_file, exclusions=None):\n    \"\"\"\n    Walk the source tree and analyze generated code\n    \"\"\"\n\n    events_dicts = {\"events\": {}, \"cmd_events\": {}, \"int_events\": {}, \"srv_events\": {}}\n\n    for dirpath, _, filenames in os.walk(src_dir):\n        if target_file in filenames:\n            full_path = os.path.join(dirpath, target_file)\n\n            with open(full_path, \"r\", encoding=\"utf8\") as file:\n                parsed_structs = parse_structs(file.read())\n\n                for item in parsed_structs:\n                    categorize_event(\n                        item[\"packagename\"],\n                        item[\"typename\"],\n                        item,\n                        exclusions,\n                        events_dicts,\n                    )\n\n    return tuple(\n        events_dicts[key]\n        for key in [\"events\", \"cmd_events\", \"int_events\", \"srv_events\"]\n    )\n\n\ndef remove_common(events):\n    \"\"\"\n    Remove common events from unique set\n    \"\"\"\n\n    common_events = {}\n    unique_events = {}\n\n    all_events = {}\n\n    for module, mod_events in events.items():\n        for event in mod_events:\n            all_events.setdefault(event, set([]))\n            all_events[event].add(module)\n\n    for event, modules in all_events.items():\n        if len(modules) > 1:\n            by_types = {}\n            for mod in modules:\n                typedef = events[mod][event][\"typedef\"]\n                by_types.setdefault(typedef, set([]))\n                by_types[typedef].add(mod)\n\n            if len(by_types) > 1:\n                pass\n                # This should not happen often. This means that same identifiers\n                # do not have equal typedefs and we may even have several different\n                # clusters with same identifier\n                # TODO\n                # print(event,file=sys.stderr)\n                # print(by_types, file=sys.stderr)\n\n            for _, val in by_types.items():\n                if len(val) > 1:\n\n                    printable = \"\"\n                    for mod in sorted(val):\n                        printable += mod\n                        printable += \", \"\n\n                    printable = printable[:-2]\n\n                    common_events.setdefault(printable, {})\n\n                    common_events[printable][event] = {}\n                    for mod in sorted(val):\n                        common_events[printable][event][mod] = events[mod][event]\n\n                else:\n                    mod = next(iter(val))\n                    unique_events.setdefault(mod, {})\n                    unique_events[mod][event] = events[mod][event]\n        else:\n            mod = next(iter(modules))\n            unique_events.setdefault(mod, {})\n            unique_events[mod][event] = events[mod][event]\n\n    return common_events, unique_events\n\n\nif __name__ == \"__main__\":\n\n    SRC_DIR = os.path.abspath(\"../../../\")\n    print(SRC_DIR)\n    TARGET_FILE = \"gen_types.go\"\n\n    OUT_FILE = \"index.rst\"\n\n    EXCLUSIONS = set(\n        [\n            \"ivxv.ee/common/collector/auth/dummy\",\n            \"ivxv.ee/common/collector/container/dummy\",\n            \"ivxv.ee/common/collector/storage/file\",\n            \"ivxv.ee/common/collector/storage/memory\",\n        ]\n    )\n\n    MOD_EVENTS, CMD_EVENTS, INT_EVENTS, SRV_EVENTS = analyze_src(\n        SRC_DIR, TARGET_FILE, EXCLUSIONS\n    )\n\n    with open(OUT_FILE, \"w\") as output_file:\n\n        RST = RstCloth(output_file)\n        RST.title(\"IVXV Backend Log Messages\")\n\n        RST.newline()\n        RST.content(\".. raw:: html\")\n        RST.newline()\n        RST.content('   <p style=\"background-color: #f99; padding: 20px;\">')\n        RST.content(\"     <strong>NB!</strong>\")\n        RST.content(\"     See on HTML-versioon dokumendist.\")\n        RST.content(\"     Tellijale antakse üle PDF-versioon.\")\n        RST.content(\"   </p>\")\n        RST.newline()\n        RST.content(\".. toctree::\")\n        RST.content(\"   :maxdepth: 4\")\n        RST.newline()\n\n        output_intro(RST, \"Overview\", \"introduction.inc\")\n\n        CMD_COMMON, CMD_UNIQUE = remove_common(CMD_EVENTS)\n        SRV_COMMON, SRV_UNIQUE = remove_common(SRV_EVENTS)\n        MOD_COMMON, MOD_UNIQUE = remove_common(MOD_EVENTS)\n        INT_COMMON, INT_UNIQUE = remove_common(INT_EVENTS)\n\n        RST.h1(\"Commands\")\n        output_section_common(\n            CMD_COMMON, RST, \"Common events\", \"Commands\", \"Typedef for common event\"\n        )\n        output_section_unique(CMD_UNIQUE, RST, \"Specific events\", \"Command\")\n\n        RST.h1(\"Services\")\n        output_section_common(\n            SRV_COMMON, RST, \"Common events\", \"Services\", \"Typedef for common event\"\n        )\n        output_section_unique(SRV_UNIQUE, RST, \"Specific events\", \"Service\")\n\n        RST.h1(\"Modules\")\n        output_section_common(\n            MOD_COMMON, RST, \"Common events\", \"Modules\", \"Typedef for common event\"\n        )\n        output_section_unique(MOD_UNIQUE, RST, \"Specific events\", \"Module\")\n\n        RST.h1(\"Internal modules\")\n        output_section_common(\n            INT_COMMON, RST, \"Common events\", \"Modules\", \"Typedef for common event\"\n        )\n        output_section_unique(INT_UNIQUE, RST, \"Specific events\", \"Module\")\n"
  },
  {
    "path": "Documentation/en/backendlogs/introduction.inc",
    "content": "The purpose of this document is to provide a reference to all possible log\nmessages in the IVXV backend.\n\nThis document is partially auto-generated from the comments in the source code.\nThis is necessary to keep the document up to date through the evolution of the\nsoftware. The document is written in english, since the source code is written\nin english for improved readability and availability to wider audiences.\n\n\nIVXV systematically logs based on the protocol description and the service\nstatus diagram. We describe general principles that are followed.\n\nAt a minimum, the following should be logged:\n\n* The fact of receiving each request and the start of processing\n\n* The handover of processing to an external component\n\n* The return of the processing sequence to the component\n\n* The end of the request processing and the result\n\n* Additionally, passing through significant stages in the process state model together with important facts about the process\n\n\nGeneral Principles:\n\n* The rsyslog service is used for logging, which records the timestamp of the log message with millisecond precision.\n\n* At the start of each session, the system generates a unique identifier, which the client application uses in its requests to the central system.\n\n* All log entries belonging to a single session contain the same session identifier. Validity of the session identifier is checked, requests with unknown, invalid or expired session identifiers are not served.\n\n* Each log entry must be uniquely identifiable.\n\n* For each log message, it must be possible to unambiguously identify the location in the source code where the message originated. To ensure this, each logged message must have a unique phrasing or contain a unique identifier. In IVXV all event identifiers and corresponding data structures are generated before the compilation. This makes it easier to follow unified pattern.\n\n* The log entry should preferably be in JSON format, where machine readability is primary and human readability is secondary for automated monitoring.\n\n* The log entries may be stacked, to include information from more technical contexts.\n\n* If in doubt - log, one can filter unnecessary information, but cannot regenerate missing information.\n\nRequirements:\n\n* The information logged must be sanitized (e.g., URL encoding) and subject to length restrictions (both overall and per parameter).\n\n* Information originating from outside the system perimeter must only be logged in a sanitized form and within a predefined length.\n\n* Certificates and the agreed cipher used during TLS handshake must be logged.\n\n* Sensitive data must not be logged Directly, it must be protected by e.g. hashing.\n"
  },
  {
    "path": "Documentation/et/audiitor/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/audiitor/annotatsioon.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\nAnnotatsioon\n------------\n\nJuhend annab suunised IVXV andmeauditi teostamiseks Ubuntu 20.04 platvormil.\nKasulik on tutvuda järgmiste dokumentidega.\n\n* https://www.valimised.ee/sites/default/files/uploads/eh/IVXV_raamistiku_yldkirjeldus_29052017.pdf\n* https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-seadistuste-koostejuhend.pdf\n* https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.pdf\n\nJuhendiga kaasneb näidisandmekomplekt ning vabas vormis `history` fail.\n\n\n"
  },
  {
    "path": "Documentation/et/audiitor/audit.rst",
    "content": "\n================================================================================\nIVXV juhend audiitorile\n================================================================================\n\nRakenduste kompileerimine\n================================================================================\n\n\nEELDUSED\n--------------------------------------------------------------------------------\n\nKõik tegevused viime läbi tavakasutaja -- näidetes `ubuntu` -- õigustes,\nkasutades vajadusel käsklust `sudo`.\n\nÜldjuhul viime kõik tegevused läbi kasutaja kodukaustas::\n\n  cd $HOME\n\nPaigaldame tarkvara, mis on vajalik audiitori tööriistade ehitamiseks::\n\n  sudo apt-get install --no-install-recommends -y autoconf automake build-essential libgmp-dev libtool git openjdk-11-jdk-headless python unzip zip wget make\n\n\nINTCHECK\n--------------------------------------------------------------------------------\n\nVerificatum miksneti tervikluskontroll toimub rakendusega `intcheck`::\n\n  wget https://github.com/vvk-ehk/intcheck/archive/master.zip\n  unzip master.zip\n  rm master.zip\n  mv intcheck-master intcheck\n  chmod +x intcheck/src/intcheck.py\n\nVeendume et rakendus on paigaldatud korrektselt::\n\n  ./intcheck/src/intcheck.py -h\n\n\nJAVA RAKENDUSED\n--------------------------------------------------------------------------------\n\nAndmeauditi läbiviimiseks läheb vaja rakendust `auditor`, mille lähtekood on\navalikustatud IVXV repositooriumis::\n\n  wget https://github.com/vvk-ehk/ivxv/archive/master.zip\n  unzip master.zip\n  rm master.zip\n  mv ivxv-master ivxv\n\n\nPaigaldame Java sõltuvuspaketid::\n\n  cd $HOME/ivxv/common/external\n  wget -O gradle-8.11.zip https://services.gradle.org/distributions/gradle-8.11-bin.zip\n  unzip gradle-8.11.zip\n  rm gradle-8.11.zip\n  cd $HOME/ivxv/common/java\n  make sync\n\nVeendume, et ettevalmistused on tehtud korrektselt::\n\n  cd $HOME/ivxv\n  make clean-java\n\nEhitame Java rakendused::\n\n  make java\n\nRVT'le tarnitavad rakendused::\n\n  $HOME/ivxv/auditor/build/distributions/auditor-1.10.3.zip\n  $HOME/ivxv/key/build/distributions/key-1.10.3.zip\n  $HOME/ivxv/processor/build/distributions/processor-1.10.3.zip\n\nKäivitatavad failid::\n\n  $HOME/ivxv/auditor/build/install/auditor/bin/auditor\n  $HOME/ivxv/key/build/install/key/bin/key\n  $HOME/ivxv/processor/build/install/processor/bin/processor\n\n\nVERIFICATUM\n--------------------------------------------------------------------------------\n\nVerificatum miksneti adapter on vajalik miksimistõendi kontrolliks::\n\n  cd $HOME\n  wget https://github.com/vvk-ehk/ivxv-mixnet-adapter/archive/master.zip\n  unzip master.zip\n  rm master.zip\n  mv ivxv-mixnet-adapter-master ivxv-verificatum\n\nVerificatum tarkvara allalaadmine::\n\n  git clone https://github.com/verificatum/verificatum-gmpmee gmpmee\n  git clone https://github.com/verificatum/vmgj\n  git clone https://github.com/verificatum/vcr\n  git clone https://github.com/verificatum/vmn\n\nTarkvara täpse versiooni hankimine ning tervikluse kontroll::\n\n  cd gmpmee\n  git checkout 4aafc31\n  rm -rf .git/\n  cd ..\n  ./intcheck/src/intcheck.py verify gmpmee ivxv-verificatum/doc/gmpmee.dirsha256sum\n\n  cd vmgj\n  git checkout 8d7d412\n  rm -rf .git/\n  cd ..\n  ./intcheck/src/intcheck.py verify vmgj ivxv-verificatum/doc/vmgj.dirsha256sum\n\n  cd vcr\n  git checkout af9fd82\n  rm -rf .git/\n  cd ..\n  ./intcheck/src/intcheck.py verify vcr ivxv-verificatum/doc/vcr.dirsha256sum\n\n  cd vmn\n  git checkout bb00543\n  rm -rf .git/\n  cd ..\n  ./intcheck/src/intcheck.py verify vmn ivxv-verificatum/doc/vmn.dirsha256sum\n\nVerificatumi adapteri ehitamine::\n\n  cd $HOME/ivxv-verificatum\n  make zipext\n\nJuhuarvugeneraatori initsialiseerimine Verificatumi jaoks::\n\n  cd $HOME\n  ./vcr/bin/vog -rndinit RandomDevice /dev/urandom\n\n\nAuditeerimine\n================================================================================\n\nSiit edasi eeldame, et lugeja on tuttav dokumendiga \"IVXV seadistuste\nkoostamise juhend\" järgmises ulatuses:\n\n* Ptk. 2, IVXV seadistused valimise korraldamise protsessis\n* Ptk. 3, IVXV rakendused\n* Ptk. 6, Auditirakendus\n* Ptk. 10, E-häälte miksimine\n\nOlgu samuti paigaldatud pakk `audit-examples.tar`, millel on järgmine\nstruktuur::\n\n   audit-conv\n   |-- auditor.yaml -- konfinäide\n   |-- inputs\n   |   |-- <RVT poolt tarnitavad sisendid>\n   |-- process\n   |   |-- <Töökataloog koos seadistustega>\n   |\n   audit-mix\n   |-- auditor.yaml -- konfinäide\n   |-- inputs\n   |   |-- <RVT poolt tarnitavad sisendid>\n   |-- process\n   |   |-- <Töökataloog koos seadistustega>\n   |\n   audit-mixver\n   |-- inputs\n   |   |-- <RVT poolt tarnitavad sisendid>\n   |\n   audit-pdec\n   |-- auditor.yaml -- konfinäide\n   |-- inputs\n   |   |-- <RVT poolt tarnitavad sisendid>\n   |-- process\n   |   |-- <Töökataloog koos seadistustega>\n   |\n   audit-vertally\n   |-- inputs\n   |   |-- <RVT poolt tarnitavad sisendid>\n   |\n   processor\n   |-- <Töötlemisrakenduse sisendid ja väljundid>\n\nTegutsemine on üldjuhul järgmine:\n\n* Tutvuge konfinäitega\n* Veenduge, et on olemas vajalik RVT sisend\n* Tehke kausta `process` konfinäitest lähtuv failistruktuur\n* Käivitage kaustas `process` rakendus ja tööriist (eelvalmendatud seadistused on seal\n  juba ees)\n\nTäpsemad juhised järgnevad.\n\nGenereeritud avalike võtmete kooskõlalisuse kontroll\n--------------------------------------------------------------------------------\n\nVõtmete genereerimise ajal tekib kaks võtit - tulemusfaili signeerimisvõti ja\nhäälte salastamise võti.\n\nTulemusfaili signeerimisvõti on kodeeritud X509 sertifikaadina failis\n`RK2051-sign.pem`. Häälte salastamise võti on antud kolmes kodeeringus:\n\n* X509 sertifikaadina failis `RK2051-enc.pem`\n* DER-kodeeritud avaliku võtmena failis `RK2051-pub.der`\n* PEM-kodeeritud avaliku võtmena failis `RK2051-pub.pem`\n\nOn võimalik kontrollida, et sertifikaat, mis sisaldab tulemusfaili\nsigneerimisvõtit, on korrektselt isesigneeritud. Seda saab teha järgnevalt::\n\n    openssl verify -CAfile RK2051-sign.pem -check_ss_sig RK2051-sign.pem\n\nKorrektse sertifikaadi korral on väljund::\n\n    RK2051-sign.pem: OK\n\nOn võimalik kontrollida, et sertifikaat, mis sisaldab häälte salastamise võtit,\non korrektselt signeeritud tulemusfaili signeerimisvõtmega. Seda saab\nteha järgnevalt::\n\n    openssl verify -CAfile RK2051-sign.pem -check_ss_sig RK2051-enc.pem\n\nKorrektselt allkirjastatud sertifikaadi korral on väljund::\n\n    RK2051-enc.pem: OK\n\n.. note:: Teadaoleva OpenSSL vea tõttu ei suuda OpenSSL versioonist `1.1.1b`\n   vanemad versioonid sertifikaadi usaldusahelat kontrollida. Eelneva kontrolli\n   õnnestumise jaoks on eelduseks vähemalt OpenSSL versioon `1.1.1b`.\n\nLisaks on võimalik kontrollida, et häälte salastamise võtme eri kodeeringud\nvastavad üksteisele. Me kontrollime, et X509 sertifikaadis olev võti vastab\nDER-kodeeritud võtmele ning lisaks, et PEM-kodeeritud võti vastab DER-kodeeritud\nvõtmele. Transitiivsuse tõttu on seega kõik kolm kodeeringut kooskõlalised.\n\nEsiteks tuleb eraldada häälte salastamise võti vastavast sertifikaadist. Kuna\nOpenSSL ei toeta kasutatavad ElGamali krüptoskeemi, siis tuleb avaliku võtme\neksportimiseks kasutada OpenSSL `asn1parse` tööriista.\n\nKõigepealt tuleb leida avaliku võtme nihe sertifikaadis::\n\n    openssl asn1parse -in RK2051-enc.pem\n\nAvalik võti on vastavas `SubjectPublicKeyInfo` väljal::\n\n    156:d=2  hl=4 l= 816 cons: SEQUENCE\n    160:d=3  hl=4 l= 415 cons: SEQUENCE\n    164:d=4  hl=2 l=   9 prim: OBJECT            :1.3.6.1.4.1.3029.2.1\n    175:d=4  hl=4 l= 400 cons: SEQUENCE\n    179:d=5  hl=4 l= 385 prim: INTEGER           :FFFFFFFFFFFFFFFFC90FDA\n        A22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08\n        798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B5\n        76625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24\n        117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163F\n        A8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C35\n        4E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783\n        A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D226\n        1898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB85\n        0458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94\n        E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B\n        18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5B\n        FCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF\n    568:d=5  hl=2 l=   1 prim: INTEGER           :02\n    571:d=5  hl=2 l=   6 prim: GENERALSTRING\n    579:d=3  hl=4 l= 393 prim: BIT STRING\n\nNäeme, et `SubjectPublicKeyInfo` välja  nihe on 156 baiti. Eraldame avaliku\nvõtme ja kontrollime vastavust väljastatud avaliku võtmega::\n\n    openssl asn1parse -in RK2051-enc.pem -strparse 156 -noout -out extracted.der\n    diff -s extracted.der RK2051-pub.der\n\nSamaväärsete võtmete korral on väljundiks::\n\n    Files extracted.der and RK2051-pub.der are identical\n\nTeiseks kontrollime DER-kodeeritud võtme vastavust PEM-kodeeritud võtmele.\nSelleks teisendame PEM-kodeeritud võtme DER-kodeeringusse ja võrdleme::\n\n    openssl asn1parse -in RK2051-pub.pem -noout -out converted.der\n    diff -s converted.der RK2051-pub.der\n\nSamaväärsete võtme korral on väljundiks::\n\n    Files converted.der and RK2051-pub.der are identical\n\nHääletamistulemuse allkirja verifitseerimine\n--------------------------------------------------------------------------------\n\nNii tavalise dekrüpteerimise kui tõestatava dekrüpteerimise käigus tekib kaks\nfaili:\n\n* Tulemusfail `RK2051.1.tally`\n* Signatuurifail `RK2051.1.tally.signature`\n\nKoos häälte salastamise võtmega genereeritakse tulemusfaili signeerimisvõti ja\nvastav sertifikaat (`RK2051-sign.pem`). Dekrüpteeritud tulemusele antakse selle\nvõtmega signatuur, mida tuleb kontrollida.\n\nEraldame signeerimisvõtme sertifikaadist avaliku võtme::\n\n  openssl x509 -in RK2051-sign.pem -noout -pubkey > sign.pub\n\nKasutame avalikku võtit tulemusfaili allkirja kontrollimiseks::\n\n  openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sigopt rsa_mgf1_md:sha256 -verify sign.pub -signature RK2051.1.tally.signature RK2051.1.tally\n\nNB! Tavalise dekrüpteerimise ja tõestatava dekrüpteerimise käigus tekkivad\ntulemusfailid peavad olema identsed. Kontrollimiseks UNIXi tööriist `diff`::\n\n  diff decout/RK2051.1.tally pdecout/RK2051.1.tally\n\nNäitefailid on pakis::\n\n  cd $HOME/audit-examples/audit-vertally\n\n\nIVXV <-> Verificatum teisenduste korrektsuse kontroll\n--------------------------------------------------------------------------------\n\nTeisenduste korrektsuse kontroll toimub tööriistaga `convert`. NB! Kaust\n`process` tuleb `auditor.yaml` põhjal sisenditest ettevalmistada::\n\n  cd $HOME/audit-examples/audit-conv/process\n  $HOME/ivxv/auditor/build/install/auditor/bin/auditor convert -c conf.bdoc -p auditor.yaml.bdoc\n\nMiksimistõendi kontroll tööriistaga `auditor`\n--------------------------------------------------------------------------------\n\nMiksimistõendi kontroll toimub tööriistaga `mixer`. NB! Kaust `process` tuleb\n`auditor.yaml` põhjal sisenditest ettevalmistada::\n\n  cd $HOME/audit-examples/audit-mix/process\n  $HOME/ivxv/auditor/build/install/auditor/bin/auditor mixer -c conf.bdoc -p auditor.yaml.bdoc\n\nLugemistõendi kontroll\n--------------------------------------------------------------------------------\n\nLugemistõendi kontroll toimub tööriistaga `decrypt`. NB! Kaust `process` tuleb\n`auditor.yaml` põhjal sisenditest ettevalmistada::\n\n  cd $HOME/audit-examples/audit-pdec/process\n  $HOME/ivxv/auditor/build/install/auditor/bin/auditor decrypt -c conf.bdoc -p auditor.yaml.bdoc\n\nMiksimistõendi kontroll Verificatumi originaaltööriistaga\n--------------------------------------------------------------------------------\n\nMiksimistõendi kontroll Verificatumi abil::\n\n  cd $HOME/audit-examples/audit-mixver\n  $HOME/ivxv-verificatum/release/mixer/bin/mix.py verify --proof-zipfile shuffle_proof.zip\n\nTöötlemise audit\n--------------------------------------------------------------------------------\n\nTäiendavalt on lisatud pakki kõik töötlemisrakenduse sisendid ja väljundid\nlihtsustamaks töötlemisprotsessi auditit. Täiendavad auditeerimistööriistad,\nnt. `integrity` on kirjeldatud dokumendis \"IVXV seadistuste koostamise juhend\".\n"
  },
  {
    "path": "Documentation/et/audiitor/history.rst",
    "content": "\n`.history`\n---------------\n\n.. literalinclude:: history.txt\n   :linenos:\n\n"
  },
  {
    "path": "Documentation/et/audiitor/history.txt",
    "content": "sudo apt update\nsudo apt upgrade\ntar xvf audit-examples.tar\nsudo apt-get install --no-install-recommends -y autoconf automake build-essential libgmp-dev libtool git openjdk-11-jdk-headless python unzip zip wget make\njava -version\njavac -version\nsudo apt install openjdk-11-jre-headless\nsudo update-alternatives --config java\njava -version\njavac -version\nwget https://github.com/vvk-ehk/intcheck/archive/master.zip\nunzip master.zip\nrm master.zip\nmv intcheck-master/ intcheck\nchmod +x intcheck/src/intcheck.py\n./intcheck/src/intcheck.py -h\n./intcheck/src/intcheck.py verify -h\nwget https://github.com/vvk-ehk/ivxv/archive/master.zip\nunzip master.zip\nrm master.zip\nmv ivxv-master/ ivxv\ncd ivxv/common/external/\nwget -O gradle-8.11.zip https://services.gradle.org/distributions/gradle-8.11-bin.zip\nunzip gradle-8.11.zip\nrm gradle-8.11.zip\ncd ..\ncd java/\nmake sync\nls ../external/\ncd ..\ncd ..\nmake clean-java\nmake java\nauditor/build/install/auditor/bin/auditor -h\nls auditor/build/distributions/*.zip\nls key/build/distributions/*.zip\nls processor/build/distributions/*.zip\nls processor/build/install/processor/bin/processor\nls key/build/install/key/bin/key\nls auditor/build/install/auditor/bin/auditor\ncd ..\nwget https://github.com/vvk-ehk/ivxv-mixnet-adapter/archive/master.zip\nunzip master.zip\nrm master.zip\nmv ivxv-mixnet-adapter-master ivxv-verificatum\ngit clone https://github.com/verificatum/verificatum-gmpmee gmpmee\ngit clone https://github.com/verificatum/vmgj\ngit clone https://github.com/verificatum/vcr\ngit clone https://github.com/verificatum/vmn\ncd gmpmee/\ngit checkout 4aafc31\nrm -rf .git/\ncd ..\n./intcheck/src/intcheck.py verify gmpmee ivxv-verificatum/doc/gmpmee.dirsha256sum\ncd vmgj/\ngit checkout 8d7d412\nrm -rf .git/\ncd ..\n./intcheck/src/intcheck.py verify vmgj ivxv-verificatum/doc/vmgj.dirsha256sum\ncd vcr/\ngit checkout af9fd82\nrm -rf .git/\ncd ..\n./intcheck/src/intcheck.py verify vcr ivxv-verificatum/doc/vcr.dirsha256sum\ncd vmn/\ngit checkout bb00543\nrm -rf .git\ncd ..\n./intcheck/src/intcheck.py verify vmn ivxv-verificatum/doc/vmn.dirsha256sum\ncd ivxv-verificatum/\nmake zipext\ncd ..\n./vcr/bin/vog -rndinit RandomDevice /dev/urandom\nopenssl verify -CAfile initout/RK2051-sign.pem -check_ss_sig initout/RK2051-sign.pem\nopenssl verify -CAfile initout/RK2051-sign.pem -check_ss_sig initout/RK2051-enc.pem\nopenssl asn1parse -in initout/RK2051-enc.pem\nopenssl asn1parse -in initout/RK2051-enc.pem -strparse 156 -noout -out extracted.der\ndiff -s extracted.der initout/RK2051-pub.der\nopenssl asn1parse -in initout/RK2051-pub.pem -noout -out converted.der\ndiff -s converted.der initout/RK2051-pub.der\ndiff decout/RK2051.1.tally pdecout/RK2051.1.tally\nopenssl x509 -in initout/RK2051-sign.pem -noout -pubkey > sign.pub\nopenssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sigopt rsa_mgf1_md:sha256 -verify sign.pub -signature decout/RK2051.1.tally.signature decout/RK2051.1.tally\ncd $HOME/audit-examples/audit-conv\ncd process\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor convert  -c conf.bdoc -p auditor.yaml.bdoc\ncat ../auditor.yaml\ncp ../inputs/bb-4.json* .\nmkdir initout\ncp ../inputs/RK2051-pub.pem initout/\ncp ../inputs/shuffled.json .\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor convert  -c conf.bdoc -p auditor.yaml.bdoc\nunzip -l ../inputs/shuffle_proof.zip\ncd ..\nunzip inputs/shuffle_proof.zip\ncd process/\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor convert  -c conf.bdoc -p auditor.yaml.bdoc\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor mixer -c conf.bdoc -p auditor.yaml.bdoc\n$HOME/ivxv/auditor/build/install/auditor/bin/auditor decrypt -c conf.bdoc -p auditor.yaml.bdoc\n$HOME/ivxv-verificatum/release/mixer/bin/mix.py verify --proof-zipfile shuffle_proof.zip\n$HOME/ivxv/processor/build/install/processor/bin/processor\n"
  },
  {
    "path": "Documentation/et/audiitor/index.rst",
    "content": "..  IVXV seadistuste koostamise juhend\n\nIVXV audiitori juhend\n==========================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 2\n   :numbered:\n\n   ylevaade\n   annotatsioon\n   audit\n   history\n"
  },
  {
    "path": "Documentation/et/audiitor/spelling_wordlist.txt",
    "content": "\nPEM\nRVT'le\n\nkonfinäitega\nkonfinäitest\n\nubuntu\n"
  },
  {
    "path": "Documentation/et/audiitor/ylevaade.rst",
    "content": "Miksimistõendi ülevaade\n=======================\n\nIlma juhuslikkust kasutamata oleks iga krüpteerimise algoritm deterministlik,\nst. valija valiku krüpteerimisel oleks tulemuseks alati sama väljund. See\nlubaks ründajal tuvastada esialgse valiku, kui ta krüpteeriks kõikvõimalikud\nvalikud ja võrdleks seda nimekirja nähtud krüpteeritud valikuga.  Seega,\nkrüpteerimine peab kasutama juhuslikkust.\n\nJuhuslikkuse kasutamine teeb krüptogrammid unikaalseks - isegi kui kaks korda\non krüpteeritud sama valik, siis krüptogrammid on erinevad. See tähendab, et\nkui ründaja on võimeline mingil ajahetkel seostama krüptogrammi ja selle\nkrüpteerinud isikut, siis on tal võimalik hiljem ainult krüptogrammi nähes\ntuletada seda andnud isik. Kuna sobivate parameetrite kasutamisel on ElGamali\navaliku võtme krüptosüsteem pikaajaliselt turvaline, siis see ei ole otseselt\nprobleemiks.\n\nProbleem tekib siis, kui on tarvis tõestada, et krüptogramm on korrektselt\ndekrüpteeritud. Kui kasutatav valikute agregeerimise algoritm töötab avateksti\nkujul sõnede peal (nagu see IVXVs on), siis iga krüptogrammi kohta tekib üks\ndekrüpteeritud avatekst. Korrektse dekrüpteerimise tõestus peab nii\nkrüptogrammi kui dekrüpteeritud avateksti siduma. Seega on võimalik ründajal\ntekitada seos isiku ja krüptogrammi, ning krüptogrammi ja vastava avateksti\nvahel, st. ta saab teada, mis valiku isik tegi.\n\nEt seda seost eemaldada, kasutatakse IVXVs krüptogrammide segamist (miksnet).\nMiksnet teeb korraga kahte operatsiooni -- järjestab sisendkrüptogrammid ümber\n(permuteerib) ja uuendab krüptogrammis olevad juhuslikkust (rerandomiseerib).\nSee tähendab, et krüptogrammid, mis lähevad miksneti sisse on väliselt täiesti\nsõltumatud krüptogrammidest, mis tulevad miksnetist välja. Kuna välise\nsõltumatuse tõttu võiks teoreetiliselt miksnet krüptogramme asendada, siis on\ntarvis lisada miksimistõend, mis tõestab krüptograafiliselt, et operatsioonid\non tehtud korrektselt ja ühtegi täiendavat operatsiooni pole tehtud.\nKontrollides miksimistõendit, on võimalik garanteerida, et miksnet on töötanud\nkorrektselt.\n\nVäikeste parameetritega kirjutatud näide\n----------------------------------------\n\nIVXV kasutab valikute krüpteerimiseks ElGamali avaliku võtme krüptosüsteemi.\nElGamali krüptosüsteemi korral on fikseeritud algebralise rühma parameetrid\nkoos generaatoriga :math:`g`. Salajane võti :math:`x` valitakse vahemikus\n:math:`[0, q-1]` ühtlase jaotusega, kus :math:`q` on rühma multiplikatiivse\nalamrühma järk. Salajasele võtmele avalik võti on defineeritud:\n\n.. math::\n   pk = (g, g^x) = (g, y).\n\nKrüpteerimaks sõne kujul valikut `V`, tuleb see kõigepealt kodeerida rühma\nelemendiks\n\n.. math::\n   m = encode(V)\n\nja seejärel arvutatakse krüptogramm kasutades ühekordset juhuarvu\n:math:`0<=r<q`\n\n.. math::\n   c = (c_1, c_2) = (m y^r, g^r).\n\nSellisel juhul piisab rerandomiseerimiseks teise ühekordse juhuarvuga\n:math:`t` arvutada\n\n.. math::\n   c' = c * (y^t, g^t) = (m y^{r + t}, g^{r + t}).\n\nPaneme veel tähele, et eelmises võrrandis :math:`(y^t, g^t)` on krüpteering\nelemendist :math:`1`.\n\nDekrüpteerimiseks arvutatakse\n\n.. math::\n   d = c_1 / c_2^x,\n\nja dekoreeritakse :math:`S = decode(d)`.\n\nJuhul kui krüptogramm on korrektselt konstrueeritud ja dekrüpteerimine on\nkorrektselt läbi viidud, siis :math:`d=m`, kuna\n\n.. math::\n   d = c_1 / c_2^x = (m y^r) / (g^{rx}) = (m y^r) / (y^r) = m.\n\nNäiteks, oletame et rühm on täisarvud mooduli :math:`p = 227` järgi. Sellisel\njuhul genereerib generaator :math:`g = 4` alamrühma järguga :math:`q = 113`.\nValime suvalise salajase võtme :math:`x = 100` ja sellele vastav avalik võti on\n:math:`pk = (g, g^x) = (4, 21)`.\n\nOletame, et on neli erinevat valikut ning nende kodeeringud alarühma on\njärgnevad:\n\n===== =========\nvalik kodeering\n===== =========\norav     16\njänes    64\nhunt     29\nkits    116\n===== =========\n\nJärgnevate valikute ja ühekordsete juhuarvude korral on krüptogrammid\njärgnevad:\n\n===== ========= ======= =============\nvalik kodeering juhuarv  krüptogramm\n===== ========= ======= =============\nkits     116       71    (62, 205)\nhunt     29        80    (161, 221)\nkits     116       64    (7, 147)\nkits     116       47    (139, 36)\norav     16        76    (26, 172)\nhunt     29        86    (30, 212)\nkits     116       88    (155, 175)\norav     16        85    (87, 212)\norav     16        32    (132, 104)\njänes    64        22    (113, 171)\n===== ========= ======= =============\n\n\nOlgu miksneti kasutatav permutatsioon :math:`\\pi` defineeritud järgnevalt:\n\n====== =======\nindeks väärtus\n====== =======\n   1      5\n   2      8\n   3      3\n   4      6\n   5      7\n   6      2\n   7      9\n   8     10\n   9      4\n  10      1\n====== =======\n\nOlgu miksneti rerandomiseerimiseks kasutatavad juhuarvud:\n\n====== ================\nindeks täiendav juhuarv\n====== ================\n   1         43\n   2        107\n   3          6\n   4         86\n   5         56\n   6         48\n   7         35\n   8        112\n   9         55\n  10        101\n====== ================\n\nSellisel juhul pärast ümberjärjestamist on krüptogrammid järgnevas järjekorras:\n\n==========   ==========  =============\n esialgne    uus indeks  permuteeritud\n==========   ==========  =============\n(62, 205)       5         (113, 171)\n(161, 221)      8         (30, 212)\n(7, 147)        3         (7, 147)\n(139, 36)       6         (132, 104)\n(26, 172)       7         (62, 205)\n(30, 212)       2         (139, 36)\n(155, 175)      9         (26, 172)\n(87, 212)      10         (161, 221)\n(132, 104)      4         (155, 175)\n(113, 171)      1         (87, 212)\n==========   ==========  =============\n\nPärast rerandomiseerimist on krüptogrammid järgnevad:\n\n==========  ================  ==================  =================\n esialgne   täiendav juhuarv  korrutatav väärtus  rerandomiseeritud\n==========  ================  ==================  =================\n(113, 171)         43              (10, 103)         (222, 134)\n(30, 212)         107              (28, 159)         (159, 112)\n(7, 147)            6              (73, 10)          (57, 108)\n(132, 104)         86              (100, 167)        (34, 116)\n(62, 205)          56              (207, 113)        (122, 11)\n(139, 36)          48              (78, 144)         (173, 190)\n(26, 172)          35              (188, 73)         (121, 71)\n(161, 221)        113              (173, 57)         (159, 112)\n(155, 175)         55              (172, 85)         (101, 120)\n(87, 212)         101              (103, 84)         (108, 102)\n==========  ================  ==================  =================\n\nKontrollime, kuidas permuteeritud ja rerandomiseeritud krüptogrammid\ndekrüpteeruvad:\n\n===========  ==============  ============\nkrüptogramm  dekrüpteeritud  dekodeeritud\n===========  ==============  ============\n(222, 134)        64            jänes\n(159, 112)        29            hunt\n(57, 108)         116           kits\n(34, 116)         16            orav\n(122, 11)         116           kits\n(173, 190)        116           kits\n(121, 71)         16            orav\n(159, 112)        29            hunt\n(101, 120)        116           kits\n(108, 102)        16            orav\n===========  ==============  ============\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_auditor/DEMO_auditor.yaml",
    "content": "# Mixneti teisenduse kontrollimine\nconvert:\n# IVXV segamise-eelse e-valimiskasti asukoht.\n  input_bb: DEMO_bb-4.json\n# IVXV segamise-järgse e-valimiskasti asukoht.\n  output_bb: DEMO_shuffled.json\n  pub: DEMO_pub.pem\n#  Verificatumi miksimistõendi protokollifaili asukoht.\n  protinfo: DEMO_proof/prot.xml\n# Verificatumi miksimistõendi asukoht.\n  proofdir: DEMO_proof/mixnet/\n\n# Miksimistõendi kontrollimine\n# Tööriist mixer kontrollib Verificatumi miksimistõendi korrektsust.\nmixer:\n  protinfo: DEMO_proof/prot.xml\n  proofdir: DEMO_proof/mixnet/\n# Kasuta mitmelõimelist implementatsiooni. Kasutatavate lõimede arv sõltub rakenduse argumentidest\n  threaded: true\n\n\n# Dekrüpteerimistõestuste verifitseerimine\n#Tööriist decrypt kontrollib dekrüpteerimistõendi korrektsust.\ndecrypt:\n  input: DEMO_decout_true/DEMO-proof\n  pub: DEMO_pub.pem\n  out: DEMO_auditout\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/DEMO.election.yaml",
    "content": "# Valimiste seadistus DEMO\n\nidentifier: DEMO\nquestions:\n  - question-1\n\nperiod:\n  servicestart: 2018-12-03T10:20:00+03:00\n  electionstart: 2018-12-03T10:30:00+03:00\n  electionstop: 2018-12-20T18:00:00+03:00\n  servicestop: 2018-12-20T18:15:00+03:00\n  verificationstop: 2018-12-20T18:20:00+03:00\n\nvoting:\n  ratelimitstart: 15\n  ratelimitminutes: 5\n\nverification:\n  count: 3\n  minutes: 30\n\n#ignorevoterlist Ringkonna identifikaator, mille valikud esitada kõigile valijatele.\n#Kui see väärtus ei ole tühi, siis kogumisteenus ei kasuta valijate\n#nimekirja ning esitab kõigile valijatele määratud ringkonna valikud ja\n#lubab kõigil, kellel õnnestub isikutuvastus ning hääle allkirja kontrollimine,\n#hääletada.\n# NB! Siin on vaja kasutada !!str märgendit, et parserid ei peaks seda väärtust\n# arvuks ning ei kaotaks esimest nulli ära.\n\n# Kasutusel ainult testimiseks\n# ignorevoterlist: !!str 0284.1\n\nvoterlist:\n# Tegu on suvalise failiga\n  key: !container DEMO_valijate_nimekiri.pem\nauth:\n  # Mobiil-ID autentimine on lubatud sest ticket on olemas\n  ticket:\n  tls:\n    roots:\n      - !container EE_Certification_Centre_Root_CA.pem.crt\n      - !container EE-GovCA2018.pem.crt\n    intermediates:\n      - !container ESTEID-SK_2015.pem.crt\n      - !container esteid2018.pem.crt\n    ocsp:\n      url: http://aia.sk.ee/esteid2015\n      # respondereid ei tohiks enam vaja minna\n      responders:\n      #  - !container ESTEID-SK_2015_AIA_OCSP_RESPONDER_201808.pem.cer\n      # Proovime ainult 1 korra, vea korral uuesti ei lähe, see on ka\n      # vaikeväärtus\n      retry: 0\n\nidentity: pnoee\n\nage:\n  method: estpic\n  timezone: Europe/Tallinn\n  # Mõnel valimisel on vanus 16!\n  limit: 18\n\nvote:\n  bdoc:\n    filecount: 4\n    bdocsize: 32768  # 32 KiB\n    filesize: 32768  # 32 KiB\n    roots:\n      - !container EE_Certification_Centre_Root_CA.pem.crt\n      - !container EE-GovCA2018.pem.crt\n    intermediates:\n      - !container ESTEID-SK_2015.pem.crt\n      - !container esteid2018.pem.crt\n    profile: BES\n\n# enne oli ria asemel EHS\n\nmid:\n  url: https://mid.sk.ee/mid-api/\n  relyingpartyuuid: 00000000-0000-0000-0000-000000000000\n  relyingpartyname: DEMO\n  language: EST\n  authmessage: Mobiil-ID isikutuvastus\n  signmessage: Mobiil-ID hääle allkirjastamine\n  messageformat: GSM-7\n  authchallengesize: 64\n  statustimeoutms: 5000\n  roots:\n    - !container EE_Certification_Centre_Root_CA.pem.crt\n  intermediates:\n    - !container ESTEID-SK_2015.pem.crt\n  ocsp:\n    url: http://aia.sk.ee/esteid2015\n    responders:\n      - !container SK_OCSP_RESPONDER_2011.pem.cer\n\nqualification:\n  - protocol: tspreg\n    conf:\n      url: http://tsa.sk.ee\n      signers:\n        - !container SK_TIMESTAMPING_AUTHORITY.pem.cer\n# manualis 1\n# See aeg oli 3600, mis lubaks, et TSA vastuse loomise ja\n# allkirjastamise ajavahe on 1h. Ei ole usutav. Jätame 10sek, mis\n# võiks olla enam kui piisav.\n      delaytime: 10\n# Lubame ühe korra peale päringu ebaõnnestumist veel proovida\n      retry: 1\n  - protocol: ocsp\n    conf:\n      url: http://aia.sk.ee/esteid2015\n      responders:\n      #  - !container ESTEID-SK_2015_AIA_OCSP_RESPONDER_201808.pem.cer\n# Lubame ühe korra peale päringu ebaõnnestumist veel proovida\n      retry: 1\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/DEMO_valijate_nimekiri.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvw9xOr556u2oqnn5x743\n0Zva6Ef7HnUQ0Gc+BU1XP9ByORiVGe9aBh/KIa0fAt6HM/e3BaZuSwD5PQ+NaPYa\nBc8NUVJtKrGoQcbaHIf6gNCavWiaKdzeanTZFsCOktSL4A8HjtFu+V00WnxsTWe2\nzxnCQ+B6u8fWVZiqIJzCUgeFzOmivATVfMAxpbIJe6wiNBtsYLVzMyztJfhUH8wn\niS9rz4U7B1EuKdBqNLrG1DymYjEnDpUg6YQ4RW6jOi4hmt9Y4vqMYsDuozrq8OLX\nzWaZWmh1lvQkkjY+ailmrL1Iberp6Pyad0CyRKTkVK3359QtkCUWgTmWNtvEEJYQ\nCwIDAQAB\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/EE-GovCA2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE+DCCBFmgAwIBAgIQMLOwlXoR0oFbj52nmRsnezAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDkw\nNTA5MTEwM1oXDTMzMDkwNTA5MTEwM1owWjELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFTAT\nBgNVBAMMDEVFLUdvdkNBMjAxODCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAMcb\n/dmAcVo/b2azEPS6CfW7fEA2KuHKC53D7ShVNvLz4QUjCdTXjds/4u99jUoYEQec\nluVVzMlgEJR1nkN2eOrLAZYxPjwG5HiI1iZEyW9QKVdeEgyvhzWWTNHGjV3HdZRv\n7L9o4533PtJAyqJq9OTs6mjsqwFXjH49bfZ6CGmzUJsHo4ICvDCCArgwEgYDVR0T\nAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAQYwNAYDVR0lAQH/BCowKAYIKwYB\nBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYBBQUHAwEwHQYDVR0OBBYEFH4p\nVuc0knhOd+FvLjMqmHHB/TSfMB8GA1UdIwQYMBaAFH4pVuc0knhOd+FvLjMqmHHB\n/TSfMIICAAYDVR0gBIIB9zCCAfMwCAYGBACPegECMAkGBwQAi+xAAQIwMgYLKwYB\nBAGDkSEBAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMA0G\nCysGAQQBg5EhAQECMA0GCysGAQQBg5F/AQEBMA0GCysGAQQBg5EhAQEFMA0GCysG\nAQQBg5EhAQEGMA0GCysGAQQBg5EhAQEHMA0GCysGAQQBg5EhAQEDMA0GCysGAQQB\ng5EhAQEEMA0GCysGAQQBg5EhAQEIMA0GCysGAQQBg5EhAQEJMA0GCysGAQQBg5Eh\nAQEKMA0GCysGAQQBg5EhAQELMA0GCysGAQQBg5EhAQEMMA0GCysGAQQBg5EhAQEN\nMA0GCysGAQQBg5EhAQEOMA0GCysGAQQBg5EhAQEPMA0GCysGAQQBg5EhAQEQMA0G\nCysGAQQBg5EhAQERMA0GCysGAQQBg5EhAQESMA0GCysGAQQBg5EhAQETMA0GCysG\nAQQBg5EhAQEUMA0GCysGAQQBg5F/AQECMA0GCysGAQQBg5F/AQEDMA0GCysGAQQB\ng5F/AQEEMA0GCysGAQQBg5F/AQEFMA0GCysGAQQBg5F/AQEGMDEGCisGAQQBg5Eh\nCgEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMBgGCCsGAQUF\nBwEDBAwwCjAIBgYEAI5GAQEwCgYIKoZIzj0EAwQDgYwAMIGIAkIBk698EqetY9Tt\n6HwO50CfzdIIjKmlfCI34xKdU7J+wz1tNVu2tHJwEhdsH0e92i969sRDp1RNPlVh\n4XFJzI3oQFQCQgGVxmcuVnsy7NUscDZ0erwovmbFOsNxELCANxNSWx5xMqzEIhV8\n46opxu10UFDIBBPzkbBenL4h+g/WU7lG78fIhA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/EE_Certification_Centre_Root_CA.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy\nMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl\nZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS\nb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy\neuuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO\nbntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw\nWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d\nMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE\n1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/\nzQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB\nBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF\nBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV\nv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG\nE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u\nuSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW\niAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v\nGVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/ESTEID-SK_2015.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGcDCCBVigAwIBAgIQRUgJC4ec7yFWcqzT3mwbWzANBgkqhkiG9w0BAQwFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE1MTIxNzEyMzg0M1oYDzIwMzAxMjE3\nMjM1OTU5WjBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy\naW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVT\nVEVJRC1TSyAyMDE1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oH6\n1NDxbdW9k8nLA1qGaL4B7vydod2Ewp/STBZB3wEtIJCLdkpEsS8pXfFiRqwDVsgG\nGbu+Q99trlb5LI7yi7rIkRov5NftBdSNPSU5rAhYPQhvZZQgOwRaHa5Ey+BaLJHm\nLqYQS9hQvQsCYyws+xVvNFUpK0pGD64iycqdMuBl/nWq3fLuZppwBh0VFltm4nhr\n/1S0R9TRJpqFUGbGr4OK/DwebQ5PjhdS40gCUNwmC7fPQ4vIH+x+TCk2aG+u3MoA\nz0IrpVWqiwzG/vxreuPPAkgXeFCeYf6fXLsGz4WivsZFbph2pMjELu6sltlBXfAG\n3fGv43t91VXicyzR/eT5dsB+zFsW1sHV+1ONPr+qzgDxCH2cmuqoZNfIIq+buob3\neA8ee+XpJKJQr+1qGrmhggjvAhc7m6cU4x/QfxwRYhIVNhJf+sKVThkQhbJ9XxuK\nk3c18wymwL1mpDD0PIGJqlssMeiuJ4IzagFbgESGNDUd4icm0hQT8CmQeUm1GbWe\nBYseqPhMQX97QFBLXJLVy2SCyoAz7Bq1qA43++EcibN+yBc1nQs2Zoq8ck9MK0bC\nxDMeUkQUz6VeQGp69ImOQrsw46qTz0mtdQrMSbnkXCuLan5dPm284J9HmaqiYi6j\n6KLcZ2NkUnDQFesBVlMEm+fHa2iR6lnAFYZ06UECAwEAAaOCAgowggIGMB8GA1Ud\nIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBSzq4i8mdVipIUq\nCM20HXI7g3JHUTAOBgNVHQ8BAf8EBAMCAQYwdwYDVR0gBHAwbjAIBgYEAI96AQIw\nCQYHBACL7EABAjAwBgkrBgEEAc4fAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93\nd3cuc2suZWUvQ1BTMAsGCSsGAQQBzh8BAjALBgkrBgEEAc4fAQMwCwYJKwYBBAHO\nHwEEMBIGA1UdEwEB/wQIMAYBAf8CAQAwQQYDVR0eBDowOKE2MASCAiIiMAqHCAAA\nAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCcGA1Ud\nJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEE\ncDBuMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5zay5lZS9DQTBKBggrBgEFBQcw\nAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRy\nZV9Sb290X0NBLmRlci5jcnQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5z\nay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEMBQAD\nggEBAHRWDGI3P00r2sOnlvLHKk9eE7X93eT+4e5TeaQsOpE5zQRUTtshxN8Bnx2T\noQ9rgi18q+MwXm2f0mrGakYYG0bix7ZgDQvCMD/kuRYmwLGdfsTXwh8KuL6uSHF+\nU/ZTss6qG7mxCHG9YvebkN5Yj/rYRvZ9/uJ9rieByxw4wo7b19p22PXkAkXP5y3+\nqK/Oet98lqwI97kJhiS2zxFYRk+dXbazmoVHnozYKmsZaSUvoYNNH19tpS7BLdsg\ni9KpbvQLb5ywIMq9ut3+b2Xvzq8yzmHMFtLIJ6Afu1jJpqD82BUAFcvi5vhnP8M7\nb974R18WCOpgNQvXDI+2/8ZINeU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/SK_OCSP_RESPONDER_2011.pem.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEvDCCA6SgAwIBAgIQcpyVmdruRVxNgzI3N/NZQTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTExMDMxODEwMjE0M1oXDTI0MDMxODEw\nMjE0M1owgZ0xCzAJBgNVBAYTAkVFMQ4wDAYDVQQIEwVIYXJqdTEQMA4GA1UEBxMH\nVGFsbGlubjEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsG\nA1UECxMET0NTUDEfMB0GA1UEAxMWU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAihvGyhMVrgReHluKln1za6gvCE/mlSREmWjJFpL9llvuEUZoPFIypYA8\ng5u1VfgkeW5gDq25jAOq4FyXeDGIa+pJn2h0o2Wc2aeppVG/emfGm/jA8jjeyMrw\nH8fAJrqVQ7c9X2xSwJEch/P2d8CfMZt5YF6gqLtPvG1b+n6otBZA5wjIFfJ/inJB\nMUvqHSz3+PLfxO2/T3Wyk/c8M9HIMqTelqyiMGRgWehiU1OsL9armv3dQrHs1wm6\nvHaxfpfWB9YAFpeo9aYqhPCxVt/zo2NQB6vxyZS0hsOrXL7SxRToOJaqsnvlbf0e\nrPPFtRHUvbojYYgl+fzlz0Jt6QJoNwIDAQABo4IBHTCCARkwEwYDVR0lBAwwCgYI\nKwYBBQUHAwkwHQYDVR0OBBYEFKWhSGFt537NmJ50nCm7vYrecgxZMIGCBgNVHSAE\nezB5MHcGCisGAQQBzh8EAQIwaTA+BggrBgEFBQcCAjAyHjAAUwBLACAAdABpAG0A\nZQAgAHMAdABhAG0AcABpAG4AZwAgAHAAbwBsAGkAYwB5AC4wJwYIKwYBBQUHAgEW\nG2h0dHBzOi8vd3d3LnNrLmVlL2FqYXRlbXBlbDAfBgNVHSMEGDAWgBQS8lo+6lYc\nv80GrPHxJcmpS9QUmTA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vd3d3LnNrLmVl\nL3JlcG9zaXRvcnkvY3Jscy9lZWNjcmNhLmNybDANBgkqhkiG9w0BAQUFAAOCAQEA\nw2sKwvTHtYGtD8Jw9mNUuj/mWiBSBEBeY2LhW8V6tjBPAPp3s6iWOh0FbVR2LUyr\nqRwgT3fyWiGsiDm/6cIqM+IblLp/8ztfRQjquhW6XCD9SK02OQ9ZSdBwcmoAApZL\nGXQC34wdgmV/hLTTNxONnDACBKz9U+Dy9a4ZT4tpNkbH8jq/BMne8FzbvRt1bjpX\nBP7gjLX+zdx8/hp0Wq4tD+f9NVX0+vm9ahEKuzx4QzPnSB7hhWM9OnLZT7noRQa+\nKWk5c+e5VoR5R2t7MjVl8Cd+2llxiSxqMSbU5/23BzAKgN+NQdrBZAzpZ7lfaAuL\nFaICP+bAm6uW2JUrM6abOw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/SK_TIMESTAMPING_AUTHORITY.pem.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEDTCCAvWgAwIBAgIQJK/s6xJo0AJUF/eG7W8BWTANBgkqhkiG9w0BAQsFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE0MDkxNjA4NDAzOFoXDTE5MDkxNjA4\nNDAzOFowYzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2Vlcmlt\naXNrZXNrdXMxDDAKBgNVBAsMA1RTQTEiMCAGA1UEAwwZU0sgVElNRVNUQU1QSU5H\nIEFVVEhPUklUWTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJPa/dQK\nemSKCNSwlMUp9YKQY6zQOfs9vgUnbzTRHCRBRdsabZYknxTI4DqQ5+JPqw8MTkDv\nb6nfDZGd15t4oY4tHXXoCfRrbMjJ9+DV+M7bd+vrBI8vi7DBCM59/VAjxBAuZ9P7\nTsg8o8BrVqqB9c0ezlSCtFg8X0x2ET3ZBtZ49UARh/XP07I7eRk/DtSLYauxJDPz\nXVEZmSJCIybclox93u8F5/o8GySbD5GYMhffOJgXmul/Vz7eR0d5SxCMvJIRrP7W\nfiJYaUjLYqL2wjFQe/nUltcGCn2KtqGCyH7vl+Xzefea6Xjc8ebTgan2FJ0UH0mH\nv98lWADKuTI2fXcCAwEAAaOBqjCBpzAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/\nBAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFLGwvffmoGkWbCDlUftc9DBic1cnMB8G\nA1UdIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMD0GA1UdHwQ2MDQwMqAwoC6G\nLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9yeS9jcmxzL2VlY2NyY2EuY3JsMA0G\nCSqGSIb3DQEBCwUAA4IBAQCopcU932wVPD6eed+sDBht4zt+kMPPFXv1pIX0Rgbi\nzaKvHWU4oHpRH8zcgo/gpotRLlLhZbHtu94pLFN6enpiyHNwevkmUyvrBWylONR1\nYhwb4dLS8pBGGFR6eRdhGzoKAUF4B4dIoXOj4p26q1yYULF5ZkZHxhQFNi5uxak9\ntgCFlGtzXumjL5jBmtWeDTGE4YSa34pzDXjz8VAjPJ9sVuOmK2E0gyWxUTLXF9Ye\nvrWzRLzVFqw+qewBV2I4of/6miZOOT2wlA/meL7zr3hnfo7KSJQmMNUjZ6lh6RBI\nVvYI0t+A/fpTKiZfviz/Xn2e4PC6i57wmH5EgOOav0UK\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_election/esteid2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFVzCCBLigAwIBAgIQdUf6rBR0S4tbo2bU/mZV7TAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDky\nMDA5MjIyOFoXDTMzMDkwNTA5MTEwM1owWDELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzAR\nBgNVBAMMCkVTVEVJRDIwMTgwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAHHOBlv\n7UrRPYP1yHhOb7RA/YBDbtgynSVMqYdxnFrKHUXh6tFkghvHuA1k2DSom1hE5kqh\nB5VspDembwWDJBOQWQGOI/0t3EtccLYjeM7F9xOPdzUbZaIbpNRHpQgVBpFX0xpL\nTgW27MpIMhU8DHBWFpeAaNX3eUpD4gC5cvhsK0RFEqOCAx0wggMZMB8GA1UdIwQY\nMBaAFH4pVuc0knhOd+FvLjMqmHHB/TSfMB0GA1UdDgQWBBTZrHDbX36+lPig5L5H\notA0rZoqEjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADCCAc0G\nA1UdIASCAcQwggHAMAgGBgQAj3oBAjAJBgcEAIvsQAECMDIGCysGAQQBg5EhAQEB\nMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzANBgsrBgEEAYOR\nIQEBAjANBgsrBgEEAYORfwEBATANBgsrBgEEAYORIQEBBTANBgsrBgEEAYORIQEB\nBjANBgsrBgEEAYORIQEBBzANBgsrBgEEAYORIQEBAzANBgsrBgEEAYORIQEBBDAN\nBgsrBgEEAYORIQEBCDANBgsrBgEEAYORIQEBCTANBgsrBgEEAYORIQEBCjANBgsr\nBgEEAYORIQEBCzANBgsrBgEEAYORIQEBDDANBgsrBgEEAYORIQEBDTANBgsrBgEE\nAYORIQEBDjANBgsrBgEEAYORIQEBDzANBgsrBgEEAYORIQEBEDANBgsrBgEEAYOR\nIQEBETANBgsrBgEEAYORIQEBEjANBgsrBgEEAYORIQEBEzANBgsrBgEEAYORIQEB\nFDANBgsrBgEEAYORfwEBAjANBgsrBgEEAYORfwEBAzANBgsrBgEEAYORfwEBBDAN\nBgsrBgEEAYORfwEBBTANBgsrBgEEAYORfwEBBjAqBgNVHSUBAf8EIDAeBggrBgEF\nBQcDCQYIKwYBBQUHAwIGCCsGAQUFBwMEMGoGCCsGAQUFBwEBBF4wXDApBggrBgEF\nBQcwAYYdaHR0cDovL2FpYS5zay5lZS9lZS1nb3ZjYTIwMTgwLwYIKwYBBQUHMAKG\nI2h0dHA6Ly9jLnNrLmVlL0VFLUdvdkNBMjAxOC5kZXIuY3J0MBgGCCsGAQUFBwED\nBAwwCjAIBgYEAI5GAQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2Muc2suZWUv\nRUUtR292Q0EyMDE4LmNybDAKBggqhkjOPQQDBAOBjAAwgYgCQgDeuUY4HczUbFKS\n002HZ88gclgYdztHqglENyTMtXE6dMBRnCbgUmhBCAA0mJSHbyFJ8W9ikLiSyurm\nkJM0hDE9KgJCASOqA405Ia5nKjTJPNsHQlMi7KZsIcTHOoBccx+54N8ZX1MgBozJ\nmT59rZY/2/OeE163BAwD0UdUQAnMPP6+W3Vd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/EE-GovCA2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE+DCCBFmgAwIBAgIQMLOwlXoR0oFbj52nmRsnezAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDkw\nNTA5MTEwM1oXDTMzMDkwNTA5MTEwM1owWjELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFTAT\nBgNVBAMMDEVFLUdvdkNBMjAxODCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAMcb\n/dmAcVo/b2azEPS6CfW7fEA2KuHKC53D7ShVNvLz4QUjCdTXjds/4u99jUoYEQec\nluVVzMlgEJR1nkN2eOrLAZYxPjwG5HiI1iZEyW9QKVdeEgyvhzWWTNHGjV3HdZRv\n7L9o4533PtJAyqJq9OTs6mjsqwFXjH49bfZ6CGmzUJsHo4ICvDCCArgwEgYDVR0T\nAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAQYwNAYDVR0lAQH/BCowKAYIKwYB\nBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYBBQUHAwEwHQYDVR0OBBYEFH4p\nVuc0knhOd+FvLjMqmHHB/TSfMB8GA1UdIwQYMBaAFH4pVuc0knhOd+FvLjMqmHHB\n/TSfMIICAAYDVR0gBIIB9zCCAfMwCAYGBACPegECMAkGBwQAi+xAAQIwMgYLKwYB\nBAGDkSEBAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMA0G\nCysGAQQBg5EhAQECMA0GCysGAQQBg5F/AQEBMA0GCysGAQQBg5EhAQEFMA0GCysG\nAQQBg5EhAQEGMA0GCysGAQQBg5EhAQEHMA0GCysGAQQBg5EhAQEDMA0GCysGAQQB\ng5EhAQEEMA0GCysGAQQBg5EhAQEIMA0GCysGAQQBg5EhAQEJMA0GCysGAQQBg5Eh\nAQEKMA0GCysGAQQBg5EhAQELMA0GCysGAQQBg5EhAQEMMA0GCysGAQQBg5EhAQEN\nMA0GCysGAQQBg5EhAQEOMA0GCysGAQQBg5EhAQEPMA0GCysGAQQBg5EhAQEQMA0G\nCysGAQQBg5EhAQERMA0GCysGAQQBg5EhAQESMA0GCysGAQQBg5EhAQETMA0GCysG\nAQQBg5EhAQEUMA0GCysGAQQBg5F/AQECMA0GCysGAQQBg5F/AQEDMA0GCysGAQQB\ng5F/AQEEMA0GCysGAQQBg5F/AQEFMA0GCysGAQQBg5F/AQEGMDEGCisGAQQBg5Eh\nCgEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMBgGCCsGAQUF\nBwEDBAwwCjAIBgYEAI5GAQEwCgYIKoZIzj0EAwQDgYwAMIGIAkIBk698EqetY9Tt\n6HwO50CfzdIIjKmlfCI34xKdU7J+wz1tNVu2tHJwEhdsH0e92i969sRDp1RNPlVh\n4XFJzI3oQFQCQgGVxmcuVnsy7NUscDZ0erwovmbFOsNxELCANxNSWx5xMqzEIhV8\n46opxu10UFDIBBPzkbBenL4h+g/WU7lG78fIhA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/ESTEID-SK_2015.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGcDCCBVigAwIBAgIQRUgJC4ec7yFWcqzT3mwbWzANBgkqhkiG9w0BAQwFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE1MTIxNzEyMzg0M1oYDzIwMzAxMjE3\nMjM1OTU5WjBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy\naW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVT\nVEVJRC1TSyAyMDE1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oH6\n1NDxbdW9k8nLA1qGaL4B7vydod2Ewp/STBZB3wEtIJCLdkpEsS8pXfFiRqwDVsgG\nGbu+Q99trlb5LI7yi7rIkRov5NftBdSNPSU5rAhYPQhvZZQgOwRaHa5Ey+BaLJHm\nLqYQS9hQvQsCYyws+xVvNFUpK0pGD64iycqdMuBl/nWq3fLuZppwBh0VFltm4nhr\n/1S0R9TRJpqFUGbGr4OK/DwebQ5PjhdS40gCUNwmC7fPQ4vIH+x+TCk2aG+u3MoA\nz0IrpVWqiwzG/vxreuPPAkgXeFCeYf6fXLsGz4WivsZFbph2pMjELu6sltlBXfAG\n3fGv43t91VXicyzR/eT5dsB+zFsW1sHV+1ONPr+qzgDxCH2cmuqoZNfIIq+buob3\neA8ee+XpJKJQr+1qGrmhggjvAhc7m6cU4x/QfxwRYhIVNhJf+sKVThkQhbJ9XxuK\nk3c18wymwL1mpDD0PIGJqlssMeiuJ4IzagFbgESGNDUd4icm0hQT8CmQeUm1GbWe\nBYseqPhMQX97QFBLXJLVy2SCyoAz7Bq1qA43++EcibN+yBc1nQs2Zoq8ck9MK0bC\nxDMeUkQUz6VeQGp69ImOQrsw46qTz0mtdQrMSbnkXCuLan5dPm284J9HmaqiYi6j\n6KLcZ2NkUnDQFesBVlMEm+fHa2iR6lnAFYZ06UECAwEAAaOCAgowggIGMB8GA1Ud\nIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBSzq4i8mdVipIUq\nCM20HXI7g3JHUTAOBgNVHQ8BAf8EBAMCAQYwdwYDVR0gBHAwbjAIBgYEAI96AQIw\nCQYHBACL7EABAjAwBgkrBgEEAc4fAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93\nd3cuc2suZWUvQ1BTMAsGCSsGAQQBzh8BAjALBgkrBgEEAc4fAQMwCwYJKwYBBAHO\nHwEEMBIGA1UdEwEB/wQIMAYBAf8CAQAwQQYDVR0eBDowOKE2MASCAiIiMAqHCAAA\nAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCcGA1Ud\nJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEE\ncDBuMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5zay5lZS9DQTBKBggrBgEFBQcw\nAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRy\nZV9Sb290X0NBLmRlci5jcnQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5z\nay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEMBQAD\nggEBAHRWDGI3P00r2sOnlvLHKk9eE7X93eT+4e5TeaQsOpE5zQRUTtshxN8Bnx2T\noQ9rgi18q+MwXm2f0mrGakYYG0bix7ZgDQvCMD/kuRYmwLGdfsTXwh8KuL6uSHF+\nU/ZTss6qG7mxCHG9YvebkN5Yj/rYRvZ9/uJ9rieByxw4wo7b19p22PXkAkXP5y3+\nqK/Oet98lqwI97kJhiS2zxFYRk+dXbazmoVHnozYKmsZaSUvoYNNH19tpS7BLdsg\ni9KpbvQLb5ywIMq9ut3+b2Xvzq8yzmHMFtLIJ6Afu1jJpqD82BUAFcvi5vhnP8M7\nb974R18WCOpgNQvXDI+2/8ZINeU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/ESTEID2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFVzCCBLigAwIBAgIQdUf6rBR0S4tbo2bU/mZV7TAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDky\nMDA5MjIyOFoXDTMzMDkwNTA5MTEwM1owWDELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzAR\nBgNVBAMMCkVTVEVJRDIwMTgwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAHHOBlv\n7UrRPYP1yHhOb7RA/YBDbtgynSVMqYdxnFrKHUXh6tFkghvHuA1k2DSom1hE5kqh\nB5VspDembwWDJBOQWQGOI/0t3EtccLYjeM7F9xOPdzUbZaIbpNRHpQgVBpFX0xpL\nTgW27MpIMhU8DHBWFpeAaNX3eUpD4gC5cvhsK0RFEqOCAx0wggMZMB8GA1UdIwQY\nMBaAFH4pVuc0knhOd+FvLjMqmHHB/TSfMB0GA1UdDgQWBBTZrHDbX36+lPig5L5H\notA0rZoqEjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADCCAc0G\nA1UdIASCAcQwggHAMAgGBgQAj3oBAjAJBgcEAIvsQAECMDIGCysGAQQBg5EhAQEB\nMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzANBgsrBgEEAYOR\nIQEBAjANBgsrBgEEAYORfwEBATANBgsrBgEEAYORIQEBBTANBgsrBgEEAYORIQEB\nBjANBgsrBgEEAYORIQEBBzANBgsrBgEEAYORIQEBAzANBgsrBgEEAYORIQEBBDAN\nBgsrBgEEAYORIQEBCDANBgsrBgEEAYORIQEBCTANBgsrBgEEAYORIQEBCjANBgsr\nBgEEAYORIQEBCzANBgsrBgEEAYORIQEBDDANBgsrBgEEAYORIQEBDTANBgsrBgEE\nAYORIQEBDjANBgsrBgEEAYORIQEBDzANBgsrBgEEAYORIQEBEDANBgsrBgEEAYOR\nIQEBETANBgsrBgEEAYORIQEBEjANBgsrBgEEAYORIQEBEzANBgsrBgEEAYORIQEB\nFDANBgsrBgEEAYORfwEBAjANBgsrBgEEAYORfwEBAzANBgsrBgEEAYORfwEBBDAN\nBgsrBgEEAYORfwEBBTANBgsrBgEEAYORfwEBBjAqBgNVHSUBAf8EIDAeBggrBgEF\nBQcDCQYIKwYBBQUHAwIGCCsGAQUFBwMEMGoGCCsGAQUFBwEBBF4wXDApBggrBgEF\nBQcwAYYdaHR0cDovL2FpYS5zay5lZS9lZS1nb3ZjYTIwMTgwLwYIKwYBBQUHMAKG\nI2h0dHA6Ly9jLnNrLmVlL0VFLUdvdkNBMjAxOC5kZXIuY3J0MBgGCCsGAQUFBwED\nBAwwCjAIBgYEAI5GAQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2Muc2suZWUv\nRUUtR292Q0EyMDE4LmNybDAKBggqhkjOPQQDBAOBjAAwgYgCQgDeuUY4HczUbFKS\n002HZ88gclgYdztHqglENyTMtXE6dMBRnCbgUmhBCAA0mJSHbyFJ8W9ikLiSyurm\nkJM0hDE9KgJCASOqA405Ia5nKjTJPNsHQlMi7KZsIcTHOoBccx+54N8ZX1MgBozJ\nmT59rZY/2/OeE163BAwD0UdUQAnMPP6+W3Vd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/SK_OCSP_RESPONDER_2011.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEvDCCA6SgAwIBAgIQcpyVmdruRVxNgzI3N/NZQTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTExMDMxODEwMjE0M1oXDTI0MDMxODEw\nMjE0M1owgZ0xCzAJBgNVBAYTAkVFMQ4wDAYDVQQIEwVIYXJqdTEQMA4GA1UEBxMH\nVGFsbGlubjEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsG\nA1UECxMET0NTUDEfMB0GA1UEAxMWU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAihvGyhMVrgReHluKln1za6gvCE/mlSREmWjJFpL9llvuEUZoPFIypYA8\ng5u1VfgkeW5gDq25jAOq4FyXeDGIa+pJn2h0o2Wc2aeppVG/emfGm/jA8jjeyMrw\nH8fAJrqVQ7c9X2xSwJEch/P2d8CfMZt5YF6gqLtPvG1b+n6otBZA5wjIFfJ/inJB\nMUvqHSz3+PLfxO2/T3Wyk/c8M9HIMqTelqyiMGRgWehiU1OsL9armv3dQrHs1wm6\nvHaxfpfWB9YAFpeo9aYqhPCxVt/zo2NQB6vxyZS0hsOrXL7SxRToOJaqsnvlbf0e\nrPPFtRHUvbojYYgl+fzlz0Jt6QJoNwIDAQABo4IBHTCCARkwEwYDVR0lBAwwCgYI\nKwYBBQUHAwkwHQYDVR0OBBYEFKWhSGFt537NmJ50nCm7vYrecgxZMIGCBgNVHSAE\nezB5MHcGCisGAQQBzh8EAQIwaTA+BggrBgEFBQcCAjAyHjAAUwBLACAAdABpAG0A\nZQAgAHMAdABhAG0AcABpAG4AZwAgAHAAbwBsAGkAYwB5AC4wJwYIKwYBBQUHAgEW\nG2h0dHBzOi8vd3d3LnNrLmVlL2FqYXRlbXBlbDAfBgNVHSMEGDAWgBQS8lo+6lYc\nv80GrPHxJcmpS9QUmTA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vd3d3LnNrLmVl\nL3JlcG9zaXRvcnkvY3Jscy9lZWNjcmNhLmNybDANBgkqhkiG9w0BAQUFAAOCAQEA\nw2sKwvTHtYGtD8Jw9mNUuj/mWiBSBEBeY2LhW8V6tjBPAPp3s6iWOh0FbVR2LUyr\nqRwgT3fyWiGsiDm/6cIqM+IblLp/8ztfRQjquhW6XCD9SK02OQ9ZSdBwcmoAApZL\nGXQC34wdgmV/hLTTNxONnDACBKz9U+Dy9a4ZT4tpNkbH8jq/BMne8FzbvRt1bjpX\nBP7gjLX+zdx8/hp0Wq4tD+f9NVX0+vm9ahEKuzx4QzPnSB7hhWM9OnLZT7noRQa+\nKWk5c+e5VoR5R2t7MjVl8Cd+2llxiSxqMSbU5/23BzAKgN+NQdrBZAzpZ7lfaAuL\nFaICP+bAm6uW2JUrM6abOw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/SK_TIMESTAMPING_AUTHORITY_2019.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIEFjCCAv6gAwIBAgIQftdGTujTD01cG10EjrASbDANBgkqhkiG9w0BAQsFADB1\r\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\r\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\r\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE5MDEwMTIxMDAwMFoXDTI0MDEwMTIx\r\nMDAwMFowejEnMCUGA1UEAwweU0sgVElNRVNUQU1QSU5HIEFVVEhPUklUWSAyMDE5\r\nMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEMMAoGA1UECwwDVFNBMRswGQYDVQQK\r\nDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNVBAYTAkVFMIIBIjANBgkqhkiG9w0B\r\nAQEFAAOCAQ8AMIIBCgKCAQEAqfl9D1lK8LveyTb3tlOJZfc0J+W3uG7czOZuQKig\r\nwkxq2JXrqndigVtSZWzNnoO+somaNgXqw0lpu+8P0NdUw+y1a4znJntvf7HK4WNr\r\nHO94DcrcxIJRdRkxRH/e3/0SE1FpX8B96IgyjeQj9Y3tgHYAfSMo0Sj2ANYc/NpF\r\n59NWf0nBzJ5QvXD80N1ybiVVu4NALKFxTOEHPRmdFdptNNEaipwHyLwaSDpXCtnX\r\nxBz6zV/jIsMYCxNl0cXlx+mkD97scmLhGB27rmI8qEPM6Tue0iGp1Tb6hIFdEQF2\r\ngZ8VPOsyJl3cpQd5GRkT63bOtHUm54TYCfw/P7NhO7pFLwIDAQABo4GcMIGZMA4G\r\nA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQU\r\nnUvt/uXDNtWuqmMvTSBvDyrTJxYwHwYDVR0jBBgwFoAUEvJaPupWHL/NBqzx8SXJ\r\nqUvUFJkwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUFBzABhhNodHRwOi8vYWlhLnNr\r\nLmVlL0NBMA0GCSqGSIb3DQEBCwUAA4IBAQApC2BNnwqlgm3KsBMbp0lWw2uGVzUX\r\niu4Cfsol6290Jzn6UftA3HjOG33vg5Dl3SV9Z97AfqgbE4A9Czms8veHwtNRLIaA\r\nHuRVm6C/GWa4+nuzNFoAK1pjjBGoPWvfYhud/bAlbYY1qF6nHA50/tFT0GGixrnm\r\nI9YsZ0tuXM9pQaoO0YnoCvw8cvMIt68WqIEST+OoCZipgdQRZ5IEJTmvE+LLBDuE\r\n87orGfxxswsy+jnOfHX8MLBOhWglrJ7RrXLNdKXOlbvhcU3vXl89gKtWWfr+OdVG\r\ndJc+/A3wdtTFhNdy7Ce0VIiZvmEx/UtUmfF41g/y+3lXOb3h6ipk037x\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/SK_TIMESTAMPING_AUTHORITY_2020.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIEFjCCAv6gAwIBAgIQYjZ9dFrZQ6tdpFC5Xj/6bjANBgkqhkiG9w0BAQsFADB1\r\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\r\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\r\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE5MTIzMTIyMDAwMFoXDTI0MTIzMTIy\r\nMDAwMFowejEnMCUGA1UEAwweU0sgVElNRVNUQU1QSU5HIEFVVEhPUklUWSAyMDIw\r\nMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEMMAoGA1UECwwDVFNBMRswGQYDVQQK\r\nDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNVBAYTAkVFMIIBIjANBgkqhkiG9w0B\r\nAQEFAAOCAQ8AMIIBCgKCAQEAxdOHoO/xEAXVYd6m64QhKHoNZhT5L+wEkO59DH4K\r\n8lW1r//kEJusfY60zDY8o9s96HYACcQOR9Yltg3T3neqRZJ5GEPt5uFzCWzuSdyx\r\nIWMacxu/sSYaln4bqbCd97ML4qVdvwPGLNGRu8Utuy0JyhyuoBICHUcgyw1O2ATl\r\nc+95zdhGvKq15gazGXTpVUYgLpInkChp1ojZCv/WFdKN3dNGB5tqn3xsdfUfGDWx\r\ne4gLFFLeXjxo0pT2Y+5hJF1+r+PllZRnu1LKXEcrHxeyyZ+KL6wSLUyvfxZ++5hd\r\n0wR1pCnVgZ+hfYaGZ+YGRJXtiIA8DFqeKZ6qAhA8a6v99QIDAQABo4GcMIGZMA4G\r\nA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQU\r\nqD4KKP6RKqGdYfXNlT35pc1GkjEwHwYDVR0jBBgwFoAUEvJaPupWHL/NBqzx8SXJ\r\nqUvUFJkwLwYIKwYBBQUHAQEEIzAhMB8GCCsGAQUFBzABhhNodHRwOi8vYWlhLnNr\r\nLmVlL0NBMA0GCSqGSIb3DQEBCwUAA4IBAQAJ06Qp/kiOhcNbEsDUVGfLuVycKjEb\r\nrGGMWnAj18S08aWx7ijXtDD9mY5CxtRUl9IbjB/eyl/Rt8RDVURtIioiNckkxC/b\r\nOHxiCj2WNCvRxo8GT/qn4M1vV/Sy8vwx/ZlYsZrlRnuo7/dqPsQyxIgRGbUp12bV\r\nKO4KQb4DNOcA6KDwcPd2zv4nBT/4XW7qD07spW9LPVKEvsOU1MV1tznjD0lC5ZL6\r\n7FdB8kKEJCbbNfqVLVBOYjBopct5qzTLLPB5LTmV8I281XzTEqeFxbFy+wo7VOT6\r\nK36OYSd+9CnPn2M/l6VfrSCi3OvaWcq+lggGR1kQzDsS4lN1JoyZqd39\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/SK_TIMESTAMPING_AUTHORITY_2021.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIEWjCCA0KgAwIBAgIQCrITQgwdM4hfdZRtSgVwszANBgkqhkiG9w0BAQsFADB1\r\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\r\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\r\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTIwMTIzMTIyMDAwMVoXDTI1MTIzMTIy\r\nMDAwMVowejEnMCUGA1UEAwweU0sgVElNRVNUQU1QSU5HIEFVVEhPUklUWSAyMDIx\r\nMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEMMAoGA1UECwwDVFNBMRswGQYDVQQK\r\nDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNVBAYTAkVFMIIBIjANBgkqhkiG9w0B\r\nAQEFAAOCAQ8AMIIBCgKCAQEAwmZuFcXZ3UGPjIEX0mldGSTiUxMcfG8Fh0f4VlAg\r\n6aN/buuRVaEpwrS7UfTD/HF7JojcJidFf7wTBd+B52oqYhya7rT/d11exeDtwIZp\r\nymksqC+F8bWoleJ3HkSByyGfuGcGGSnowaCjcZqTT2YCT40PdfJfPIaUqobjNC9i\r\ndFP/FOzgHWu8hUiOAixZ+X22r0CVgTnNW0/xiaRPq/PgpgDAsxlYDABonFKiCEfH\r\nyK5T1rjV585lfwWBcPo5jnI9tIyT3fSB06QZ0i4rmFcPli/0XvyHrGNNpJNPJ9lb\r\n9d0VhcPwktoDr2nBFgBzpjRufwVPjQCBuDVidkuMEjLOTwIDAQABo4HgMIHdMA4G\r\nA1UdDwEB/wQEAwIGwDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQU\r\nEDIBfAAH5r+iCV+irQKE3Tk2/h8wHwYDVR0jBBgwFoAUEvJaPupWHL/NBqzx8SXJ\r\nqUvUFJkwcwYIKwYBBQUHAQEEZzBlMB8GCCsGAQUFBzABhhNodHRwOi8vYWlhLnNr\r\nLmVlL0NBMEIGCCsGAQUFBzAChjZodHRwOi8vYy5zay5lZS9FRV9DZXJ0aWZpY2F0\r\naW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwDQYJKoZIhvcNAQELBQADggEBACnG\r\nDxtyt0EmeLyGhwW01/rg6q9KStXW65qwNnTdW7QpY+3Q8Oc64zJAAOAkfcaSa1Bq\r\nlJmO7QMkSnpeEa5AH//48bdfZ0RYRGnEpoqq6L5Qi6iCHBduRDxrea0bR7s/UaIB\r\n9PMR6jNU7Y4hSlAZCTxZvsuOwgbYzU1kJipc5mh4nSDU3qyL7vPefgQAgLMOhMI7\r\n8ZFSHGxGJf+BNOaHzD4IYBRd81Facnr5+hfD2gNFPcuf9DPFVinKUG9c4XuKj6V3\r\n0fGBBZoSfju53Jk6/aGfKwKWLpN13Sh4RMb+KL2S/mDIMKRVCst901nPorgq58Bj\r\nd/zm6CptMqABrIpGRl8=\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_ivxv/ivxv.properties",
    "content": "ca = EE-GovCA2018.pem.crt, ESTEID2018.pem.crt, ESTEID-SK_2015.pem.crt\nocsp = SK_OCSP_RESPONDER_2011.pem.crt\ntsa = SK_TIMESTAMPING_AUTHORITY_2019.pem.crt, SK_TIMESTAMPING_AUTHORITY_2020.pem.crt, SK_TIMESTAMPING_AUTHORITY_2021.pem.crt\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_key.decrypt_f/DEMO_key.decrypt_f.yaml",
    "content": "util:\n  listreaders: true\n\ntestkey:\n    identifier: DEMO\n    out: initout\n    threshold: 5\n    parties: 9\n# Kaartidele automaatne terminalide määramine. Vaikimise väärtus on tõene.\n    fastmode: true\n\ndecrypt:\n# Valimise unikaalne identifikaator.\n  identifier: DEMO\n# Küsimuste arv anonüümistatud e-valimiskastis. Vaikimisi väärtus on 1.\n  questioncount: 1\n  protocol:\n    recover:\n      threshold: 5\n      parties: 9\n# decrypt.anonballotbox Töötlemisrakenduse või miksimisrakenduse poolt loodud e-valimiskast anonüümistatud häältega.\n  anonballotbox: DEMO_bb-4.json\n  anonballotbox_checksum: DEMO_bb-4.json.sha256sum.bdoc\n  candidates: DEMO_valikud.bdoc\n  districts: DEMO_jaoskonnad.bdoc\n# Krüptogrammide korrektsuse kontrollimine enne dekrüpteerimist. Juhul kui krüptogrammide sisend ei tule usaldatud allikast, siis tuleb kontrollida krüptogrammide korrektsust.\n# Usaldatud allikad on töötlemisrakendus ning miksija. Vaikimisi väärus on väär.\n  check_decodable: false\n# decrypt.provable Valikuline korrektse dekrüpteerimise tõestuse väljastamine. Vaikimisi väärtus on tõene.\n  provable: false\n  out: DEMO_decout_false\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_key.decrypt_t/DEMO_key.decrypt_t.yaml",
    "content": "util:\n  listreaders: true\n\ntestkey:\n    identifier: DEMO\n    out: initout\n    threshold: 5\n    parties: 9\n# Kaartidele automaatne terminalide määramine. Vaikimise väärtus on tõene.\n    fastmode: true\n\ndecrypt:\n# Valimise unikaalne identifikaator.\n  identifier: DEMO\n# Küsimuste arv anonüümistatud e-valimiskastis. Vaikimisi väärtus on 1.\n  questioncount: 1\n  protocol:\n    recover:\n      threshold: 5\n      parties: 9\n# decrypt.anonballotbox Töötlemisrakenduse või miksimisrakenduse poolt loodud e-valimiskast anonüümistatud häältega.\n  anonballotbox: DEMO_shuffled.json\n  anonballotbox_checksum: DEMO_shuffled.json.sha256sum.bdoc\n  candidates: DEMO_valikud.bdoc\n  districts: DEMO_jaoskonnad.bdoc\n# Krüptogrammide korrektsuse kontrollimine enne dekrüpteerimist. Juhul kui krüptogrammide sisend ei tule usaldatud allikast, siis tuleb kontrollida krüptogrammide korrektsust.\n# Usaldatud allikad on töötlemisrakendus ning miksija. Vaikimisi väärus on väär.\n  check_decodable: false\n# decrypt.provable Valikuline korrektse dekrüpteerimise tõestuse väljastamine. Vaikimisi väärtus on tõene.\n  provable: true\n  out: DEMO_decout_true\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_key.groupgen/key.groupgen.yaml",
    "content": "groupgen:\n paramtype: mod\n length: 3072\n init_template: key.init.template.yaml\n random_source:\n   - random_source_type: system\n#  - random_source_type: file\n#    random_source_path: randomness_file\n#  - random_source_type: DPRNG\n#    random_source_path: seed_file\n#  - random_source_type: stream\n#    random_source_path: /dev/urandom\n   - random_source_type: user\n     random_source_path: server.exe\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_key.init/key.init.yaml",
    "content": "init:\n  identifier: DEMO\n  paramtype:\n    mod:\n      p: 5809605995369958062791915965639201402176612226902900533702900882779736177890990861472094774477339581147373410185646378328043729800750470098210924487866935059164371588168047540943981644516632755067501626434556398193186628990071248660819361205119793693985433297036118232914410171876807536457391277857011849897410207519105333355801121109356897459426271845471397952675959440793493071628394122780510124618488232602464649876850458861245784240929258426287699705312584509625419513463605155428017165714465363094021609290561084025893662561222573202082865797821865270991145082200656978177192827024538990239969175546190770645685893438011714430426409338676314743571154537142031573004276428701433036381801705308659830751190352946025482059931306571004727362479688415574702596946457770284148435989129632853918392117997472632693078113129886487399347796982772784615865232621289656944284216824611318709764535152507354116344703769998514148343807\n      g: 2\n  out: initout\n  skiptest: true\n  fastmode: true\n  signaturekeylen: 2048\n  signcn: SIGNATURE-DEMO\n  signsn: 1\n  enccn: ENCRYPTION-DEMO\n  encsn: 2\n  required_randomness: 128\n  random_source:\n#  - random_source_type: file\n#    random_source_path: randomness_file\n  - random_source_type: system\n#  - random_source_type: DPRNG\n#    random_source_path: seed_file\n#  - random_source_type: stream\n#    random_source_path: /dev/urandom\n  - random_source_type: user\n    random_source_path: server.exe\n\n  genprotocol:\n    desmedt:\n      threshold: 5\n      parties: 9\n\nutil:\n  listreaders: true\n\ntestkey:\n  out: initout\n  threshold: 5\n  parties: 9\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_processor_TS/DEMO_processor_TS.yaml",
    "content": "# check         - I etapp: e-valimiskasti tervikluse kontroll ja e-hääletanute nimekirja väljastamine\ncheck:\n# Hääled - tulevad Kogujalt ehk RIA-st ************ KONTROLLI FAILI NIME **************\n  ballotbox: DEMO_votes.zip\n  ballotbox_checksum: DEMO_votes.zip.sha256sum.bdoc\n\n# ajatemplid, tulevad SK-st ************ KONTROLLI FAILI NIME **************\n# Registreerimisteenusest pärit registreerimisandmed.\n# Kui määramata, siis ei kontrollita e-valimiskastis sisalduvate häälte vastavust registreerimisandmetega.\n  registrationlist: exporter_output_20181221.zip\n  registrationlist_checksum: exporter_output_20181221.zip.sha256sum.bdoc\n\n# tskey annab RIA (registreerimispäringute tegemise sert)\n  tskey: DEMO_tskey.pem\n\n# Kahtlane, kas on vajalik kui nimekirju pole.\n  districts: DEMO_jaoskonnad.bdoc\n#  DEMO puhul valijate nimekirju pole\n  vlkey: DEMO_valijate_nimekiri.pem\n  voterlists:\n    -  path: DEMO_valijate_nimekiri.txt\n       signature: DEMO_valijate_nimekiri.txt.signature\n# check.districts_mapping Valijate nimekirjas oleva ringkonna ja jaoskonna\n# teisendusfail (valikuline). csv\n  election_start: 2018-12-17T09:00:00+02:00\n  out: DEMO_out-1\n\n#  squash        - II etapp: korduvate e-häälte tühistamine\nsquash:\n  ballotbox: DEMO_out-1/DEMO-bb-1.json\n  ballotbox_checksum: DEMO_out-1/DEMO-bb-1.json.sha256sum.bdoc\n  districts: DEMO_jaoskonnad.bdoc\n  # Tühistus- ja ennistusnimekirjade loend. Võib olla tühi.\n  # revocationlists:\n  out: DEMO_out-2\n\n#  revoke        - III etapp: topelthääletanute häälte tühistamine (tühistus- ja ennistusnimekirjade rakendamine) ja lugemisele minevate e-hääletanute nimekirja väljastamine\n\nrevoke:\n  ballotbox: DEMO_out-2/DEMO-bb-2.json\n  ballotbox_checksum: DEMO_out-2/DEMO-bb-2.json.sha256sum.bdoc\n  districts: DEMO_jaoskonnad.bdoc\n  revocationlists:\n#   - DEMO_tuhistusnimekiri.bdoc\n  out: DEMO_out-3\n\n#  anonymize     - IV etapp: e-häälte anonüümistamine\n\nanonymize:\n  ballotbox: DEMO_out-3/DEMO-bb-3.json\n  ballotbox_checksum: DEMO_out-3/DEMO-bb-3.json.sha256sum.bdoc\n  enckey: DEMO_pub.pem\n  out: DEMO_out-4\n\n#  verify        - Abifunktsioon: allkirjastatud konteineri verifitseerimine\nverify:\n  file: processor.yaml.bdoc\n\n#  export        - Valimissedelite allkirjastatud konteinerite eksportimine\nexport:\n  ballotbox: DEMO_votes.zip\n  ballotbox_checksum: DEMO_votes.zip.sha256sum.bdoc\n  out: DEMO_fullvotes\n\n#  stats         - E-valimiskasti statistika genereerimine\nstats:\n  ballotbox: DEMO_votes.zip\n  #election_day Valimispäev. Kõikide e-hääletanute vanused arvutatakse statistika tarbeks selle kuupäeva suhtes.\n  election_day: 2018-12-24\n  #Statistikaperioodi algusaeg (valikuline). Sellest varasema hääletusajaga hääli statistikasse ei kaasata.\n  period_start: 2018-12-17T09:00:00+02:00\n  #Statistikaperioodi lõppaeg (valikuline). Sellest hilisema hääletusajaga hääli statistikasse ei kaasata.\n#  period_end: 2018-12-20T18:00:00+02:00\n# POLE VAJALIKUD kui valijate nimekirja pole\n  districts: DEMO_jaoskonnad.bdoc\n  vlkey: DEMO_valijate_nimekiri.pem\n  voterlists:\n    -  path: DEMO_valijate_nimekiri.txt\n       signature: DEMO_valijate_nimekiri.txt.signature\n  out: DEMO_stats\n\n#  statsdiff     - E-valimiskasti statistika võrdlemine\nstatsdiff:\n#Statistika võrdluse alusfail JSON-vormingus.\n  compare: DEMO_stats/DEMO-stats.json\n#Võrreldav statistika fail JSON-vormingus.\n  to: misaganes.json\n#Tööriista väljundfail. Sellesse faili salvestatakse statistikate vahe JSON-vormingus.\n  diff: stats_versus_misiganes.json\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_technical/ca.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDcDCCAligAwIBAgIJANLdelAg8Qa9MA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAkVFMQwwCgYDVQQKDANSSUExGjAYBgNVBAsMEUlWWFYgQ2VydGlmaWNhdGVz\nMRMwEQYDVQQDDApTZXJ2aWNlIENBMCAXDTE4MTIwNjEyMDYwMVoYDzIxMTgxMTEy\nMTIwNjAxWjBMMQswCQYDVQQGEwJFRTEMMAoGA1UECgwDUklBMRowGAYDVQQLDBFJ\nVlhWIENlcnRpZmljYXRlczETMBEGA1UEAwwKU2VydmljZSBDQTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAO0TzgHFg3Ka1xWV2mFCOf7EwNLuyZ2HW0yC\n+85twpJh0FZxyg+aqyIX6u2zUVUfco+d6EeMkyaWMmwL/vD2/aTBa2pMKlKzWXnM\nIVpoowtvNvFG3E9i+FDLDh8VerRRrBpb61IBmDDG/GwQosPLCe3Nd/Lfj3AYYmCO\nmsw3wV+bzvrEdAvhBwOvnZLLmiQsaF2fX2txPGi2tcmleVLlAPtCfLVNts91mAyf\nqk0Z/5RsNudPOuMxFffp1Qyw3V1LkK80Eh5gRdTiptd5KeIbf+TuhKrLlSBRu394\n+Wp6kLioo6hjccOFpaYizEy1zZG4JYiCTyC2ViGZ2hhbZ63pDFcCAwEAAaNTMFEw\nHQYDVR0OBBYEFETA+enwqYHW9VgmSlY5ZY7BmiE/MB8GA1UdIwQYMBaAFETA+enw\nqYHW9VgmSlY5ZY7BmiE/MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD\nggEBAJAqwsvprdjIfoamXx6HjPioVko5UIWqy0X6Q8Cy92IGv7BjEmEvTM0I8BGq\nKCQAZO+t3BddmngIZeZD/Re4QUZtHnzoJni+42NhN7Sm9Xc7hkJkqhs0qyJqLuqG\nSIxrQMV+fb3faJFqvqGKEsdRJL6OTd3ovKQNnZjBy8MkyoPLzeQwZHCAg0ZjU2on\nst23dPvhKDNZoZpZ5XX47mcGe+L0JHb9zTARwhuy4x/cwwzAMy0lVhxyFKVQ0bjK\nsNyXXvDAkerkeBxdXzXYJzEBxse7+DFKt6y3s28RBJK26TFXru6M7SkioCpen2lM\nOLq/43IJmvzMjtUNQbt6a0dXysc=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_technical/technical.yaml",
    "content": "debug: true\n\nsnidomain: inttest.ivxv.ee\n\nfilter:\n  tls:\n    handshaketimeout: 20\n    ciphersuites:\n      - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n      # Vanemate nutiseadmete tugi.\n      - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\n      - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n  codec:\n    rwtimeout: 10\n    requestsize: 16384  # 16 KiB\n    logrequests: true\n\nnetwork:\n  - id: zone1\n    services:\n      proxy:\n        - id:          proxy@proxy1.ivxv.ee\n          address:     ivxv-rp1:443\n      mid:\n        - id:          mid@mid1.ivxv.ee\n          address:     ivxv1:4441\n      choices:\n        - id:          choices@choices1.ivxv.ee\n          address:     ivxv1:4442\n      voting:\n        - id:          voting@voting1.ivxv.ee\n          address:     ivxv1:4443\n      verification:\n        - id:          verification@verification1.ivxv.ee\n          address:     ivxv1:4444\n      storage:\n        - id:          storage@storage1.ivxv.ee\n          address:     ivxv1:2379\n          peeraddress: ivxv1:2380\n  - id: zone2\n    services:\n      proxy:\n        - id:          proxy@proxy2.ivxv.ee\n          address:     ivxv-rp2:443\n      mid:\n        - id:          mid@mid2.ivxv.ee\n          address:     ivxv2:4441\n      choices:\n        - id:          choices@choices2.ivxv.ee\n          address:     ivxv2:4442\n      voting:\n        - id:          voting@voting2.ivxv.ee\n          address:     ivxv2:4443\n      verification:\n        - id:          verification@verification2.ivxv.ee\n          address:     ivxv2:4444\n      storage:\n        - id:          storage@storage2.ivxv.ee\n          address:     ivxv2:2379\n          peeraddress: ivxv2:2380\n  - id: zone3\n    services:\n      proxy:\n        - id:          proxy@proxy3.ivxv.ee\n          address:     ivxv-rp3:443\n      mid:\n        - id:          mid@mid3.ivxv.ee\n          address:     ivxv3:4441\n      choices:\n        - id:          choices@choices3.ivxv.ee\n          address:     ivxv3:4442\n      voting:\n        - id:          voting@voting3.ivxv.ee\n          address:     ivxv3:4443\n      verification:\n        - id:          verification@verification3.ivxv.ee\n          address:     ivxv3:4444\n      storage:\n        - id:          storage@storage3.ivxv.ee\n          address:     ivxv3:2379\n          peeraddress: ivxv3:2380\n  - id: zone4\n    services:\n      log:\n        - id:          log@log1.ivxv.ee\n          address:     log1:20514\n      backup:\n        - id:          backup@backup.ivxv.ee\n          address:     backup\n\nlogging:\n  - address: logmon\n    port: 20514\n\n\nstorage:\n  protocol: etcd\n  conf:\n    ca: !container ca.pem\n    conntimeout: 5\n    optimeout: 10\n    size: 16777216 # bytes\n    bootstrap:\n      - storage@storage1.ivxv.ee\n      - storage@storage2.ivxv.ee\n      - storage@storage3.ivxv.ee\nbackup: [\"03:00\", \"09:00\", \"15:00\", \"21:00\"]\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/DEMO.trust.yaml",
    "content": "# Usaldusjuure seadistus DEMO\ncontainer:\n  bdoc:\n    filecount: 50\n    bdocsize: 104857600  # 100 MiB\n    filesize: 104857600  # 100 MiB\n    roots:\n      - !container EE_Certification_Centre_Root_CA.pem.crt\n      - !container EE-GovCA2018.pem.crt\n    intermediates:\n      - !container ESTEID-SK_2015.pem.crt\n      - !container esteid2018.pem.crt\n    profile: TS\n    tsp:\n      signers:\n        - !container SK_TIMESTAMPING_AUTHORITY.pem.cer\n      delaytime: 10\n    tsdelaytime: 60\nauthorizations:\n    - M,T,3\n    - L,I,3\n    - M,L,3\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/EE-GovCA2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE+DCCBFmgAwIBAgIQMLOwlXoR0oFbj52nmRsnezAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDkw\nNTA5MTEwM1oXDTMzMDkwNTA5MTEwM1owWjELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFTAT\nBgNVBAMMDEVFLUdvdkNBMjAxODCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAMcb\n/dmAcVo/b2azEPS6CfW7fEA2KuHKC53D7ShVNvLz4QUjCdTXjds/4u99jUoYEQec\nluVVzMlgEJR1nkN2eOrLAZYxPjwG5HiI1iZEyW9QKVdeEgyvhzWWTNHGjV3HdZRv\n7L9o4533PtJAyqJq9OTs6mjsqwFXjH49bfZ6CGmzUJsHo4ICvDCCArgwEgYDVR0T\nAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAQYwNAYDVR0lAQH/BCowKAYIKwYB\nBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYBBQUHAwEwHQYDVR0OBBYEFH4p\nVuc0knhOd+FvLjMqmHHB/TSfMB8GA1UdIwQYMBaAFH4pVuc0knhOd+FvLjMqmHHB\n/TSfMIICAAYDVR0gBIIB9zCCAfMwCAYGBACPegECMAkGBwQAi+xAAQIwMgYLKwYB\nBAGDkSEBAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMA0G\nCysGAQQBg5EhAQECMA0GCysGAQQBg5F/AQEBMA0GCysGAQQBg5EhAQEFMA0GCysG\nAQQBg5EhAQEGMA0GCysGAQQBg5EhAQEHMA0GCysGAQQBg5EhAQEDMA0GCysGAQQB\ng5EhAQEEMA0GCysGAQQBg5EhAQEIMA0GCysGAQQBg5EhAQEJMA0GCysGAQQBg5Eh\nAQEKMA0GCysGAQQBg5EhAQELMA0GCysGAQQBg5EhAQEMMA0GCysGAQQBg5EhAQEN\nMA0GCysGAQQBg5EhAQEOMA0GCysGAQQBg5EhAQEPMA0GCysGAQQBg5EhAQEQMA0G\nCysGAQQBg5EhAQERMA0GCysGAQQBg5EhAQESMA0GCysGAQQBg5EhAQETMA0GCysG\nAQQBg5EhAQEUMA0GCysGAQQBg5F/AQECMA0GCysGAQQBg5F/AQEDMA0GCysGAQQB\ng5F/AQEEMA0GCysGAQQBg5F/AQEFMA0GCysGAQQBg5F/AQEGMDEGCisGAQQBg5Eh\nCgEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMBgGCCsGAQUF\nBwEDBAwwCjAIBgYEAI5GAQEwCgYIKoZIzj0EAwQDgYwAMIGIAkIBk698EqetY9Tt\n6HwO50CfzdIIjKmlfCI34xKdU7J+wz1tNVu2tHJwEhdsH0e92i969sRDp1RNPlVh\n4XFJzI3oQFQCQgGVxmcuVnsy7NUscDZ0erwovmbFOsNxELCANxNSWx5xMqzEIhV8\n46opxu10UFDIBBPzkbBenL4h+g/WU7lG78fIhA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/EE_Certification_Centre_Root_CA.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy\nMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl\nZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS\nb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy\neuuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO\nbntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw\nWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d\nMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE\n1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/\nzQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB\nBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF\nBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV\nv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG\nE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u\nuSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW\niAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v\nGVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/ESTEID-SK_2015.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGcDCCBVigAwIBAgIQRUgJC4ec7yFWcqzT3mwbWzANBgkqhkiG9w0BAQwFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE1MTIxNzEyMzg0M1oYDzIwMzAxMjE3\nMjM1OTU5WjBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy\naW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVT\nVEVJRC1TSyAyMDE1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oH6\n1NDxbdW9k8nLA1qGaL4B7vydod2Ewp/STBZB3wEtIJCLdkpEsS8pXfFiRqwDVsgG\nGbu+Q99trlb5LI7yi7rIkRov5NftBdSNPSU5rAhYPQhvZZQgOwRaHa5Ey+BaLJHm\nLqYQS9hQvQsCYyws+xVvNFUpK0pGD64iycqdMuBl/nWq3fLuZppwBh0VFltm4nhr\n/1S0R9TRJpqFUGbGr4OK/DwebQ5PjhdS40gCUNwmC7fPQ4vIH+x+TCk2aG+u3MoA\nz0IrpVWqiwzG/vxreuPPAkgXeFCeYf6fXLsGz4WivsZFbph2pMjELu6sltlBXfAG\n3fGv43t91VXicyzR/eT5dsB+zFsW1sHV+1ONPr+qzgDxCH2cmuqoZNfIIq+buob3\neA8ee+XpJKJQr+1qGrmhggjvAhc7m6cU4x/QfxwRYhIVNhJf+sKVThkQhbJ9XxuK\nk3c18wymwL1mpDD0PIGJqlssMeiuJ4IzagFbgESGNDUd4icm0hQT8CmQeUm1GbWe\nBYseqPhMQX97QFBLXJLVy2SCyoAz7Bq1qA43++EcibN+yBc1nQs2Zoq8ck9MK0bC\nxDMeUkQUz6VeQGp69ImOQrsw46qTz0mtdQrMSbnkXCuLan5dPm284J9HmaqiYi6j\n6KLcZ2NkUnDQFesBVlMEm+fHa2iR6lnAFYZ06UECAwEAAaOCAgowggIGMB8GA1Ud\nIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBSzq4i8mdVipIUq\nCM20HXI7g3JHUTAOBgNVHQ8BAf8EBAMCAQYwdwYDVR0gBHAwbjAIBgYEAI96AQIw\nCQYHBACL7EABAjAwBgkrBgEEAc4fAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93\nd3cuc2suZWUvQ1BTMAsGCSsGAQQBzh8BAjALBgkrBgEEAc4fAQMwCwYJKwYBBAHO\nHwEEMBIGA1UdEwEB/wQIMAYBAf8CAQAwQQYDVR0eBDowOKE2MASCAiIiMAqHCAAA\nAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCcGA1Ud\nJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEE\ncDBuMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5zay5lZS9DQTBKBggrBgEFBQcw\nAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRy\nZV9Sb290X0NBLmRlci5jcnQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5z\nay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEMBQAD\nggEBAHRWDGI3P00r2sOnlvLHKk9eE7X93eT+4e5TeaQsOpE5zQRUTtshxN8Bnx2T\noQ9rgi18q+MwXm2f0mrGakYYG0bix7ZgDQvCMD/kuRYmwLGdfsTXwh8KuL6uSHF+\nU/ZTss6qG7mxCHG9YvebkN5Yj/rYRvZ9/uJ9rieByxw4wo7b19p22PXkAkXP5y3+\nqK/Oet98lqwI97kJhiS2zxFYRk+dXbazmoVHnozYKmsZaSUvoYNNH19tpS7BLdsg\ni9KpbvQLb5ywIMq9ut3+b2Xvzq8yzmHMFtLIJ6Afu1jJpqD82BUAFcvi5vhnP8M7\nb974R18WCOpgNQvXDI+2/8ZINeU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/SK_OCSP_RESPONDER_2011.pem.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEvDCCA6SgAwIBAgIQcpyVmdruRVxNgzI3N/NZQTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTExMDMxODEwMjE0M1oXDTI0MDMxODEw\nMjE0M1owgZ0xCzAJBgNVBAYTAkVFMQ4wDAYDVQQIEwVIYXJqdTEQMA4GA1UEBxMH\nVGFsbGlubjEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsG\nA1UECxMET0NTUDEfMB0GA1UEAxMWU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAihvGyhMVrgReHluKln1za6gvCE/mlSREmWjJFpL9llvuEUZoPFIypYA8\ng5u1VfgkeW5gDq25jAOq4FyXeDGIa+pJn2h0o2Wc2aeppVG/emfGm/jA8jjeyMrw\nH8fAJrqVQ7c9X2xSwJEch/P2d8CfMZt5YF6gqLtPvG1b+n6otBZA5wjIFfJ/inJB\nMUvqHSz3+PLfxO2/T3Wyk/c8M9HIMqTelqyiMGRgWehiU1OsL9armv3dQrHs1wm6\nvHaxfpfWB9YAFpeo9aYqhPCxVt/zo2NQB6vxyZS0hsOrXL7SxRToOJaqsnvlbf0e\nrPPFtRHUvbojYYgl+fzlz0Jt6QJoNwIDAQABo4IBHTCCARkwEwYDVR0lBAwwCgYI\nKwYBBQUHAwkwHQYDVR0OBBYEFKWhSGFt537NmJ50nCm7vYrecgxZMIGCBgNVHSAE\nezB5MHcGCisGAQQBzh8EAQIwaTA+BggrBgEFBQcCAjAyHjAAUwBLACAAdABpAG0A\nZQAgAHMAdABhAG0AcABpAG4AZwAgAHAAbwBsAGkAYwB5AC4wJwYIKwYBBQUHAgEW\nG2h0dHBzOi8vd3d3LnNrLmVlL2FqYXRlbXBlbDAfBgNVHSMEGDAWgBQS8lo+6lYc\nv80GrPHxJcmpS9QUmTA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vd3d3LnNrLmVl\nL3JlcG9zaXRvcnkvY3Jscy9lZWNjcmNhLmNybDANBgkqhkiG9w0BAQUFAAOCAQEA\nw2sKwvTHtYGtD8Jw9mNUuj/mWiBSBEBeY2LhW8V6tjBPAPp3s6iWOh0FbVR2LUyr\nqRwgT3fyWiGsiDm/6cIqM+IblLp/8ztfRQjquhW6XCD9SK02OQ9ZSdBwcmoAApZL\nGXQC34wdgmV/hLTTNxONnDACBKz9U+Dy9a4ZT4tpNkbH8jq/BMne8FzbvRt1bjpX\nBP7gjLX+zdx8/hp0Wq4tD+f9NVX0+vm9ahEKuzx4QzPnSB7hhWM9OnLZT7noRQa+\nKWk5c+e5VoR5R2t7MjVl8Cd+2llxiSxqMSbU5/23BzAKgN+NQdrBZAzpZ7lfaAuL\nFaICP+bAm6uW2JUrM6abOw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/SK_TIMESTAMPING_AUTHORITY.pem.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEDTCCAvWgAwIBAgIQJK/s6xJo0AJUF/eG7W8BWTANBgkqhkiG9w0BAQsFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE0MDkxNjA4NDAzOFoXDTE5MDkxNjA4\nNDAzOFowYzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2Vlcmlt\naXNrZXNrdXMxDDAKBgNVBAsMA1RTQTEiMCAGA1UEAwwZU0sgVElNRVNUQU1QSU5H\nIEFVVEhPUklUWTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJPa/dQK\nemSKCNSwlMUp9YKQY6zQOfs9vgUnbzTRHCRBRdsabZYknxTI4DqQ5+JPqw8MTkDv\nb6nfDZGd15t4oY4tHXXoCfRrbMjJ9+DV+M7bd+vrBI8vi7DBCM59/VAjxBAuZ9P7\nTsg8o8BrVqqB9c0ezlSCtFg8X0x2ET3ZBtZ49UARh/XP07I7eRk/DtSLYauxJDPz\nXVEZmSJCIybclox93u8F5/o8GySbD5GYMhffOJgXmul/Vz7eR0d5SxCMvJIRrP7W\nfiJYaUjLYqL2wjFQe/nUltcGCn2KtqGCyH7vl+Xzefea6Xjc8ebTgan2FJ0UH0mH\nv98lWADKuTI2fXcCAwEAAaOBqjCBpzAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/\nBAwwCgYIKwYBBQUHAwgwHQYDVR0OBBYEFLGwvffmoGkWbCDlUftc9DBic1cnMB8G\nA1UdIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMD0GA1UdHwQ2MDQwMqAwoC6G\nLGh0dHA6Ly93d3cuc2suZWUvcmVwb3NpdG9yeS9jcmxzL2VlY2NyY2EuY3JsMA0G\nCSqGSIb3DQEBCwUAA4IBAQCopcU932wVPD6eed+sDBht4zt+kMPPFXv1pIX0Rgbi\nzaKvHWU4oHpRH8zcgo/gpotRLlLhZbHtu94pLFN6enpiyHNwevkmUyvrBWylONR1\nYhwb4dLS8pBGGFR6eRdhGzoKAUF4B4dIoXOj4p26q1yYULF5ZkZHxhQFNi5uxak9\ntgCFlGtzXumjL5jBmtWeDTGE4YSa34pzDXjz8VAjPJ9sVuOmK2E0gyWxUTLXF9Ye\nvrWzRLzVFqw+qewBV2I4of/6miZOOT2wlA/meL7zr3hnfo7KSJQmMNUjZ6lh6RBI\nVvYI0t+A/fpTKiZfviz/Xn2e4PC6i57wmH5EgOOav0UK\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/example-config/DEMO_trust/esteid2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFVzCCBLigAwIBAgIQdUf6rBR0S4tbo2bU/mZV7TAKBggqhkjOPQQDBDBaMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDky\nMDA5MjIyOFoXDTMzMDkwNTA5MTEwM1owWDELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzAR\nBgNVBAMMCkVTVEVJRDIwMTgwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAHHOBlv\n7UrRPYP1yHhOb7RA/YBDbtgynSVMqYdxnFrKHUXh6tFkghvHuA1k2DSom1hE5kqh\nB5VspDembwWDJBOQWQGOI/0t3EtccLYjeM7F9xOPdzUbZaIbpNRHpQgVBpFX0xpL\nTgW27MpIMhU8DHBWFpeAaNX3eUpD4gC5cvhsK0RFEqOCAx0wggMZMB8GA1UdIwQY\nMBaAFH4pVuc0knhOd+FvLjMqmHHB/TSfMB0GA1UdDgQWBBTZrHDbX36+lPig5L5H\notA0rZoqEjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADCCAc0G\nA1UdIASCAcQwggHAMAgGBgQAj3oBAjAJBgcEAIvsQAECMDIGCysGAQQBg5EhAQEB\nMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzANBgsrBgEEAYOR\nIQEBAjANBgsrBgEEAYORfwEBATANBgsrBgEEAYORIQEBBTANBgsrBgEEAYORIQEB\nBjANBgsrBgEEAYORIQEBBzANBgsrBgEEAYORIQEBAzANBgsrBgEEAYORIQEBBDAN\nBgsrBgEEAYORIQEBCDANBgsrBgEEAYORIQEBCTANBgsrBgEEAYORIQEBCjANBgsr\nBgEEAYORIQEBCzANBgsrBgEEAYORIQEBDDANBgsrBgEEAYORIQEBDTANBgsrBgEE\nAYORIQEBDjANBgsrBgEEAYORIQEBDzANBgsrBgEEAYORIQEBEDANBgsrBgEEAYOR\nIQEBETANBgsrBgEEAYORIQEBEjANBgsrBgEEAYORIQEBEzANBgsrBgEEAYORIQEB\nFDANBgsrBgEEAYORfwEBAjANBgsrBgEEAYORfwEBAzANBgsrBgEEAYORfwEBBDAN\nBgsrBgEEAYORfwEBBTANBgsrBgEEAYORfwEBBjAqBgNVHSUBAf8EIDAeBggrBgEF\nBQcDCQYIKwYBBQUHAwIGCCsGAQUFBwMEMGoGCCsGAQUFBwEBBF4wXDApBggrBgEF\nBQcwAYYdaHR0cDovL2FpYS5zay5lZS9lZS1nb3ZjYTIwMTgwLwYIKwYBBQUHMAKG\nI2h0dHA6Ly9jLnNrLmVlL0VFLUdvdkNBMjAxOC5kZXIuY3J0MBgGCCsGAQUFBwED\nBAwwCjAIBgYEAI5GAQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2Muc2suZWUv\nRUUtR292Q0EyMDE4LmNybDAKBggqhkjOPQQDBAOBjAAwgYgCQgDeuUY4HczUbFKS\n002HZ88gclgYdztHqglENyTMtXE6dMBRnCbgUmhBCAA0mJSHbyFJ8W9ikLiSyurm\nkJM0hDE9KgJCASOqA405Ia5nKjTJPNsHQlMi7KZsIcTHOoBccx+54N8ZX1MgBozJ\nmT59rZY/2/OeE163BAwD0UdUQAnMPP6+W3Vd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/haldusteenus/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/haldusteenus/allalaadimised.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\n.. _allalaadimised:\n\nVäljavõtete allalaadimised\n==========================\n\nVäljavõtete allalaadimise leht avaneb menüüvalikust ``Allalaadimised``.\n\n\nHääletamise detailstatistika allalaadimine\n------------------------------------------\n\nDetailstatistika on JSON-vormingus ja see genereeritakse hääletamisteenuses.\n\n\nHääletamise seansside väljavõtte allalaadimine\n----------------------------------------------\n\nHääletamise ja hääle kontrollimise seansside väljavõte on CSV-vormingus ja see\nkoostatakse logiseire teenuses.\n\n\nE-valimiskasti allalaadimine\n----------------------------\n\nE-valimiskasti allalaadimise vormil on võimalik:\n\n* Algatada e-valimiskasti koostamist;\n\n* Koostatud e-valimiskaste alla laadida.\n\nE-valimiskasti on võimalik koostada konsolideerimata ja konsolideeritud kujul.\nKonsolideerimine on ressursinõudlik tegevus ja seda on vaja kasutada vaid\njuhul, kui häälte kogumisel on esinenud probleeme talletusteenusega.\n\nEnne hääletamisperioodi lõppu allalaaditav e-valimiskast on mõeldud varundamiseks,\npärast hääletusperioodi lõppu allalaaditud e-valimiskast on mõeldud häälte\nkokkulugemiseks.\n\n.. attention::\n\n   E-valimiskasti koostamine võib olla aeganõudev tegevus! See sõltub kogutud häälte\n   arvust, varundatud e-valimiskastide arvust, teenusmasinate riistvaralisest\n   võimekusest, teenusmasinaid ühendava võrgu läbilaskevõimest ja süsteemi\n   koormusest.\n\n\nTöötlemisrakenduse sisendi aluse allalaadimine\n----------------------------------------------\n\nTöötlemisrakenduse sisendi alus on häälte töötlemiseks vajalike sisendfailide\nkomplekt ZIP-vormingus konteinerina ja see koostatakse haldusteenuses.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/annotatsioon.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nAnnotatsioon\n------------\n\nIVXV kogumisteenuse haldusteenuse\nkasutajaliidese kasutusjuhend.\n\nKäesolev juhend on mõeldud kogumisteenuse haldamisega tegelevatele kasutajatele\nning katab järgnevaid valdkondi:\n\n* Kogumisteenusele seadistuste, nimekirjade ja volituste rakendamine;\n\n* Kogumisteenuse seisundi jälgimine;\n\n* Kogumisteenuse väljavõtete allalaadimine\n  (e-valimiskast, hääletamise detailstatistika, hääletamisseansside nimekiri);\n\n* Kogumisteenuse haldussündmuste logi jälgimine.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/index.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nIVXV kogumisteenuse haldusliidese kasutusjuhend\n=======================================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n\n.. toctree::\n   :maxdepth: 2\n\n   annotatsioon\n   ylevaade\n   yldseisund\n   nimekirjad\n   statistika\n   kasutajad\n   teenused\n   allalaadimised\n   seadistused\n   logi\n"
  },
  {
    "path": "Documentation/et/haldusteenus/kasutajad.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nKasutajate haldus\n=================\n\nKasutajate halduse leht avaneb menüüvalikust ``Kasutajad``.\n\n\nKasutajate nimekiri\n-------------------\n\nKasutajate nimekirjas kuvatakse kõigi haldusteenuses registreeritud kasutajate\nandmeid sordituna kasutaja ID-kaardi üldnime (CN - *Common Name*) järgi:\n\n#. Järjekorranumber nimekirjas;\n\n#. Üldnimi;\n\n#. Kasutaja rollid.\n\nKasutaja volituste laadimise vorm\n---------------------------------\n\nKasutaja volituste laadimiseks kogumisteenusesse on lehe allosas laadimisvorm.\nLaadida on lubatud ainult volitatud kasutajate poolt digitaalselt signeeritud\nvolitusi.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/logi.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nHaldussündmuste logi jälgimine\n==============================\n\nKogumisteenuse sündmuste logi sirvimise leht avaneb menüüvalikust\n``Logiraamat``.\n\nLogis on järgmised väljad:\n\n#. ``Aeg`` - sündmuse registreerimise aeg;\n\n#. ``Teenus`` - teenuse identifikaator;\n\n#. ``Tase`` - logisündmuse tase (``INFO`` või ``ERROR``);\n\n#. ``Sündmus`` - sündmuse liigi identifikaator;\n\n#. ``Kirjeldus`` - sündmuse tekstiline kirjeldus.\n\nLogi on võimalik filtreerida ja välja väärtuse järgi sortida.\n\nLogisündmused\n-------------\n\nKogumisteenuse olekud:\n\n:COLLECTOR_INIT:\n   Kogumisteenuse lähtestamine (käsuga :command:`ivxv-collector-init`);\n\n:COLLECTOR_RESET:\n   Kogumisteenuse seadistuste lähtestamine (usaldusjuure laadimine);\n\n:COLLECTOR_STATE_CHANGE:\n   Kogumisteenuse olekumuutus;\n\nKorralduste laadimine:\n\n:CMD_LOAD:\n   Korraldusfaili haldusteenusesse laadimine;\n\n:CMD_LOADED:\n   Edukalt laaditud korraldusfaili registreerimine haldusteenuses;\n\n:CMD_REMOVED:\n   Korraldusfaili eemaldamine haldusteenusest;\n\n:VOTER_LIST_DOWNLOADED:\n   Valijate muudatusnimekirja allalaadimine;\n\n:VOTER_LIST_DOWNLOAD_FAILED:\n   Valijate muudatusnimekirja nurjunud allalaadimine;\n\n:VOTER_LIST_NOT_FOUND:\n   Valijate järgmist muudatusnimekirja ei leitud Valimiste Infosüsteemis;\n\nKasutajaõiguste muutused:\n\n:PERMISSION_SET:\n   Kasutajale õiguse määramine;\n\n:PERMISSION_RESET:\n   Kasutaja õiguste lähtestamine;\n\nHääletusperioodi registreerimine:\n\n:SET_ELECTION_TIME:\n   Hääletusperioodi algus- ja lõpuaegade registreerimine;\n\nMikroteenuste haldus:\n\n:SERVICE_REGISTER:\n   Teenuse registreerimine haldusteenuses;\n\n:SERVICE_CONFIG_APPLY:\n   Seadistuse rakendamine teenusele;\n\n:SERVICE_STATE_CHANGE:\n   Teenuse olekumuutus;\n\n:SECRET_INSTALL:\n   Saladuse laadimine teenusele.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/nimekirjad.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nNimekirjade haldamine\n=====================\n\nNimekirjade haldamise leht avaneb menüüvalikust ``Nimekirjad``.\n\n\nValikute nimekiri\n-----------------\n\nValikute nimekirja kohta kuvatakse nimekirja seisundit ja laaditud nimekirja\nkorral selle versiooni (nimekirja signeerija andmed koos digiallkirja\najatempliga).\n\nNimekirja võimalikud seisundid on:\n\n#. Laadimata;\n\n#. Laaditud haldusteenusesse;\n\n#. Rakendatud kogumisteenusele.\n\n\nValijate nimekirjad\n-------------------\n\nValijate nimekirjade kohta kuvatakse nimekirjade koguarvu, nimekirjade arvu\nseisundi kaupa ning loetelu kõigi kogumisteenuses registreeritud nimekirjade\nkohta (versioon ja seisund).\n\nValijate algnimekirja korral on nimekirja versioon nimekirja signeerija andmed\nkoos digiallkirja ajatempliga; muudatusnimekirja korral nimekirja allalaadimise\nURL koos allalaadimise hetke ajatempliga.\n\nNimekirja võimalikud seisundid on:\n\n#. Rakendamise ootel;\n\n#. Rakendatud kogumisteenusele;\n\n#. Vigane;\n\n#. Vahele jäetud.\n\n\nRingkondade nimekiri\n--------------------\n\nRingkondade nimekirja kohta kuvatakse nimekirja seisundit ja laaditud nimekirja\nkorral selle versiooni (nimekirja signeerija andmed koos digiallkirja\najatempliga).\n\nNimekirja võimalikud seisundid on:\n\n#. Laadimata;\n\n#. Laaditud haldusteenusesse;\n\n#. Rakendatud kogumisteenusele.\n\n\nNimekirjade laadimine kogumisteenusesse\n---------------------------------------\n\nNimekirjade laadimiseks kogumisteenusesse on lehe allosas laadimisvorm. Laadida\non lubatud ainult volitatud kasutajate poolt digitaalselt signeeritud\nnimekirju.\n\n.. note::\n\n   Nimekirjade laadimise järjekord ei ole oluline. Enne nimekirjade laadimist\n   tuleb laadida valimiste seadistused.\n\n.. important::\n\n   Valikute nimekirja on võimalik kogumisteenusele rakendada ainult üks kord!\n"
  },
  {
    "path": "Documentation/et/haldusteenus/seadistused.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nSeadistuste rakendamise seisundid\n=================================\n\nSeadistuste seisundite jälgimise leht avaneb menüüvalikust ``Seadistused``.\n\nSeisundit kuvatakse järgmiste seadistuste kohta:\n\n* Usaldusjuure seadistused;\n\n* Tehniline seadistus;\n\n* Valimiste seadistus;\n\n* Valikute nimekiri;\n\n* Ringkondade nimekiri;\n\n* Valijate nimekirjad.\n\nSeisundit ei kuvata järgmiste seadistuste kohta:\n\n* Kasutaja volitused (on laadimisel kohe rakendatud).\n\nHaldusteenusesse laaditud seadistuste kohta kuvatakse:\n\n* Seadistuse rakendamise seisundit;\n\n* Aktiivse seadistuse versiooni;\n\n* Seadistuste rakendamise katsete arvu;\n\n* Rakendamisele saadetud seadistuse kohta ka rakendamise logi.\n\n\nSeadistuste laadimine kogumisteenusesse\n---------------------------------------\n\nTehnilise seadistuse ja valimiste seadistuste laadimiseks kogumisteenusesse on\nlehe allosas laadimisvorm. Laadida on lubatud ainult volitatud kasutajate poolt\ndigitaalselt signeeritud seadistuspakke. Seadistuste laadimise järjekord ei ole\noluline. Valimiste seadistuse laadimine on eelduseks valimiste nimekirjade\nlaadimisele.\n\n.. note::\n\n   Usaldusjuure seadistuse laadib kogumisteenuse haldur käsurealt. Enne\n   usaldusjuure laadimist pole kogumisteenuse haldusliidest võimalik kasutada.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/spelling_wordlist.txt",
    "content": ""
  },
  {
    "path": "Documentation/et/haldusteenus/statistika.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nStatistika jälgimine\n====================\n\nStatistika jälgimise leht avaneb menüüvalikust ``Statistika``.\nHääletamise detailstatistika allalaadimine on kirjeldatud lõigus\n:ref:`allalaadimised`.\n\n\nStatistika genereerimine\n------------------------\n\nStatistika vaates kuvatavad andmed genereeritakse ja uuendatakse korrapäraselt\nlogimonitoris ning laaditakse automaatselt haldusliidesesse.\n\nHaldusliideses kuvatav statistika kajastab kogumisteenuse seisundit mõningase\nviivitusega (harilikult kuni 10 minutit).\n\n\nStatistika sisu\n---------------\n\nKuvatavad andmed on järgmised:\n\n#. Kogutud häälte arv;\n\n#. Hääletajate arv;\n\n#. Kontrollitud häälte arv;\n\n#. Hääle kontrollijate arv;\n\n#. Hääletajate jaotumine:\n\n   #. Sugude kaupa;\n\n   #. Vanusegruppide kaupa;\n\n   #. Autentimisvahendite kaupa;\n\n   #. Operatsioonisüsteemide kaupa;\n\n   #. Riikide kaupa (vastavalt hääletaja IP-aadressile).\n\n#. Korduvhääletamiste statistika.\n\nStatistikat on võimalik filtreerida ringkondade ja jaoskondade kaupa (juhul,\nkui ringkondade nimekiri on laaditud haldusteenusesse).\n"
  },
  {
    "path": "Documentation/et/haldusteenus/teenused.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nTeenuste haldus\n===============\n\nTeenuste haldamise leht avaneb menüüvalikust ``Teenused``.\n\n\nTeenuste kokkuvõte\n------------------\n\nTeenuste kokkuvõttes kuvatakse kõikide registreeritud teenuste ülevaadet\nseisundi kaupa. Iga seisundi järel kuvatakse selles seisundis olevate teenuste\narvu:\n\n#. Paigaldamata – teenus pole paigaldatud;\n\n#. Paigaldatud – teenus on paigaldatud ja sellele on rakendatud usaldusjuure\n   seadistus, kogumisteenuse tehniline seadistus. Valimiste seadistus on\n   teenusele rakendamata;\n\n#. Seadistatud – teenusele on rakendatud kõik seadistused ja teenus on töökorras;\n\n#. Tõrge – teenuse toimimises on avastatud tõrge;\n\n#. Eemaldatud – teenuse on kogumisteenuse koosseisust eemaldatud.\n\n\nTeenuste nimekiri\n-----------------\n\nTeenuste nimekirjas kuvatakse kõiki haldusteenuses registreeritud teenuseid.\nIga teenuse kohta kuvatakse:\n\nTeenuste halduse vaates kuvatakse alamteenuste nimekirja, mis on sorditud\nteenuse identifikaatori järgi:\n\n#. Teenuse identifikaator;\n\n#. Teenuse alamvõrk;\n\n#. Teenuse liik;\n\n#. Teenuse seisund;\n\nKui haldusteenus on alamteenuse kohta tuvastanud seadistuste puudumise või\nveaolukorra, siis vastav teave kuvatakse teenuse all. Iga teenuse kohta\nkuvatakse korraga vaid ühte teadet.\n\nNimekirjas teenuse kirjel klõpsates avaneb kirje all tabel täpsema infoga:\n\n#. Teenuse korrasoleku kontrolli poolt tuvastatud järjestikune vigade arv\n   (ainult teenustel, mis on olekus ``seadistatud`` või ``tõrge``);\n\n#. Teenuse korrasoleku kontrolli viimase läbiviimise aeg\n   (ainult teenustel, mis on olekus ``seadistatud`` või ``tõrge``);\n\n#. Teenusele rakendadud tehnilise seadistuse versioon;\n\n#. Teenusele rakendadud valimiste seadistuse versioon;\n\n#. Teenuse IP-aadress ja port;\n\n#. Teenuse TLS-sertifikaadi kontrollsumma (SHA256);\n\n#. Teenuse TLS-sertifikaadile vastava võtme kontrollsumma (SHA256);\n\n#. Mobiil-ID/Smart-ID/Web eID tugiteenuste jagatud krüptimissaladuse kontrollsumma\n   (SHA256);\n"
  },
  {
    "path": "Documentation/et/haldusteenus/yldseisund.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nÜldseisund\n==========\n\nÜldseisundi leht avaneb menüüvalikust ``Üldseisund``.\n\nKogumisteenuse üldseisund\n-------------------------\n\nKogumisteenuse üldseisundi vaates kuvatakse järgnevaid andmeid:\n\n#. Valimiste identifikaator;\n\n#. Kogumisteenuse seisund;\n\n#. Käimasolev hääletusetapp;\n\n#. Kogumisteenusele rakendatud seadistuste ja nimekirjade versioonid;\n\n#. Mikroteenuste seisundite kokkuvõte;\n\n#. Kogumisteenuse tarkvarapakkide seisund;\n\n#. Kogumisteenuses registreeritud kasutajate arv;\n\n#. Kogumisteenusesse laaditud korralduste arv.\n\nKogumisteenuse üldseisundi tuvastamiseks kogub haldusteenus korrapäraselt\nalamteenustelt nende seisundi kohta andmeid ja peab selle üle arvestust.\n\n.. note::\n\n   Kogumisteenusesse laaditud korraldused jagunevad aktiivseteks ja\n   arhiveeritud korraldusteks. Aktiivsed korraldused on hetkel teenusele\n   rakendatud. Arhiveeritud korraldused on süsteemi laaditud, kuid pole\n   rakendatud - näiteks uuema versiooniga asendatud korraldused.\n"
  },
  {
    "path": "Documentation/et/haldusteenus/ylevaade.rst",
    "content": "..  IVXV kogumisteenuse haldusliidese kasutusjuhend\n\nÜlevaade\n========\n\nHaldusliidese funktsionaalsus\n-----------------------------\n\nHaldusliides on kogumisteenuse haldamise teenuse veebipõhine kasutajaliides ja\nsellel on järgmised funktsioonid:\n\n* Kogumisteenuse seisundi ja ajaloo kohta ülevaate esitamine:\n\n   * Koondülevaade;\n\n   * Teenuste seisund;\n\n   * Valmisnimekirjade seisund;\n\n   * Üldstatistika;\n\n   * Kogumisteenuse haldussündmuste logi;\n\n* Korralduste laadimine kogumisteenusesse;\n\n* Kasutajate haldamine;\n\n* Väljavõtete allalaadimine:\n\n  * Hääletamise detailstatistika allalaadimine;\n\n  * Hääletamisseansside nimekirja allalaadimine;\n\n  * E-valimiskasti allalaadimine.\n\n\nLigipääse haldusliidesele\n-------------------------\n\nKogumisteenuse haldusliides on kasutatav veebisirviku abil. Haldusliidesele\nligipääsuks vajaliku ``URL-i`` annab kasutajatele IVXV süsteemihaldur.\n\nHaldusliidesele pääsevad ligi ainult volitatud kasutajad, kes on ennast\nautentinud ID-kaardiga. Kasutajale pakutavate funktsioonide hulk sõltub\nkasutaja volitustest.\n\n\nKasutajaliidese ülevaade\n------------------------\n\nLehe ülaosas asub lehe päis, kus on kirjas haldusliidese nimi ja ikoon\nsisselogitud kasutaja andmete vaatamiseks.\n\nLehe vasakus servas asub menüüriba, mille abil saab navigeerida alamlehtede vahel.\n\n.. note::\n\n   Kasutajaliides skaleerub ka madala resolutsiooniga ekraanidele, siis\n   peidetakse menüüosa vaikimisi vaatest ja see on avatav lehe päisest.\n\n\nSeadistuste, nimekirjade ja volituste koostamine ning laadimine\n---------------------------------------------------------------\n\nKasutajaliides kaudu on võimalik süsteemi laadida kogumisteenuse seadistusi,\nvalikute- ja valijate nimekirju ning kasutajate volitusi.\n\nNende andmete peavad olema vormistatud digitaalselt signeeritud korraldusena.\nSeadistuspakkide ettevalmistamine on kirjeldatud dokumendis ``IVXV seadistuste\nkoostamise juhend``.\n\nSüsteemi laaditud seadistuspakke on võimalik alla laadida, kui klõpsata\nkasutajaliideses seadistuspaki versiooniandmetel.\n"
  },
  {
    "path": "Documentation/et/kasutusmall/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/kasutusmall/index.rst",
    "content": "..  IVXV kasutusmallid\n\nIVXV kasutusmallid\n==========================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 3\n   :numbered:\n\n   sissejuhatus\n   tegijad\n   kasutusmallid\n"
  },
  {
    "path": "Documentation/et/kasutusmall/kasutusmallid.rst",
    "content": "..  IVXV kasutusmallid\n\nKasutusmallid\n=============\n\nHääletamiseelne etapp\n---------------------\n\nKorraldaja - Valimise defineerimine\n```````````````````````````````````\n\nKirjeldus\n'''''''''\n\nRangelt võttes ei ole tegemist elektroonilise hääletamise infosüsteemi kasutusmalliga, kuid see on elektroonilise hääletamise alguspunkt - Korraldaja kirjeldab valimise, küsimused, ringkonnad, valijad ning erinevate rollide täitjad e-hääletamise põhiprotsessides - Lugeja, Koguja, Töötleja jt.\n\nLugeja - Häälte salastamise ja avamise võtme genereerimine\n``````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\n\nLugeja genereerib võtmepaari e-häälte salastamiseks ja avamiseks. Avamisvõtme osakud jagatakse Võtmehaldurite vahel.\n\nEeltingimus\n'''''''''''\n\n#. Lugejal on olemas Võtmerakendus.\n#. Läviskeemi tehniline seadistus on olemas.\n#. Leidub Läviskeemi rakendamiseks vajalik hulk Võtmehaldureid ning võtmekandjaid.\n\nPäästik\n'''''''\n\nLugeja algatab võtmepaari genereerimise Võtmerakenduses.\n\nPõhiprotsess\n''''''''''''\n\n#. Võtmerakendus verifitseerib konfiguratsiooni digitaalallkirja.\n#. Võtmerakendus genereerib võtmeosakud vastavalt läviskeemi spetsifikatsioonile.\n#. Võtmerakendus küsib Lugejalt võtmekandjaid osakute talletamiseks.\n#. Võtmerakendus talletab osakud võtmekandjatele.\n#. Võtmerakendus kasutab häälte salastamise võtit sõnumi krüpteerimiseks.\n#. Võtmerakendus testib võtmekandjaid ning taastab 2 erineva kvoorumiga võtme sõnumi dekrüpteerimiseks.\n#. Võtmerakendus allkirjastab krüpteerimisvõtme allkirjastamisvõtmega.\n\nLaiendid\n''''''''\n\n- Tehniliste vigade tekkimisel kõigis põhivoo sammudes logitakse sündmus tehniliste vigade logisse. Kasutajale väljastatakse  teade veasituatsiooni kohta..\n\nJäreltingimus\n'''''''''''''\n\nHäälte salastamise ja avamise võtmepaar on edukalt genereeritud, avamise võti on talletatud osakutena võtmekandjatel.\n\n\nKorraldaja - Valijarakenduse seadistuse loomine\n```````````````````````````````````````````````\n\nKirjeldus\n'''''''''\n\nKorraldaja konfigureerib Valijarakenduse poolt kasutatavad tekstid, sertifikaadid, ikoonid ja valimise parameetrid.\n\nEeltingimus\n'''''''''''\nSeadistamiseks on täidetud järgmised tingimused:\n\n- Korraldaja on Valijarakenduse tekstid ette valmistanud\n- Korraldaja on valimise parameetrid ette valmistanud\n- Häälte salastamise võti on olemas.\n\nPõhiprotsess\n''''''''''''\n#. Korraldaja käivitab Seadistamisrakenduse.\n#. Korraldaja sisestab valimise ja küsimuste identifikaatorid ning tüübid.\n#. Korraldaja sisestab valimise küsimused.\n#. Korraldaja laadib häälte salastamise võtme.\n#. Korraldaja konfigureerib Valijarakenduse kasutajaliidese – tekstid, fondid, värvid, ikoonid.\n#. Korraldaja kontrollib kõigi Valijarakenduse vaadete vastavust soovitule.\n#. Korraldaja salvestab loodud konfiguratsiooni.\n\nLaiendid\n''''''''\n\nJäreltingimus\n'''''''''''''\nSeadistamise lõpuks on olemas Valijarakenduse konfiguratsioon, millest on puudu Kogumisteenuse tehniline info.\n\nKorraldaja - Kontrollrakenduse seadistuse loomine\n`````````````````````````````````````````````````\n\nEeltingimus\n'''''''''''\nKorraldajal on olemas\n\n#. Hääletaja allkirja kontrolliks vajalikud sertifikaadid\n#. Registreerimisteenuse kinnituse kontrolliks vajalikud sertifikaadid\n#. Kogumisteenuse allkirja kontrolliks vajalikud sertifikaadid\n#. Käimasoleval valimisel häälte krüpteerimiseks kasutatav avalik võti\n#. Kontrollrakenduse tekstid HTML kujul\n#. Kontrollrakenduse fondid\n#. Kontrollrakenduse värvid\n#. Käimasoleva valimise identifikaator ja küsimused\n\nPõhiprotsess\n''''''''''''\n1. Korraldaja loob Kontrollrakenduse seadistuse\n2. Korraldaja veendub seadistamise õnnestumises vaadates läbi rakenduse vaated\n\nLaiendid\n''''''''\n1. Vaadete mittesobivuse korral muudab Korraldaja seadistusi ning alustab põhiprotsessi algusest.\n\n\nKoguja - Kogumisteenuse seadistamine\n````````````````````````````````````\n\nKirjeldus\n'''''''''\nKoguja valmistab Kogumisteenuse valimiseks ette.\n\nEeltingimus\n'''''''''''\n#. Kogumisteenuse operatsioonisüsteem on paigaldatud\n#. Kogumisteenuse tarkvarapakid on paigaldatud.\n\nPäästik\n'''''''\nKasutusmall algab kui Koguja logib enne hääletamisperioodi algust Kogumisteenusesse.\n\nPõhiprotsess\n''''''''''''\n1. Koguja viib läbi alljärgnevad sammud:\n\n    a. Laeb konfiguratsioonipakkide verifitseerimiseks vajaliku sertifikaatide konfiguratsiooni.\n    b. Laeb Tuvastusteenuse konfiguratsiooni.\n    c. Laeb Allkirjastamisteenuse konfiguratsiooni.\n    d. Laeb Registreerimisteenuse konfiguratsiooni.\n    e. Laeb mikroteenuste võrgukonfiguratsiooni.\n    f. Laeb talletustehnoloogia konfiguratsiooni.\n    g. Laeb volitatud nimekirjade laadijate konfiguratsiooni.\n    h. Laeb digitaalselt allkirjastatud jaoskondade/ringkondade nimekirja.\n    i. Laeb digitaalselt allkirjastatud valikute nimekirja.\n    j. Laeb signeeritud valijate nimekirja.\n    k. Kogumisteenus kontrollib digitaalallkirju konfiguratsioonipakkidel ja nimekirjadel (tehniline kasutusmall Digitaalallkirja kehtivuse kontroll Kogumisteenuses).\n    l. Kogumisteenus kontrollib tuvastatud allkirjastaja volitusi süsteemis.\n    m. Kogumisteenus kontrollib konfiguratsiooni vormilist ja sisulist kooskõlalisust ning initsialiseerib sisemised andmestruktuurid.\n\nLaiendid\n''''''''\n- Kui põhiprotsessi sammus 1.k  ei tuvastata digitaalallkirja  kehtivust, siis väljastatakse veateade, sündmus logitakse vigade logisse, ühtegi muudatust ei rakendata.\n- Kui põhiprotsessi sammus 1.l  ei tuvastata  digitaalallkirjastaja vastavat volitust volitatud isikute nimekirjast, siis väljastatakse veateade, sündmus logitakse vigade logisse, ühtegi muudatust ei rakendata.\n- Kui põhivoo sammus 1.m tuvastatakse vorminguprobleemid, siis väljastatakse veateade, sündmus logitakse vigade logisse, ühtegi muudatust ei rakendata.\n- Kui põhivoo sammus 1.m tuvastatakse ebakooskõla konfiguratsioonis – valed valimised, korduv jaoskond, korduv ringkond, jaoskond olematus ringkonnas, korduv kandidaat, olematu ringkonnaga kandidaat, korduv valija, olematu jaoskonnaga Hääletaja vms. – siis väljastatakse veateade, sündmus logitakse vigade logisse, ühtegi muudatust ei rakendata.\n- Tehniliste vigade tekkimisel kõigis põhivoo sammudes logitakse sündmus tehniliste vigade logisse. Kasutajale väljastatakse  teade veasituatsiooni kohta..\n\nJäreltingimus\n'''''''''''''\nKogumisteenus on kooskõlaliselt seadistatud ning seda on võimalik viia hääletamisetappi.\n\n\nKoguja - Valijarakenduse valmendamine\n`````````````````````````````````````\n\nKirjeldus\n'''''''''\nKoguja valmendab Valijarakenduse.\n\nEeltingimus\n'''''''''''\nValmendamiseks on täidetud järgmised tingimused:\n\n- Korraldaja on loonud omapoolse Valijarakenduse konfiguratsioonifaili\n- Valijarakendus on kompileeritud kõigi toetatavate platvormide jaoks.\n- Valijarakendus on seadistamata.\n- Kogumisteenuse TLS-sertifikaadi usaldamiseks vajalikud sertifikaadid on olemas.\n- Kogumisteenuse URI on teada.\n\nPõhiprotsess\n''''''''''''\n#. Koguja käivitab Seadistamisrakenduse.\n#. Koguja laadib Korraldaja loodud Valijarakenduse konfiguratsioonifaili.\n#. Koguja laadib Kogumisteenuse TLS-sertifikaadi usaldamiseks vajalikud sertifikaadid.\n#. Koguja rakendab loodud seadistuse Valijarakendusele.\n#. Koguja kontrollib kõigi Valijarakenduse vaadete vastavust soovitule.\n\nLaiendid\n''''''''\nMõne vajaliku ressurssi puudumisel seadistamine katkestatakse ning protsessi alustatakse uuesti kui kõik eeltingimused on täidetud.\n\nJäreltingimus\n'''''''''''''\nSeadistamise lõpuks Valijarakendus käivitub ning kasutab seadistatud ressursse.\n\n\nKoguja - Kontrollrakenduse valmendamine\n```````````````````````````````````````\n\nEeltingimus\n'''''''''''\nKogujal on olemas\n\n1. Kompileeritud Kontrollrakendus\n2. Kontrollrakenduse allkirjastamiseks sobiv võtmepaar\n3. Kogumisteenuse võrguaadress\n4. Kogumisteenuse sertifikaadi verifitseerimiseks vajalikud sertifikaadid\n\nPõhiprotsess\n''''''''''''\n1. Koguja seadistab Kontrollrakenduse Kogumisteenuse võrguaadressi ja vastavate sertifikaatidega.\n2. Koguja pakendab rakenduse\n3. Koguja allkirjastab pakendatud rakenduse\n\nKorraldaja - Proovihääletamine\n``````````````````````````````\n\nKirjeldus\n'''''''''\n\nKorraldaja koostöös Koguja, Töötleja, Lugeja ning Võtmehalduritega veendub Kogumisteenuse valmisolekus elektrooniliseks hääletamiseks ning kõigi komponentide kooskõlalises seadistatuses.\n\nEeltingimus\n'''''''''''\n\n#. e-häälte salastamise võti on loodud ja testitud\n#. Kogumisteenus on seadistatud\n#. Valijarakendus on seadistatud\n#. Kontrollrakendus on seadistatud\n\nPõhiprotsess\n''''''''''''\n1. Korraldaja viib läbi testhääletamise\n\n   #. Koguja viib Kogumisteenuse hääletamisetappi\n   #. Korraldaja annab Valijarakenduse abil ühe või mitu häält\n   #. Korraldaja kontrollib Kontrollrakenduse abil hääli\n   #. Koguja peatab Kogumisteenuse ja väljastab e-valimiskasti\n   #. Töötleja genereerib e-hääletanute nimekirja\n   #. Töötleja loob lugemisele minevate anonüümistatud e-häälte nimekirja\n   #. Lugeja koos Võtmehalduritega aktiveerib Võtmerakenduse ja häälte avamise võtme\n   #. Lugeja dekrüpteerib anonüümistatud hääled ja väljastab hääletamistulemuse\n\n2. Koguja lõpetab testhääletamise ning viib Kogumisteenuse algsesse olekusse, kus talletatud häälte andmebaas on tühi.\n\nLaiendid\n''''''''\n\n- Testhääletamine võib sisaldada ka miksimise ja auditeerimise töövooge.\n\nJäreltingimus\n'''''''''''''\nElektroonilise hääletamise süsteemi komponendid on kontrollitult kooskõlaliselt seadistatud.\n\nHääletamisetapp\n---------------\n\nKorraldaja - Hääletamise alustamine\n```````````````````````````````````\n\nKirjeldus\n'''''''''\nHääletamise alustamine viib Kogumisteenuse hääletamisetappi - algab valikute nimekirjade väljastamine, häälte talletamine ja kontrollpäringutele vastamine.\n\nPäästik\n'''''''\n\n#. Valimise seadistustes näidatud hääletamise algusaeg jõuab kätte.\n#. Korraldaja edastab Haldusteenuse vahendusel digitaalallkirjastatud korralduse hääletamise alustamiseks.\n\nPõhiprotsess\n''''''''''''\n\n1. Kogumisteenus alustab valikute nimekirjade väljastamist, häälte talletamist ja kontrollpäringutele vastamist.\n\nJäreltingimus\n'''''''''''''\n\nKogumisteenus väljastab valikute nimekirju, talletab hääli ning vastab kontrollpäringutele.\n\nHääletaja - Elektrooniline hääletamine Valijarakendusega\n````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nHääletaja kasutab Valijarakendust elektroonilise hääle andmiseks käimasoleval valimisel, mille valijate nimekirja ta kuulub.\n\nEeltingimus\n'''''''''''\nHääletaja on laadinud oma arvutisse käimasoleva valimise jaoks seadistatud Valijarakenduse.\n\nPäästik\n'''''''\nKasutusmall käivitub kui Hääletaja on käivitanud Valijarakenduse.\n\nPõhiprotsess\n''''''''''''\n#. Hääletaja autendib ennast elektroonilise isikutunnistusega (tehniline kasutusmall Autentimine Valijarakenduses) Kogumisteenusele.\n#. Kogumisteenus saadab isikukoodi alusel Valijarakendusele (tehniline kasutusmall Valikute nimekirjade väljastamine Valijarakendusele):\n\n    - Hääletaja elukohajärgse valimisringkonna küsimuste ja valikute nimekirja valimisel,\n    - teavituse varasema hääletamise kohta, kui sellel valimisel on sama isikukoodi kohta juba talletatud elektrooniline hääl.\n\n3. Valijarakendus esitab Hääletaja isikuandmed, käimasoleva valimise kirjelduse ning küsimused.\n#. Valijarakendus esitab Hääletaja elukohajärgse valimisringkonna valikute nimekirja.\n#. Hääletaja teeb kõigi küsimuste kontekstis kuvatud valikute hulgast valiku.\n#. Valijarakendus esitab Hääletajale tehtud valikute andmed (valiku nimi, valiku number, teatud valimistel ka valimisnimekirja nimi või üksikkandidaat) ning küsib kinnitust valikutele.\n#. Hääletaja kinnitab tehtud valikud.\n#. Valijarakendus krüpteerib Hääletaja valikud valimise avaliku võtmega ning algatab hääle digitaalse allkirjastamise (tehniline kasutusmall Digitaalne allkirjastamine Valijarakenduses) Hääletaja elektroonilise isikutunnistusega. Valijarakendus saadab digitaalselt allkirjastatud hääle Kogumisteenusele talletamiseks (tehniline kasutusmall Hääle talletamine Kogumisteenuses).\n#. Valijarakendus verifitseerib Kogumisteenuse poolt vastusena saadetud Registreerimiskinnitust, kuvab Hääletajale teate hääle edukast talletamisest ning hääle kontrollimiseks vajaliku kontrollkoodi.\n\nLaiendid\n''''''''\n- Hääletaja võib põhiprotsessi läbiviimisel kasutada ligipääsetavustehnoloogiaid (nt. ekraanilugerid).\n- Põhiprotsessi mis tahes etapis asetleidvad vead on fataalsed ning toovad kaasa hääletamisprotsessi katkestamise. Vea põhjuse kõrvaldamise korral tuleb kasutusmall uuesti käivitada.\n\nOlulisimad vead:\n\n- Elektroonilise isikutunnistuse kasutamine ebaõnnestus\n- Sidehäire Valijarakenduse ja Kogumisteenuse vahel\n- Autentimine ebaõnnestus\n- Hääletajal ei ole hääleõigust käimasoleval valimisel\n- Digitaalne allkirjastamine ebaõnnestus\n- Tehnilised vead\n\nJäreltingimus\n'''''''''''''\nÕnnestunud elektroonilise hääletamise korral kuvatakse Hääletajale kontrollkood, mille alusel on võimalik kontrollida elektroonilise hääle jõudmist Kogumisteenusesse ning vastavust Hääletaja tahtele.\nKasutusmalli katkestamine enne elektroonilise hääle digitaalset allkirjastamist ei mõjuta Hääletaja Kogumisteenuses eelnevalt talletatud häält.\n\nHääletaja - Elektroonilise hääle kontrollimine Kontrollrakendusega\n``````````````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nHääletaja kasutab vahetult peale hääletamist Valijarakendusega Kontrollrakendust elektroonilise hääle korrektse jõudmise Kogumisteenusesse kontrollimiseks.\n\nEeltingimus\n'''''''''''\n#. Hääletaja valduses on Kontrollrakendusega mobiilseade.\n#. Hääletaja on kasutanud Valijarakendust hääletamiseks.\n#. Hääle kontrollimise ajaaken ei ole veel möödunud.\n#. Valijarakenduse viimane vaade on ekraanil avatud ning kuvab kontrollimiseks vajalikku infot sisaldavat QR-koodi.\n\nPäästik\n'''''''\nKasutusmall käivitub kui Hääletaja on käivitanud Kontrollrakenduse.\n\nPõhiprotsess\n''''''''''''\nPõhiprotsess on ühesuunaline, tagasi liikumised protsessis eeldavad Kontrollrakenduse töö lõpetamist.\n\n#. Kontrollrakendus laadib Kogumisteenusest seadistused.\n#. Kontrollrakendus kuvab tervitusteksti.\n#. Hääletaja suunab mobiilseadme kaamera Valijarakenduses kuvatavale QR-koodile.\n#. Kontrollrakendus analüüsib QR-koodi ning tuvastab sealt hääle krüpteerimiseks kasutatud juhuarvu ning häält Kogumisteenuses identifitseeriva sessiooniidentifikaatori.\n#. Kontrollrakendus pöördub sessiooniidentifikaatoriga Kogumisteenuse poole.\n#. Kontrollrakendus verifitseerib Kogumisteenuse sertifikaadi ning kuvab Hääletajale info verifitseerimise õnnestumise kohta.\n#. Kontrollrakendus laadib Kogumisteenusest alla Kogumisteenuse allkirjastatud hääle, Registreerimisteenuse kinnituse ja häälega seotud valikute nimekirjad (tehniline kasutusmall Hääle väljastamine kontrollimiseks Kogumisteenusest).\n#. Kontrollrakendus veendub, et hääl vastab kontrollprotokolli nõuetele. Kontrollrakendus verifitseerib Registreerimisteenuse kinnitust ning Hääletaja allkirja. Kontrollrakendus kuvab Hääletajale info verifitseerimiste õnnestumise kohta, hääle allkirjastanud Hääletaja andmed ning küsimused, mille kohta antud hääles leidub krüpteeritud tahteavaldus.\n#. Hääletaja käivitab kontrollalgoritmi.\n#. Kontrollrakendus puhastab vaate hääle allkirjastanud Hääletaja andmetest.\n#. Kontrollrakendus tuvastab krüpteeritud tahteavalduse sisu kasutades kontrollalgoritmi ja QR-koodist saadud juhuarvu.\n#. Kontrollrakendus kuvab iga krüpteeritud tahteavalduse kohta Hääletajale küsimuse identifikaatorit ja tuvastatud valikut.\n\nLaiendid\n''''''''\n#. Kui Kontrollrakendus tuvastab võrguühenduse puudumise, siis suunatakse Hääletaja võrguühendust aktiveerima.\n#. Kui Kontrollrakendus tuvastab vea Kogumisteenuse sertifikaadis, siis lõpetatakse rakenduse töö veateatega.\n#. Kui Kogumisteenusest ei saadud häält või Registreerimisteenuse kinnitust, siis juhendatakse Hääletajat Kliendituge informeerima ja lõpetatakse rakenduse töö veateatega.\n#. Kui Kogumisteenusest tuleb teade, et konkreetse hääle kontrollimine ei ole enam võimalik (aeg või korrad ületatud), siis lõpetatakse rakenduse töö vastavasisulise teatega.\n#. Kui Kogumisteenusest saadud hääl ei vasta nõuetele, siis lõpetatakse rakenduse töö veateatega.\n#. Kui Hääletaja allkirja verifitseerimine ebaõnnestub, siis lõpetatakse rakenduse töö veateatega.\n#. Kui Registreerimisteenuse kinnituse verifitseerimine ebaõnnestub, siis lõpetatakse rakenduse töö veateatega.\n#. Kui kontrollalgoritm ei leia kandidaatide nimekirjast sobivat valikut, siis kuvatakse konkreetse valimise ja küsimuse identifikaatori järel veateade.\n#. Kui QR koodis sisaldub rohkem kui ühe küsimuse kontrollkood, siis teostatakse kontroll kõigi viidatud küsimuste jaoks.\n#. Kui Kogumisteenusest saadetud allkirjastatud hääles on krüpteeritud hääli, mille kohta puudub kontrollkood, siis esitab Kontrollrakendus Hääletajale nende valimiste/küsimuste identifikaatorid ning vastavasisulise hoiatuse. Ülejäänud hääled kontrollitakse.\n#. Kui Kogumisteenusest saadetud allkirjastatud hääles ei ole kõiki hääli, mille kohta on kontrollkood, siis esitab Kontrollrakendus Hääletajale nende valimiste/küsimuste identifikaatorid ning vastavasisulise hoiatuse. Ülejäänud hääled kontrollitakse.\n#. Kui kontrollalgoritm lõpetab veateatega, siis juhendatakse Hääletajat Kliendituge informeerima.\n\nJäreltingimus\n'''''''''''''\nKui hääle kontrollimisega ületatakse lubatud kontrollimiste limiit, siis Kogumisteenus rohkem häält kontrollida ei võimalda.\n\n\nKogumisteenuse Haldur - Kogumisteenuse seisundi kuvamine\n````````````````````````````````````````````````````````\n\nPäästik\n'''''''\nKogumisteenuse Haldur siseneb Kogumisteenuse Haldusteenusesse.\n\nPõhiprotsess\n''''''''''''\n\n#. Haldusteenus autendib kasutaja ning tuvastab autenditud kasutaja volitused Haldusteenuses\n#. Vastavalt volituste määrale kuvab Kogumisteenus alamhulka järgmisest informatsioonist:\n\n   #. Valimise identifikaator, küsimused, hääletusetapp\n   #. Töötavad teenusserverid\n   #. Laetud nimekirjad - ringkonnad, valijad, valikud\n   #. Hääletamise statistika\n   #. Kogumisteenuse volitatud kasutajad\n   #. Kogumisteenuse tehniline logi\n\n\nKorraldaja - Hääletamise lõpetamine\n```````````````````````````````````\n\nKirjeldus\n'''''''''\nHääletamise lõpetamine lõpetab häälte vastuvõtmise Kogumisteenuse poolt. Hääletamise lõpetamine toimub järk-järgult – Hääletajad, kes on saanud kandidaatide nimekirja enne hääletamise ametlikku lõppu, peavad saama mõistliku aja jooksul hääletada.\n\nPäästik\n'''''''\n\n#. Valimise seadistustes näidatud hääletamise lõpetamise aeg jõuab kätte.\n#. Korraldaja edastab Haldusteenuse vahendusel digitaalallkirjastatud korralduse hääletamise lõpetamiseks.\n\nPõhiprotsess\n''''''''''''\n1. Kogumisteenus vastab kõigile kandidaatide nimekirjade päringutele veateatega, kuid jätkab häälte talletamispäringute teenindamist.\n2. Kogumisteenus lõpetab korralduses/konfiguratsioonis näidatud aja möödudes häälte vastuvõtmise.\n3. Kogumisteenus vastab kõigile Valijarakendustelt tulevatele päringutele veateatega.\n\nJäreltingimus\n'''''''''''''\nKogumisteenus ei võta enam uusi hääli vastu.\n\n\nTöötlusetapp\n------------\n\nKoguja - E-valimiskasti väljastamine\n````````````````````````````````````\n\nKirjeldus\n'''''''''\n\nKoguja ekspordib e-valimiskasti Kogumisteenuse andmebaasist.\n\nEeltingimus\n'''''''''''\n\nElektrooniline hääletamine on lõpetatud.\n\nPäästik\n'''''''\n\nKoguja valib Haldusteenusest e-valimiskasti eksportimise funktsionaalsuse.\n\nPõhiprotsess\n''''''''''''\n\n#. Kogumisteenus teeb häälte andmebaasist väljavõtte, mis sisaldab kogu talletamisteenuses sisalduvat häältega seotud infot.\n\nJäreltingimus\n'''''''''''''\n\nE-valimiskast on eksporditud.\n\n\nTöötleja - E-valimiskasti verifitseerimine\n``````````````````````````````````````````\n\nKirjeldus\n'''''''''\n\nTöötleja verifitseerib Kogujalt saadud e-valimiskasti kooskõlalisust ning selle vastavust Registreerimisteenuselt saadud informatsioonile.\n\nEeltingimus\n'''''''''''\n\n- E-valimiskast on väljastatud\n- Registreerimiskinnitused on väljastatud\n\nPäästik\n'''''''\n\nTöötleja käivitab vastava funktsionaalsuse Töötlemisrakenduse kasutajaliideses.\n\nPõhiprotsess\n''''''''''''\n\n#. Töötlemisrakendus laadib seadistused\n#. Töötlemisrakendus kontrollib seadistuste digitaalallkirja\n#. Töötlamisrakendus kontrollib seadistuste kooskõlalisust\n#. Töötlemisrakendus laadib e-valimiskasti\n#. Töötlemisrakendus kontrollib e-häälte digitaalallkirja\n#. Töötlemisrakendus laadib registreerimiskinnitused\n#. Töötlemisrakendus verifitseerib registreerimiskinnitused\n#. Töötlemisrakendus kontrollib e-valimiskasti ja registreerimiskinnituste kooskõlalisust\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Kasutajale väljastatakse  teade veasituatsiooni kohta ning protsess peatatakse.\n- Kasutajal on võimalus viia protsess läbi selliselt et vigadega seotud Hääletajad eraldatakse muust e-valimiskastist. Tulemuseks on probleemsete häälte raport ja puhastatud e-valimiskast.\n\nJäreltingimus\n'''''''''''''\n\nE-valimiskasti verifitseerimise raport identifitseerib üheselt korrektsed ja probleemsed hääled.\n\n\nTöötleja - Elektrooniliselt hääletanute nimekirja koostamine\n````````````````````````````````````````````````````````````\nKirjeldus\n'''''''''\nPeale hääletamisperioodi lõppu koostab Töötleja valimisjaoskondade kaupa nimekirjad elektrooniliselt hääletanud isikutest. Nimekirjad on nii inim- kui masinloetavad.\n\nEeltingimus\n'''''''''''\n\nE-valimiskast on edukalt verifitseeritud.\n\nPäästik\n'''''''\nTöötleja käivitab vastava funktsionaalsuse Töötlemisrakenduse kasutajaliideses.\n\nPõhiprotsess\n''''''''''''\n1. Töötlemisrakendus tühistab korduvad hääled, jättes iga Hääletaja kohta alles ajaliselt kõige viimasena antud hääle.\n2. Töötlemisrakendus sorteerib kehtivad hääled valimisjaoskondade kaupa.\n3. Töötlemisrakendus loob elektrooniliselt hääletanute nimekirjade faili.\n\n    1. Iga kehtiva hääle kohta lisab Töötlemisrakendus faili ühe kirje, kus sisaldub isikukood, nimi, valimisjaoskonna number ja rea number valimisringkonna nimekirjas. Andmed võetakse valijate nimekirjast.\n\n4. Töötlemisrakendus esitab vastuvõetud häälte koguarvu ning elektrooniliselt hääletanute arvu.\n5. Töötleja salvestab nimekirja välisele andmekandjale.\n\nLaiendid\n''''''''\nKui põhiprotsessi sammus 3 ei tuvastata isikukoodi valijate nimekirjast, siis logitakse vastavasisuline informatsioon ning jätkatakse põhiprotsessiga.\n\nJäreltingimus\n'''''''''''''\nKehtivad hääled on  kantud e-hääletanute nimekirja. Korduvad hääled on tühistatud.\n\nTöötleja - Miksimisele/Lugemisele minevate e-häälte anonümiseerimine\n````````````````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\n\nTöötleja rakendab tühistus- ja ennistusnimekirju ning loob lugemisele minevate e-häälte anonümiseeritud hulga.\n\nEeltingimus\n'''''''''''\n\nE-valimiskast on edukalt verifitseeritud ning elektrooniliselt hääletanute nimekiri koostatud.\n\nPäästik\n'''''''\n\nTöötleja käivitab vastava funktsionaalsuse Töötlemisrakenduse kasutajaliideses.\n\nPõhiprotsess\n''''''''''''\n\n    Töötlemisrakendus verifitseerib tühistus- ja ennistusnimekirjade digitaalallkirjad.\n    Töötlemisrakendus kontrollib tühistus- ja ennistusnimekirjade kooskõlalisust järjekorras.\n    Töötlemisrakendus rakendab tühistus- ja ennistusnimekirju järjekorras.\n    Töötlemisrakendus koostab miksimisele/lugemisele minevate häälte nimekirja eraldades krüptogrammid digitaalallkirjadest.\n    Töötlemisrakendus väljastab lõpliku elektrooniliselt hääletanute nimekirja masinloetavas vormingus.\n\nLaiendid\n''''''''\n\n - Kui põhiprotsessi sammus 1 Korraldaja allkirja verifitseerimine või volituse kontroll ebaõnnestuvad, siis väljastatakse veateade Ühtegi muudatust ei rakendata.\n\nJäreltingimus\n'''''''''''''\n\nMiksimisele/lugemisele minevate häälte hulk on koostatud, elektrooniliselt hääletanute nimekiri on väljastatud.\n\nMiksija - Miksimine\n```````````````````\n\nKirjeldus\n'''''''''\n\nEeltingimus\n'''''''''''\n\nPäästik\n'''''''\n\nPõhiprotsess\n''''''''''''\n\n#. Miksija käivitab Miksimisrakenduse ning laadib anonüümistatud e-hääled\n#. Miksimisrakendus rerandomiseerib ning permuteerib e-hääled, tulemuseks on uued e-hääled\n#. Miksimisrakendus genereerib tõestuse, et uued e-hääled on algsete e-häältega sisuliselt samaväärsed\n#. Miksimisrakendus väljastab nii miksitud hääled kui Miksimistõendi\n\nLaiendid\n''''''''\n\nJäreltingimus\n'''''''''''''\n\nAlgsete häältega tõestatavalt samaväärsed, miksitud hääled on väljastatud.\n\nLugemisetapp\n------------\n\nLugeja - Elektroonilise hääletamise tulemuse kindlaks tegemine\n``````````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nTühistusperioodi lõppedes sorteeritakse ümbrikud valimisringkondade kaupa. Välised ümbrikud avatakse. s.t. digitaalallkirjad eemaldatakse, järgi jäävad häälte salastamise võtmega krüpteeritud hääled, mis dekrüpteeritakse Võtmerakendusega.\n\nPõhiprotsess\n''''''''''''\n#. Lugeja algatab häälte kokku lugemise Võtmerakendusega.\n#. Võtmehaldurid aktiveerivad vastavalt võtmehalduse protseduuridele häälte avamise võtme.\n#. Võtmerakendus  loeb krüpteeritud hääled väliselt andmekandjalt.\n#. Võtmerakendus teostab häältefaili tehnilise kontrolli ning algatab häälte krüpteerimise.\n#. Võtmerakendus kontrollib dekrüpteeritud hääle vastavust avakujul hääle vormingule.\n#. Võtmerakendus kontrollib hääle kehtivust, veendudes et dekrüpteerimisel selgunud kandidaat kuulus antud ringkonnas valikute hulka.\n#. Võtmerakendus summeerib arvesse minevad hääled jaoskondade, ringkondade ja kandidaatide kaupa.\n#. Võtmerakendus kannab hääletamistulemuse andmekandjale.\n#. Hääletamistulemus imporditakse valimiste infosüsteemi.\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Kasutajale väljastatakse  teade veasituatsiooni kohta ning protsess peatatakse.\n- Põhiprotsessi sammudes 4 ja 5 tehtavate kontrollide ebaõnnestumise korral loetakse antud hääl kehtetuks ning protsessi jätkatakse järgmise krüpteeritud hääle juurest.\n\nJäreltingimus\n'''''''''''''\nHääletamistulemus on sisestatud valimiste infosüsteemi.\n\nAuditeerimisetapp\n-----------------\n\nAudiitor - Auditeerimine\n````````````````````````\n\nKirjeldus\n'''''''''\n\nPeale hääletamistulemuse väljaselgitamist on Audiitoril võimalik kontrollida Miksimistõendit ja Lugemistõendit.\n\n\nEeltingimus\n'''''''''''\n\n- Hääletamistulemus, krüptogrammid ning lugemistõend on olemas\n- Miksimiseelsed krüptogrammid, miksimisjärgsed krüptogrammid ning miksimistõend on olemas\n\nPõhiprotsess\n''''''''''''\n\n1. Audiitor kasutab Auditirakendust Lugemistõendi kontrollimiseks\n2. Audiitor kasutab Auditirakendust Miksimistõendi kontrollimiseks\n\nLaiendid\n''''''''\n\nJäreltingimus\n'''''''''''''\n\nLugemistõend ja Miksimistõend on kontrollitud.\n\n\nTehnilised kasutusmallid\n------------------------\n\nAutentimine Valijarakenduses\n````````````````````````````\n\nKirjeldus\n'''''''''\nValijarakenduse vahendusel ja elektroonilise isikutunnistuse abil tuvastab Kogumisteenus Hääletaja isiku.\n\nPäästik\n'''''''\nKasutusmall käivitatakse kui Hääletaja käivitab Valijarakenduse ning suundub hääletama.\n\nPõhiprotsess – ID kaart\n'''''''''''''''''''''''\n1. Hääletaja sisestab ID-kaardi lugejasse.\n2. Valijarakendus pöördub Kogumisteenuse poole protokolli algatamiseks.\n3. Kogumisteenus saadab Valijarakendusele oma sertifikaadi.\n4. Valijarakendus kontrollib Kogumisteenuse sertifikaati.\n5. Kogumisteenus nõuab Hääletaja autentimist TLS-protokolli kohaselt.\n6. Valijarakendus küsib valijalt ID-kaardi autentimisvõtme kasutamiseks PIN1 koodi.\n7. Hääletaja sisestab PIN1 koodi.\n8. Valijarakendus ja Kogumisteenus viivad läbi TLS-protokolli, Kogumisteenuse saadetakse Hääletaja sertifikaat.\n9. Kogumisteenus kontrollib Hääletaja sertifikaati.\n10. Kogumisteenus tuvastab Hääletaja isikukoodi.\n\nPõhiprotsess – Mobiil-ID\n''''''''''''''''''''''''\n1. Hääletaja sisestab Valijarakendusse oma Mobiil-ID SIM kaarti sisaldava mobiiltelefoni numbri.\n2. Valijarakendus pöördub Kogumisteenuse poole protokolli algatamiseks.\n3. Kogumisteenus saadab Valijarakendusele oma sertifikaadi.\n4. Valijarakendus kontrollib Kogumisteenuse sertifikaati.\n5. Valijarakendus saadab telefoninumbri Kogumisteenusele.\n6. Kogumisteenus algatab autentimise Mobiil-ID teenuse kaudu.\n7. Kogumisteenus saadab Mobiil-ID kontrollkoodi Valijarakendusele, mis kuvab seda valijale.\n8. Hääletaja saab mobiiltelefonile autentimissõnumi.\n9. Hääletaja võrdleb autentimissõnumi kontrollkoodi Valijarakenduses kuvatavaga.\n10. Hääletaja sisestab autentimisvõtme kasutamiseks PIN1 koodi.\n11. Kogumisteenus ja Mobiil-ID teenus viivad läbi autentimise, Kogumisteenuse saadetakse Hääletaja sertifikaat.\n12. Kogumisteenus kontrollib Hääletaja sertifikaati.\n13. Kogumisteenus tuvastab Hääletaja isikukoodi.\n14. Valijarakendus küsib regulaarselt, kas autentimine on toimunud, Kogumisteenus vastab.\n\nLaiendid\n''''''''\n- Kui ID-kaarti ei ole lugejas, siis katkestab Valijarakendus töö veateatega (ID-kaart).\n- Kui Kogumisteenus ei ole kättesaadav, siis katkestab Valijarakendus töö veateatega.\n- Kui Kogumisteenuse sertifikaadi kehtivust ei õnnestu kontrollida või kui sertifikaat ei vasta Kogumisteenuse nimele, siis katkestab Valijarakendus töö veateatega.\n- Kui ID-kaardi kasutamine ebaõnnestub, siis katkestab Valijarakendus töö veateatega. (ID-kaart)\n- Kui Kogumisteenuses annab Hääletaja sertifikaadi kontroll negatiivse tulemuse, siis saadetakse Valijarakendusele veateade ning autentimine katkestatakse.\n- Kui Mobiil-ID protokolli läbiviimine ebaõnnestub, siis vastab Kogumisteenus Valijarakenduse päringule veateatega ning autentimine katkestatakse. (Mobiil-ID)\n\nJäreltingimus\n'''''''''''''\nKogumisteenus teab Hääletaja sertifikaati ning on tuvastanud sellest isikukoodi.\n\nDigitaalne allkirjastamine Valijarakenduses\n```````````````````````````````````````````\n\nKirjeldus\n'''''''''\nValijarakenduse abil antakse digitaalallkirja vahendi abil krüpteeritud häälele digitaalallkiri.\n\nEeltingimus\n'''''''''''\nHääletaja on teinud oma valikud ja kinnitanud need. Valijarakendus on valikud krüpteerinud Kogumisteenuse avaliku võtmega.\nMobiil-ID korral teab Kogumisteenus juba Hääletaja Mobiil-ID SIM kaardiga mobiiltelefoninumbrit, kuna autentimine on edukalt toimunud.\n\nPäästik\n'''''''\nHääletaja on valinud Valijarakenduses hääletamise funktsionaalsuse, kõik valikud on krüpteeritud Kogumisteenuse avaliku võtmega.\n\nPõhiprotsess – ID-kaart\n'''''''''''''''''''''''\n1. Valijarakendus moodustab BES vormingus BDOC konteineri.\n2. Valijarakendus lisab konteinerisse andmefailidena krüpteeritud valikud.\n3. Valijarakendus moodustab allkirjastatava räsi BDOC standardi kohaselt.\n4. Valijarakendus algatab räsi signeerimise ID-kaardiga.\n5. Hääletaja sisestab PIN2 koodi allkirjastamisvõtme kasutamiseks.\n6. Valijarakendus lisab Hääletaja signatuuri ja allkirjastamissertifikaadi BDOC konteinerile.\n\nPõhiprotsess – Mobiil-ID\n''''''''''''''''''''''''\n1. Valijarakendus moodustab BES vormingus BDOC konteineri.\n2. Valijarakendus lisab konteinerisse andmefailidena krüpteeritud valikud.\n3. Valijarakendus saadab konteineri Kogumisteenusele.\n4. Kogumisteenus moodustab allkirjastatava räsi BDOC standardi kohaselt.\n5. Kogumisteenus algatab räsi signeerimise Mobiil-ID teenusega.\n6. Kogumisteenus saadab Mobiil-ID kontrollkoodi Valijarakendusele, mis kuvab seda valijale.\n7. Hääletaja saab mobiiltelefonile allkirjastamissõnumi.\n8. Hääletaja võrdleb allkirjastamissõnumi kontrollkoodi Valijarakenduses kuvatavaga.\n9. Hääletaja sisestab allkirjastamisvõtme kasutamiseks PIN2 koodi.\n10. Kogumisteenus ja Mobiil-ID teenus viivad läbi allkirjastamise, Kogumisteenuse saadetakse digitaalselt allkirjastatud ja kehtivuskinnitusega TS vormingus BDOC konteiner.\n11. Valijarakendus küsib regulaarselt, kas allkirjastamine on toimunud, Kogumisteenus vastab.\n12. TS vormingus BDOC konteiner edastatakse Valijarakendusele verifitseerimiseks.\n\nLaiendid\n''''''''\n- Kui ID-kaarti ei ole lugejas, siis katkestab Valijarakendus töö veateatega (ID-kaart).\n- Kui Kogumisteenus ei ole kättesaadav, siis katkestab Valijarakendus töö veateatega.\n- Kui ID-kaardi kasutamine ebaõnnestub, siis katkestab Valijarakendus töö veateatega. (ID-kaart)\n- Kui Mobiil-ID protokolli läbiviimine ebaõnnestub, siis vastab Kogumisteenus Valijarakenduse päringule veateatega ning allkirjastamine katkestatakse. (Mobiil-ID)\n\nJäreltingimus\n'''''''''''''\nHääletaja hääl on digitaalselt allkirjastatud.\n\nDigitaalallkirja kehtivuse kontroll Kogumisteenuses\n```````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nKogumisteenus kontrollib digitaalallkirju ID-kaardi ja Mobiil-ID'ga allkirjastatud e-häältel ning seadistamisel ja tühistamisel kasutatavatel sisendfailidel.\n\nEeltingimus\n'''''''''''\nKogumisteenusede sertifikaatide konfiguratsioon on tehtud. Kogumisteenuse kehtivuskinnitusteenuse konfiguratsioon on tehtud.\n\nPäästik\n'''''''\nKogumisteenus algatab digitaalallkirja kehtivuse kontrolli.\n\nPõhiprotsess\n''''''''''''\n1. Kogumisteenus tuvastab profiili, millest lähtudes kontroll läbi viia:\n\n    1. ID-kaardiga allkirjastatud hääl võib olla BES või TS vormingus, tohib sisaldada mitut andmefaili ja täpselt ühte allkirja.\n    2. Mobiil-ID'ga allkirjastatud hääl peab olema TS vormingus, tohib sisaldada mitut andmefaili ja täpselt ühte allkirja.\n    3. Muud failid peavad olema TS vormingus, tohivad sisaldada täpselt ühte andmefaili ja täpselt ühte allkirja.\n\n2. Kogumisteenus veendub allkirja vastavuses nõutud profiilile.\n3. Kui allkiri on BES vormingus, siis pöördub Kogumisteenus kehtivuskinnitusteenuse poole sertifikaadi kehtivuse kontrolliks. Kogumisteenus liidab kehtivuskinnituse digitaalallkirjale, tulemuseks on TS vormingus allkiri.\n4. Kogumisteenus kontrollib TS vormingus allkirja:\n\n    1. Signatuuri.\n    2. Sertifikaadi kehtivuskinnitust.\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Välja kutsuvale protsessile väljastatakse  teade veasituatsiooni kohta.\n- Kehtivuskinnituse laadimise ebaõnnestumisel põhiprotsessi sammus 3 logitakse sündmus tehniliste vigade logisse. Välja kutsuvale protsessile väljastatakse  teade veasituatsiooni kohta.\n- Kontrollide ebaõnnestumisel põhiprotsessi sammus 4 logitakse sündmus tehniliste vigade logisse. Välja kutsuvale protsessile väljastatakse teade veasituatsiooni kohta.\n\nValikute nimekirjade väljastamine Valijarakendusele\n```````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nKogumisteenus väljastab Valijarakendusele valikute nimekirja.\n\nEeltingimus\n'''''''''''\nHääletaja autentimine on õnnestunult toimunud.\n\nPäästik\n'''''''\nValijarakendus on pöördunud Kogumisteenuse poole valikute nimekirja alla laadimiseks.\n\nPõhiprotsess\n''''''''''''\n1. Kogumisteenus kontrollib isikukoodi kasutades, millisesse valimisringkonda Hääletaja kuulub.\n2. Kogumisteenus kontrollib, kas Hääletaja kohta on juba mõni hääl talletatud.\n3. Kogumisteenus saadab Valijarakendusele ringkonnakohase kandidaatide nimekirja kõigi valimise kohta, kus isikukood on nimekirja kantud ning informatsiooni võimaliku korduvhääletamise kohta.\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Valijarakendust teavitatakse protsessi ebaõnnestumisest.\n- Kui põhiprotsessi sammus 2 on selge, et Hääletaja ei kuulu käimasoleva valimise valijate nimekirja, siis saadetakse Valijarakendusele vastavasisuline veateade.\n\nHääle talletamine Kogumisteenuses\n`````````````````````````````````\n\nKirjeldus\n'''''''''\nKogumisteenus talletab Hääletaja hääle.\n\nEeltingimus\n'''''''''''\nHääle digitaalne allkirjastamine Valijarakenduses on edukalt toimunud.\n\nPäästik\n'''''''\nValijarakendus on saatnud hääle talletamisele.\n\nPõhiprotsess\n''''''''''''\n1. Kogumisteenus kontrollib hääle digitaalallkirja (tehniline kasutusmall Digitaalallkirja kehtivuse kontroll Kogumisteenuses). Kontrolli käigus tagatakse muu hulgas kehtivuskinnituse lisamine allkirjale.\n2. Kogumisteenus kontrollib hääle allkirjastanud ja valikute nimekirja laadimisel TLS protokollis autenditud isikukoodide samasust.\n3. Kogumisteenus registreerib hääle vastuvõtmise Registreerimisteenuses.\n4. Kogumisteenus genereerib häälega seostatud unikaalse identifikaatori.\n5. Kogumisteenus salvestab hääle koos unikaalse identifikaatoriga.\n6. Kogumisteenus tagastab Valijarakendusele õnnestumise teate koos häälele viitava unikaalse identifikaatoriga ja Registreerimisteenuse kinnitusega\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Valijarakendust teavitatakse protsessi ebaõnnestumisest. Häält ei talletata.\n- Kui põhiprotsessi sammus 1 digitaalallkirja kontroll ebaõnnestub, siis logitakse sündmus tehniliste vigade logisse. Valijarakendust teavitatakse protsessi ebaõnnestumisest. Häält ei talletata.\n- Kui põhiprotsessi sammus 2 isikukoodid erinevad, siis logitakse sündmus tehniliste vigade logisse. Valijarakendust teavitatakse protsessi ebaõnnestumisest. Häält ei talletata.\n\nJäreltingimus\n'''''''''''''\nDigitaalselt allkirjastatud hääl on varustatud kehtivuskinnitusega, Kogumisteenuse poolse kinnitusega ning talletatud Kogumisteenuses seostatuna unikaalse identifikaatoriga.\n\nHääle väljastamine kontrollimiseks Kogumisteenusest\n```````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nKogumisteenus väljastab Kontrollrakendusele kontrollimiseks allkirjastatud hääle ja registreerimiskinnituse.\n\nPäästik\n'''''''\nKontrollrakendus on esitanud Kogumisteenusele unikaalse identifikaatoriga hääle kontrollimise päringu.\n\nPõhiprotsess\n''''''''''''\n1. Kogumisteenus kontrollib, kas unikaalsele identifikaatorile vastab elektrooniline hääl.\n2. Kogumisteenus kontrollib, kas unikaalsele identifikaatorile vastava hääle kontrollimine on veel võimalik vastavalt aja ja kordade tingimustele.\n3. Kogumisteenus suurendab konkreetse hääle kontrollimise kordade loendurit.\n4. Kogumisteenus väljastab Kontrollrakendusele unikaalse identifikaatori poolt viidatud hääle koos ringkonna valikute nimekirjaga ning häälele vastava registreerimiskinnitusega.\n\nLaiendid\n''''''''\n- Tehniliste vigade tekkimisel kõigis põhiprotsessi sammudes logitakse sündmus tehniliste vigade logisse. Kontrollrakendust teavitatakse protsessi ebaõnnestumisest. Häält ei väljastata.\n- Kui põhiprotsessi sammus 1 ei leita unikaalsele identifikaatorile vastavat häält, siis logitakse sündmus tehniliste vigade logisse. Kontrollrakendust teavitatakse protsessi ebaõnnestumisest. Häält ei väljastata.\n- Kui põhiprotsessi sammus 2 tehtavad kontrollid annavad negatiivse tulemuse, siis logitakse sündmus tehniliste vigade logisse. Kontrollrakendust teavitatakse protsessi ebaõnnestumisest. Häält ei väljastata.\n\nElektrooniliste häälte avalduse alusel tühistamine/ennistamine\n``````````````````````````````````````````````````````````````\n\nKirjeldus\n'''''''''\nKorraldaja poolt kinnitatud digitaalallkirjastatud tühistus-/ennistuskannete faili import Töötleja poolt. Töötlemisrakendus teostab tühistused/ennistused, logides tegevused. Töötlejale kuvatakse tühistamise/ennistamise tulem – aruanne tühistustest/ennistustest koos tulemusega.\n\nPäästik\n'''''''\n\nTöötlemisrakendus käivitab vastava funktsionaalsuse.\n\nPõhiprotsess\n''''''''''''\n#. Töötlemisrakendus verifitseerib avalduse digitaalallkirja.\n#. Töötlemisrakendus kontrollib, et avaldus oleks allkirjastatud volitatud kasutaja poolt.\n#. Töötlemisrakendus loeb failist kandeid ning viib operatsioonid täide märkides hääle kas tühistatuks või ennistatuks.\n#. Kogumisteenus kuvab avalduse rakendamise tulemusi.\n\nLaiendid\n''''''''\n- Kui põhiprotsessi sammus 1 tuvastatakse digitaalallkirja mittekehtivus, siis logitakse tehniliste vigade logisse vastav kanne ning protsess peatatakse.\n- Kui põhiprotsessi sammus 2 tuvastatakse, et avalduse allkirjastajal ei ole vastavaid volitusi, siis logitakse tehniliste vigade logisse vastav kanne ning protsess peatatakse.\n- Kui põhiprotsessi sammus 3 tuvastatakse, et isikukoodiga identifitseeritud Hääletaja hääl on mittesobivas olekus või Hääletaja ei ole hääletanud, siis logitakse aruandesse vastav kanne ning protsessi jätkatakse järgmise kandega.\n- Tehniliste vigade tekkimisel kõigis põhivoo sammudes logitakse sündmus tehniliste vigade logisse. Kasutajale väljastatakse  teade veasituatsiooni kohta ning protsess peatatakse.\n\nJäreltingimus\n'''''''''''''\nTühistatud hääled ei lähe lugemisele, ennistatud hääled lähevad lugemisele.\n"
  },
  {
    "path": "Documentation/et/kasutusmall/sissejuhatus.rst",
    "content": "..  IVXV kasutusmallid\n\nSissejuhatus\n============\n\nDokument kirjeldab elektroonilise hääletamise süsteemi kasutusmallimudeli. Dokument on jaotatud kaheks osaks: tegijad ja kasutusmallid. Esimeses osas on kirjeldatud kasutusmallimudeli tegijad. Teises osas on kirjeldatud elektroonilise hääletamise süsteemi kasutusmallid.\n\nViited\n------\n1.  [UML] – UMLi kontsentraat. Objektmodelleerimise standardkeele UML 2.0 lühijuhend. Martin Fowler. Cybernetica AS.\n\nKasutatud metoodika\n-------------------\n\nKasutusmallimudel\n`````````````````\n    Kasutusmallid on abivahend, mis aitab aru saada süsteemi talitlusnõuetest. Kasutusmallid esitavad süsteemi välisvaate kirjeldades süsteemi kasutajate ja süsteemi vahelisi interaktsioone kasutajate eesmärkide täitmiseks. Kasutusmallimudel koondab endas kõik analüüsitava süsteemi olulised kasutusmallid [UML].\n\nTegija\n``````\n    Tegija on roll, mida kasutaja mängib süsteemi suhtes. Tegija ei pea olema inimene. Kui modelleeritav süsteem annab mingit teenust teisele arvutisüsteemile, on see teine süsteem tegija.\n\nKasutusmall\n```````````\n\nKirjeldus\n'''''''''\n\nKasutusmall on kogum stsenaariume, mida omavahel ühendab ühine kasutaja eesmärk. Iga stsenaarium on süsteemis tehtavate toimingute jada, mis annab tegijale nähtava ja kasuliku tulemuse.\n\nEeltingimus\n'''''''''''\n\nEeltingimus kirjeldab tingimusi, mille täidetuse peab süsteem tagama, enne kui ta laseb alustada kasutusmalli täitmist.\n\nPäästik\n'''''''\n\nPäästik spetsifitseerib sündmuse, mis käivitab kasutusmalli.\n\nPõhiprotsess\n''''''''''''\n\nPõhiprotsess kirjeldab kasutusmalli peastsenaariumi.\n\nLaiendid\n''''''''\n\nLaiend kasutusmallis nimetab oleku, mis tuleneb teistsugustest interaktsioonidest kui need, mis on kirjeldatud õnnestumise peastsenaariumis, ja ütleb, millised on need erinevused.\n\nJäreltingimus\n'''''''''''''\n\nJäreltingimuse täidetus garanteeritakse süsteemi poolt kasutusmalli täitmise lõpus.\n\nE-hääletamise etapid\n--------------------\n\nE-hääletamine jaguneb korralduslikult viieks etapiks:\n\n- hääletamiseelne etapp\n- hääletamisetapp\n- töötlusetapp\n- lugemisetapp\n- auditeerimisetapp\n\nMõisted\n-------\n\nMõiste *isik* võib tähistada nii füüsilist kui juriidilist isikut. Eeldame, et konkreetsete toimingute läbiviija on alati üheselt indentifitseeritav füüsiline isik, kes võib tegutseda juriidilise isiku volitatud esindajana.\n\nMõiste *serverisüsteem* tähistab terviklikku kogumit tarkvaralisi ja riistvaralisi komponente, mis koostoimes realiseerivad konkreetset protokolli ning osutavad pikema aja vältel teenust paljudele kasutajatele.\n\nMõiste *liides* tähistab selgelt spetsifitseeritud kokkupuutepunkti süsteemi komponentide vahel, mis võimaldab komponentide vahelist infovahetust.\n\nMõiste *rakendus* tähistab tarkvaralist komponenti, mida käivitatakse konkreetsel ajahetkel ühel riistvaralisel komponendil ühe kasutaja poolt. Rakendus võib oma ülesande täitmiseks suhelda servrisüsteemiga.\n\nMõiste *teenus* tähistab süsteemivälist komponenti, millega süsteem oma ülesannete täitmiseks konkreetsete liideste kaudu andmeid vahetab.\n"
  },
  {
    "path": "Documentation/et/kasutusmall/tegijad.rst",
    "content": "..  IVXV kasutusmallid\n\nTegijad\n=======\n\nKorraldaja\n----------\n\n*Korraldaja* on isik, kes korraldab valimisi mille raamides elektroonilise hääletamise süsteemi kasutatakse. Korraldaja määrab elektroonilise hääletamise seadistuse, sealhulgas kõik ülejäänud rollitäitjad. Korraldaja haldab Kogumisteenust sisuliselt.\n\nKorraldaja tegeleb hääletamiseelsel etapil valijate, valimisjaoskondade, ringkondade ja valikute- kandidaatide (valimistel) või vastusevariantide (rahvahääletusel) nimekirja koostamise ja/või kinnitamisega.\n\nKorraldaja loob ja/või kinnitab Valimisrakenduse ning Kontrollrakenduse seadistused.\n\nKorraladaja tegeleb hääletamisetapil valijate nimekirjade täiendamise ja/või täienduste kinnitamisega.\n\nKorraldaja tegeleb töötlusetapil e-häälte tühistamis- ja ennistamisavalduste koostamise ja/või kinnitamisega.\n\nKorraldaja võib täita ka elektroonilise hääletamise põhiprotsesside toimimisega seotud konkreetseid rolle. Näiteks hoiab Korraldaja tavajuhtumitel ka e-hääletamise süsteemi põhisaladust – häälte avamise võtit ning seega täidab ta ka häälte avaja ja summeerija ehk Lugeja rolli.\n\nHääletaja\n---------\n\n*Hääletaja* on isik, kes kasutab elektroonilise hääletamise süsteemi hääletamisetapil\n\n- hääletamiseks Valijarakendusega,\n- hääle kontrollimiseks Kontrollrakendusega.\n\nHääletajal on elektrooniline isikutunnistus – ID-kaart, Mobiil-ID, Smart-ID või Web eID.\n\nKoguja\n------\n\n*Koguja* on isik, kes haldab Kogumisteenust, Valijarakendust ja Kontrollrakendust tehniliselt.\n\n*Kogumisteenus* on serverisüsteem, mis tuvastab hääleõiguslikkust Tuvastusteenuse abil, väljstab kandidaatide nimekirja, aitab Valijarakendusel Allkirjastamisteenuse abil e-häält moodustada, talletab hääli e-valimiskasti ning registreerib neid Registreerimisteenuses. Kogumisteenus vastab Kontrollrakenduse poolt tehtud hääle tervikluse kontrollpäringutele.\n\n*Kogumisteenuse Haldur* on kas Korraldaja, Koguja või Klienditugi. Koguja on Kogumisteenuse tehniline haldur. Kogumisteenuse sisuline haldur on Korraldaja. Kogumisteenus pakub informatiivset liidest Klienditoele.\n\n*Haldusteenus* on teenus, mida Kogumisteenuse Haldur kasutab Kogumisteenuselt informatsiooni saamiseks või Kogumisteenusele korralduste edastamiseks.\n\n*Valijarakendus* on rakendus, mida Hääletaja kasutab e-hääle andmiseks käimasolevatel valimistel. Valijarakendus suhtleb Kogumisteenusega ning võimaldab Hääletajal teha valikut, seda krüpteerida ja digitaalselt allkirjastada. Valijarakendus kuvab QR-koodi, mille alusel saab Hääletaja Kontrollrakendusega kontrollida e-hääle korrektset jõudmist Kogumisteenusesse.\n\nKoguja valmistab kompileeritud valijarakendusest ette seadistatud valijarakenduse, mis allkirjastatakse ja levitatakse valijateni.\n\n*Kontrollrakendus* on rakendus, mis võimaldab Hääletajal arvutist erineval nutiseadme platvormil veenduda, et tema e-hääl jõudis Kogumisteenusesse ja Registreermisteenusesse ning väljendas tema tahet korrektselt.\n\nKoguja seadistab kontrollrakenduse selliselt, et too on võimeline võrgust tegelikke seadistusi laadima ning allkirjastab kontrollrakenduse.\n\nKoguja on Valijarakenduse ja Kontrollrakenduse tehniline haldur, sisuline haldur on Korraldaja.\n\nKoguja poolt täidetavad põhiprotsessid leiavad aset hääletamisetapil, alustavad ning lõpetavad tegevused toimuvad nii hääletamiseelsel kui töötlusetapil.\n\nKoguja digiallkirjastab hääletamisperioodi lõpul Töötlejale üle antavad andmed (e-hääled ja logid).\n\nTöötleja\n--------\n\n*Töötleja* on isik, kes kasutades Töötlemisrakendust töötleb töötlusetapil hääletamisperioodil kogutud e-hääli:\n\n- kontrollib digitaalallkirju ja Kogujalt saadud andmete täielikkust,\n- tühistab korduvad e-hääled ning paralleelhääletamise kasutamisel ka nende Hääletajate e-hääled, kes hääletasid jaoskonnas eelhääletamise ajal,\n- anonüümistab e-hääled, eemaldades nendelt isikulised digitaalallkirjad, olles eelnevalt need sorteerinud ringkondade kaupa\n\nTöötleja võib e-hääli täiendavalt krüptograafiliselt anonüümistada kasutades Miksimisrakendust.\n\n*Töötlemisrakendus* on rakendus, mille abil kontrollitakse häälte individuaalset terviklust ja e-valimiskasti terviklust, tühistatakse hääli, väljastatakse hääletanute nimekirju ning ringkondade kaupa rühmitatud anonüümistatud hääli. Töötlemisrakenduse sisendi annavad Koguja, Registreerimisteenus ja Korraldaja. Töötlemisrakendust võib käitada ka Audiitor Töötleja töötulemuste kontrollimiseks.\n\nMiksija\n-------\n\n*Miksija* on isik, kes töötlusetapil anonüümistab e-hääled krüptograafiliselt kasutades Miksimisrakendust.\n\n*Miksimisrakendus* on rakendus, mille sisendiks on ringkondade kaupa rühmitatud anonüümistatud krüpteeritud hääled ning mis väljastab krüptograafiliselt segatud hääled selliselt, et neid ei ole võimalik sisendiga vastavusse viia. Miksimine toimub sellisel moel, et nii sisend- kui ka väljundhäälte dekrüpteerimine ja tulemi summeerimine annab sama resultaadi. Miksimirakendus väljastab lisaks segatud häältele miksimistõendi, mis kinnitab sisend- ja väljundhäälte semantilist samaväärsust.\n\nLugeja\n------\n\n*Lugeja* on isik, kes kasutades Võtmerakendust\n\n- hääletamiseelsel etapil genereerib häälte salastamise ja avamise võtme,\n- lugemisetapil avab krüpteeritud hääled ning summeerib need e-hääletamise tulemusteks.\n\nLugeja võib toimetada ainuisikuliselt, üldjuhul kaitstakse e-hääletamise salastamise võti läviskeemiga, kus ühe tervikvõtme asemel luuakse mitu võtmeosakut ning võtmeoperatsioonid on teostatavad ainult teatud kvoorumi võtmeosakute osavõtul. Sellisel juhul assisteerivad Lugejat Võtmehaldurid.\n\n*Võtmehaldur* on isik, kelle ülesandeks on talle usaldatud võtmeosaku alalhoidmine ning selle andmine Lugeja käsutusse ainult siis kui selleks on seaduslik alus.\n\n*Võtmerakendus* on rakendus, millega genereeritakse iga hääletamise jaoks häälte salastamise ja häälte avamise võti. Võtmerakenduse abil toimub ka häälte lugemine ja tulemuse väljastamine.\n\nAudiitor\n--------\n\n*Audiitor* on isik, kes auditeerimisetapil kontrollib süsteemi kirjelduse ja Korraldaja poolt avaldatud süsteemi kesksete osapoolte vahel liikunud andmete põhjal andmete terviklust ning kooskõlalisust. Audiitor kasutab oma töös Auditirakendust. Kui Audiitor kontrollib ka Töötleja toimimise korrektsust, siis kasutab Audiitor ka Töötlemisrakendust.\n\n*Auditirakendus* on rakendus mis võimaldab kontrollida Lugeja ja Miksija töö korrektsust. Lugeja töö korrektsust on võimalik kontrollida ka avalikult.\n\nKlienditugi\n-----------\n\n*Klienditugi* on isik, kelle poole Hääletaja hääletamisetapil probleemide korral pöördub. Klienditugi abistab Kogumisteenusest saadud info abil Hääletajat probleemide lahendamisel.\n\nTuvastusteenus\n--------------\n\n*Tuvastusteenus* on teenus, mida kasutatakse vajadusel hääletaja identiteedi tuvastamiseks.\n\nAllkirjastamisteenus\n--------------------\n\n*Allkirjastamisteenus* on teenus, mida kasutatakse vajadusel hääle\nallkirjastamisel ja sellele kehtivuskinnituse saamisel. Vajadus\nAllkirjastamisteenuse järele sõltub allkirja andmise vahendist - Mobiil-ID,\nSmart-ID, Web eID ja ID-kaardi korral on nende teenuste ülesehitus erinev.\n\nRegistreerimisteenus\n--------------------\n\n*Registreerimisteenus* on teenus, mille abil Kogumisteenus peab registreerima kõik Valijarakendustelt saadud hääled. Pärast hääletamisperioodi lõppu edastab teenuseosutaja info registreeritud häälte kohta Töötlejale.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/algseadistamine.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nSüsteemi algseadistamine\n========================\n\nSüsteemi algseadistamine tähendab süsteemi paigaldamist ning seadistamist\nläbiviidavate valimiste tarbeks.\n\n.. _nouded-platvormile:\n\nNõuded kasutatavale platvormile\n-------------------------------\n\nKogumisteenus töötab platvormil ``Ubuntu 20.04 LTS (Focal Fossa)``.\n\nVäliste teenuste kaardistamine\n------------------------------\n\nKogumisteenuse poolt toetatavate ja läbiviidavas hääletamises kasutatavate\nväliste teenuste (Mobiil-ID, Smart-ID, OCSP jms) kaardistamise käigus koostatakse\nnimekiri välistest teenustest ja nendega andmevahetuseks vajalikest andmetest\n(võrguaadress, port jms).\n\nVäliste teenuste andmed on sisendiks kogumisteenuse tehnilise seadistuse\nkoostamisel (:ref:`seadistuste_koostamine`).\n\nVäliste teenuste kaardistamise tulemusena on kogumisteenuse osutajal olemas\nnimekiri kogumisteenuse poolt kasutatavatest välistest teenustest koos teenuste\nkasutamiseks vajalike parameetritega.\n\n\nTugiteenuste ettevalmistamine\n-----------------------------\n\nKogumisteenuse tugiteenusteks on:\n\n#. Tehnilise seire teenus;\n\n#. Logiseire teenus;\n\n#. Varundusteenus.\n\nTehnilise seire ettevalmistamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. important::\n\n   Kogumisteenuse osutaja peab kogumisteenuse töötamiseks eraldatud riistvara\n   jälgimiseks läbi viima riistvara tehnilist seiret.\n\nTehnilise seire teenus on `Zabbix <http://www.zabbix.com/>`_ tarkvaral põhinev\nseire- ja teavitussüsteem. Zabbix serveri paigaldab  ja seadistab\nkogumisteenuse osutaja iseseisvalt.\n\nTehnilisse seiresse võib hõlmata ka kogumisteenuse tarkvaralisi komponente nagu\nkirjeldatud lõigus \":ref:`etcd-zabbix`\".\n\nSeire toimimiseks on tarvis määrata seire eest vastutavad isikud ning tagada\nnende vahetu teavitamine seireprogrammi poolt avastatud kõrvalekalletest.\n\nLisaks standardsele tehnilisele seirele (teenusmasinate\nprotsessori-/kettakasutus jms.) viib kogumisteenuse haldusteenus läbi\nalamteenuste seiret ja teavitab tehnilise seire serverit avastatud\nkõrvalekalletest.\n\n.. todo::\n\n   Lisada haldusteenusele tehnilise seire teavituste tugi!\n\nTehnilise seire ettevalmistamise tulemusena on kogumisteenuse osutajal olemas\ntehnilise seire server, kuhu on paigaldatud seiretarkvara ning kus on\nkirjeldatud tehnilise seire eest vastutavad isikud ja nende teavitamise\nmeetodid.\n\n\nLogiseire ettevalmistamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLogiseire teenus koosneb rsyslog logiserverist koos analüüsi- ja\nvisualiseerimistarkvaraga (Log Monitor, `Grafana <https://grafana.com/>`_).\n\nLogiseire ettevalmistamine ja integreerimine kogumisteenusega on kirjeldatud\ndokumendis ``IVXV tegevuslogi\nseirelahendus``.\n\nLogiseire ettevalmistamise tulemusena on kogumisteenuse osutajal olemas\nlogiseire server, kuhu on paigaldatud logiseire tarkvara ning kus on\nkirjeldatud logiseire andmetele ligipääsevad isikud.\n\n\nVarunduse ettevalmistamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nVarundusteenus on kogumisteenuse osutaja poolt paigaldatud ja seadistatud\nvarundusserver, mis vastutab kogumisteenuse sisemises varundusserveris\nkoostatud varukoopiate säilimise eest.\n\nVarunduse ettevalmistamise tulemusena on kogumisteenuse osutajal olemas\nvarundusserver, mis on suuteline kogumisteenuse varundusliidese kaudu andmeid\nvarundama.\n\n\n.. _seadistuste_koostamine:\n\nKogumisteenuse seadistuste koostamine\n-------------------------------------\n\nKogumisteenuse seadistused koosnevad kolmest eraldiseisvast osast:\n\n#. **Usaldusjuure seadistus** sisaldab andmed seadistuste (kaasa arvatud\n   usaldusjuure enda) allkirjade kontrollimiseks ja nimekirja kogumisteenuse\n   haldurite volitustest.\n\n#. **Kogumisteenuse tehniline seadistus** määrab kogumisteenuse tehnilised\n   parameetrid, hääletuse läbiviimiseks kasutatavad teenused, samuti ka\n   kogumisteenuse koosseisu kuulvad alamteenused.\n\n#. **Valimiste seadistus** määrab ühe valimise seadistuse.\n\nSeadistuste koostamine on kirjeldatud dokumendis IVXV-JSK-\\* \"Elektroonilise hääletamise\ninfosüsteemi IVXV seadistuste koostamise juhend\". Kogumisteenusele\nrakendatavad seadistused peavad olema pakendatud ASiC-E konteinerisse ja olema\nsigneeritud volitatud kasutaja poolt.\n\nKogumisteenuse seadistuste koostamise tulemusena on kogumisteenuse osutajal\nolemas kogumisteenuse seadistamiseks vajalikud seadistuspakid. Kõik seadistused\non signeeritud isiku(te) poolt, kelle volitused on kirjeldatud usaldusjuure\nseadistustes või kelle volitused on määratud eraldiseisvate korralduste abil.\n\n\n.. _taristu-paigaldamine:\n\nKogumisteenuse taristu paigaldamine\n-----------------------------------\n\nKogumisteenuse taristu eraldatakse teenuse osutamiseks vastavalt koostatud\nseadistustele (:ref:`seadistuste_koostamine`).\n\nIgas teenusmasinas:\n\n#. peab olema seadistatud hostinimi (fail :file:`/etc/hostname`);\n\n#. peab olema paigaldatud SSH teenus (tarkvarapakk ``openssh-server``);\n\n#. peab olema paigaldatud tehnilise seire teenuse agent\n   (tarkvarapakk ``zabbix-agent``);\n\n#. peab olema tagatud õige kellaaeg\n   (näiteks õige kellaaja teenuse ``ntp`` abil).\n\n#. peab olema seadistatud nimelahendus, mis võimaldab kõikide teenusmasinate\n   aadresse lahendada;\n\n#. peab olema seadistatud Eesti lokaat koos UTF-8 kooditabeli toega\n   ``et_EE.UTF-8`` (kas tarkvarapakk ``locales`` koos nimetatud lokaadi\n   seadistamisega või tarkvarapakk ``locales-all``, mis paigaldab kõik toetatud\n   lokaadid).\n\n.. note::\n\n   Iga teenusmasina poolt kasutatav nimelahendus peab tagama, et suhtluseks\n   kasutatavate hostide nimed lahenduvad korrektselt.\n\n   Vältima peab olukordi, kus hostinimi lahendub mitmeks aadressiks või\n   teistele hostidele kättesaamatuks aadressiks.\n\n   Järgnev näide kirjeldab võimalikku olukorda failis :file:`/etc/hosts`, kus\n   operatsioonisüsteemi paigalduse järel on hostinimi ``ivxv123`` määratud kahele\n   liidesele. Sellise seadistuse puhul võib tekkida olukord, kus aadressile\n   ``ivxv123`` ühendusi vastu võtma seadistatud teenus hakkab kuulama kohalikul\n   liidesel ``127.0.0.1`` ja pole avaliku liidese ``192.168.10.1`` kaudu\n   teistele teenustele kättesaadav.\n\n   .. code-block:: text\n\n      # /etc/hosts\n      127.0.0.1     ivxv123\n      192.168.10.1  ivxv123\n\nKogumisteenuse taristu jaoks eraldatud hostidest tuleb koostada nimekiri, kus\non kirjas hosti asukohaks olev alamvõrk, hosti nimi, IP-aadress, SSH-serveri\navalik võti ja hostile plaanitud teenused.\n\nKogumisteenuse taristu nimekirja näide:\n\n.. code-block:: text\n\n   Valimiste infrastruktuuri andmed\n\n     Alamvõrk: zone1\n\n        IP-aadress: 172.16.238.10\n        Hostinimi: admin\n        SSH-serveri avalik võti:\n          ecdsa-sha2-nistp256 AAAAE2VjZHNhLX...SgtbbE= root@admin\n\n        IP-aadress: 172.16.238.41\n        Hostinimi: ivxv1\n        SSH-serveri avalik võti:\n          ecdsa-sha2-nistp256 AAAAE2VjZHNhLX...mN8ul0= root@ivxv1\n\n     Alamvõrk: zone2\n\n        IP-aadress: 172.16.100.63\n        Hostinimi: ivxv2\n        SSH-serveri avalik võti:\n          ecdsa-sha2-nistp256 AAAE2VjZHNhLXN...rtWT7A= root@ivxv2\n\nKogumisteenuse taristusse kuuluvad hostid tuleb lisada tehnilisse seiresse.\n\nKogumisteenuse taristu paigaldamise tulemusena on kogumisteenuse osutajal\nolemas dokumenteeritud platvorm kogumisteenuse paigaldamiseks ettenähtud\nkonfiguratsiooniga. Kõik taristusse kuuluvad (virtuaal)masinad on tehnilise\nseire teenuse poolt kättesaadavad ja nende seisundis pole tuvastatud probleeme.\n\n\nVõrgupääsude loomine\n--------------------\n\nKogumisteenuse paigaldamiseks ja seadistamiseks on vajalik seadustustele\nvastavate võrgupääsude olemasolu.\n\n\nSüsteemiülemad ja kasutajad\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n#. Kogumisteenuse süsteemiülemate arvutitest haldusteenusesse\n   (protokoll SSH, port 22);\n\n#. Haldusteenuse kasutajate arvutitest haldusteenusesse\n   (protokoll HTTPS, port 443);\n\n#. Kogumisteenuse süsteemiülemate arvutitest logiseire teenusesse\n   (protokoll SSH, port 22);\n\n#. Logiseire kasutajate arvutitest logiseire teenusesse\n   (protokoll HTTPS, port 443).\n\n\nTeenuste omavaheline suhtlus\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n#. Haldusteenusest kõikidesse mikroteenustesse\n   (protokoll SSH, port 22);\n\n#. Haldusteenusest logiseire teenusesse\n   (protokoll SSH, port 22);\n\n#. Kõigist mikroteenuste hostidest kõikidesse logikogumisteenustesse\n   (protokoll RELP, port 20514);\n\n#. Kõigist mikroteenuste hostidest kõikidesse välistesse logikogumisteenustesse\n   (sh, ka logiseire teenusesse)\n   (protokoll RELP, port 20514);\n\n#. Kõigist mikroteenuste hostidest logiseire teenusesse\n   (protokoll SSH, port 22);\n\n#. Kõigist logikogumisteenuste hostidest logiseire teenusesse\n   (protokoll SSH, port 22);\n\n#. Vahendusteenusest teistesse mikroteenustesse peale talletusteenuse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele);\n\n#. Nimekirjateenusest talletusteenustesse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele);\n\n#. Hääletamisteenusest talletusteenustesse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele);\n\n#. Kontrolliteenusest talletusteenustesse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele);\n\n#. Talletusteenusest teistesse talletusteenustesse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele);\n\n#. Mobiil-ID tugiteenusest välisesse Mobiil-ID teenusesse\n   (protokoll HTTP(S), port vastavalt tehnilisele seadistusele);\n\n#. Smart-ID tugiteenusest välisesse Smart-ID teenusesse\n   (protokoll HTTP(S), port vastavalt tehnilisele seadistusele);\n\n#. Teistest mikroteenustest Session status tugiteenusesse\n   (protokoll RPC, port vastavalt tehnilisele seadistusele);\n\n#. Hääletamisteenusest välisesse kvalifitseerimisteenusesse\n   (protokoll HTTP(S), port vastavalt tehnilisele seadistusele);\n\n#. Varundusteenusest haldusteenusesse\n   (protokoll SSH, port 22);\n\n#. Varundusteenusest logikogumisteenustesse\n   (protokoll SSH, port 22);\n\n#. Varundusteenusest talletusteenustesse\n   (protokoll SSH, port 22);\n\n\nHääletaja\n^^^^^^^^^\n\n#. Hääletaja kasutatavast seadmest vahendusteenusesse\n   (protokoll TLS, port vastavalt tehnilisele seadistusele, eeldatavalt 443).\n\n\nHaldusteenuse paigaldamine\n--------------------------\n\nHaldusteenuse paigaldamine toimub haldusteenuse hostil.\n\nHaldusteenuse paigaldamiseks tuleb kopeerida **kõik** kogumisteenuse\ntarkvarapakid haldusteenuse masina kataloogi :file:`/etc/ivxv/debs/`. Nendest pakkidest\npaigaldatakse haldusteenus, samuti kasutab haldusteenus neid pakke alamteenuste\npaigaldamiseks.\n\nHaldusteenuse sõltuvuste paigaldamine:\n\n.. include:: genereeritud-failid/haldusteenuse_soltuvuste_paigaldamine.inc\n\nHaldusteenuse paigaldamine:\n\n.. include:: genereeritud-failid/haldusteenuse_paigaldamine.inc\n\n.. important::\n\n   Haldusteenuse edasine kasutamine toimub haldusteenuse konto alt. Selleks\n   tuleb halduril luua SSH-ligipääs haldusteenuse kontole ``ivxv-admin``.\n   Soovitav on autentimine teha ID-kaardi põhiseks (vaata\n   :ref:`configure-ssh-idcard-auth`).\n\nHaldusteenuse paigaldamise tulemusena on kogumisteenuse osutajal teenuse\nhaldamiseks vajalik liides.\n\n\nHaldusteenuse seadistamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nOsad haldusteenuse protsessid käivitatakse korrapärase intervalliga cron\nteenuse abil. Nende protsesside puhul väljastatakse võimalik tõrkeinfo\nstandardväljunditesse ja cron edastab selle e-posti teel käivitaja konto\naadressile. Seetõttu tuleb haldusteenuse masinasse paigaldada meiliserver ja\nseadistada see nii, et kõigile masinas asuvatele kontodele (nt.\n``root@localhost``) saadetavad sõnumid edastatakse teenuse halduritele.\n\n\nKogumisteenuse taristu hõlmamine haldusesse\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusteenus kasutab kogumisteenuse haldamiseks SSH protokolli. Selleks, et\nhaldusteenusel oleks võimalik teisi teenushoste usaldada, tuleb\nhaldusteenusesse lisada hallatavate teenushostide SSH-serveri võtmed.\n\nNäide hosti ``ivxv1`` SSH-võtmete lisamisest haldusteenuse usaldatavate hostide\nhulka::\n\n   ivxv-admin@admin $ ssh-keyscan ivxv1 >> ~/.ssh/known_hosts\n   # ivxv1:22 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2\n   # ivxv1:22 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2\n   # ivxv1:22 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.2\n\nSelleks, et haldusteenusel oleks võimalik teenushostidesse tarkvara paigaldada,\ntuleb haldusteenuse kontole ``ivxv-admin`` luua SSH-ligipääs teenushostide\njuurkasutaja kontole.\n\n.. note::\n\n   Haldusteenus vajab juurkasutaja ligipääsu alamteenuse tarkvara\n   paigaldamiseks. Pärast edukat paigaldamist ühel hostil eemaldab\n   haldusteenus ligipääsu selle hosti juurkasutaja kontole.\n\nHaldusteenuse konto SSH-võtmepaari avalik võti asub kasutaja ``ivxv-admin``\nkodukataloogi all failis :file:`.ssh/id_ed25519.pub` ja see on genereeritud\nhaldusteenuse paigaldamise käigus. Vajadusel võib haldur selle võtme asendada\n(kuid see peab toimuma enne, kui võti on üle kantud hallatavatesse\nteenusmasinatesse).\n\nTeenusmasinas tuleb haldusteenuse konto SSH avalik võti panna faili\n:file:`/root/.ssh/authorized_keys`. See fail peab kuuluma juurkasutajale ja\nolema loetav ainult juurkasutaja poolt (faili pääsuõigused ``0600``).\n\nKogumisteenuse taristu haldusesse hõlmamise tulemusena on haldusteenusel\nusaldusväärne ligipääs kogumisteenuse taristusse kuuluvatele teenusmasinate\njuurkasutaja kontodele.\n\n\nLogiseire lahenduse ühendamine haldusteenusega\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLogiseire lahenduse kasutamise korral peab haldusteenusel olema ligipääs\nlogiseire lahendusele, et sealt kogutud statistikat alla laadida ja vajadusel\nvärskendada logiseire poolt analüüsitavaid logisid.\n\nSelleks, et haldusteenusel oleks usaldus logiseire teenuse vastu, tuleb\nhaldusteenusesse lisada logiseire teenuse hosti (käesolevas näites nimega\n``logmonitor``) SSH-serveri võtmed::\n\n   ivxv-admin@admin $ ssh-keyscan -t ecdsa logmonitor >> ~/.ssh/known_hosts\n\nSelleks, et haldusteenus pääseks logiseire kontole ligi, tuleb haldusteenuse\nkonto SSH avalik võti panna logiseire konto ``logmon`` volitatud võtmete faili\n:file:`~logmon/.ssh/authorized_keys`. See fail peab kuuluma logimonitori\nkasutajale ja olema loetav ainult selle kasutaja poolt (faili pääsuõigused\n``0600``).\n\nLogiseire lahenduse haldusteenusega ühendamise tulemusena on tegevuslogi\nseirelahendus haldusteenusele kättesaadav ning haldusteenusel on võimalik\nseirelahendusest statistikaandmeid laadida ning seirelahenduse andmehoidlasse\najakohaseid logiandmeid üle kanda.\n\n\nHaldusteenuse veebiliidese vaikimisi TLS-sertifikaadi asendamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusteenuse paigalduse käigus genereeritakse kasutajaliidese veebiserveri\nTLS-sertifikaat koos krüptovõtmega ja tugeva Diffie-Hellman grupifailiga (vaata\nhttps://weakdh.org/). Vajadusel on halduril võimalik need asendada.\n\nFailide asukohad:\n\n* Veebiserveri TLS-sertifikaadi võti:\n  :file:`/etc/ssl/private/ivxv-admin-default.key`\n\n* Veebiserveri TLS-sertifikaat: :file:`/etc/ssl/certs/ivxv-admin-default.crt`\n\n* Diffie-Hellmani grupifail: :file:`/etc/ssl/dhparams.pem`\n\nAsendatud failide rakendamiseks tuleb veebiserver taaskäivitada käsuga\n:command:`service apache2 restart` ja veenduda, et veebiliides töötab.\n\nHaldusteenuse veebiliidese vaikimisi TLS-sertifikaadi asendamise tulemusena\nkasutab haldusteenuse veebiliides turvalist sertifikaati.\n\n\nHaldusteenuse vaikimisi autentimissertifikaadi asendamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusteenuse paigalduse käigus genereeritakse haldusteenuse\nautentimissertifikaat koos krüptovõtmega (haldusteenus kasutab seda Valimiste\nInfosüsteemiga infovahetusel autentimiseks). Vajadusel on halduril võimalik\nneed asendada.\n\nFailide asukohad:\n\n* Autentimissertifikaadi võti: :file:`/etc/ssl/private/ivxv-admin-client.key`\n\n* Autentimissertifikaat: :file:`/etc/ssl/certs/ivxv-admin-client.crt`\n\nHaldusteenuse vaikimisi autentimissertifikaadi asendamise tulemusena kasutab\nhaldusteenus Valimiste Infosüsteemi autentimiseks turvalist sertifikaati.\n\nAutentimissertifikaadi testimine:\n\n.. code-block:: shell-session\n\n   $ openssl s_client -connect vis-address:443 \\\n     -cert /etc/ssl/certs/ivxv-admin-client.crt \\\n     -key /etc/ssl/private/ivxv-admin-client.key\n\n\nHaldusteenuse lähtestamine\n--------------------------\n\nHaldusteenuse lähtestamine toimub käsuga :ref:`ivxv-collector-init`. Selle\nkäigus puhastatakse haldusteenuse andmekataloogid ja lähtestatakse andmebaas.\n\n\nSeadistuste ja valimisnimekirjade rakendamine kogumisteenusele\n--------------------------------------------------------------\n\n.. note::\n\n   Käesolevas lõigus ja alamlõikudes tähendab \"seadistuspakk\" nii seadistusi\n   sisaldavat faili kui valimisnimekirja faili, mis on signeeritud volitatud\n   isiku poolt.\n\nKogumisteenusele tuleb rakendada järgmised seadistuspakid:\n\n#. Usaldusjuur – laaditakse alati esimesena;\n\n#. Kogumisteenuse tehniline seadistus – laaditakse enne valimiste seadistust;\n\n#. Valimiste seadistus – laaditakse enne nimekirju;\n\n#. Valikute nimekiri;\n\n#. Ringkondade nimekiri;\n\n#. Valijate algnimekiri.\n\nEttevalmistatud seadistuspakkide rakendamiseks tuleb läbi viia järgmised\ntegevused:\n\n#. Ülekandmine haldusteenuse masinasse;\n\n#. Laadimine haldusteenusesse;\n\n#. Rakendamine alamteenustele.\n\n.. hint::\n\n   Seadistuspakkide ettevalmistamine on kirjeldatud lõigus\n   \":ref:`seadistuste_koostamine`\".\n\n.. attention::\n\n   Usaldusjuure seadistuse laadimisega kaasneb alati ka\n   kogumisteenuse haldusteenuse andmebaasi lähtestamine!\n\nSeadistuste ja valimisnimekirjade kogumisteenusele rakendamise tulemusena on\nkogumisteenus seadistatud ettenähtud perioodil osutama nõuetekohast häälte\nkogumise teenust.\n\n\nSeadistuspaki ülekandmine haldusteenuse masinasse\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSeadistuspaki ülekandmine haldusteenuse masinasse toimub üle `SCP\n<https://en.wikipedia.org/wiki/Secure_copy>`_ protokolli. Seadistuspakk peab\nolema kättesaadav haldusteenuse kasutajakontole ``ivxv-admin``.\n\nNäide::\n\n   $ scp seadistus.asice ivxv-admin@admin:\n   seadistus.asice              100%   15KB  79.5KB/s   00:00\n\n.. note::\n\n   Kogumisteenus osutaja võib seadistuspakkide ülekandmiseks kasutada ka muid\n   meetodeid, näiteks irdmeediat.\n\nSeadistuspaki ülekandmise tulemusena on seadistuspakk haldusteenuse poolt\nligipääsetaval andmekandjal.\n\n\nSeadistuspaki laadimine haldusteenusesse\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSeadistuspakk laaditakse haldusteenusesse käsuga :ref:`ivxv-cmd-load`. Selle\nkäigus kontrollib haldusteenus seadistuspaki signeerinud isiku volitusi ja\nvalideerib seadistuste sisu ning kooskõlalisust. Laadimise tulemusena on\nseadistuspakk valmis rakendamiseks hallatavatele teenustele.\n\nNäide: Usaldusjuure laadimine haldusteenusesse:\n\n.. include:: genereeritud-failid/haldusteenus-laadi_usaldusjuure_seadistused.inc\n\nSeadistuspaki haldusteenusesse laadimise tulemusena on haldusteenus valmis\nrakendama seadistuspakki alamteenustele. Seadistuspaki versiooni kuvatakse\nhaldusteenuse olekuandmetes.\n\n.. seealso:: * :ref:`korralduste-valideerimine`\n\n\nSeadistuste rakendamine alamteenustele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusteenusesse laaditud seadistuspakid rakendatakse hallatavatele teenustele\nkäsuga :ref:`ivxv-config-apply`. Rakendamine on võimalik tehniliste seadistuse\nlaadimise järel, kuna tehnilise seadistusega tekivad haldusteenusesse andmed\nhallatavate teenuste kohta.\n\nSeadistuste rakendamise käigus haldusteenus:\n\n* Paigaldab seadistatava teenuse tarkvara (tehnilise seadistuse laadimisel, kui\n  pole eelnevalt paigaldatud);\n\n* Kannab seadistuspaki üle hallatava teenuse hosti failisüsteemi;\n\n* Valimiste seadistuse laadimisel lubab ja käivitab seadistatava teenuse.\n\n.. note::\n\n   Seadistuste rakendamise järjekord on kirjeldatud utiliidi ivxv-config-apply\n   abiteabe lõigus (vaata :ref:`ivxv-config-apply`).\n\nNäide: Haldusteenusesse laaditud seadistuste rakendamine hallatavatele\nteenusele::\n\n   ivxv-admin@admin $ ivxv-config-apply\n   INFO: Technical config is signed by ÕIGE,VALIK,44444444444 2017-06-07T12:05:44Z\n   INFO: Service choices@choices1.ivxv.ee: Applying technical config\n   SERVICE choices@choices1.ivxv.ee: Installing service to host \"ivxv1\"\n   SERVICE choices@choices1.ivxv.ee: Querying state of the service software package \"ivxv-choices\"\n   SERVICE choices@choices1.ivxv.ee: Copying software package files to service host\n   SERVICE choices@choices1.ivxv.ee: Checking state of dpkg database in service host\n   SERVICE choices@choices1.ivxv.ee: Installing dependencies for package \"ivxv-common\"\n   Reading package lists...\n   Building dependency tree...\n   Reading state information...\n   ...\n   SERVICE voting@voting3.ivxv.ee: Set trust config file permissions in service host\n   SERVICE voting@voting3.ivxv.ee: Trust root config successfully applied to service\n   SERVICE voting@voting3.ivxv.ee: Applying technical config to service\n   SERVICE voting@voting3.ivxv.ee: Copying technical config to service host\n   SERVICE voting@voting3.ivxv.ee: Set technical config file permissions in service host\n   SERVICE voting@voting3.ivxv.ee: Technical config successfully applied to service\n   SERVICE voting@voting3.ivxv.ee: Registering technical config version \"ÕIGE,VALIK,44444444444 2017-06-07T12:05:44Z\" in management database\n   SERVICE voting@voting3.ivxv.ee: Registering service state as \"INSTALLED\" in management database\n   INFO: Service voting@voting3.ivxv.ee: technical config config applied successfully\n   INFO: 15 configuration packages successfully applied\n\nSeadistuste alamteenustele rakendamise tulemusena on hallatavad teenused\nseadistatud ja nende seisund on haldusteenusest jälgitav.\n\n.. seealso:: * :ref:`korralduste-laadimine-rakendamine`\n\n\nKogumisteenuse krüptovõtmete rakendamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Teenuste krüptovõtmete ja TLS-sertifikaatide** rakendamine toimub käsuga\n:ref:`ivxv-secret-load`.\n\n.. hint::\n\n   Teenuse krüptovõtmete seisundit on võimalik väljastada käsuga\n   :command:`ivxv-status --service=<service-id>` (vaata :ref:`ivxv-status`)\n\nVõtme laadimine teenusele:\n\n.. code-block:: shell-session\n\n   $ ivxv-secret-load --service=<teenuse-id> tls-key tls.key\n\nSertifikaadi laadimine teenusele:\n\n.. code-block:: shell-session\n\n   $ ivxv-secret-load --service=<teenuse-id> tls-cert tls.pem\n\n.. important::\n\n   Igale teenuse isendile tuleb rakendada selle\n   isendi jaoks genereeritud võti ja sertifikaat!\n\n**Hääletamisteenuse ajatemplipäringute signeerimisvõtme** rakendamine toimub\nkäsuga :ref:`ivxv-secret-load`:\n\n.. code-block:: shell-session\n\n   $ ivxv-secret-load tsp-regkey tspreg.key\n\n.. note::\n\n   Hääletamisteenuse ajatemplipäringute signeerimisvõti on vaja rakendada vaid\n   juhul, kui ajatempliteenust kasutatakse registreerimisteenuseks (valimiste\n   seadistuses on ``qualification/protocol`` välja väärtuseks ``tspreg``).\n\n**Mobiil-ID/Smart-ID/Web eID identsustõendi võtme** rakendamine toimub\nkäsuga :ref:`ivxv-secret-load`:\n\n.. code-block:: shell-session\n\n   $ ivxv-secret-load mid-token-key mobid-shared-secret.key\n\n.. note::\n\n   Mobiil-ID/Smart-ID/Web eID identsustõendi võti on vaja rakendada vaid\n   juhul, kui Mobiil-ID/Smart-ID/Web eID tugiteenus on kasutusel\n   (valimiste seadistuses on olemas plokk ``auth.ticket``).\n\nKogumisteenuse krüptovõtmete rakendamise tulemusena on hallatavate teenuste\nsuhtluskanalid varustatud kanali turvamiseks vajalike krüptovõtmetega, samuti\non teenustel olemas krüptovõtmed muude oluliste operatsioonide jaoks.\n\n\nAlgseadistamise tulemuse kontrollimine\n--------------------------------------\n\nAlgseadistamise tegevuste tulemusena on kogumisteenus eeldatavalt valmis\nhääletuse läbiviimiseks. Tulemust on võimalik kontrollida kogumisteenuse oleku\njälgimisega, mis on kirjeldatud süsteemi haldustoimingute lõigus\n(:ref:`kogumisteenuse-oleku-jälgimine`).\n\nHääletuse läbiviimiseks seadistatud kogumisteenuse olek on \"Seadistatud\"\n(CONFIGURED). Oleku \"Paigaldatud\" puhul tuleb kontrollida mikroteenuste\nseisundit ja seisundi taustainfot.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/annotatsioon.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nAnnotatsioon\n------------\n\nKäesolev juhend käsitleb tööd elektroonilise hääletamise raamistiku IVXV\nkogumisteenuse tarkvaraga süsteemiülema vaatepunktist ning kirjeldab tarkvara\nkõiki võimalusi kogu e-hääletusprotsessi ulatuses. Süsteemiülemalt eeldatakse\ne-hääletuse põhiterminoloogia tundmist.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/e-valimiskasti_koostamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Valimiste kokkuvõtete tegemine\n             features/end-to-end-10-collector-ha-setup.feature:892\n       Step: Haldur ekspordib haldusteenusest kogutud hääled\n             features/end-to-end-10-collector-ha-setup.feature:970\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-export-votes /output/voting/HA-SETUP/exported-votes.zip\n   INFO: Creating backup copy from current ballot box\n   SERVICE backup@backup.ivxv.ee: Copying list of known SSH hosts to service host\n   # Preparing ballot box backup file in voting service voting@voting1.ivxv.ee\n   # Creating ballot box backup file ballot-box-20211228_1155.zip\n   Exporting votes: 0\n   Exporting votes: 1\n   Exporting votes: 2\n   Exporting votes: 3\n   Exporting votes: 4\n   Exporting votes: 5\n   Exporting votes: 6\n   Exporting votes: 7\n   Exporting votes: 8\n   # Copying backup file ballot-box-20211228_1155.zip to backup service\n   # Removing backup file ballot-box-20211228_1155.zip from voting service\n   INFO: Copying ballot box to management service\n   SERVICE backup@backup.ivxv.ee: Copying ballot box from service host\n   INFO: Collected votes archive is written to '/output/voting/HA-SETUP/exported-votes.zip'\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_kogumisteenuse_tehnilised_seadistused.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Kogumisteenuse tehniliste seadistuste rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:404\n       Step: Haldur laadib haldusteenusesse kogumisteenuse tehnilised seadistused\n             features/end-to-end-10-collector-ha-setup.feature:438\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load technical /output/voting/HA-SETUP/config/HA-SETUP.technical.asice\n   INFO: Config file is signed by: ROPKA,KIVIVALVUR,32608320001 2021-12-28T11:38:40Z\n   INFO: User ROPKA,KIVIVALVUR,32608320001 with role 'admin' is authorized to execute 'technical' commands\n   INFO: Using signature 'ROPKA,KIVIVALVUR,32608320001 2021-12-28T11:38:40Z' as config file version\n   INFO: Config file version is 'ROPKA,KIVIVALVUR,32608320001 2021-12-28T11:38:40Z'\n   INFO: Loading command 'collectors technical configuration' from file '/output/voting/HA-SETUP/config/HA-SETUP.technical.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/HA-SETUP.technical.asice' (collectors technical configuration)\n   command_file:INFO: Validating collectors technical configuration\n   command_file:INFO: Files in collectors technical configuration package are valid\n   INFO: Collectors technical configuration file loaded successfully\n   INFO: Collectors technical configuration file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_ringkondade_nimekiri.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Ringkondade nimekirja rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:586\n       Step: Nimekirjade haldur laadib ringkondade nimekirja haldusteenusesse\n             features/end-to-end-10-collector-ha-setup.feature:630\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load districts /output/voting/HA-SETUP/config/districts.asice\n   INFO: Config file is signed by: NÕID,VÄIKE,33333333333 2021-12-28T11:49:17Z\n   INFO: User NÕID,VÄIKE,33333333333 with role 'election-conf-manager' is authorized to execute 'districts' commands\n   INFO: Using signature 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:17Z' as config file version\n   INFO: Config file version is 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:17Z'\n   INFO: Loading command 'districts list' from file '/output/voting/HA-SETUP/config/districts.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/districts.asice' (districts list)\n   command_file:INFO: Validating districts list\n   command_file:INFO: Files in districts list package are valid\n   INFO: Some voting lists are already loaded, executing consistency checks: ivxv-config-validate --election=/etc/ivxv/election.bdoc --choices=/etc/ivxv/choices.bdoc --districts=/output/voting/HA-SETUP/config/districts.asice\n   INFO: Validating elections configuration file '/etc/ivxv/election.bdoc'\n   ...\n   command_file:INFO: Files in districts list package are valid\n   INFO: Config files are valid\n   INFO: Detected election ID 'HA-SETUP' from election config\n   INFO: Election ID consistency check succeeded\n   INFO: Checking districts and choices lists consistency\n   INFO: Voting lists consistency check succeeded\n   INFO: Writing simplified district list to '/var/lib/ivxv/admin-ui-data/districts.json'\n   INFO: Districts list file loaded successfully\n   INFO: Districts list file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_usaldusjuure_seadistused.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Kogumisteenuse tehniliste seadistuste rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:404\n       Step: Haldur laadib haldusteenusesse usaldusjuure seadistused\n             features/end-to-end-10-collector-ha-setup.feature:412\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load trust /output/voting/HA-SETUP/config/HA-SETUP.trust.asice\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/HA-SETUP.trust.asice' (trust root configuration)\n   command_file:INFO: Validating trust root configuration\n   command_file:INFO: Files in trust root configuration package are valid\n   INFO: Config file is signed by: ORAV,IVAN,30809010001 2021-12-28T11:38:37Z\n   INFO: User ORAV,IVAN,30809010001 with role 'admin' is authorized to execute 'trust' commands\n   INFO: Using signature 'ORAV,IVAN,30809010001 2021-12-28T11:38:37Z' as config file version\n   INFO: Config file version is 'ORAV,IVAN,30809010001 2021-12-28T11:38:37Z'\n   INFO: Loading command 'trust root configuration' from file '/output/voting/HA-SETUP/config/HA-SETUP.trust.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/HA-SETUP.trust.asice' (trust root configuration)\n   command_file:INFO: Validating trust root configuration\n   command_file:INFO: Files in trust root configuration package are valid\n   INFO: Resetting collector management database\n   db:INFO: Initializing management database '/var/lib/ivxv/db/ivxv-management.db'\n   Removing crontab (if exist)\n   no crontab for ivxv-admin\n   INFO: Trust root configuration file loaded successfully\n   INFO: Resetting user permissions\n   INFO: Trust root configuration file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_valijate_nimekiri.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Valijate algnimekirja rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:642\n       Step: Nimekirjade haldur laadib valijate algnimekirja haldusteenusesse\n             features/end-to-end-10-collector-ha-setup.feature:666\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load voters /output/voting/HA-SETUP/config/voters-0.asice\n   INFO: Config file is signed by: NÕID,VÄIKE,33333333333 2021-12-28T11:49:37Z\n   INFO: User NÕID,VÄIKE,33333333333 with role 'election-conf-manager' is authorized to execute 'voters' commands\n   INFO: Using signature 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:37Z' as config file version\n   INFO: Config file version is 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:37Z'\n   INFO: Loading command 'voters list' from file '/output/voting/HA-SETUP/config/voters-0.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/voters-0.asice' (voters list)\n   command_file:INFO: Validating voters list\n   command_file:INFO: Files in voters list package are valid\n   INFO: Some voting lists are already loaded, executing consistency checks: ivxv-config-validate --election=/etc/ivxv/election.bdoc --choices=/etc/ivxv/choices.bdoc --districts=/etc/ivxv/districts.bdoc --voters=/output/voting/HA-SETUP/config/voters-0.asice\n   INFO: Validating elections configuration file '/etc/ivxv/election.bdoc'\n   ...\n   command_file:INFO: Files in voters list package are valid\n   INFO: Config files are valid\n   INFO: Detected election ID 'HA-SETUP' from election config\n   INFO: Election ID consistency check succeeded\n   INFO: Checking districts and choices lists consistency\n   INFO: Checking districts and voter list changeset #0 consistency\n   INFO: Voting lists consistency check succeeded\n   INFO: Voters list file loaded successfully\n   INFO: Voters list file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_valikute_nimekiri.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Valikute nimekirja rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:528\n       Step: Nimekirjade haldur laadib valikute nimekirja haldusteenusesse\n             features/end-to-end-10-collector-ha-setup.feature:574\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load choices /output/voting/HA-SETUP/config/choices.asice\n   INFO: Config file is signed by: NÕID,VÄIKE,33333333333 2021-12-28T11:49:03Z\n   INFO: User NÕID,VÄIKE,33333333333 with role 'election-conf-manager' is authorized to execute 'choices' commands\n   INFO: Using signature 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:03Z' as config file version\n   INFO: Config file version is 'NÕID,VÄIKE,33333333333 2021-12-28T11:49:03Z'\n   INFO: Loading command 'choices list' from file '/output/voting/HA-SETUP/config/choices.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/choices.asice' (choices list)\n   command_file:INFO: Validating choices list\n   command_file:INFO: Files in choices list package are valid\n   INFO: Choices list file loaded successfully\n   INFO: Choices list file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenus-laadi_valimiste_seadistused.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Lugemisperioodi alustamine\n             features/end-to-end-10-collector-ha-setup.feature:885\n       Step: Haldur määrab kogumisteenuse uueks perioodiks \"lõpetatud\"\n             features/end-to-end-10-collector-ha-setup.feature:888\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load election /output/voting/HA-SETUP/config/HA-SETUP.election.asice\n   INFO: Config file is signed by: NÕID,VÄIKE,33333333333 2021-12-28T11:54:10Z\n   INFO: User NÕID,VÄIKE,33333333333 with role 'election-conf-manager' is authorized to execute 'election' commands\n   INFO: Using signature 'NÕID,VÄIKE,33333333333 2021-12-28T11:54:10Z' as config file version\n   INFO: Config file version is 'NÕID,VÄIKE,33333333333 2021-12-28T11:54:10Z'\n   INFO: Loading command 'elections configuration' from file '/output/voting/HA-SETUP/config/HA-SETUP.election.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/HA-SETUP.election.asice' (elections configuration)\n   command_file:INFO: Validating elections configuration\n   command_file:INFO: Files in elections configuration package are valid\n   INFO: Elections configuration file loaded successfully\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/HA-SETUP.election.asice' (elections configuration)\n   INFO: Elections configuration file is registered in management service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenuse_paigaldamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Kogumisteenuse haldusteenuse paigaldus\n             features/end-to-end-10-collector-ha-setup.feature:370\n       Step: Haldur paigaldab haldusteenuse\n             features/end-to-end-10-collector-ha-setup.feature:376\n\n.. code-block:: text\n\n   root@admin # dpkg -i /etc/ivxv/debs/ivxv-common_1.7.8_all.deb /etc/ivxv/debs/ivxv-admin_1.7.8_amd64.deb\n   Selecting previously unselected package ivxv-common.\n   (Andmebaasi lugemine ... 13710 files and directories currently installed.)\n   Preparing to unpack .../debs/ivxv-common_1.7.8_all.deb ...\n   Unpacking ivxv-common (1.7.8) ...\n   Selecting previously unselected package ivxv-admin.\n   Preparing to unpack .../ivxv-admin_1.7.8_amd64.deb ...\n   Unpacking ivxv-admin (1.7.8) ...\n   Paki ivxv-common (1.7.8) paikasättimine ...\n   # Adding user group 'ivxv'\n   Adding group `ivxv' (GID 109) ...\n   ...\n     systemctl restart apache2\n   # Starting Apache web server\n   # Restarting rsyslog log server\n   Created symlink /etc/systemd/system/multi-user.target.wants/ivxv-admin.service → /lib/systemd/system/ivxv-admin.service.\n   Created symlink /etc/systemd/system/multi-user.target.wants/ivxv-admin-agent.service → /lib/systemd/system/ivxv-admin-agent.service.\n   /usr/lib/python3/dist-packages/schematics/validate.py:121: SyntaxWarning: \"is\" with a literal. Did you mean \"==\"?\n     if not kwargs or kwargs.pop('context', 0) is 0:\n   Processing triggers for rsyslog (8.2001.0-1ubuntu1.1) ...\n   invoke-rc.d: WARNING: No init system and policy-rc.d missing! Defaulting to block.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/haldusteenuse_soltuvuste_paigaldamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Kogumisteenuse haldusteenuse paigaldus\n             features/end-to-end-10-collector-ha-setup.feature:370\n       Step: Haldur paigaldab haldusteenuse\n             features/end-to-end-10-collector-ha-setup.feature:376\n\n.. code-block:: text\n\n   root@admin # apt-get install --yes --quiet adduser openssh-server openssl rsync rsyslog rsyslog-relp sudo tzdata locales libc6 python3-bottle python3-dateutil python3-debian python3-docopt python3-fasteners python3-jinja2 python3-jsonschema python3-openssl python3-pkg-resources python3-yaml python3:any apache2 cron fonts-font-awesome javascript-common language-pack-et libapache2-mod-wsgi-py3 libjs-bootstrap libjs-jquery libjs-jquery-datatables libjs-jquery-datatables-extensions python3-gdbm python3-requests ssl-cert\n   Reading package lists...\n   Building dependency tree...\n   Reading state information...\n   adduser is already the newest version (3.118ubuntu2).\n   cron is already the newest version (3.0pl1-136ubuntu1).\n   fonts-font-awesome is already the newest version (5.0.10+really4.7.0~dfsg-1).\n   javascript-common is already the newest version (11).\n   libapache2-mod-wsgi-py3 is already the newest version (4.6.8-1ubuntu3).\n   libjs-jquery is already the newest version (3.3.1~dfsg-3).\n   libjs-jquery on määratud käsitsi paigaldatuks.\n   ...\n   Paki apache2-data (2.4.41-4ubuntu3.8) paikasättimine ...\n   Paki openssl (1.1.1f-1ubuntu2.10) paikasättimine ...\n   Paki rsync (3.1.3-8ubuntu0.1) paikasättimine ...\n   invoke-rc.d: WARNING: No init system and policy-rc.d missing! Defaulting to block.\n   Paki apache2-utils (2.4.41-4ubuntu3.8) paikasättimine ...\n   Paki apache2 (2.4.41-4ubuntu3.8) paikasättimine ...\n   invoke-rc.d: WARNING: No init system and policy-rc.d missing! Defaulting to block.\n   invoke-rc.d: WARNING: No init system and policy-rc.d missing! Defaulting to block.\n   Processing triggers for systemd (245.4-4ubuntu3.13) ...\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/kasutaja_lisamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Kogumisteenuse tehniliste seadistuste rakendamine\n             features/end-to-end-10-collector-ha-setup.feature:404\n       Step: Haldur määrab haldusteenuses kasutajale \"NÕID,VÄIKE,33333333333\" rolli \"election-conf-manager\"\n             features/end-to-end-10-collector-ha-setup.feature:471\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-cmd-load user /output/voting/HA-SETUP/config/user-NÕID,VÄIKE,33333333333-election-conf-manager.asice\n   INFO: Config file is signed by: ORAV,IVAN,30809010001 2021-12-28T11:46:31Z\n   INFO: User ORAV,IVAN,30809010001 with role 'admin' is authorized to execute 'user' commands\n   INFO: Using signature 'ORAV,IVAN,30809010001 2021-12-28T11:46:31Z' as config file version\n   INFO: Config file version is 'ORAV,IVAN,30809010001 2021-12-28T11:46:31Z'\n   INFO: Loading command 'user permissions configuration' from file '/output/voting/HA-SETUP/config/user-NÕID,VÄIKE,33333333333-election-conf-manager.asice'\n   command_file:INFO: Loading command file '/output/voting/HA-SETUP/config/user-NÕID,VÄIKE,33333333333-election-conf-manager.asice' (user permissions configuration)\n   command_file:INFO: Validating user permissions configuration\n   command_file:INFO: Files in user permissions configuration package are valid\n   INFO: User permissions configuration file loaded successfully\n   INFO: Resetting user 'NÕID,VÄIKE,33333333333' permissions\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/mikroteenuse_kaivitamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Mikroteenuse seiskamine ja käivitamine\n             features/end-to-end-10-collector-ha-setup.feature:512\n       Step: Haldur käivitab teenuse \"voting@voting2.ivxv.ee\"\n             features/end-to-end-10-collector-ha-setup.feature:518\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-service restart voting@voting2.ivxv.ee\n   INFO: Restarting service voting@voting2.ivxv.ee\n   SERVICE voting@voting2.ivxv.ee: Restarting service\n   SERVICE voting@voting2.ivxv.ee: Registering service state as 'CONFIGURED' in management database (last state: 'FAILURE')\n   SERVICE voting@voting2.ivxv.ee: Service restarted successfully\n   INFO: Service voting@voting2.ivxv.ee restarted"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/mikroteenuse_seiskamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Mikroteenuse seiskamine ja käivitamine\n             features/end-to-end-10-collector-ha-setup.feature:512\n       Step: Haldur seiskab teenuse \"voting@voting2.ivxv.ee\"\n             features/end-to-end-10-collector-ha-setup.feature:515\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-service stop voting@voting2.ivxv.ee\n   INFO: Stopping service voting@voting2.ivxv.ee\n   SERVICE voting@voting2.ivxv.ee: Stopping service\n   SERVICE voting@voting2.ivxv.ee: Service stopped successfully\n   INFO: Service voting@voting2.ivxv.ee stopped"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/mikroteenuse_seisundi_tuvastamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Mikroteenuse seiskamine ja käivitamine\n             features/end-to-end-10-collector-ha-setup.feature:512\n       Step: Haldur tuvastab teenuse \"voting@voting2.ivxv.ee\" seisundi\n             features/end-to-end-10-collector-ha-setup.feature:516\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-service ping voting@voting2.ivxv.ee\n   INFO: Pinging service voting@voting2.ivxv.ee\n   SERVICE voting@voting2.ivxv.ee: Registering background info: Ping error: * ivxv-voting@voting@voting2.ivxv.ee.service - IVXV voting service\n   SERVICE voting@voting2.ivxv.ee: ERROR: Pinging service failed\n   ERROR: Failed to query service voting@voting2.ivxv.ee status"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/genereeritud-failid/töötlemisrakenduse_sisendi_koostamine.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * steps/test_util/report.py\n\n..  Feature: Kogumisteenuse käivitamine kõrgkäideldava konfiguratsiooniga\n             features/end-to-end-10-collector-ha-setup.feature:7\n   Scenario: Valimiste kokkuvõtete tegemine\n             features/end-to-end-10-collector-ha-setup.feature:892\n       Step: Haldur genereerib haldusteenuses töötlemisrakenduse sisendi aluse\n             features/end-to-end-10-collector-ha-setup.feature:973\n\n.. code-block:: text\n\n   ivxv-admin@admin $ ivxv-generate-processor-input /output/voting/HA-SETUP/processor.cfg.zip\n   INFO: Generating processor application config\n   command_file:INFO: Loading command file '/etc/ivxv/election.bdoc' (elections configuration)\n   INFO: Creating input file for processor application\n   INFO: Preparing container structure in directory '/tmp/tmp6jswwi8i'\n   INFO: Copying district list 'HA-SETUP.districts.json.bdoc'\n   INFO: Copying voter list signing key 'voterfile.pub.key'\n   INFO: Copying voter list #0 content '00.HA-SETUP.voters.utf'\n   INFO: Copying voter list #0 signature '00.HA-SETUP.voters.sig'\n   INFO: Copying voter list #1 content '01.HA-SETUP.voters.utf'\n   INFO: Copying voter list #1 signature '01.HA-SETUP.voters.sig'\n   ...\n   INFO: Adding '02.HA-SETUP.voters.sig' to ZIP container\n   INFO: Adding '02.HA-SETUP.voters.utf' to ZIP container\n   INFO: Adding '03.HA-SETUP.voters.sig' to ZIP container\n   INFO: Adding '03.HA-SETUP.voters.utf' to ZIP container\n   INFO: Adding 'HA-SETUP.districts.json.bdoc' to ZIP container\n   INFO: Adding 'HA-SETUP.processor.yaml' to ZIP container\n   INFO: Adding 'ts.key' to ZIP container\n   INFO: Adding 'voterfile.pub.key' to ZIP container\n   INFO: Processor input is written to '/output/voting/HA-SETUP/processor.cfg.zip'\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/haldusteenus.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\n.. _haldusteenus:\n\nHaldusteenus\n============\n\nHaldusteenus on kogumisteenuse haldamiseks mõeldud lahendus. Haldusteenus\npaigaldatakse eraldiseisvasse masinasse ja selle kaudu toimub kogumisteenuse\njuhtimine paigaldusest kuni seiskamiseni.\n\nHaldusteenuse funktsioonid on:\n\n#. Kogumisteenuse alamteenuste haldamine:\n\n   #. Seadistuste ja valimisnimekirjade laadimine;\n\n   #. Alamteenuste paigaldus selleks ettevalmistatud masinatesse;\n\n   #. Alamteenustele seadistuste ja nimekirjade rakendamine;\n\n   #. Valijate nimekirjade uuenduste hankimine Valimiste Infosüsteemist;\n\n#. E-valimiskasti koostamine töötlemiseks;\n\n#. Valimiste üldstatistika jälgimine;\n\n#. Valijate statistika allalaadimine;\n\n#. E-valimiskasti ja logide korrapärane varundamine;\n\n#. Kogumisteenuse seisundi seire;\n\nHaldusteenus suhtleb hallatavate teenustega üle `SSH\n<https://en.wikipedia.org/wiki/Secure_Shell>`_-kanali. Suhtluse algatab alati\nhaldusteenus. Usaldus teenusmasinate vastu luuakse süsteemihalduri abiga pärast\nteenuseid majutavate masinate paigaldamist.\n\nTeenust majutava masina paigaldamise järel loob haldur haldusteenusele\nligipääsu teenusmasina juurkontole, et haldusteenusel oleks võimalik teenuse\ntarkvara paigaldada.  Pärast viimase teenuse paigaldamist teenuseid majutavasse\nmasinasse eemaldab haldusteenus ligipääsu juurkontole.\n\n\nHaldusteenuse koosseis\n----------------------\n\nHaldusteenuse kasutajaliides koosneb kahest osast:\n\n#. Haldamise põhifunktsionaalsus on teostatud :ref:`käsureautiliitide\n   <utiliidid>` abil;\n\n#. Graafiline kasutajaliides on veebipõhine liides, mille funktsionaalsuse\n   tagavad käsureautiliidid.\n\n   .. seealso::\n\n      Graafilise kasutajaliidese kasutusjuhend asub dokumendis\n      ``IVXV kogumisteenuse haldusliidese kasutusjuhend``.\n\nLisaks töötavad deemonprotsessid:\n\n#. Veebiserver graafilise kasutajaliidese jaoks;\n\n#. Haldusdeemon veebiserveri poolt vahendatud päringute käivitamiseks;\n\n#. Agentdeemon teenuste seisundi jälgimiseks.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/haldustoimingud.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nSüsteemi haldustoimingud\n========================\n\n.. _kogumisteenuse-oleku-jälgimine:\n\nKogumisteenuse oleku jälgimine\n------------------------------\n\nKogumisteenuse olekuandmed registreeritakse haldusteenuse andmebaasis. Oleku\nkuvamiseks on utiliit :ref:`ivxv-status`.\n\nOlekus kuvatakse järgmisi andmeid:\n\n* Valimise ID, faas, algus- ja lõpuaeg;\n\n* Haldusteenusesse laaditud konfiguratsioon:\n\n   * Seadistuspakkide versioonid;\n\n   * Valikute nimekirja ja ringkondade nimekirja versioonid;\n\n   * Valijate nimekirjade versioonid ja olekud;\n\n* Teenuste nimekiri koos rakendatud seadistuste versioonidega, teenuse seisundi\n  ja selle viimase tuvastamise ajaga;\n\n* Väliste teenuste seisundid;\n\n* Haldusteenuse andmehoidla statistika.\n\nSõltuvalt kogumisteenuse seisundist võib oleku kuvamise utiliit jätta mõned\nandmeblokid kuvamata (kui need pole jooksva seisundi puhul olulised). Täieliku\nandmestiku väljastamiseks vaata utiliidi :ref:`ivxv-status` abiteavet.\n\nMikroteenuste oleku jälgimise ning oleku ja võimaliku veainfo registreerimisega\nhaldusteenuse andmebaasis tegeleb haldusteenuse :ref:`agentdeemon\n<ivxv-agent-daemon>`.\n\nValijate muudatusnimekirjade hankimine Valimiste Infosüsteemist toimub\nutiliidiga :ref:`ivxv-voter-list-download`, mis käivitatakse teenuse `cron`\npoolt veerandtunnise intervalliga.\n\nKogumisteenuse haldusteenuse sündmuste logi kuvamiseks on utiliit\n:ref:`ivxv-eventlog-dump`.\n\n.. important::\n\n   Haldusteenus tagab kogumisteenuse alamteenuste olekuandmetes vajaliku teabe\n   teenuse töökorda seadmiseks. See võib olla järgmine:\n\n   #. Teave puuduvate seadistuste kohta (seadistusfailid, võtmed jms). Seda\n      kuvatakse kuni teenus on varustatud kõigi käivitamiseks vajalike\n      seadistustega.\n\n   #. Veateade - alamteenuse haldusvahendite (seadistuste kontrollivahend,\n      teenuse haldusvahend) veaväljund mittetöötava teenuse kohta.\n\n\n.. _korralduste-valideerimine:\n\nKorralduste valideerimine\n-------------------------\n\nKorraldusfailide valideerimine võimaldab veenduda korralduste vastavuses\nvormistusnõuetele ning tuvastada vigased või mittekooskõlalised korraldused.\n\nValideerimine toimub käsuga :ref:`ivxv-config-validate`.\n\nValimise seadistuse valideerimise näide::\n\n   $ ivxv-config-validate --election=valimise-seadistus-TEST2017.asice\n\n\nKorralduste kooskõlalisuse valideerimine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKooskõla valideerimine viiakse läbi kahel juhul:\n\n#. Kui valideerimise käsule antakse korraga valideerimiseks mitu korraldust;\n\n#. Korralduse laadimisel juhul, kui laaditava korraldusega kooskõla nõudev\n   oluline korraldus juba haldusteenusesse laaditud.\n\nKorralduste kooskõla valideerimise kontrollid:\n\n#. Korraga valimiste seadistust ja/või nimekirju (valikute, ringkondade või\n   valijate) valideerides kontrollitakse valimiste identifikaatori kooskõla.\n\n#. Korraga mitut valijate nimekirja valideerides viiakse läbi järgnevad\n   kontrollid:\n\n   * Nimekirjade korrektne järjestus;\n\n   *  Muudatusnimekirjas kontrollitakse:\n\n      #. Valija topeltlisamist ja -eemaldamist;\n\n      #. Valija eemaldamist pärast tema lisamist sama muudatusnimekirjaga.\n\n      #. Valija eemaldamist ringkonnast, kuhu teda pole lisatud;\n\n#. Korraga ringkondade ja valikute nimekirja valideerides viiakse läbi\n   järgnevad kontrollid:\n\n   * Igas valimisringkonnas peab olema kirjeldatud vähemalt üks valik;\n\n   * Iga valik peab olema seotud olemasoleva valimisringkonnaga.\n\n#. Korraga ringkondade nimekirja ja valijate nimekirju valideerides viiakse\n   läbi järgnevad kontrollid:\n\n   * Igas valimisjaoskonnas peab olema vähemalt üks valija;\n\n   * Iga valimisnimekirja kantud isik peab olema seotud olemasoleva\n     ringkonnaga;\n\n#. Valijate nimekirja(de) ja ringkondade nimekirja valideerimisel\n   kontrollitakse valijale määratud ringkonna olemasolu ringkondade nimekirjas.\n\nKui valimiste seadistuses on määratud välisriigis asuvale valijale määratav\nringkonna haldusüksuse EHAK-kood (parameeter ``voterforeignehak``), peab\nvalikute, valijate ja ringkondade nimekirjade vastavuse valideerimisel olema\nkaasatud ka valimiste seadistus ja seadistuste valideerimisel tehakse täiendavad\nkontrollid:\n\n#. Valijate nimekirja ja ringkondade nimekirja kooskõla valideerides\n   kontrollitakse, et ringkondade nimekirjas on parameetriga määratud\n   haldusüksuses olemas valijale määratud ringkond.\n\n\n.. _korralduste-laadimine-rakendamine:\n\nKorralduste laadimine ja rakendamine\n------------------------------------\n\nKogumisteenuse korraldused koostatakse signeeritud korralduspakkidena, millega\nkirjeldatakse kasutaja identifikaator (*Common Name* ehk CN väli ID-kaardilt)\nja rollide nimekiri.\n\nSõltuvalt korraldusest tuleb rakendamiseks kasutada ühte või kahte käsku.\nHaldusteenust puudutavad korralduste rakendamiseks piisab nende laadimisest\nhaldusteenusesse. Alamteenuseid puudutavad korraldused (näiteks seadistuspakid)\ntuleb pärast haldusteenusesse laadimist rakendada ka hallatavatele teenustele.\n\nKorralduste laadimine haldusteenusesse toimub käsuga :ref:`ivxv-cmd-load`.\nLaadimise käigus viiakse läbi ka :ref:`korralduse valideerimine\n<korralduste-valideerimine>`, vigane või mittekooskõlaline korraldus jäetakse\nlaadimata.\n\nValikute nimekirja korralduse rakendamise näide:\n\n.. include:: genereeritud-failid/haldusteenus-laadi_valikute_nimekiri.inc\n\n.. seealso::\n\n   * Käsu :ref:`ivxv-cmd-load` abiteave;\n\n   * Korralduste rollide kirjeldus ja korralduste koostamise juhend asuvad\n     dokumendis ``IVXV seadistuste\n     koostamise juhend``.\n\n\nTeenuse isendi seisundi tuvastamine\n-----------------------------------\n\nMikroteenuse isendi seisundi tuvastamiseks on utiliit :ref:`ivxv-service`,\nmillega on võimalik teenuse seisundit vahetult küsida (utiliit\n:ref:`ivxv-status` kuvab andmebaasis puhverdatavat seisundit).\n\nTeenuse seisundi päringu näide:\n\n.. include:: genereeritud-failid/mikroteenuse_seisundi_tuvastamine.inc\n\n\n.. _teenuse-taaskäivitamine:\n\nTeenuse (taas)käivitamine\n-------------------------\n\nMikroteenuste käivitamiseks ja taaskäivitamiseks on utiliit\n:ref:`ivxv-service`.\n\nTeenuse taaskäivitamise näide:\n\n.. include:: genereeritud-failid/mikroteenuse_kaivitamine.inc\n\n.. note::\n\n   Protseduuri nimetamine käivitamiseks või taaskäivitamiseks sõltub teenuse\n   protsessi seisundist. Tehniliselt on tegemist sarnaste protseduuridega, kus\n   esmalt veendutakse, et teenus seisab (vajadusel jäetakse see seisma) ja siis\n   püütakse käivitada hetkel kehtivate seadistustega.\n\n\n.. _teenuse-seiskamine:\n\nTeenuse seiskamine\n------------------\n\nMikroteenuste seiskamiseks on utiliit :ref:`ivxv-service`.\n\nTeenuse seiskamise näide:\n\n.. include:: genereeritud-failid/mikroteenuse_seiskamine.inc\n\n\n.. _teenuse-asendamine:\n\nTeenuse isendi asendamine\n-------------------------\n\nTeenuse isendi asendamine koosneb ühe mikroteenuse isendi eemaldamisest (vt.\n:ref:`teenuse-eemaldamine`) ja teise sama funktsiooniga mikroteenuse isendi\nlisamisest (vt. :ref:`teenuse-lisamine`).\n\n\n.. _teenuse-lisamine:\n\nTeenuse isendi lisamine\n-----------------------\n\nTeenuse isendi lisamiseks tuleb vajadusel teenust hostiv server ette valmistada\n(vt.  :ref:`taristu-paigaldamine`) ning rakendada uus tehniline seadistus, mis\nsisaldab lisatavat teenuse isendit.\n\n.. important::\n\n   Lisatava isendi identifikaator ei tohi kattuda ühegi teise, ka minevikus\n   eemaldatud isendi identifikaatoriga.\n\n\n.. _teenuse-eemaldamine:\n\nTeenuse isendi eemaldamine\n--------------------------\n\nTeenuse isendi eemaldamiseks tuleb:\n\n#. Teenuse isend seisma jätta (vt. :ref:`teenuse-seiskamine`);\n\n#. Keelata teenuse isendi uuesti käivitamine (vt. allpool);\n\n#. Rakendada uus tehniline seadistus, mis eemaldatavat isendit enam ei sisalda.\n\n.. important::\n\n   Teenuse isendi eemaldamisel kogumisteenuse koosseisust on oluline\n   eemaldatava isendi täielik elimineerimine.\n\n   Teenuste isendid kasutavad üksteisele usalduse tõestamiseks kindla\n   sertifitseerimiskeskuse (CA) poolt välja antud sertifikaate, kuid ei kasuta\n   sama meetodit eemaldatud isendi usalduse tühistamiseks (vastava protseduuri\n   rakendamise liigse keerukuse tõttu).\n\n   Seetõttu on oluline veenduda, et kogumisteenusest eemaldatud teenuse isend\n   on enne uue seadistuse rakendamist täielikult süsteemist eemaldatud.\n   Vastasel juhul tekib oht, et eemaldatav isend jätkab tegutsemist ja häirib\n   kogumisteenuse tööd.\n\nTeenuse isendi käivitamise keelamiseks teenuse eemaldamisel tuleb eemaldada\nvastava teenuse tarkvarapakk teenuse hostist:\n\n* Nimekirjateenuse paki eemaldamine:\n\n   .. code-block:: text\n\n      $ apt purge ivxv-choices\n\n* Mobiil-ID tugiteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-mid\n\n* Smart-ID tugiteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-smartid\n\n* Web eID tugiteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-webeid\n\n* Session status tugiteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-sessionstatus\n\n* Vahendusteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge haproxy\n\n* Talletusteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge etcd-server\n\n* Kontrolliteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-verification\n\n* Hääletamisteenuse paki eemaldamine:\n\n   .. code-block:: shell-session\n\n      $ apt purge ivxv-voting\n\n\nKasutajate haldus\n-----------------\n\nKasutajate algsed kirjeldused määratakse usaldusjuure seadistuses, hilisem\nhaldus toimub vastavate korralduste abil.\n\nKasutajate halduse korraldused rakendatakse käsuga :ref:`ivxv-cmd-load` (vaata\n:ref:`korralduste-laadimine-rakendamine`).\n\nKasutajaõiguste määramise korralduse rakendamise näide:\n\n.. include:: genereeritud-failid/kasutaja_lisamine.inc\n\n.. attention::\n\n   Juba lisatud kasutajate eemaldamine süsteemist pole võimalik. Kasutaja\n   eemaldamise asemel tuleb kasutaja rolliks määrata \"õigusteta kasutaja\".\n\n.. seealso::\n\n   * Kasutajate rollide kirjeldus ja volituste korralduste koostamise juhend\n     asuvad dokumendis ``IVXV seadistuste koostamise juhend``.\n\n   * Korralduste rakendamine on kirjeldatud lõigus\n     :ref:`korralduste-laadimine-rakendamine`.\n\n\nTarkvarauuenduste rakendamine\n-----------------------------\n\nTarkvarauuendused jagunevad kogumisteenuse vaatepunktist kaheks:\noperatsioonisüsteemi uuendused ja kogumisteenuse uuendused.\n\nOperatsioonisüsteemi tarkvarapakkide uute versioonide paigaldamine pole\nkogumisteenuse dokumentatsioonis käsitletud. Süsteemiülem peab tagama\najakohaste turvauuenduste rakendamise kogumisteenuses kasutatavate\noperatsioonisüsteemidele;\n\nKogumisteenuse tarkvarapakkide uute versioonide paigaldamine toimub järgnevalt:\n\n#. Uuenenud tarkvarapakid kopeeritakse haldusteenuse kataloogi\n   :file:`/etc/ivxv/debs` (soovitavalt juurkasutaja õigustes);\n\n#. Haldusteenuse tarkvara uuendatakse juurkasutaja õigustes käsuga\n   :command:`dpkg -i /etc/ivxv/debs/ivxv-common_1.0_all.deb\n   /etv/ivxv/debs/ivxv-admin_1.0_amd64.deb` (tegelik versiooninumber erineb\n   käesolevas näites kasutatud versioonist);\n\n#. Hallatavate teenuste tarkvara uuendamine toimub haldusteenuse kasutaja\n   ``ivxv-admin`` õigustes käsuga :ref:`ivxv-update-packages`.\n\n\nVarundamine\n-----------\n\nVarundamine hõlmab kolme liiki andmeid:\n\n#. Haldusteenuse seadistused;\n\n#. Kogumisteenuse e-valimiskast;\n\n#. Kogutud logid.\n\nVarukoopia loomine toimub haldusteenuse masinas utiliidi\n:ref:`ivxv-backup` abil, varukoopiad talletatakse varundusserveri\nkataloogis :file:`/var/backups/ivxv`.\n\n\nHaldusteenus seadistuste varundamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusteenuse seadistustest varundatakse järgmised andmed:\n\n#. :file:`etc/` - haldusteenusesse laaditud tarkvarapakid ja hetkel kehtivad\n   seadistusfailid;\n\n#. :file:`admin-ui-permissions/` - haldusteenuse kasutajaliidese pääsuõigused;\n\n#. :file:`commands/` - kõik haldusteenusese laaditud korraldusfailid.\n\nHaldusteenuse varundamist viiakse läbi haldusteenuses, varundatavad andmed\nkopeeritakse varundusserverisse.\n\n.. hint::\n\n   Haldusteenuse andmete tõhusamaks varundamiseks ja taasteks on soovitav\n   kasutada haldusteenuse virtuaalmasina dünaamilist tõmmist (*snapshot dump*).\n\nHaldusteenuse seadistuste varukoopiast taastamise protseduuri pole\nkogumisteenuses ette nähtud.\n\nHaldusteenuse seadistusete varukoopia loomise näide::\n\n   $ ivxv-backup management-conf\n\n\nKogumisteenuse e-valimiskasti varundamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKogumisteenuse e-valimiskasti varundamine toimub talletusteenuses kogutud häältest\ne-valimiskasti loomisega ja selle kopeerimisega varundusteenusesse. Varundatud andmete\ntaastamine toimub hääletuse järel e-valimiskasti väljastamise käigus, kus\ntalletusteenuses olevatest häältest ja varukoopiatesse salvestatud häältest\npannakse kokku töötlemisele minev e-valimiskast.\n\nE-valimiskasti varundamist viib läbi haldusteenus. Varukoopia loomine toimub\ntalletusteenuses ja see kopeeritakse varundusserverisse.\n\nVarukoopia on sama vorminguga, nagu kogumisteenuse poolt väljastatav e-valimiskast\n(ZIP64).\n\nVarundamise andmemahtu saab arvutada järgmise meetodiga:\n``häälte arv * 12,1 kB * pakkimistegur``.\n\nNäiteks saja tuhande hääle suurus, kus pakkimistegur on 0,4 = 472 MB.\n\nE-valimiskasti varukoopia loomise näide::\n\n   $ ivxv-backup ballot-box\n\n\nLogide varundamine\n^^^^^^^^^^^^^^^^^^\n\nLogikogumisteenustes kogutud logifailide varundamine toimub logifailide\n:file:`/var/log/ivxv/ivxv-YYYY-MM-DD-HH.log` kopeerimisega varundusserverisse. Logide\nvarundamist viib läbi haldusteenus.\n\nLogide varukoopiast taastamise protseduuri pole kogumisteenuses ette nähtud.\n\nLogikogumisteenusesse kogutud logist varukoopia loomise näide::\n\n   $ ivxv-backup log\n\n\n.. _konsolideeritud-e-valimiskasti-koostamine:\n\nKonsolideeritud e-valimiskasti koostamine\n-----------------------------------------\n\nKonsolideeritud e-valimiskast koostatakse talletusteenusesse kogutud häältest ja\nvarundusteenusesse varundatud e-valimiskastidest. Konsolideerimise protsess koosneb\njärgmistest sammudest:\n\n#. Talletusteenusesse kogutud hääled varundatakse varundusteenusesse. Selle\n   tulemusena on varundusteenusesse salvestatud kõik kogutud e-valimiskastid;\n\n#. Varundusteenuses koostatakse konsolideeritud e-valimiskast;\n\n#. Konsolideeritud e-valimiskast kopeeritakse haldusteenusesse.\n\nKonsolideeritud e-valimiskasti koostamise näide:\n\n.. include:: genereeritud-failid/e-valimiskasti_koostamine.inc\n\n\nTöötlemisrakenduse sisendi aluse koostamine\n-------------------------------------------\n\nTöötlemisrakenduse sisendi alus on häälte töötlemiseks vajalike sisendfailide\nkomplekt, mis genereeritakse kogumisteenuses salvestatud andmete põhjal.\nKomplekti koosseis on järgmine:\n\n#. Ringkondade nimekiri;\n\n#. Valijate nimekirjad;\n\n#. E-valimiskast kogutud häältega;\n\n#. Häälte registreerimispäringute valideerimisandmed;\n\n#. Töötlemisrakenduse seadistused.\n\nVäljund on ZIP-konteiner, mis sisaldab järgmisi faile:\n\n#. Ringkondade nimekiri digitaalselt signeerituna\n   :file:`<election-id>.districts.json.asice`;\n\n#. Valijate nimekirjade signeerimisvõtme avalik võti\n   :file:`voterfile.pub.key`;\n\n#. Valijate nimekirjad\n   :file:`<changeset_no>.<election-id>.voters.utf`;\n\n#. Valijate nimekirjade signatuurid\n   :file:`<changeset_no>.<election-id>.voters.sig`;\n\n#. Valijate nimekirja vahelejätmise korraldused\n   :file:`<changeset_no>.<election-id>.voters-skip.yaml.asice`;\n\n#. Registreerimispäringute verifitseerimise avalik võti\n   :file:`ts.key`;\n\n#. Töötlemisrakenduse seadistuste mall e-valimiskasti verifitseerimiseks\n   :file:`<election-id>.processor.yaml`.\n\nTöötlemisrakenduse sisendi alus koostatakse utiliidi\n:ref:`ivxv-generate-processor-input` abil. Näide:\n\n.. include:: genereeritud-failid/töötlemisrakenduse_sisendi_koostamine.inc\n\nHääletamise statistika eksportimine\n-----------------------------------\n\nHäälestamise statistika koostatakse hääletusteenuses ja see koosneb kahest\nosast: üldstatistika (hääletajate koguarv) ja detailstatistika. Üldstatistika\nkopeeritakse haldusteenusesse ja eksporditakse Valimiste Infosüsteemi 15\nminutilise intervalliga. Detailstatistika koostatakse ja eksporditakse\nValimiste Infosüsteemi käsitsi.\n\nHäälestamise statistika importimine ja eksportimine toimub haldusteenuse\nmasinas utiliidi :ref:`ivxv-voterstats` abil. Üldstatistika importimise ja\neksportimise automaatika on teostatud cron-teenuse abil ja kirjeldatud failis\n:file:`/etc/cron.d/ivxv-admin`.\n\n\nHääletamise seansside väljavõtte koostamine\n-------------------------------------------\n\nHääletamise ja hääle kontrollimise seansside väljavõte on CSV-vormingus ja see\nkoostatakse logiseire teenuses.\n\nVäljavõtet on võimalik koostada anonüümistatud kujul, kus kasutajate\nisikukoodid ja IP-aadressid on asendatud anonüümsete väärtustega.\n\nVõimalik on valida, kas väljastada kõik hääletamise seansid või ainult hääle\nkontrollimisega seansid.\n\n:ref:`ivxv-voting-sessions`\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/index.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nIVXV kogumisteenuse haldusjuhend\n========================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 2\n\n   annotatsioon\n   ylevaade\n   haldusteenus\n   algseadistamine\n   haldustoimingud\n   krahhitaaste\n   seadistused\n   protseduurid\n   lisad\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/krahhitaaste.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nKrahhitaaste\n============\n\nKogumisteenus on projekteeritud nii, et teenuse või selle osade krahhimise\ntagajärjel ei tekiks andmekadu või oleks see minimaalne.\n\n\nEeldused edukaks krahhitaasteks\n-------------------------------\n\nKõrgkäideldav seadistus\n^^^^^^^^^^^^^^^^^^^^^^^\n\nPeamine eeldus edukaks krahhitaasteks on kogumisteenuse paigaldamine\nkõrgkäideldava seadistusega, mis määrab vähemalt kolme talletusteenuse isendi\nkasutamise. Lisaks on krahhiolukorra kiiremaks lahendamiseks kasulik eraldada\nmikroteenustele ühe lisaisendite komplekti paigalduseks vajalik taristu.\n\n\nLogikoguja kasutamine\n^^^^^^^^^^^^^^^^^^^^^\n\nKogumisteenuse seadistus peab kirjeldama logikogumisteenuse, et mikroteenuste\npoolt toodetavad logisid oleks võimalik lihtsal moel kokku koguda. Soovitav on\nkasutada mitut logikogujat erinevas füüsilises lokatsioonis, et minimeerida\nlogikirjete kaotsimineku võimalust.\n\n\nVarundusteenuse kasutamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKogumisteenuse seadistus peab kirjeldama varundusteenuse ning automaatse\nvarundamise ajad piisava sagedusega. Samuti on soovitav teha varukoopiad ka\nvarundusteenusest.\n\nAutomaatne varundamine tagab e-valimiskasti koopia säilimise :ref:`talletusteenuse\ntäieliku krahhi <talletusteenuste-täielik-krahh>` korral.\n\n.. note::\n\n   Varundusteenus on soovitav paigaldada teistest kogumisteenuse isenditest\n   füüsiliselt eraldi, et võimalikud eriolukorrad (näiteks tulekahju) ei\n   mõjutaks korraga nii varundusteenust kui teisi teenuseid.\n\nVarundusteenus on projekteeritud kogumisteenuse andmetest automaatsete\nvarukoopiate loomiseks ühte kohta ning nende kättesaadavaks tegemiseks\noperatsioonidele, mis varukoopiaid kasutavad (näiteks häälte kokkulugemine).\n\n.. note::\n\n   Kogumisteenuse osutaja peaks kaaluma võimalust teha varundusteenusest\n   täiendavaid varukoopiaid, et tagada varundatud andmete säilimine ka\n   varundusteenuse krahhi korral.\n\nValmisolek krahhiks\n^^^^^^^^^^^^^^^^^^^\n\nKogumisteenuse krahh mõjutab kõiki e-hääletamise komponente, erilist\ntähelepanu tuleb pöörata valijarakenduste ja kontrollrakenduste\nnimelahendusele ning TLS ühenduste usaldamiseks vajalikele\nsertifikaatidele.\n\nHääletamise edukaks läbiviimiseks tuleb tagada, et nimeserverid\nsisaldaks kogu hääletusperioodi vältel ajakohast infot\nhääletamissüsteemi sisendpunktide kohta - siis suudavad\nvalijarakendused ja kontrollrakendused vastavalt muutuvatele oludele\nnimesid korrektselt lahendada.\n\n#. Krahhimise tuvastamisel tuleb esimeste tegevuste hulgas eemaldada\n   nimelahendusest krahhinud teenus, et rakendused enam selle poole\n   pöörduda ei saaks.\n#. Kui teenused pärast krahhi uuesti töökorda saadakse, tuleb\n   viimase sammuna nimelahenduses panna uute teenuste aadressid\n   lahenduma vastavalt rakendustes defineeritule.\n\nKui kogumisteenusesse lisatakse uusi mikroteenuseid (eeldatavalt\npärast krahhimist), siis on tarvis tagada lisatud teenuste\nusaldusväärsus rakendustes.\n\nTeenuse plaanimisel tuleb luua serdid/võtmed ka võimalike\nasendusteenuste jaoks (choices, mid, voting). Need võtmed tuleb\npakendada valijarakendusse, et pärast krahhi poleks tarvis hakata uut\nrakendust levitama. Kui sertifikaadid luuakse ühe CA alt, siis piisab\nvalijarakendusse vastava CA sertifikaadi pakendamisest.\nKontrollrakenduste jaoks tuleb seadistustes alati näidata konkreetsed\nteenussertifikaadid, kuid kontrollrakenduste seadistuste muutmine ei\neelda kontrollrakenduste uuesti levitamist.\n\nTeenuste taastamine krahhist\n----------------------------\n\n\nMikroteenuse isendi krahh ilma andmekaota\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nMikroteenuse isendi krahh ilma andmekaota võib esineda teenuste puhul, mis ei\ntegele andmete säilitamisega (nimekirjateenus, hääletusteenus, kontrolliteenus\nvõi mobiil-id tugiteenus). Sellises olukorras piisab teenuse isendi\ntaastamiseks kas teenuse taaskäivitamisest (kui see on võimalik) või teenuse\nisendi asendamisest uuega.\n\n.. seealso::\n\n   * :ref:`teenuse-taaskäivitamine`\n\n   * :ref:`teenuse-asendamine`\n\n   * :ref:`recovery-stateless`\n\nLogikogumisteenuse isendi krahh\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLogikogumisteenuse krahh võib esineda nii logiandmete riknemisega kui ka ilma.\n\nIlma logiandmete riknemiseta krahh tähendab olukorda, kus rsyslog teenus seisab\nja ei võta seetõtte teenustelt logikirjeid vastu ning salvestatud logifailid ei\nole rikutud. Sellises olukorras piisab teenuse isendi töökorda seadmiseks selle\ntaaskäivitamisest.\n\nLogikogumisteenuse krahh koos logiandmete riknemisega nõuab teenuse isendi\nasendamist uuega.\n\nKui logiandmete riknemisega kaasneb alati logiandmete kadu, siis ilma\nriknemiseta krahhi puhul tuleb samuti selle võimalusega arvestada. Logisid\nedastatakse üle RELP-protokolli, mis on küllalt töökindel, kuid vaatamata\nsellele võib logiedastus katkeda olukorras, kus logi genereeriva teenuse hostil\non rsyslogi isendit taaskäivitatud ajal, mil logikoguja rsyslog isend ei\ntöötanud.\n\n.. seealso::\n\n   * :ref:`teenuse-taaskäivitamine`\n\n   * :ref:`teenuse-asendamine`\n\n   * `RELP - The Reliable Event Logging Protocol\n     <https://www.rsyslog.com/doc/relp.html>`_\n\n   * :ref:`recovery-logcollection`\n\nVarundusteenuse isendi krahh\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nVarundusteenuse isendi krahh tähendab varundusteenusesse varundatud andmete\nriknemist. Teenuse taastamiseks tuleb varundusteenus uuesti paigaldada ja\nvarundatud andmed taastada. Andmete taastamine varundusserverisse võib toimuda\nka pärast häälte kogumise lõppemist, kuid enne häälte kokkulugemist.\n\n.. note::\n\n   Varundusprotseduuride käivitamist juhitakse haldusteenusest ja seetõttu pole\n   varundusteenust võimalik käivitada ega seisma jätta.\n\n.. seealso::\n\n   * :ref:`recovery-backupservice`\n\nTalletusteenuse isendi krahh\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTalletusteenuse ühe isendi krahhimisel piisab isendi asendamisest uuega.\n\nTalletusteenuseid saab lisada ja eemaldada ainult siis, kui klastris on\nvähemalt kvoorumi jagu töökorras talletusteenuse isendeid. Kvoorumi suurus on\nN/2+1 ümardatud alla, kus N on seadistatud isendite arv (näiteks kolme\nseadistatud isendi korral on kvoorumi suurus kaks).\n\nKui talletusteenuse isendeid jääb alles vähem kui kvoorumi jagu, siis tuleb\nteha kõigile isenditele uus paigaldus (vt.\n:ref:`talletusteenuste-täielik-krahh`).\n\nTalletusteenuse kvoorumist tingitud piirangud:\n\n#. Talletusteenuse isendite arvu ei ole kunagi võimalik vähendada ühele;\n\n#. Talletusteenuste isendite eemaldamisel peab arvestama kvoorumi säilimisega.\n\n   Näide: kui on seadistatud 6 talletusteenuse isendit (kvoorum=4), siis sealt\n   ei saa korraga eemaldada kolme isendit (jääks järgi kolm isendit,\n   kvoorum=2), kuna seadistatud isendite hulk oleks siis väiksem kui algne\n   kvoorum. Kõigepealt tuleb eemaldada üks (jääb järgi 5 isendit, kvoorum=3)\n   isend ja alles pärast seda saab eemaldada ülejäänud kaks.\n\n.. seealso::\n\n   * :ref:`teenuse-taaskäivitamine`\n\n   * :ref:`teenuse-asendamine`\n\n   * :ref:`recovery-storageservice`\n\n.. _talletusteenuste-täielik-krahh:\n\nTalletusteenuste täielik krahh ehk kogumisteenuse täielik asendamine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTalletusteenuste täielikul asendamisel tuleb koostada uus tehniline seadistus,\nmis vastab järgmistele tingimustele:\n\n* ei sisalda ühtegi vana talletusteenust;\n\n* kõik uued talletusteenused on loetletud parameetri ``storage.conf.bootstrap``\n  nimekirjas.\n\n.. important::\n\n   Talletusteenuste täielikul asendamisel tuleb arvestada järgnevada:\n\n   * enne asendamist kogutud hääled säilivad varundusserveritesse tehtud\n     varukoopiates;\n\n   * varukoopia loomise ja krahhi vahel kogutud hääled lähevad kaotsi;\n\n   * valikute, ringkondade ja valijate nimekirjad tuleb teenustele uuesti rakendada.\n\n.. seealso::\n\n   * :ref:`recovery-fullstorage`\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/lisad.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nLisad\n=====\n\n.. _utiliidid:\n\nUtiliidid\n---------\n\nKogumisteenuse haldamise käsureautiliitide ülevaade ja abiteave.\n\n.. contents:: .\n   :local:\n   :depth: 1\n\n\nAndmehoidla utiliidid\n^^^^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-create-data-dirs.inc\n\n.. include:: utiliitide-abiteave/ivxv-db-reset.inc\n\n.. include:: utiliitide-abiteave/ivxv-db-dump.inc\n\n\nTeenuse seisundi utiliidid\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-status.inc\n\n.. include:: utiliitide-abiteave/ivxv-service.inc\n\n\nSündmuste logi utiliidid\n^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-eventlog-dump.inc\n\n\nKasutajate halduse utiliidid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-users-list.inc\n\n\nSeadistusutiliidid\n^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-collector-init.inc\n\n.. include:: utiliitide-abiteave/ivxv-cmd-load.inc\n\n.. include:: utiliitide-abiteave/ivxv-config-validate.inc\n\n.. include:: utiliitide-abiteave/ivxv-config-apply.inc\n\nSeadistuste rakendamine hallatavatele teenustele on võimalik siis, kui\nhaldusteenusesse on laaditud kogumisteenuse tehnilised seadistused.\n\nSeadistuste rakendamise järjekord:\n\n#. Tehnilised seadistused koos usaldusjuure seadistustega.\n\n   #. Teenuse tarkvara paigaldamine;\n\n   #. Haldusteenuse ligipääsu loomine hallatava teenuse kontole;\n\n   #. Teenuse logimisseadistuste rakendamine;\n\n   #. Haldusteenuse ligipääsu eemaldamine teenuse hosti juurkasutaja kontole\n      (ainult juhul, kui teenusmasinas pole rohkem seadistamata teenuseid);\n\n   #. Usaldusjuure rakendamine teenusele;\n\n   #. Tehniliste seadistuste rakendamine teenusele;\n\n#. Valikute nimekiri;\n\n#. Ringkondade nimekiri;\n\n#. Valijate nimekirjad;\n\nLogikogumisteenus erineb teistest hallatavatest teenustest:\n\n#. Logikogumisteenus seadistatakse enne teisi teenuseid, et tagada võimalikult\n   varajane logi kogumine.\n\n#. Logikogumisteenustele ei rakendata muid seadistusi peale logikogumisteenuse\n   seadistuste (usaldusjuure seadistusi, kogumisteenuse tehnilised seadistusi\n   ja valimiste seadistusi logikogumisteenus ei vaja).\n\nValimisnimekirjade (valikute ja valijate nimekirjad) rakendamine tähendab\nnimekirja ülekandmist talletusteenusesse vastavat nimekirja teenindava teenuse kaudu.\n\nNäiteks valikute nimekiri rakendatakse vaid ühele (juhuslikult valitud)\nnimekirjateenusele, mis kannab nimekirja talletusteenusesse. Talletusteenuse\nkaudu on nimekiri kättesaadav kõigile teistele nimekirjateenustele.\n\n.. include:: utiliitide-abiteave/ivxv-voter-list-download.inc\n\n.. include:: utiliitide-abiteave/ivxv-secret-load.inc\n\n.. include:: utiliitide-abiteave/ivxv-copy-log-to-logmon.inc\n\n.. include:: utiliitide-abiteave/ivxv-update-packages.inc\n\n.. include:: utiliitide-abiteave/ivxv-backup-crontab.inc\n\n\nAndmete eksportimise ja varundamise utiliidid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-export-votes.inc\n\n.. include:: utiliitide-abiteave/ivxv-backup.inc\n\n.. include:: utiliitide-abiteave/ivxv-generate-processor-input.inc\n\n.. include:: utiliitide-abiteave/ivxv-voterstats.inc\n\n.. include:: utiliitide-abiteave/ivxv-voting-sessions.inc\n\n\nDeemonid\n^^^^^^^^\n\n.. include:: utiliitide-abiteave/ivxv-agent-daemon.inc\n\n\nSisemised utiliidid\n^^^^^^^^^^^^^^^^^^^\n\n.. attention::\n\n   Sisemised utiliidid on kasutusel haldusdeemoni poolt alamteenuste\n   haldamiseks ja neid ei ole reeglina tarvis eraldi käivitada.\n\n.. include:: utiliitide-abiteave/ivxv-admin-helper.inc\n\n.. include:: utiliitide-abiteave/ivxv-admin-sudo.inc\n\n\nSeadistusfailid\n---------------\n\n.. _ivxv-logcollector.conf:\n\nLogikogumisteenuse seadistusfail\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. literalinclude:: ../../../common/collector/config/rsyslog-logcollector.conf\n   :name: /etc/rsyslog.d/ivxv-logcollector.conf\n   :language: text\n   :linenos:\n\n\nLisaseadistused\n---------------\n\n.. _configure-ssh-idcard-auth:\n\nSSH kasutajate autentimine ID-kaardi abil\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSSH-teenusesse on võimalik autentida ID-kaardi avaliku võtmega abil, kasutades\nselleks PKCS#11 toega SSH-klienti ``kitty.exe`` (http://kitty.9bis.net/).\n\nTurvakaalutustel tuleks keelata haldusliidese SSH-teenusesse parooliga\nautentimine. Parooliga autentimise keelamiseks tuleb seadistusfailis\n:file:`/etc/ssh/sshd_config` määrata parameetri ``PasswordAuthentication``\nväärtuseks ``no``::\n\n   # To disable tunneled clear text passwords, change to no here!\n   PasswordAuthentication no\n\nVolitatud kasutajate faili asukoht (:file:`/etc/ssh/kasutajad`) tuleb failis\n:file:`/etc/ssh/sshd_config` määrata parameetriga ``AuthorizedKeysFile``:\n\n   ``AuthorizedKeysFile /etc/ssh/kasutajad``\n\n.. important::\n\n   Seadistusfailis ``/etc/ssh/sshd_config`` tehtud muutuse rakendamiseks tuleb\n   SSH teenus taaskäivitada::\n\n      # service ssh restart\n      [ ok ] Restarting OpenBSD Secure Shell server: sshd.\n\nID-kaardi isikutuvastamise sertifikaadiga autenditava kasutaja ülesseadmine\nkäib järgmiselt:\n\n#. Kasutajale konto loomine:\n\n   .. code-block:: shell-session\n\n      # adduser --disabled-password kasutajanimi\n      # usermod -a -G www-data kasutajanimi\n\n#. Kasutaja ID-kaardi isikutuvastamise sertifikaadi salvestamine PEM-vormingus\n   faili :file:`usercert.cer` (ID-kaardi haldusvahendi abil);\n\n#. Sertifikaadist kasutaja avaliku võtme eraldamine ja salvestamine faili\n   :file:`userpubkey.pem`:\n\n   .. code-block:: shell-session\n\n      # openssl x509 -in usercert.cer -pubkey -noout > userpubkey.pem\n\n#. Avaliku võtme teisendamine PKCS#8 vormingusse, kasutaja tunnusega\n   varustamine ja salvestamine SSH volitatud kasutajate faili\n   :file:`/etc/ssh/kasutajad`:\n\n   .. code-block:: shell-session\n\n      # KEY=$(ssh-keygen -i -m PKCS8 -f userpubkey.pem)\n      # echo \"$KEY kasutaja@eesti.ee\" >> /etc/ssh/kasutajad\n\n#. Kontrollimine, kas lisatud kirje on kujul ``ssh-rsa PKCS8-võti``\n   kasutajatunnus:\n\n   .. code-block:: shell-session\n\n      # tail -1 /etc/ssh/kasutajad\n      ssh-rsa AAAAB3NzaC1yc2EAAAAELGuiTwAAAIEAxZf/TuSrGJEU1PlfkY9jJ33VOYVZ9Vao0Uiytlf8\n      7HJu/78fCIB7m05J7ibpMhsZoZ4DElU7ve0VwbvdDS3srh1OhiQcUjpznTlx4rIM1vkHwadrHtmF+BNi\n      DwbLbbdD5y3puGcLH+sLuwba6Vuc3aU0QuqzenYmY9pV7w9y0wc= kasutaja@eesti.ee\n\n\nAndmehoidla\n-----------\n\nHaldusteenuse andmeid hoitakse failisüsteemis ja andmebaasis. Failisüsteemis\nhoitakse andmeid, mis on pärit välistest süsteemidest ja on haldusteenusesse\nüle kantud faili kujul. Andmebaasis hoitakse andmeid, mis on genereeritud\nhaldusteenuse töö käigus.\n\n\nFailisüsteemis hoitavad andmed\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n*  :file:`/etc/ivxv/` -- kogumisteenusele rakendatud ja hetkel kehtivad\n   seadistus- ja nimekirjafailid;\n\n*  :file:`/var/lib/ivxv/` -- kogumisteenuse haldusteenuse andmefailid;\n\n*  :file:`/var/lib/ivxv/admin-ui-data/` -- haldusteenuse veebiliidese jaoks\n   serveeritavad JSON-failid;\n\n*  :file:`/var/lib/ivxv/admin-ui-data/status.json` -- kogumisteenuse seisundi\n   koondandmed;\n\n*  :file:`/var/lib/ivxv/admin-ui-permissions/` -- haldusteenuse veebiliidese\n   kasutajaõigused (Apache veebiserveri jaoks);\n\n*  :file:`/var/lib/ivxv/ballot-box/` -- allalaaditava e-valimiskasti salvestamise kataloog;\n\n*  :file:`/var/lib/ivxv/commands/` -- kogumisteenuse juhtimiseks rakendatud\n   korraldusfailide ajalugu;\n\n*  :file:`/var/lib/ivxv/commands/<command-type>-<timestamp>.bdoc` --\n   digitaalselt allkirjastatud korraldus ``ASiC-E`` vormingus.\n\n*  :file:`/var/lib/ivxv/commands/<command-type>-<timestamp>.json` --\n   korralduse olekufail JSON-vormingus.\n\n*  :file:`/var/lib/ivxv/db/` -- haldusteenuse andmebaasi kataloog;\n\n*  :file:`/var/lib/ivxv/db/ivxv-management.db` -- haldusteenuse andmebaasi\n   fail;\n\n*  :file:`/var/lib/ivxv/ivxv-management-events.log` -- haldusteenuse sündmuste\n   logi;\n\n*  :file:`/var/lib/ivxv/service/` -- muud teenusespetsiifilised failid\n   (nt. registreerimisvõtmest eraldatud avalik võti);\n\n*  :file:`/var/lib/ivxv/upload/` -- kogumisteenusesse veebiliidese kaudu\n   laaditud failid;\n\nAndmebaasis hoitavad andmed\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nAndmevälja nimi ja kirjeldus:\n\n* ``collector/state`` -- kogumisteenuse olek;\n\n* ``config/election`` -- kogumisteenuses rakendatud valimiste seadistusele\n  digiallkirja andnud volitatud kasutaja andmed kujul ``<CN> <timestamp>``;\n\n* ``config/technical`` -- kogumisteenuses rakendatud tehnilisele seadistusele\n  digiallkirja andnud volitatud kasutaja andmed kujul ``<CN> <timestamp>``;\n\n* ``config/trust`` -- kogumisteenuses rakendatud usaldusjuure seadistusele\n  digiallkirja andnud volitatud kasutaja andmed kujul ``<CN> <timestamp>``;\n\n* ``election/election-id`` -- valimiste identifikaator;\n\n* ``election/electionstart`` -- valimiste algusaeg;\n\n* ``election/electionstop`` -- valimiste lõpuaeg;\n\n* ``election/servicestart`` -- kogumisteenuse käivitamise aeg;\n\n* ``election/servicestop`` -- kogumisteenuse seiskamise aeg;\n\n* ``host/<hostname>/state`` -- teenushosti seisund;\n\n* ``list/choices`` -- haldusteenusesse laaditud valikute nimekirjale\n  digiallkirja andnud volitatud kasutaja andmed kujul ``<CN> <timestamp>``;\n\n* ``list/choices-loaded`` -- nimekirjateenustesse laaditud valikute\n  nimekirjale digiallkirja andnud volitatud kasutaja andmed kujul ``<CN>\n  <timestamp>``;\n\n* ``list/districts`` -- nimekirjateenustesse laaditud ringkondade\n  nimekirjale digiallkirja andnud volitatud kasutaja andmed kujul ``<CN>\n  <timestamp>``;\n\n* ``list/districts-loaded`` -- nimekirjateenustesse laaditud ringkondade\n  nimekirjale digiallkirja andnud volitatud kasutaja andmed kujul ``<CN>\n  <timestamp>``;\n\n* ``list/voters0000`` -- haldusteenusesse laaditud valijate algnimekirjale\n  digiallkirja andnud volitatud kasutaja andmed kujul ``<CN> <timestamp>``;\n\n* ``list/voters<list-number>`` (``list-number >= 01``) -- haldusteenusesse\n  laaditud valijate muudatusnimekirja allalaadimise hetke ajatempel;\n\n* ``list/voters<list-number>-state`` -- nimekirjateenustesse laaditud valijate\n  nimekirja olek.\n\n  Võimalikud väärtused:\n\n  1. ``PENDING`` - laaditud haldusteenusesse;\n\n  2. ``APPLIED`` - rakendatud nimekirjateenusele;\n\n  3. ``INVALID`` - nimekiri on märgitud vigaseks ja ootab halduri otsust\n     vahelejätmise kohta (ainult muudatusnimekirja korral);\n\n  4. ``SKIPPED`` - nimekiri on vahele jäetud (ainult muudatusnimekirja korral).\n\n* ``logmonitor/address`` -- seireteenuse aadress või võrgunimi;\n\n* ``logmonitor/last-data`` -- viimase seireteenusest statistikafaili hankimise\n  aeg;\n\n* ``user/<idcode>`` -- haldusteenuse kasutaja nimi ja rollid kujul\n  ``<surname,name> <role>[,<role>]``;\n\n* ``service/<service-id>/service-type`` -- Teenuse liik;\n\n* ``service/<service-id>/technical-conf-version`` -- Teenusele rakendatud\n  tehnilise seadistuse versioon;\n\n* ``service/<service-id>/election-conf-version`` -- Teenusele rakendatud\n  valimiste seadistuse versioon;\n\n* ``service/<service-id>/network`` -- Teenusele alamvõrgu nimi;\n\n* ``service/<service-id>/state`` -- Teenuse olek;\n\n* ``service/<service-id>/ping-errors`` -- Teenuse elusoleku kontrollimise\n  järjestikuste vigade arv;\n\n* ``service/<service-id>/last-data`` -- Teenuse viimase oleku hankimise aeg;\n\n* ``service/<service-id>/ip-address`` -- Teenuse IP-aadress;\n\n* ``service/<service-id>/bg_info`` -- Teenuse taustainfo stringina (näiteks\n  elusoleku kontrolli käigus genereeritud veateade);\n\n* ``service/<service-id>/backup-times`` -- Varundusteenuse automaatvarunduse\n  kellaajad;\n\n* ``service/<service-id>/mid-token-key`` -- Mobiil-ID/Smart-ID/Web eID tugiteenuse\n  identsustõendi võtmefaili kontrollsumma (SHA256);\n\n* ``service/<service-id>/tls-cert`` -- Teenuse TLS-sertifikaadi faili\n  kontrollsumma (SHA256);\n\n* ``service/<service-id>/tls-key`` -- Teenuse TLS-sertifikaadi võtmefaili\n  kontrollsumma (SHA256);\n\n* ``service/<service-id>/tspreg-key`` -- Hääletamisteenuse ajatempliteenuse\n  signeerimisvõtme faili kontrollsumma (SHA256);\n\nKasutatud tähised:\n\n* ``<command-type>`` -- korralduse liik:\n\n   #. ``trust`` -- usaldusjuure seadistused;\n\n   #. ``technical`` kogumisteenuse seadistused;\n\n   #. ``election`` valimiste seadistused;\n\n* ``<CN>`` -- ID-kaardi CN väli kujul ``PEREKONNANIMI,EESNIMI,ISIKUKOOD``;\n\n* ``<config-type>`` on seadistuse liik. Usaldusjuure seadistus on ``trust``,\n  valimiste seadistus on ``election`` ja kogumisteenuse tehniline\n  seadistus on ``tech``;\n\n* ``<hostname>`` teenushosti nimi;\n\n* ``<list-number>`` valimisnimekirja kahekohaline järjekorranumber, esimene nimekiri\n  kannab numbrit 01.\n\n* ``<service-id>`` teenuse identifikaator kogumisteenuse seadistustest;\n\n* ``<timestamp>`` on ajatempel ISO-8601 vormingus.\n\n\n.. _etcd-zabbix:\n\nKlastri seisundi monitoorimine Zabbixiga\n----------------------------------------\n\nEtcd klaster tagab süsteemi toimimise ka olukorras, kus mõni klastriliige kaotab\ntöövõime (krahh, võrguühenduse kadumine jms.). Siiski on oluline selliseid\nsündmuseid monitoorida ning nende algpõhjus tuvastada. Etcd krahhimise\ntuvastamiseks tuleb talletusteenuste logidest (``ivxv-YYYY-MM-DD-HH.log``) monitoorida\n``ivxv.ee/service/storage.EtcdTerminatedError`` kirjet.\n\nTäiendavalt saab etcd käsureakliendiga küsida klastri liikmete olekut. Kuna\nIVXV klastris on kõik klient-päringud autenditud, siis tuleb korraldus\nkäivitada mõnes ``ivxv-storage`` teenuse masinas kasutajakonto\n``ivxv-storage`` (või juurkasutaja) õigustes:\n\n.. code-block:: shell-session\n\n   # ivxv-storage@ivxv1:~$ env ETCDCTL_API=3 etcdctl \\\n         --cacert /var/lib/ivxv/service/storage@storage1.ivxv.ee/ca.pem \\\n         --cert /var/lib/ivxv/service/storage@storage1.ivxv.ee/tls.pem \\\n         --key /var/lib/ivxv/service/storage@storage1.ivxv.ee/tls.key \\\n         --endpoints ivxv1:2379,ivxv2:2379,ivxv3:2379 \\\n         endpoint status\n\n   ivxv1:2379, 2d0df029f29770a4, 3.2.17, 25 kB, true, 12, 15\n   ivxv2:2379, d4a9ae16c8557764, 3.2.17, 25 kB, false, 12, 15\n   ivxv3:2379, e8914f4e0b89b80f, 3.2.17, 25 kB, false, 12, 15\n\nVastuses on veergude tähendused järgmised:\n\n #. klastri liige;\n #. klastri liikme identifikaator;\n #. etcd versioon;\n #. baasi suurus (max 8GB ehk 8589934592);\n #. kas konkreetne klastri liige on hetkel juht;\n #. RAFT ametiaeg (sisuliselt toimunud juhi-valimiste arv);\n #. RAFT indeks - etcd kirjutamisoperatsioonide arv (sh.\n    konfiguratsiooni muutused).\n\nMonitooringule on oluline parameeter RAFT ametiaeg. Selle väärtuse muutumine\ntähendab juhivahetust, mis üldjuhul on seotud probleemidega klastri töös -\nolemasolev juht ei vasta piisavalt kiiresti klastri liikmete päringutele.\n\nKäsurea seletus:\n\n * ``env ETCDCTL_API=3``: kasutame etcd API versiooni 3 (Ubuntu versioonis\n   20.04 LTS on ``etcdctl`` API vaikeversioon veel 2);\n * ``--cacert``: usaldame ainult servereid, mille sertifikaat on antud selle CA\n   poolt;\n * ``--cert`` ja ``--key``: kasutame klient-autentimiseks ivxv1 talletusteenuse\n   sertifikaati ja võtit;\n * ``--endpoints``: millistele serveritele päring saata. Siin võib kõigi kolme\n   asemel ka ainult ühe loetleda: sellisel juhul on väljundis vaid üks rida.\n   Kasulik nt kui Zabbix tahab igas talletusteenuses küsida ainult selle isendi\n   kohta;\n * ``endpoint status``: küsime loetletud serverite olekut.\n\n\nVäljundit on võimalik küsida ka masinloetavas JSON-vormingus\n(parameeter ``-w json``):\n\n   .. code-block:: shell-session\n\n      ivxv-storage@ivxv1:~$ env ETCDCTL_API=3 etcdctl \\\n          --cacert /var/lib/ivxv/service/storage@storage1.ivxv.ee/ca.pem \\\n          --cert /var/lib/ivxv/service/storage@storage1.ivxv.ee/tls.pem \\\n          --key /var/lib/ivxv/service/storage@storage1.ivxv.ee/tls.key \\\n          --endpoints ivxv1:2379,ivxv2:2379,ivxv3:2379 \\\n          endpoint status -w json\n\n      [{\"Endpoint\":\"ivxv1:2379\",\"Status\":{\"header\":{\"cluster_id\":1867986262344190226,\"member_id\":3246514969358332068,\"revision\":1,\"raft_term\":12},\"version\":\"3.2.17\",\"dbSize\":24576,\"leader\":3246514969358332068,\"raftIndex\":15,\"raftTerm\":12}},\n      {\"Endpoint\":\"ivxv2:2379\",\"Status\":{\"header\":{\"cluster_id\":1867986262344190226,\"member_id\":15323970619978381156,\"revision\":1,\"raft_term\":12},\"version\":\"3.2.17\",\"dbSize\":24576,\"leader\":3246514969358332068,\"raftIndex\":15,\"raftTerm\":12}},\n      {\"Endpoint\":\"ivxv3:2379\",\"Status\":{\"header\":{\"cluster_id\":1867986262344190226,\"member_id\":16758262885041944591,\"revision\":1,\"raft_term\":12},\"version\":\"3.2.17\",\"dbSize\":24576,\"leader\":3246514969358332068,\"raftIndex\":15,\"raftTerm\":12}}]\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/protseduurid.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nProtseduurid\n============\n\n\n.. _recovery-stateless:\n\nJuhend: olekuvaba mikroteenuse isendi taastamine krahhist\n---------------------------------------------------------\n\n.. include:: recovery/recovery_stateless.inc\n\n\n\n.. _recovery-logcollection:\n\nJuhend: logikogumisteenuse isendi taastamine krahhist\n-----------------------------------------------------\n\n.. include:: recovery/recovery_logcollection.inc\n\n\n\n.. _recovery-backupservice:\n\nJuhend: varundusteenuse isendi taastamine krahhist\n--------------------------------------------------\n\n.. include:: recovery/recovery_backupservice.inc\n\n\n\n.. _recovery-storageservice:\n\nJuhend: talletusteenuse isendi taastamine krahhist\n--------------------------------------------------\n\n.. include:: recovery/recovery_storageservice.inc\n\n\n\n.. _recovery-fullstorage:\n\nJuhend: talletusteenuse taastamine täielikust krahhist\n------------------------------------------------------\n\n.. include:: recovery/recovery_fullstorage.inc\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/recovery/recovery_backupservice.inc",
    "content": "\nKrahhi toimumise indikaatorid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusveebiliides.\nVali **Teenused** vasakult menüüst. Kui varundusteenus on krahhinud,\nsiis tema **Seisund** on **Tõrge**.\n\nHalduskäsurealiides.\nLogi sisse **ivxv-admin** kasutajana ja päri teenuste statistikat::\n\n   $ sudo su - ivxv-admin\n   $ ivxv-status\n\nKui varundusteenus on krahhinud, siis tema **State** on **FAILURE**.\n\n\nKrahhist taastumise eeldused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhist taastumine on alati võimalik.\n\n\nKrahhi võimalik mõju hääletamistulemusele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nVarundusteenuse krahh mõjutab hääletamistulemust (andmekadu), kui täiendavalt\nkrahhib ka talletusteenus.\n\n\nKrahhist taastumise sammud\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Kasuta :ref:`recovery-stateless` juhendis oleva\n     **Teenuse masin on krahhinud, v.a vahendus- ja x-tee teenused**\n     peatükki, v.a sammud 8, 10, 12-16.\n\n.. note::\n\n   Punktis 10 tuleb välja vahetada ainult varundusteenuse **id** ja **address**\n   väljad:\n\n   .. code-block:: yaml\n\n     - id: zone0\n       services:\n         log:\n           - id:          backup@backup1.ivxv.ee\n             address:     backup1\n\n   uute vastu:\n\n   .. code-block:: yaml\n\n     - id: zone0\n       services:\n         log:\n           - id:          backup@backup2.ivxv.ee\n             address:     backup2\n\nSSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n    $ sudo su - ivxv-admin\n\nSeadista SSH ühenduse loomiseks *prompt* sõnumi\n*Are you sure you want to continue connecting (yes/no)? yes* kinnitamist\nvältida, näiteks, kui varundusteenuse masina DNS **address** on *backup2*::\n\n    $ ssh backup2\n    $ echo \"StrictHostKeyChecking no\" >> /var/lib/ivxv/user/ivxv-admin/.ssh/config\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/recovery/recovery_fullstorage.inc",
    "content": "Krahhi toimumise indikaatorid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusveebiliides.\nVali **Teenused** vasakult menüüst. Kui talletusteenused on krahhinud,\nsiis tema **Seisund** on **Tõrge**.\n\nHalduskäsurealiides.\nLogi sisse **ivxv-admin** kasutajana ja päri teenuste statistikat::\n\n   $ sudo su - ivxv-admin\n   $ ivxv-status\n\nKui talletusteenused on krahhinud, siis tema **State** on **FAILURE**.\n\n\nKrahhist taastumise eeldused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhi taastumiseks piisab kui teenuste CA TLS privaatvõti ja sertifikaat on\nalles, mida saab kasutada teenuse varuvõtmete ja -sertifikaatide\ngenereerimiseks. Samas, saab ka enne valimisi teenuse varuvõtmed ja\n-sertifikaadid ette genereerida.\n\nKui kõikide teenuste CA on sama, siis tasub valija- ja kontrollrakenduste\nseadistusse panna hoopis CA TLS sertifikaadi. Juhul kui CA on teenustel erinev,\nsiis peab panema kõik teenuste TLS sertifikaadid rakenduste seadistusse\nvahetult enne valimisi.\n\n\nKrahhi võimalik mõju hääletamistulemusele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKui krahhib talletusteenuse klaster, siis kindlasti tegu on andmekaoga.\nAndmekao suurus sõltub varundusteenusest ehk kui tihti e-urni varundati.\n\nLisaks, juba hääletanud valijal võivad tekkida segadus, kui teist korda\nhääletades, kuvatakse valijarakenduses \"Teie pole veel hääletanud\"\ntervitustekst.\n\n\nKrahhist taastumise sammud\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Tee :ref:`recovery-storageservice` juhendis oleva\n     **Teenuse andmed (võti, sertifikaat jne) on rikutud või masina krahh**\n     peatükki protseduurid läbi iga uue koguja masina jaoks.\n\n.. note::\n\n   Punktis 10 tuleb välja vahetada kõik vanad **id** ja **address** kirjed\n   uute vastu, lisaks tuleb muuta ka talletusteenuse **bootstrap**\n   seadistust:\n\n   .. code-block:: yaml\n\n      bootstrap:\n      - storage@storage1.ivxv.ee\n      - storage@storage2.ivxv.ee\n      - storage@storage3.ivxv.ee\n\n   selliselt:\n\n   .. code-block:: yaml\n\n      bootstrap:\n      - storage@storage4.ivxv.ee\n      - storage@storage5.ivxv.ee\n      - storage@storage6.ivxv.ee\n\nNüüd tuleb taastada koguja krahhieelne seisund, v.a kaotsi läinud hääled:\n\n   #. SSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Laadi ringkondade nimekiri::\n\n         $ ivxv-config-apply --type=districts\n\n   #. Laadi valikute nimekiri::\n\n         $ ivxv-config-apply --type=choices\n\n   #. Laadi nii valijate alg-, kui ka kõik vahepealsed muudatusnimekirjad::\n\n         $ ivxv-config-apply --type=voters\n\n.. warning::\n   Ära unusta, et valimiste lõpus tuleb hääli alla laadida koos varundatud\n   häältega, et vähemalt mingis ulatuses tagada e-urni terviklikkus::\n\n      $ ivxv-export-votes --consolidate\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/recovery/recovery_logcollection.inc",
    "content": "Krahhi toimumise indikaatorid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusveebiliides.\nVali **Teenused** vasakult menüüst. Kui logikogumisteenus on krahhinud,\nsiis tema **Seisund** on **Tõrge**.\n\nHalduskäsurealiides.\nLogi sisse **ivxv-admin** kasutajana ja päri teenuste statistikat::\n\n   $ sudo su - ivxv-admin\n   $ ivxv-status\n\nKui logikogumisteenus on krahhinud, siis tema **State** on **FAILURE**.\n\n\nKrahhist taastumise eeldused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhist taastumine on alati võimalik.\n\n\nKrahhi võimalik mõju hääletamistulemusele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHääletamistulemusele mõju puudub, küll aga krahhinud logikogumisteenusega kaob\nka osa varundatud logidest, mida enam taastada ei ole võimalik. Samas, need\nlogid on ikkagi endiselt alles koguja masinates, ning ka logimonitori\nmasinas. Ainus koht kus avaldub logikogumisteenuse krahh on varundusteenuse\nmasin, kus jääbki puudu osa varundatud logidest.\n\n\nKrahhist taastumise sammud\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n.. seealso::\n\n   * Kasuta :ref:`recovery-stateless` juhendis oleva\n     **Teenuse masin on krahhinud, v.a vahendus- ja x-tee teenused**\n     peatükki, v.a sammud 10, 12-16.\n\n\n.. note::\n\n   Punktis 10 tuleb välja vahetada ainult logikogumisteenuse **id** ja\n   **address** väljad:\n\n   .. code-block:: yaml\n\n     - id: zone0\n       services:\n         log:\n           - id:          log@log1.ivxv.ee\n             address:     log1:20514\n\n   uute vastu:\n\n   .. code-block:: yaml\n\n     - id: zone0\n       services:\n         log:\n           - id:          log@log2.ivxv.ee\n             address:     log2:20514\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/recovery/recovery_stateless.inc",
    "content": "Krahhi toimumise indikaatorid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusveebiliides.\nVali **Teenused** vasakult menüüst. Kui teenus on krahhinud,\nsiis tema **Seisund** on **Tõrge**.\n\nHalduskäsurealiides.\nLogi sisse **ivxv-admin** kasutajana ja päri teenuste statistikat::\n\n   $ sudo su - ivxv-admin\n   $ ivxv-status\n\nKui teenus on krahhinud, siis tema **State** on **FAILURE**.\n\n\nKrahhist taastumise eeldused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhist taastumiseks piisab kui teenuste CA TLS privaatvõti ja sertifikaat on\nalles, mida saab kasutada teenuse varuvõtmete ja -sertifikaatide\ngenereerimiseks. Samas, saab ka enne valimisi teenuse varuvõtmed ja\n-sertifikaadid ette genereerida.\n\nKui kõikide teenuste CA on sama, siis tasub valija- ja kontrollrakenduste\nseadistusse panna hoopis CA sertifikaat. Juhul kui CA on teenustel erinev,\nsiis peab panema kõik teenuste TLS sertifikaadid rakenduste seadistusse\nvahetult enne valimisi.\n\n\nKrahhi võimalik mõju hääletamistulemusele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSõltuvalt teenusest võivad tagajärjed olla järgmised:\n\n   * kui krahhivad nimekirja-, Mobiil-ID, vahendus-, seansi-, Smart-ID,\n     hääletamis-, Web-eID teenused, siis valijal ei õnnestu hääletada\n\n   * kui krahhivad järjekorra- ja x-tee teenused, siis VIS ei saa pärida\n     andmeid x-tee kaudu\n\n   * kui krahhib verifitseerimisteenus, siis valijal ei õnnestu oma häält\n     verifitseerida\n\n\nKrahhist taastumise sammud\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Talletusteenust antud juhendis ei käsitleta, selleks\n     on eraldi juhend :ref:`recovery-storageservice`\n\nKrahhi saab liigitada järgmiselt:\n\n   #. Teenus on terve, ning vajab ainult taaskäivitamist, v.a x-tee teenus\n\n   #. Teenuse andmed (TLS privaatvõti, sertifikaat jne) on rikutud, v.a\n      vahendus- ja x-tee teenused\n\n   #. Teenuse lähtekoodis on viga, v.a x-tee teenus\n\n   #. X-tee teenuse taastumine, sh. krahh ja lähtekoodi viga\n\n   #. Teenuse masin on krahhinud, v.a vahendus- ja x-tee teenused\n\n   #. Vahendusteenuse masin on krahhinud\n\nTeenus on terve, ning vajab ainult taaskäivitamist, v.a x-tee teenus\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n   #. SSH logi sisse haldus masinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Taaskäivita teenus, näiteks nimekirjateenust::\n\n         $ ivxv-service restart choices@choices1.ivxv.ee\n\nTeenuse andmed (TLS privaatvõti, sertifikaat jne) on rikutud, v.a vahendus- ja x-tee teenused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n   #. Vaheta välja krahhinud teenuse **id** tehnilises seadistuses\n      varuteenuse **id** vastu, mille TLS privaatvõti ja sertifikaat on\n      genereeritud\n\n   #. Allkirjasta tehniline seadistus\n\n   #. Laadi seadistus läbi haldusveebiliidese **Seadistused**\n\n   #. SSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Laadi TLS privaatvõti ja sertifikaat, näiteks nimekirjateenus::\n\n         $ ivxv-secret-load --service=choices@choices4.ivxv.ee tls-key /opt/ivxv-install/service_definition/choices@choices4.ivxv.ee.key\n         $ ivxv-secret-load --service=choices@choices4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/choices@choices4.ivxv.ee.pem\n\n   #. Kui tegu on nimekirja-, Mobiil-ID, Smart-ID, hääletamis-, Web-eID või\n      verifitseerimisteenusega siis tuleb üles laadida ka küpsiste\n      allkirjatamiseks privaatvõti::\n\n         $ ivxv-secret-load mid-token-key /opt/ivxv-install/service_definition/ticket.key\n\n   #. Kui tegu on hääletamisteenusega siis tuleb üles laadida TSP vastuse\n      räsi allkirjastamiseks privaatvõti::\n\n         $ ivxv-secret-load tsp-regkey /opt/ivxv-install/service_definition/tspreg.key\n\n   #. Käivita teenused::\n\n         $ ivxv-config-apply --type=election\n\nTeenuse lähtekoodis on viga, v.a x-tee teenus\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n   #. Kopeeri uus *.deb* pakk haldusmasinasse\n\n   #. SSH logi sisse haldusmasinasse **sudo** kasutajana::\n\n         $ sudo su\n\n   #. Kopeeri *.deb* pakk */etc/ivxv/debs/* kausta\n\n   #. Logi **sudo** kasutajalt välja::\n\n         $ exit\n\n   #. SSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Uuenda teenuse *.deb* pakki kõikides masinates, näiteks nimekirjateenus::\n\n         $ ivxv-update-packages --service=choices --package=/etc/ivxv/debs/ivxv-choices_1.9.12~dev_amd64.deb\n\n   #. Taaskäivita teenus igal hostil, näiteks taaskäivitame nimekirjateenuse\n      kolmeõlgse seadistuse korral::\n\n         $ ivxv-service restart choices@choices1.ivxv.ee\n         $ ivxv-service restart choices@choices2.ivxv.ee\n         $ ivxv-service restart choices@choices3.ivxv.ee\n\nX-tee teenuse taastumine, sh. krahh ja lähtekoodi viga\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n   #. Esmalt tasub proovida taaskäivitada systemd x-tee teenust::\n\n         $ sudo systemctl restart xroad-service\n\n   #. Ebaõnnestumise korral tuleb tarnida uus *.deb* pakk, ning\n      vana pakk eemaldada::\n\n         $ sudo apt purge xroad-service\n\n   #. Kui uus *.deb* pakk on masinasse kopeeritud, tuleb see paigaldada::\n\n         $ sudo apt install ./xroad-service_*.deb\n\n   #. Kasuta x-tee teenuse juhendit, et paigaldada TLS sertifikaadid õigesse\n      kaustadesse\n\n   #. Käivita systemd x-tee teenus::\n\n         $ sudo systemctl start xroad-service\n\nTeenuse masin on krahhinud, v.a vahendus- ja x-tee teenused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n   #. Paigalda värske Ubuntu masin\n\n      .. important::\n         Palun täpsusta üle, millist Ubuntu versiooni parasjagu\n         kasutatakse\n\n   #. Paigalda teenuse masinasse baastarkvara pakid APT\n      pakihoidlast::\n\n         $ sudo apt-get install acl locales unzip zstd --yes\n\n   #. Nüüd tuleb haldus-, koguja-, varundus-, logi-, vahendus-, logi\n      monitooringu masinate **ip** ja DNS **nimi** lisada uue teenuse masina\n      */etc/hosts* faili\n\n   #. Seejärel tuleb haldus-, koguja-, varundus-, logi-, vahendus masinates\n      kustutada krahhinud teenuse masina **ip** ja DNS **nimi** ning lisada\n      uus\n\n      .. attention::\n\n         See on kõige olulisem samm, kuna siin peab valima sellise DNS\n         **nime**, mille tulevikus lisame tehnilistesse seadistustesse.\n\n         Reegel on lihtne, et iga uue teenuse masina DNS **nimi** peab olema\n         unikaalne ja ei tohi olla sama, mis krahhinud teenuste masinatele\n         olid varem omistatud. Haldusteenuse andmebaasis on registreeritud\n         kõigi süsteemis leiduvate masinate DNS **nimed**, isegi krahhinud\n         isendite omad. Haldusteenuse eripära seisneb selles, et see ei\n         paigalda tarkvara masinatele, mille DNS **nimi** on juba\n         haldusteenuse andmebaasis registreeritud.\n\n   #. SSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Kuna haldusteenus paigaldab vajaliku taristu kõikidele süsteemi masinatele\n      automaatselt, siis on vaja vältida SSH ühenduse loomisel *prompt* sõnumi\n      *Are you sure you want to continue connecting (yes/no)? yes* kinnitamist.\n\n      Kustuta vana teenusmasina SSH avalik võti */etc/ssh/ssh_known_hosts*\n      failist, ning lisa uus, näiteks, kui uue teenuse masina DNS **nimi** on\n      *ivxv4*::\n\n         $ ssh-keyscan -t ecdsa ivxv4 | grep -v ^# >> /etc/ssh/ssh_known_hosts\n\n   #. SSH logi sisse uue teenuse masinasse **root** kasutajana::\n\n         $ sudo su\n\n   #. Kuna haldusteenus edastab teenuse logid logimonitori, lisaks\n      *syslog* *RELP* protokollile, ka üle SSH protokolli, siis on vaja samuti\n      vältida SSH ühenduse loomisel *prompt* sõnumi kinnitamist, näiteks,\n      monitooringu masina *logmonitor* DNS **address** korral::\n\n         $ ssh-keyscan -t ecdsa logmonitor | grep -v ^# >> /etc/ssh/ssh_known_hosts\n\n   #. Kuna haldusteenus peab paigaldama tarkvara APT pakihoidlast, mis\n      on võimatu juurkasutaja õiguseid omandamata, pead kopeerima haldusmasina\n      SSH avaliku võtme, mis asub\n      */var/lib/ivxv/user/ivxv-admin/.ssh/id_ed25519.pub* failis, ning lisama\n      selle uue teenuse masina */root/.ssh/authorized_keys* faili\n\n   #. Nüüd tuleb tehnilistes seadistustes välja vahetada teenuste **id** ja\n      **nimed** (**address**), näiteks:\n\n      .. code-block:: yaml\n\n         - id: zone1\n            services:\n               mid:\n                  - id:         mid@mid1.ivxv.ee\n                   address:     ivxv1:4441\n               choices:\n                  - id:         choices@choices1.ivxv.ee\n                   address:     ivxv1:4442\n               voting:\n                  - id:         voting@voting1.ivxv.ee\n                   address:     ivxv1:4443\n               verification:\n                  - id:         verification@verification1.ivxv.ee\n                   address:     ivxv1:4444\n               smartid:\n                  - id:         smartid@smartid1.ivxv.ee\n                   address:     ivxv1:4445\n               votesorder:\n                  - id:         votesorder@votesorder1.ivxv.ee\n                   address:     ivxv1:4446\n               webeid:\n                  - id:         webeid@webeid1.ivxv.ee\n                   address:     ivxv1:4447\n                   origin:      https://ivxv1.test.ivxv.ee:443\n               sessionstatus:\n                  - id:         sessionstatus@sessionstatus1.ivxv.ee\n                   address:     ivxv1:4448\n\n      uute **id** ja **address** vastu:\n\n      .. code-block:: yaml\n\n         - id: zone1\n            services:\n               mid:\n                  - id:         mid@mid4.ivxv.ee\n                   address:     ivxv4:4441\n               choices:\n                  - id:         choices@choices4.ivxv.ee\n                   address:     ivxv4:4442\n               voting:\n                  - id:         voting@voting4.ivxv.ee\n                   address:     ivxv4:4443\n               verification:\n                  - id:         verification@verification4.ivxv.ee\n                   address:     ivxv4:4444\n               smartid:\n                  - id:         smartid@smartid4.ivxv.ee\n                   address:     ivxv4:4445\n               votesorder:\n                  - id:         votesorder@votesorder4.ivxv.ee\n                   address:     ivxv4:4446\n               webeid:\n                  - id:         webeid@webeid4.ivxv.ee\n                   address:     ivxv4:4447\n                   origin:      https://ivxv4.test.ivxv.ee:443\n               sessionstatus:\n                 - id:          sessionstatus@sessionstatus4.ivxv.ee\n                   address:     ivxv4:4448\n\n      .. attention::\n         Pane tähele, et on muutunud nii **id** kui ka **address**.\n         Vahendus- ja talletusteenuste **id** ja **address** jäävad samaks\n         **siis ja ainult siis**, kui need teenused ei olnud krahhinud masina\n         koosseisus, vastasel juhul tuleb sarnaselt muuta ka nende teenuste\n         **id** ja **address**.\n\n   #. Allkirjasta uus tehniline seadistus. Laadi see läbi\n      haldusveebiliidese, vasakult valides **Seadusted**.\n\n   #. SSH logi sisse haldusmasinasse **ivxv-admin** kasutajana::\n\n         $ sudo su - ivxv-admin\n\n   #. Laadi kõik teenuste TLS privaatvõtmed ja sertifikaadid, näiteks::\n\n         $ ivxv-secret-load --service=choices@choices4.ivxv.ee tls-key /opt/ivxv-install/service_definition/choices@choices4.ivxv.ee.key\n         $ ivxv-secret-load --service=voting@voting4.ivxv.ee tls-key /opt/ivxv-install/service_definition/voting@voting4.ivxv.ee.key\n         $ ivxv-secret-load --service=mid@mid4.ivxv.ee tls-key /opt/ivxv-install/service_definition/mid@mid4.ivxv.ee.key\n         $ ivxv-secret-load --service=smartid@smartid4.ivxv.ee tls-key /opt/ivxv-install/service_definition/smartid@smartid4.ivxv.ee.key\n         $ ivxv-secret-load --service=sessionstatus@sessionstatus4.ivxv.ee tls-key /opt/ivxv-install/service_definition/sessionstatus@sessionstatus4.ivxv.ee.key\n         $ ivxv-secret-load --service=verification@verification4.ivxv.ee tls-key /opt/ivxv-install/service_definition/verification@verification4.ivxv.ee.key\n         $ ivxv-secret-load --service=votesorder@votesorder4.ivxv.ee tls-key /opt/ivxv-install/service_definition/votesorder@votesorder4.ivxv.ee.key\n         $ ivxv-secret-load --service=webeid@webeid4.ivxv.ee tls-key /opt/ivxv-install/service_definition/webeid@webeid4.ivxv.ee.key\n         $ ivxv-secret-load --service=choices@choices4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/choices@choices4.ivxv.ee.pem\n         $ ivxv-secret-load --service=voting@voting4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/voting@voting4.ivxv.ee.pem\n         $ ivxv-secret-load --service=mid@mid4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/mid@mid4.ivxv.ee.pem\n         $ ivxv-secret-load --service=smartid@smartid4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/smartid@smartid4.ivxv.ee.pem\n         $ ivxv-secret-load --service=sessionstatus@sessionstatus4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/sessionstatus@sessionstatus4.ivxv.ee.pem\n         $ ivxv-secret-load --service=verification@verification4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/verification@verification4.ivxv.ee.pem\n         $ ivxv-secret-load --service=votesorder@votesorder4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/votesorder@votesorder4.ivxv.ee.pem\n         $ ivxv-secret-load --service=webeid@webeid4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/webeid@webeid4.ivxv.ee.pem\n\n   #. Lisa küpsiste signeerimisvõti::\n\n      $ ivxv-secret-load mid-token-key /opt/ivxv-install/service_definition/ticket.key\n\n   #. Lisa hääletamisteenuse TSP räsi signeerimisvõti::\n\n      $ ivxv-secret-load tsp-regkey /opt/ivxv-install/service_definition/tspreg.key\n\n   #. Käivita teenused::\n\n      $ ivxv-config-apply --type=election\n\nVahendusteenuse masin on krahhinud\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Kasuta käesoleva juhendi\n     **Teenuste masin on krahhinud, v.a vahendus- ja x-tee teenused**\n     peatükki, v.a punktid 6, 10-12.\n\n.. note::\n\n   Punktis 8 tuleb ainult asendada vana vahendusteenuse **id** ja **address**:\n\n   .. code-block:: yaml\n\n     - id: zone1\n       services:\n         proxy:\n           - id:          proxy@proxy1.ivxv.ee\n             address:     proxy1:443\n\n   uute vastu:\n\n   .. code-block:: yaml\n\n     - id: zone1\n       services:\n         proxy:\n           - id:          proxy@proxy4.ivxv.ee\n             address:     proxy4:443\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/recovery/recovery_storageservice.inc",
    "content": "Krahhi toimumise indikaatorid\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nHaldusveebiliides.\nVali **Teenused** vasakult menüüst. Kui talletusteenus on krahhinud,\nsiis tema **Seisund** on **Tõrge**.\n\nHalduskäsurealiides.\nLogi sisse **ivxv-admin** kasutajana ja päri teenuste statistikat::\n\n   $ sudo su - ivxv-admin\n   $ ivxv-status\n\nKui talletusteenus on krahhinud, siis tema **State** on **FAILURE**.\n\n\nKrahhist taastumise eeldused\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhi taastumiseks piisab kui teenuste CA TLS privaatvõti ja sertifikaat on\nalles, mida saab kasutada teenuse varuvõtmete ja -sertifikaatide\ngenereerimiseks. Samas, saab ka enne valimisi teenuse varuvõtmed ja\n-sertifikaadid ette genereerida.\n\nKui kõikide teenuste CA on sama, siis tasub valija- ja kontrollrakenduste\nseadistusse panna hoopis CA TLS sertifikaadi. Juhul kui CA on teenustel erinev,\nsiis peab panema kõik teenuste TLS sertifikaadid rakenduste seadistusse\nvahetult enne valimisi.\n\n\nKrahhi võimalik mõju hääletamistulemusele\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKõik päringud mis saadetakse vastu krahhinud talletusteenuse\nmasinat ebaõnnestuvad. Valija ei saa autentida, ega hääletada.\n\n\nKrahhist taastumise sammud\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKrahhi saab liigitada järgmiselt:\n\n   #. Talletusteenus on terve, ning vajab ainult taaskäivitamist\n\n   #. Talletusteenuse andmed (võti, sertifikaat jne) on rikutud või masina\n      krahh\n\n   #. Talletusteenuse lähtekoodis on viga\n\nTalletusteenus on terve, ning vajab ainult taaskäivitamist\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Kasuta :ref:`recovery-stateless` juhendis oleva\n     **Teenus on terve, ning vajab ainult taaskäivitamist, v.a x-tee teenus**\n     peatükki\n\nTalletusteenuse andmed (võti, sertifikaat jne) on rikutud või masina krahh\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. seealso::\n\n   * Kasuta :ref:`recovery-stateless` juhendis oleva\n     **Teenuse masin on krahhinud, v.a vahendus- ja x-tee teenused**\n     peatükki\n\n\n.. note::\n\n   Punktis 10 tuleb asendada vana talletusteenuse **id** ja **address** väljad:\n\n   .. code-block:: yaml\n\n     - id: zone1\n       services:\n         storage:\n           - id:          storage@storage1.ivxv.ee\n             address:     ivxv1:2379\n             peeraddress: ivxv1:2380\n\n   uute vastu:\n\n   .. code-block:: yaml\n\n      - id: zone1\n        services:\n          storage:\n            - id:          storage@storage4.ivxv.ee\n              address:     ivxv4:2379\n              peeraddress: ivxv4:2380\n\n   Punktis 12 tuleb uuendada ka talletusteenuse TLS privaatvõti::\n\n      $ ivxv-secret-load --service=storage@storage4.ivxv.ee tls-key /opt/ivxv-install/service_definition/storage@storage4.ivxv.ee.key\n\n   ja sertifikaat:::\n\n      $ ivxv-secret-load --service=storage@storage4.ivxv.ee tls-cert /opt/ivxv-install/service_definition/storage@storage4.ivxv.ee.pem\n\nVeendu, et asendamine läks sujuvalt läbi:\n\n   #. SSH logi uue koguja masinasse *root* kasutajana::\n\n      $ sudo su\n\n   #. Päri andmebaasi klastri seisu, näiteks, kui klaster koosneb\n      *ivxv4*, *ivxv2*, *ivxv3* masinatest::\n\n      $ cd /var/lib/ivxv/service/storage@storage4.ivxv.ee\n      $ env ETCDCTL_API=3 etcdctl --endpoints=ivxv4:2379,ivxv2:2379,ivxv3:2379 --cert tls.pem --key tls.key --cacert ca.pem endpoint status -w table\n\nTalletusteenuse lähtekoodis on viga\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nKui talletusteenuse lähtekoodis on viga, siis seda saab\nasendada uuema versiooniga, olemasoleva andmebaasi\nklastrit rikumata.\n\n.. seealso::\n\n   * Kasuta :ref:`recovery-stateless` juhendis oleva\n     **Teenuse lähtekoodis on viga, v.a x-tee teenus**\n     peatükki\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/seadistused.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nKogumisteenuse seadistused\n==========================\n\nLogimise seadistused\n--------------------\n\nKogumisteenuse logi hoitakse logi tekkimise asukohas ja dubleeritakse\nlogiserveritesse. Logide kogumiseks ja edastamiseks kasutatakse vaikimisi\n*syslog*-teenust ``rsyslog``.\n\nKogumisteenus toetab kahte liiki logiservereid, mis\nkirjeldatakse kogumisteenuse tehnilises seadistuses.\n\n#. Kogumisteenuse logikogumisteenus on kogumisteenuse sisemine teenus ja seda\n   võib süsteemis olla mitu isendit.\n\n#. Tegevusmonitooringu server on kogumisteenuse jaoks\n   väline teenus ja seda võib olla ainult üks isend.\n\nKogumisteenusele tehniliste seadistuse rakendamisel paigaldab haldusteenus\nlogikogumisteenuse enne teise teenuseid, et teenuste poolt toodetav logi\nsaaks võimalikult varakult ka logikogumisteenusesse kogutud.\n\n.. note::\n\n   Kogumisteenuse logiteated tekivad pärast valimiste seadistuse esmakordset\n   laadimist, kuna teenused käivitatakse selle seadistuse laadimise järel.\n\nLogi tootva teenuse logimise korraldus\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLogi tootva teenuse logimise seadistuse genereerib haldusteenus vastavalt\ntehnilistele seadistustele.\n\n#. Iga teenus logib kohalikku *syslog*-teenusesse;\n\n#. Kõigi teenusmasinate *syslog*-teenused on seadistatud kogumisteenuse logi\n   salvestama kohalikku failisüsteemi (:file:`/var/log/ivxv/ivxv-YYYY-MM-DD-HH.log`);\n\n#. Kõigi teenusmasinate (peale logikogumisteenuse) *syslog*-teenused on\n   seadistatud edastama üle võrgu:\n\n   #. Kõiki logikirjeid logikogumisteenusesse (protokoll: RELP);\n\n   #. Kogumisteenuse logikirjeid tegevuslogi monitooringu serverisse\n      (protokoll: RELP);\n\n\nLogikogumisteenuse korraldus\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nLogikogumisteenuse seadistusfail tuleb teenuse tarkvarapakist\n(:ref:`ivxv-logcollector.conf <ivxv-logcollector.conf>`).\n\n#. Logikogumisteenus võtab logikirjeid vastu RELP-protokolli kaudu;\n\n#. Kogumisteenuse logikirjeid kirjutatakse JSON-vormingus faili\n   :file:`/var/log/ivxv/ivxv-YYYY-MM-DD-HH.log` (välja arvatud päringu- ja\n   silumislogi);\n\n#. Kogumisteenuse päringulogi kirjutatakse rsyslogi standardvormingus faili\n   :file:`/var/log/ivxv/ivxv-request-YYYY-MM-DD-HH.log`;\n\n#. Kogumisteenuse silumislogi ja teiste oluliste teenuste (haproxy, etcd,\n   rsyslog, sshd) logi kirjutatakse rsyslogi standardvormingus faili\n   :file:`/var/log/ivxv/ivxv-debug-YYYY-MM-DD-HH.log`.\n\n\nTalletamisteenuse seadistused\n-----------------------------\n\nHetkel ainus talletamisteenuse teostus kasutab hajusat võti-väärtus andmebaasi\n``etcd``. Korraga käivitatakse mitu ``etcd`` isendit, mis saavutavad omavahel\nkonsensuse talletatud andmete osas.\n\nTalletusteenuse sujuvaks tööks võib olla vajalik osade ``etcd`` parameetrite\nhäälestamine konkreetse evituskeskkonna jaoks. Selleks tuleb teenuse masinas\nluua fail :file:`/etc/default/ivxv` ning sinna lisada järgmistes jaotistes\nkirjeldatud read. Pärast faili loomist või selle sisu muutmist tuleb uute\nväärtuste rakendamiseks talletusteenus taaskäivitada. Parameetri puudumise\nkorral kasutatakse vaikeväärtust.\n\nSeadistuste väärtuste valimisel on abiks ``etcd`` dokumentatsioon aadressil\nhttps://coreos.com/etcd/docs/latest/tuning.html.\n\nAjaparameetrid\n^^^^^^^^^^^^^^\n\n``etcd`` klaster valib ühe liikmetest juhiks, mis koordineerib kõiki\nandmemuudatusi. Lisaks pingib juht perioodiliselt kõiki ülejäänud klastri\nliikmeid aitamaks tuvastada olukorda, kus ühendus juhiga on katkenud: kui mõni\nklastri liikmetest pole piisavalt kaua ühtegi pingi saanud, algatab see uue\njuhi valimise.\n\nSuurema võrgu- või kettalatentsuse tagajärjel võib juhi ping liialt viibida\nning põhjustada uue juhi valimise. Tõrgete korral on juhivahetus süsteemi\nloomulik osa, ent töötava süsteemi puhul tarbetu koormus. Seetõttu tuleks\nseadistada juhi pingimise tihedust ``ETCD_HEARTBEAT_INTERVAL`` ning teiste\nliikmete ooteaega ``ETCD_ELECTION_TIMEOUT`` vastavalt evituskeskkonna\nlatentsusele::\n\n   ETCD_HEARTBEAT_INTERVAL=100\n   ETCD_ELECTION_TIMEOUT=1000\n\nMõlemad väärtused on millisekundites ning vaikimisi vastavalt 100ms ja 1000ms.\n\n.. important::\n\n   Ühes klastris peavad kõigil talletamisteenuse isenditel olema samad\n   ajaparameetrid. Vastasel korral võib esineda stabiilsusprobleeme erinevate\n   pingi ootuste tõttu.\n\n\nHetkvõtete parameetrid\n^^^^^^^^^^^^^^^^^^^^^^\n\n``etcd`` peab logi kõigist andmemuudatustest. Vältimaks logi liiga suureks\nkasvamist tehakse andmebaasi seisust perioodiliselt hetkvõtteid ning eelnev\nlogi kustutatakse. Kui talletamisteenus kasutab liiga palju mälu või\nkettaruumi, siis võib aidata tihedam hetkvõtete tegemine.\n\nUus hetkvõte tehakse iga ``ETCD_SNAPSHOT_COUNT`` andmemuudatuse järel, seega\nmadalam väärtus toob kaasa tihedamad hetkvõtted ning väiksema logi suuruse::\n\n   ETCD_SNAPSHOT_COUNT=10000\n\nVaikimisi tehakse hetkvõte iga 10000 muudatuse järel.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/spelling_wordlist.txt",
    "content": "id\nx\n\napache\ncron\netcd\nhaproxy\nrsyslog\nrsyslogi\nsshd\nsystemd\nubuntu\nzabbix\nzabbixiga\n\nivxv\nvoter\nlist\ndownload\ndb\nreset\ncollector\ninit\ncmd\nload\ncopy\nlog\nto\nlogmon\nupdate\npackages\nbackup\ncrontab\nconfig\nvalidate\napply\nsecret\nexport\nvotes\ncreate\ndata\ndirs\ndump\ngenerate\nprocessor\ninput\nchoices\nvoting\neventlog\nusers\nvoterstats\ndaemon\nhelper\n\nCSV\nASiC\nIP\nPEM\nSSH\n\n\nesmakordset\nhosti\nhostid\nhostide\nhostidel\nhostidele\nhostidest\nhostil\nhostile\nhostinimi\nhostist\nhostiv\nkrahhib\nkrahhimise\nkrahhinud\n\npingib\npingimise\n\nDiffie\nHellmani\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-admin-helper.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-admin-helper kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-admin-helper:\n\nivxv-admin-helper\n`````````````````\n\n:command:`ivxv-admin-helper --help`:\n\n.. code-block:: text\n\n   Usage:\n       ivxv-admin-helper check-service-config <service-type> <service-id>\n           Check service configuration\n\n       ivxv-admin-helper copy-logs-to-logmon <hostname> <logmonitor-address>\n           Copy IVXV service log files to Log Monitor\n\n       ivxv-admin-helper restart-service <service-type> <service-id>\n                                         <systemctl-service-id>\n           Restart service\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-admin-sudo.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-admin-sudo kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-admin-sudo:\n\nivxv-admin-sudo\n```````````````\n\n:command:`ivxv-admin-sudo --help`:\n\n.. code-block:: text\n\n   Usage:\n       ivxv-admin-sudo backup-ballot-box <voting-host> <service-id>\n                                         <backup-filename>\n           Backup ballot box (in backup service)\n\n       ivxv-admin-sudo backup-log <log-host> <backup-timestamp>\n           Backup log file (in backup service)\n\n       ivxv-admin-sudo create-ssh-access <account-name>\n           Create management service access to account in service host\n\n       ivxv-admin-sudo init-host\n           Initialize service host\n\n       ivxv-admin-sudo init-service <service-id>\n           Initialize service data directory.\n           Value 'backup' is used for all backup services\n\n       ivxv-admin-sudo install-pkg <package-filename>\n           Install IVXV package with dependencies\n\n       ivxv-admin-sudo prepare-ballot-box-backup <service-id> <backup-filename>\n           Prepare votes backup file in voting service\n\n       ivxv-admin-sudo remove-admin-root-access\n           Remove management service access to service host root account\n\n       ivxv-admin-sudo rsyslog-config-apply\n           Apply rsyslog config file for IVXV logging\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-agent-daemon.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-agent-daemon kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-agent-daemon:\n\nivxv-agent-daemon\n`````````````````\n\n:command:`ivxv-agent-daemon --help`:\n\n.. code-block:: text\n\n   IVXV Collector Management Service agent daemon.\n\n   Usage: ivxv-agent-daemon [--get-stats] [--register-status]\n\n   Options:\n       --get-stats         Copy statistics from Log Monitor to\n                           Management Service without daemonizing.\n       --register-status   Register collector state (if not registered).\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-backup-crontab.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-backup-crontab kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-backup-crontab:\n\nivxv-backup-crontab\n```````````````````\n\n:command:`ivxv-backup-crontab --help`:\n\n.. code-block:: text\n\n   Generate crontab for IVXV backup automation.\n\n   This utility must be called as editor by crontab utility:\n\n       $ env VISUAL=ivxv-backup-crontab crontab -e\n\n   Usage: ivxv-backup-crontab <filename>\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-backup.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-backup kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-backup:\n\nivxv-backup\n```````````\n\n:command:`ivxv-backup --help`:\n\n.. code-block:: text\n\n   Backup IVXV collector data.\n\n   Usage: ivxv-backup management-conf\n          ivxv-backup ballot-box [<voting_service_id>]\n          ivxv-backup log\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-cmd-load.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-cmd-load kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-cmd-load:\n\nivxv-cmd-load\n`````````````\n\n:command:`ivxv-cmd-load --help`:\n\n.. code-block:: text\n\n   Load command to IVXV Collector Management Service.\n\n   Usage: ivxv-cmd-load [--autoapply] [--show-version] <type> FILE\n\n   Options:\n       <type>              Command type. Possible values are:\n                           - election: election config\n                           - technical: collector technical config\n                           - trust: trust root config\n                           - choices: choices list\n                           - districts: districts list\n                           - voters: voters list or voters list skipping\n                           - user: user account and role(s)\n       --autoapply         Apply command file automatically (by Agent Daemon).\n       --show-version      Output config file version and exit.\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-collector-init.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-collector-init kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-collector-init:\n\nivxv-collector-init\n```````````````````\n\n:command:`ivxv-collector-init --help`:\n\n.. code-block:: text\n\n   Initialize IVXV Collector.\n\n   Usage: ivxv-collector-init [--force]\n\n   Options:\n       --force     Don't ask user confirmation\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-config-apply.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-config-apply kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-config-apply:\n\nivxv-config-apply\n`````````````````\n\n:command:`ivxv-config-apply --help`:\n\n.. code-block:: text\n\n   Apply loaded IVXV Collector config to services.\n\n   Usage: ivxv-config-apply [--type=<type>] ... [<service-id>] ...\n\n   Options:\n       --type=<type>    Config type. Possible values are:\n                        - election: election config file\n                        - technical: collector technical config file\n                        - choices: choices list\n                        - districts: districts list\n                        - voters: voters list\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-config-validate.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-config-validate kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-config-validate:\n\nivxv-config-validate\n````````````````````\n\n:command:`ivxv-config-validate --help`:\n\n.. code-block:: text\n\n   Validate IVXV collector config files.\n\n   Validate single config files. Also validate voting lists consistency if\n   multiple lists are provided.\n\n   Usage:\n       ivxv-config-validate [--plain] [--trust=<trust-file>]\n           [--technical=<technical-file>] [--election=<election-file>]\n           [--choices=<choices-file>] [--districts=<districts-file>]\n           [--voters=<voters-file> ...]\n\n   Options:\n       --plain     Validate plain config file (Default: BDOC container)\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-copy-log-to-logmon.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-copy-log-to-logmon kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-copy-log-to-logmon:\n\nivxv-copy-log-to-logmon\n```````````````````````\n\n:command:`ivxv-copy-log-to-logmon --help`:\n\n.. code-block:: text\n\n   Copy IVXV log files from service hosts to Log Monitor.\n\n   This utility transports collected IVXV log files from IVXV services\n   (including Log Collector Service) to Log Monitor.\n\n   Usage: ivxv-copy-log-to-logmon [--log-level=<level>] [<hostname> ...]\n\n   Options:\n       <hostname>              Service host name.\n       --log-level=<level>     Logging level [Default: INFO].\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-create-data-dirs.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-create-data-dirs kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-create-data-dirs:\n\nivxv-create-data-dirs\n`````````````````````\n\n:command:`ivxv-create-data-dirs --help`:\n\n.. code-block:: text\n\n   Create IVXV Collector Management Service data directories.\n\n   NOTE: Directory owners and permissions are not set by this utility!\n\n   Usage: ivxv-create-data-dirs\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-db-dump.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-db-dump kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-db-dump:\n\nivxv-db-dump\n````````````\n\n:command:`ivxv-db-dump --help`:\n\n.. code-block:: text\n\n   Dump IVXV Collector Management Service database.\n\n   Usage: ivxv-db-dump [<key>] ...\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-db-reset.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-db-reset kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-db-reset:\n\nivxv-db-reset\n`````````````\n\n:command:`ivxv-db-reset --help`:\n\n.. code-block:: text\n\n   Reset IVXV Collector Management Service database.\n\n   Usage: ivxv-db-reset [--force]\n\n   Options:\n       --force     Don't ask user confirmation\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-eventlog-dump.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-eventlog-dump kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-eventlog-dump:\n\nivxv-eventlog-dump\n``````````````````\n\n:command:`ivxv-eventlog-dump --help`:\n\n.. code-block:: text\n\n   Dump IVXV Collector Management event log in human readable format.\n\n   Usage: ivxv-eventlog-dump\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-export-votes.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-export-votes kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-export-votes:\n\nivxv-export-votes\n`````````````````\n\n:command:`ivxv-export-votes --help`:\n\n.. code-block:: text\n\n   Export collected votes.\n\n   This utility copies current ballot box from voting service to backup\n   service and outputs ballot box content.\n\n   Usage: ivxv-export-votes [--consolidate] <output-file>\n\n   Options:\n       --consolidate   Consolidate all collected votes\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-generate-processor-input.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-generate-processor-input kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-generate-processor-input:\n\nivxv-generate-processor-input\n`````````````````````````````\n\n:command:`ivxv-generate-processor-input --help`:\n\n.. code-block:: text\n\n   Generate input for processor application.\n\n   This utility generates ZIP container with data files\n   for processor application to validate ballot box:\n\n       1. District list;\n       2. Voter lists;\n       3. Validation key for vote registration requests;\n       4. Configuration for processor application.\n\n   Usage: ivxv-generate-processor-input <output-file>\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-secret-load.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-secret-load kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-secret-load:\n\nivxv-secret-load\n````````````````\n\n:command:`ivxv-secret-load --help`:\n\n.. code-block:: text\n\n   Load secret data to IVXV services.\n\n   This utility loads file that contains secret data to services.\n\n   Supported secret types are:\n\n       tls-cert - TLS certificate for service.\n\n           Certificate (and key) is used for securing\n           communication between services and service instances.\n\n       tls-key - TLS key for service.\n\n           Key is used together with service certificate.\n\n       tsp-regkey - PKIX TSP registration key for voting services.\n\n           Key is used for signing Time Stamp Protocol requests.\n\n           Key file must be in PEM format and must be not password protected.\n\n       mid-token-key - Mobile ID identity token for\n                       choices, mobile-id and voting services.\n\n           Key file must be 32 bytes long.\n\n   Usage: ivxv-secret-load [--service=<service-id>] <secret-type> <keyfile>\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-service.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-service kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-service:\n\nivxv-service\n````````````\n\n:command:`ivxv-service --help`:\n\n.. code-block:: text\n\n   Manage IVXV services.\n\n   Usage: ivxv-service <action> <service-id> ...\n\n   Options:\n       <action>    Management action: start, stop, restart, ping\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-status.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-status kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-status:\n\nivxv-status\n```````````\n\n:command:`ivxv-status --help`:\n\n.. code-block:: text\n\n   Output IVXV Collector state.\n\n   Usage: ivxv-status [--json] [--service=<service-id> ...] [<filter> ...]\n\n   Options:\n       --json                  Output full data in JSON format.\n                               Note: filters have no effect in JSON output.\n       --service=<service-id>  Filter output by service ID.\n                               Note: This filter conflicts other section\n                               filters than \"smart\" or \"service\".\n       <filter>                Filter output by section. Possible values are:\n                                 * collector - collector state;\n                                 * election - election data;\n                                 * config - versions of loaded config;\n                                 * list - versions of loades lists;\n                                 * service - service information;\n                                 * ext - external service information;\n                                 * storage - storage information;\n                                 * smart - output only relevant data;\n                                 * all - output all data;\n                               [Default: smart].\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-update-packages.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-update-packages kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-update-packages:\n\nivxv-update-packages\n````````````````````\n\n:command:`ivxv-update-packages --help`:\n\n.. code-block:: text\n\n   Update service packages in IVXV service hosts.\n\n   This utility checks versions of software packages in service hosts\n   and installs new versions if required.\n\n   Usage: ivxv-update-packages [--force]\n\n   Options:\n       --force     Update even package version does not require update\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-users-list.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-users-list kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-users-list:\n\nivxv-users-list\n```````````````\n\n:command:`ivxv-users-list --help`:\n\n.. code-block:: text\n\n   List IVXV Collector Management Service registered users.\n\n   Usage: ivxv-users-list\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-voter-list-download.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-voter-list-download kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-voter-list-download:\n\nivxv-voter-list-download\n````````````````````````\n\n:command:`ivxv-voter-list-download --help`:\n\n.. code-block:: text\n\n   Download next available voter list changeset from VIS to IVXV Collector\n   Management Service.\n\n   Usage:\n       ivxv-voter-list-download [--output-report=<filepath>] [--log-level=<level>]\n\n   Options:\n       --output-report=<filepath>  Write JSON report about HTTP request to VIS.\n       --log-level=<level>         Logging level [Default: INFO].\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-voterstats.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-voterstats kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-voterstats:\n\nivxv-voterstats\n```````````````\n\n:command:`ivxv-voterstats --help`:\n\n.. code-block:: text\n\n   Import voter stats from voting service and export common stats to VIS.\n\n   Usage: ivxv-voterstats <TYPE> [--action=<action>] [--file=<file>]\n               [--service-id=<service_id>] [--log-level=<level>]\n\n   Options:\n       <TYPE>                      Stats type \"common\" or \"detail\".\n       --action=<action>           Limit actions for \"common\" stats type.\n                                   Possible values are \"import\" and \"export\".\n                                   [Default: all]\n       --file=<file>               Path to stats file.\n       --service-id=<service_id>   Voting service ID [Default: random].\n       --log-level=<level>         Logging level [Default: INFO].\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/utiliitide-abiteave/ivxv-voting-sessions.inc",
    "content": ".. Elektroonilise hääletamise infosüsteem IVXV\n\n.. Käsureautiliidi ivxv-voting-sessions kasutusjuhend\n\n.. SEE FAIL ON GENEREERITUD AUTOMAATTESTI POOLT:\n.. * features/steps/autodoc.py\n.. * features/autodoc-utilities.feature\n\n.. _ivxv-voting-sessions:\n\nivxv-voting-sessions\n````````````````````\n\n:command:`ivxv-voting-sessions --help`:\n\n.. code-block:: text\n\n   Import list of voting sessions from Log Monitor.\n\n   Session data is in CSV format.\n\n   Usage: ivxv-voting-sessions (vote | verify) <output_file> [--anonymize]\n               [--log-level=<level>]\n\n   Options:\n       <output_file>               Write sessions to file.\n       --anonymize                 Anonymize session data\n                                   (IP addresses and ID codes).\n       --log-level=<level>         Logging level [Default: INFO].\n"
  },
  {
    "path": "Documentation/et/kogumisteenuse_haldusjuhend/ylevaade.rst",
    "content": "..  IVXV kogumisteenuse haldusjuhend\n\nÜlevaade\n========\n\nKogumisteenuse ülevaade\n-----------------------\n\nIVXV kogumisteenus on elektroonilise\nhääletuse käigus hääletajate teenindamiseks ja häälte kogumiseks mõeldud\ntarkvara.\n\nKogumisteenus koosneb mikroteenustest ja nende haldamiseks mõeldud\nhaldusteenusest. Haldusteenuse kasutamine on käsureapõhine. Osade funktsioonide\nkasutamist on laiendatud veebipõhise liidesega, mida on kirjeldatud dokumendis\n``IVXV kogumisteenuse haldusliidese\nkasutusjuhend``.\n\n.. attention::\n\n   Kogumisteenus paigaldatakse ja seadistatakse eraldi iga hääletuse\n   läbiviimiseks. Ühe kogumisteenusega on korraga võimalik teenindada ainult\n   ühte hääletust.\n\n\nLisamaterjalid\n--------------\n\nKäesolevas dokumendis kasutatakse mõisteid ja definitsioone, mis on kirjeldatud\ndokumendis ``IVXV-ÜK-0.95 Elektroonilise hääletamise üldraamistik ja selle\nkasutamine Eesti riiklikel valimistel``:\n\n* E-hääletamise etapid;\n\n* Süsteemi osapooled ja komponendid.\n\n\nKogumisteenuse kasutajate rollid\n--------------------------------\n\nKogumisteenuses on kasutusel järgnevad rollid:\n\n#. **Kogumisteenuse haldur** tegeleb kogumisteenuse tehnilise haldamisega;\n\n#. **Valimiste haldur** tegeleb valimiste seadistuste kehtestamisega;\n\n#. **Vaataja** pääseb ligi haldusteenuse kaudu väljastatavatele seisundi- ja\n   statistikaandmetele;\n\nRollide täpsem kirjeldus asub dokumendis ``Elektroonilise hääletamise\ninfosüsteemi IVXV seadistuste koostamise juhend``.\n\n\nSüsteemi komponendid\n--------------------\n\nKogumisteenus\n^^^^^^^^^^^^^\n\n**Haldusteenus** on kogumisteenuse haldamise teenus. Haldusteenuse kaudu\njuhitakse ja jälgitakse kogumisteenust alates paigaldusest kuni mahavõtmiseni.\nVaata lähemalt lõigus :ref:`haldusteenus`.\n\n**Logikoguja** on kogumisteenuse sisemine logiserver, mis kogub ja säilitab\nkõigi kogumisteenuste alamteenuste logisid. Logikogujasse kogutud logid antakse\nvalimiste lõppedes üle korraldajale.\n\n**Sisemine varundus** on kogumisteenuse varundusteenus, mis varundab kõigi\nalamteenuste andmeid ja teeb need lihtsa liidese (failisüsteemi kataloog) kaudu\nkättesaadavaks välisele varundusteenusele.\n\n**Alamteenused** on kogumisteenuse eri lõikude eest vastutavad teenused.\n\n\n.. _tugiteenused:\n\nTugiteenused\n^^^^^^^^^^^^\n\n**Logiseire** on kogumisteenuse logide analüüsiks ja jälgimiseks mõeldud\nseireprogramm.\n\n**Tehniline seire** on kogumisteenuse tehnilise toimimise jälgimiseks mõeldud\nseireprogramm.\n\n**Väline varundus** on kogumisteenuse sisemisest varunduse poolt varundatud\nandmete säilitamiseks mõeldud väline varundusteenus.\n\n\n.. _välisteenused:\n\nVälised teenused\n^^^^^^^^^^^^^^^^\n\nVälised teenused on läbiviidavatele valimistele kehtestatud nõuetest sõltuvad\nteenused, millega kogumisteenus on võimeline liidestuma. Väliste teenuste hulka\nkuuluvad Registreerimisteenus, Ajatempliteenus, Mobiil-ID\nteenus, Smart-ID teenus, OCSP teenus vms.\n\n\nÜlevaade toimingutest\n---------------------\n\n* Hääletamiseelsel etapil:\n\n   * Kirjeldatakse kogumisteenuse poolt kasutatavad :ref:`välised teenused\n     <välisteenused>`;\n\n   * Valmistatakse ette kogumisteenuse :ref:`tugiteenused <tugiteenused>`;\n\n   * Koostatakse kogumisteenuse seadistused (usaldusjuur, tehnilised\n     seadistused ja valimiste seadistused);\n\n   * Genereeritakse teenuse toimimiseks vajalikud krüptovõtmed ja\n     sertifikaadid;\n\n   * Valmistatakse ette kogumisteenuse käitamiseks vajalik taristu;\n\n   * Paigaldatakse haldusteenus;\n\n   * Rakendatakse seadistused haldusteenusele, mille põhjal haldusteenus\n     paigaldab ja seadistab kogumisteenuse alamteenused.\n\n* Hääletamisetapil\n\n   * Jälgitakse teenuse toimimist;\n\n   * Luuakse e-valimiskastist varukoopiaid.\n\n* Töötlusetapil\n\n   * Eksporditakse kogumisteenusesse kogutud andmed:\n\n      #. konsolideeritud e-valimiskast kogutud häältega.\n\n* Lugemisetapil\n\n   * Lugemisetapil kogumisteenust ei kasutata;\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/annotatsioon.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\nAnnotatsioon\n------------\n\nKäesolev dokument sisaldab elektroonilise hääletamise infosüsteemi IVXV\nrakenduste ja kogumisteenuse seadistuste ülevaadet ja koostamise juhendit.\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/auditirakendus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _app-auditor:\n\nAuditirakendus\n==============\n\nIVXV võtmerakendus võimaldab kasutada tõestatavat dekrüpteerimist -\nkoos tulemusega väljastatakse lugemistõend e-häälte korrektse avamise\nkohta. Vältimaks häälte salajasuse rikkumist lugemistõendi kontrollil\nvõimaldab IVXV kasutada häälte miksimist, mis säilitab häälte sisu\nkuid eemaldab krüptograafiliselt seose konkreetse hääle ja selle hääle\nandnud isiku vahel.\n\nIVXV kasutab e-häälte miksimiseks tarkvara Verificatum, mis võtab\nsisendiks krüpteeritud hääled ning annab väljundiks miksitud\nkrüpteeritud hääled ja miksimistõendi.\n\nMiksimistõendi ja lugemistõendi kontroll toimub auditirakenduse tööriistadega\n*convert*, *mixer* ja *decrypt*.\n\nTöötlemisrakenduse toimingute kontroll toimub auditirakenduse tööriistaga\n*integrity*.\n\n#. Tööriist *convert* kontrollib, teisenduste korrektsust IVXV\n   andmevormingute ja Verificatumi andmevormingute vahel.\n#. Tööriist *mixer* kontrollib miksimistõendi korrektsust.\n#. Tööriist *decrypt* kontrollib lugemistõendi korrektsust.\n#. Tööriist *integrity* kontrollib töötlemisrakenduse logide korrektsust.\n\nE-häälte korrektse kokkulugemise kontrolliks on vajalik ja piisav\nkasutada kõiki nelja auditirakenduse tööriista.\n\nKõigi tööriistade kasutamine eeldab allkirjastatud usaldusjuure ja\nkonkreetse tööriista seadistuste olemasolu. Alljärgnevalt kirjeldame\nkonkreetsete tööriistade seadistusi.\n\n.. _auditor-convert:\n\nE-häälte korrektse teisendamise kontroll\n----------------------------------------\n\nVerificatumi poolt koostatud miksimistõendi formaat on erinev IVXV raamistikus\nkasutatavast formaadist, samuti erinevad IVXV ning Verificatumi\nkrüpteeritud häälte formaadid. IVXV raamistikku on pakendatud\nadapterid formaaditeisendusteks, auditirakendus pakub võimalust nende\nteisenduste korrektsuse kontrolliks.\n\nTööriist *convert* kontrollib, et Verificatumi poolt väljastatud\nmiksimistõend vastab failidele IVXV raamistikus.\n\n:convert.input_bb: IVXV miksimiseelse e-valimiskasti asukoht.\n\n:convert.output_bb: IVXV miksimisjärgse e-valimiskasti asukoht.\n\n:convert.pub: IVXV avaliku võtme asukoht.\n\n:convert.protinfo: Verificatumi miksimise protokollifaili asukoht.\n\n:convert.proofdir: Verificatumi miksimistõendi asukoht.\n\n:file: `auditor.convert.yaml`:\n\n.. literalinclude:: config-examples/auditor.convert.yaml\n   :language: yaml\n   :linenos:\n\n.. _auditor-mix:\n\nE-häälte miksimistõendi kontroll\n--------------------------------\n\nTööriist *mixer* kontrollib Verificatumi miksimistõendi korrektsust.\n\n:mixer.protinfo: Verificatumi miksimistõendi protokollifaili asukoht.\n\n:mixer.proofdir: Verificatumi miksimistõendi asukoht.\n\n:mixer.threaded: Kasuta mitmelõimelist implementatsiooni. Vaikimisi\n                 väärtus on väär. Kasutatavate lõimede arv sõltub\n                 käsurea-argumentidest. Käsurea-argumentide puudumise\n                 korral valitakse optimaalne lõimede arv lähtudes\n                 tuvastatud tuumade arvust.\n\n:file:`auditor.mixer.yaml`:\n\n.. literalinclude:: config-examples/auditor.mixer.yaml\n   :language: yaml\n   :linenos:\n\n.. _auditor-decrypt:\n\nE-häälte lugemistõendi kontroll\n-------------------------------\n\nTööriist *decrypt* kontrollib lugemistõendi korrektsust.\n\n:decrypt.proofs: Kehtivate sedelite lugemistõendi asukoht.\n\n:decrypt.pub: Dekrüpteerimiseks kasutatud salajasele võtmele vastava avaliku\n              võtme asukoht.\n\n:decrypt.discarded: Loend kehtetutest sedelitest.\n\n:decrypt.anon_bb: Töötlemisrakenduse või miksimisrakenduse poolt loodud\n                  e-valimiskast anonüümistatud häältega.\n\n:decrypt.plain_bb: Dekrüpteeritud valimiskast.\n\n:decrypt.tally: Elektroonilise hääletamise tulemus.\n\n:decrypt.candidates: Valimise valikute nimekiri allkirjastatud kujul.\n\n:decrypt.districts: Valimise ringkondade nimekiri allkirjastatud kujul.\n\n:decrypt.out: Lugemistõendi kontrolli tulemuste asukoht. Tegemist on\n              kataloogiga kuhu salvestatakse sedelid, mille\n              lugemistõend oli kehtetu.\n\n:decrypt.invalidity_proofs: Valikuline kehtetute sedelite lugemistõendi\n                            asukoht.\n\n:decrypt.abort_early: Valikuline auditirakenduse peatamine esimese läbikukutud\n                      kontrolli korral. Vaikimisi väärtus on tõene.\n\n:file:`auditor.decrypt.yaml`:\n\n.. literalinclude:: config-examples/auditor.decrypt.yaml\n   :language: yaml\n   :linenos:\n\n.. _auditor-integrity:\n\nTöötlemisrakenduse logide kontroll\n----------------------------------\n\nTööriist *integrity* kontrollib, et töötlemisrakenduse poolt väljastatud\nlogid ühendavad e-valimiskasti anonüümitud e-valimiskastiga.\n\n:integrity.ballotbox: Kogumisteenusest väljastatud e-valimiskast.\n\n:integrity.anon_bb: Töötlemisrakenduse poolt loodud e-valimiskast\n                    anonüümistatud häältega.\n\n:integrity.log_accepted: Vastuvõetud häälte *log1* fail.\n\n:integrity.log_squashed: Tühistatud korduvhäälte häälte *log2* fail.\n\n:integrity.log_revoked: Jaoskonnainfo põhjal tühistatud ja ennistatud häälte\n                        *log2* fail.\n\n:integrity.log_anonymised: Lugemisele läinud häälte *log3* fail.\n\n:integrity.bb_errors: E-valimiskasti töötlemisvigade raport.\n\n:integrity.abort_early: Valikuline auditirakenduse peatamine esimese\n                        läbikukutud kontrolli korral. Vaikimisi\n                        väärtus on tõene.\n\n:file: `auditor.integrity.yaml`:\n\n.. literalinclude:: config-examples/auditor.integrity.yaml\n   :language: yaml\n   :linenos:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\nMjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\nVVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\nA1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\nQOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\nQZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\nEuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\nnAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\nAwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\nAwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\nCp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\nhhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\nLnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\ndHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\nn8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\nfbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\nF5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\ndknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\nAgm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\npIX8UV3N9w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/Stats_Server_Certificate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlQCCQCc0vA55LL2hDANBgkqhkiG9w0BAQsFADB4MRwwGgYDVQQDDBNp\ndnh2LWFkbWluLWRlbW9jZXJ0MSUwIwYDVQQKDBxJVlhWIGNvbGxlY3RvciBhZG1p\nbmlzdHJhdG9yMRIwEAYDVQQLDAlCREQgdGVzdHMxEDAOBgNVBAcMB1Rlc3RFbnYx\nCzAJBgNVBAYTAkVFMB4XDTE2MTIwOTIxMTMzN1oXDTI2MTIwNzIxMTMzN1oweDEc\nMBoGA1UEAwwTaXZ4di1hZG1pbi1kZW1vY2VydDElMCMGA1UECgwcSVZYViBjb2xs\nZWN0b3IgYWRtaW5pc3RyYXRvcjESMBAGA1UECwwJQkREIHRlc3RzMRAwDgYDVQQH\nDAdUZXN0RW52MQswCQYDVQQGEwJFRTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBANqBKqPiPEv4TE3UnO6pym8ejzsY6WaTSnqJE2mSoXBKquc/tK3eLon4\nXdkgJI6wz/x2ZkPbUUuQW1NBhHGXY7W5Yu5OQ1lIKi/UAirPoQg4qsyyXkti51+R\nHeepaKB8ll7+ALXBTDLvNbIn9NN2pPp3Lf+j0vJDvgL2aQYfcUMKe0E6Vdr0r2fY\nDShCrLssNNlkY9wHPxBPKY2MPeIQxrCSI8OAjH5vGXoU0QLUAxTh6kFowU438tGp\n3Hxa4IbaaACNhghRudxcRi8SYCRAgc3zuzOFtkU2k3aetZT60Zcpuds+ZlPQl8Sd\nRJ6ToGQ7dO/Pvwpc+D8MEmwFSgHJb7ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\nIaOkADkkoJo+zFhKL4EYJpnq22nCB2FHYjuHQDzyyjfHU9r2J11s66ol4tWj/k9u\nFnQq9xE19L+QI3DsDA/q8XlRlhMKYRE8Pm5zpoC99+6QS34qbOvNR65pq3WH6J3m\nhWLulbJhEjgR5KXZSIzPWxt781sx1n9BEQCWsm3aCPGQofuEOcjTlZYuHSymSF5I\n5q9FAcI0jsquZVdlWsIg3QucSlS+wZVM/1GjTWorAekXTAhrLhIrTdpvNwo+fysy\nAJXROOJbkLUlVaAJFFT+CAUl1qWd9M1CmTYc91RFrlKN/hVItIJeIRGunE5N9dl4\n2/RTqnxfqfwSZuQ3DcRKSw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/TEST_of_EE_Certification_Centre_Root_CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\r\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\r\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\r\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\r\nGA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\r\ncnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\r\naWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\r\nxRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\r\nywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\r\nMmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\r\nVNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\r\nkV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\r\npwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\r\nA1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\r\nAgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\r\nBwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\r\nI+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\r\nBmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\r\nSnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\r\ntH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\r\nSfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/TEST_of_ESTEID-SK_2015.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\nMjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\naWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\nA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\nz8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\nzs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\nJ7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\nh5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\not3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\nAAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\nHQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\nIARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\nay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\nAQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\nAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\nBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\nfTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\nAQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\naWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\nMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\nY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\nAH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\nDgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\nyPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\nWFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\nPKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/TEST_of_SK_OCSP_RESPONDER_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\nMjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\nb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\nLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\ngOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\nr1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\nz00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\nOM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\nwB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\nRrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\nFIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\nAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\naQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\nJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\nGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\nczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\nBgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\naC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\nMHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\nnH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\nmawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\ndh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/android-ios-config.json",
    "content": "{\n  \"appConfig\": {\n    \"colors\": {\n      \"btn_background\": \"#F0F0F0\",\n      \"btn_background_focused\": \"#CFCFCF\",\n      \"btn_close_view_focused\": \"#1995B4\",\n      \"btn_foreground\": \"#727272\",\n      \"btn_verify_background_center\": \"#1AABE1\",\n      \"btn_verify_background_end\": \"#00A1DC\",\n      \"btn_verify_background_focused\": \"#1693C0\",\n      \"btn_verify_background_start\": \"#30B4E5\",\n      \"btn_verify_foreground\": \"#FFFFFF\",\n      \"error_window\": \"#FF0000\",\n      \"error_window_close_btn_focused\": \"#B60010\",\n      \"error_window_foreground\": \"#FFFFFF\",\n      \"error_window_shadow\": \"#770000\",\n      \"frame_background\": \"#AA444444\",\n      \"lbl_background\": \"#33B5E5\",\n      \"lbl_close_timeout_background_center\": \"#F9D303\",\n      \"lbl_close_timeout_background_end\": \"#F7C804\",\n      \"lbl_close_timeout_background_start\": \"#FEEC00\",\n      \"lbl_close_timeout_foreground\": \"#454444\",\n      \"lbl_close_timeout_shadow\": \"#C6A002\",\n      \"lbl_foreground\": \"#FFFFFF\",\n      \"lbl_inner_container_background\": \"#FFFFFF\",\n      \"lbl_inner_container_foreground\": \"#404040\",\n      \"lbl_outer_container_background\": \"#EAEAEA\",\n      \"lbl_outer_container_foreground\": \"#404040\",\n      \"lbl_outer_inner_container_divider\": \"#595959\",\n      \"lbl_shadow\": \"#008EC2\",\n      \"loading_window_background\": \"#33B5E5\",\n      \"loading_window_foreground\": \"#FFFFFF\",\n      \"main_window\": \"#33B5E5\",\n      \"main_window_foreground\": \"#FFFFFF\",\n      \"main_window_shadow\": \"#005777\"\n    },\n    \"elections\": {\n      \"question-1\": \"Prototüübi test\"\n    },\n    \"errors\": {\n      \"bad_config_message\": \"Seadistuse viga\",\n      \"bad_device_message\": \"Seadmel puudub kaamera, verifitseerimist ei ole võimalik läbi viia\",\n      \"bad_server_response_message\": \"Tehniline viga, palun teavita valimiste korraldajat abi@valimised.ee\",\n      \"bad_verification_message\": \"Valiku tuvastamine ebaõnnestus\",\n      \"bad_version_message\": \"Rakenduse versioon ei ole ajakohane, palun uuenda oma rakendust\",\n      \"camera_permission_required_message\": \"Rakenduse kasutamiseks peab olema kaamera kasutamine lubatud\",\n      \"error_title_bad_version\": \"Rakenduse versiooni viga\",\n      \"error_title_default\": \"Viga\",\n      \"error_title_network\": \"Internetiühendus puudub\",\n      \"get_config_message\": \"Seadistuse laadimine ebaõnnestus\",\n      \"no_network_message\": \"Palun veendu, et nutiseadme internetiühendus on aktiivne\",\n      \"problem_qrcode_message\": \"QR-koodi ei õnnestunud tuvastada\",\n      \"send_server_request_message\": \"Serveriga ühendumine ebaõnnestus, kontrolli internetiühendust või teavita valimiste korraldajat abi@valimised.ee\"\n    },\n    \"params\": {\n      \"close_interval\": 1000,\n      \"close_timeout\": 30000,\n      \"con_timeout_1\": 3000,\n      \"con_timeout_2\": 15000,\n      \"help_url\": \"https://www.valimised.ee/verify/help/index.html\",\n      \"ocsp_service_cert\": [],\n      \"public_key\": \"<valimiste_avalik_võti_pem>\",\n      \"tspreg_client_cert\": \"<collector_tspreg_cert_pem>\",\n      \"tspreg_service_cert\": \"<SK_TIMESTAMPING_AUTHORITY_CERT>\",\n      \"verification_sni\": \"verification.ivxv.valimised.ee\",\n      \"verification_tls\": [\n        \"<collector1_tls_pem>\",\n        \"<collector2_tls_pem>\",\n        \"<collector3_tls_pem>\"\n      ],\n      \"verification_url\": [\n        \"collector1:port\",\n        \"collector2:port\",\n        \"collector3:port\"\n      ]\n    },\n    \"texts\": {\n      \"a11y_qr_view_opened\": \"Alusta QR-koodi skaneerimist\",\n      \"btn_close\": \"Sulge\",\n      \"btn_more\": \"Abi\",\n      \"btn_next\": \"Edasi\",\n      \"btn_ok\": \"Ok\",\n      \"btn_packet_data\": \"Andmeside\",\n      \"btn_update\": \"Värskenda\",\n      \"btn_verify\": \"Kuva minu valik\",\n      \"btn_wifi\": \"Wifi\",\n      \"lbl_choice\": \"Tuvastatud valik\",\n      \"lbl_close_timeout\": \"Rakendus suletakse XX sekundi pärast!\",\n      \"lbl_no_choice\": \"Valikut ei tehtud\",\n      \"lbl_vote\": \"Hääle kontrollimine\",\n      \"lbl_vote_signer\": \"E-hääle allkirjastaja: \",\n      \"lbl_vote_txt\": \"Sinu QR-koodile vastav hääl on talletatud valimiste serveris\",\n      \"loading\": \"Laadin...\",\n      \"notification_message\": \"Kontrollimine lõpetatud\",\n      \"notification_title\": \"Riigi valimisteenistus\",\n      \"welcome_message\": \"Hääle kontrollimiseks suuna nutiseadme kaamera arvuti ekraanil kuvatavale QR-koodile\"\n    },\n    \"versions\": {\n      \"android_version_code\": 42,\n      \"ios_bundle_version\": \"2.1.19\"\n    }\n  }\n}"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/auditor.convert.yaml",
    "content": "convert:\n  input_bb: TESTCONF-bb-4.json\n  output_bb: TESTCONF-shuffled.json\n  pub: TESTCONF-pub.pem\n  protinfo: prot.xml\n  proofdir: mixnet/\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/auditor.decrypt.yaml",
    "content": "decrypt:\n  proofs: decout/TESTCONF-proof\n  pub: initout/TESTCONF-pub.pem\n  discarded: decout/TESTCONF-invalid\n  anon_bb: TESTCONF-shuffled.json\n  plain_bb: decout/TESTCONF.TESTQUESTION.plain\n  tally: decout/TESTCONF.TESTQUESTION.tally\n  candidates: TESTCONF.choices.bdoc\n  districts: TESTCONF.districts.bdoc\n  out: auditout\n  invalidity_proofs: decout/TESTCONF-proof-invalid\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/auditor.integrity.yaml",
    "content": "integrity:\n  ballotbox: TESTCONF.votes.zip\n  anon_bb: TESTCONF-bb-4.json\n  log_accepted: out-1/TESTCONF.TESTQUESTION.check.log1\n  log_squashed: out-2/TESTCONF.TESTQUESTION.squash.log2\n  log_revoked: out-3/TESTCONF.TESTQUESTION.revoke.log2\n  log_anonymised: out-4/TESTCONF.TESTQUESTION.anonymize.log3\n  bb_errors: out-1/ballotbox_errors.txt\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/auditor.mixer.yaml",
    "content": "mixer:\n  protinfo: prot.xml\n  proofdir: mixnet/\n  threaded: true\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/etcd_CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDgDCCAmigAwIBAgIJAN8HvtqlwnNYMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV\nBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBD\nZXJ0aWZpY2F0ZXMxEDAOBgNVBAMMB2V0Y2QgQ0EwIBcNMTcwNzI3MTIyMjIwWhgP\nMjExNzA3MDMxMjIyMjBaMFQxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYg\nT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxEDAOBgNVBAMMB2V0\nY2QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8xktrfvOSwiKg\nsd2CfOQ2PobheaMqhbd+zHyj+Vmw0KIc+LwRoPK6FiGVmeIJXp7bN3QbbZdbpTfh\nap1xJr/wYu5JJO8DEy5i+Ea5IBGf6fZ6RTVngtbigln0FCFyRqumvD5yuZRbOnV6\nw1Roe1GmhtrrW3QTHR5OSc8FjLcw21RkjrKbc0VtLoaGTU0CB7gSLUoHgXdOb3n5\nn5pLa/N/4VjfIEqL7U4Pi9eFRl1tcKnj3+AwHp5HAHZgN937YDqXAtbWL7BwAToR\nCcW5ESm2xq6i46wg/OLhNU4bA8kfxqVCuCk/QDX3tvo4pkavC5bXMybc+3LfRfjO\nk7KKr+2XAgMBAAGjUzBRMB0GA1UdDgQWBBRWhI+0eFROcX+wY8h8jCU9SuXlHDAf\nBgNVHSMEGDAWgBRWhI+0eFROcX+wY8h8jCU9SuXlHDAPBgNVHRMBAf8EBTADAQH/\nMA0GCSqGSIb3DQEBCwUAA4IBAQBOs9gwX28/Q2dvU9zFxHxVZ0x3jv2DAjAqBGHp\nI5ZbVjoWdjxurMwid9X6CF0nwqtHZQieGfKKt3wRv7G05yKZNlWkxSBfBiQwTwSs\nMp2rj5zXDdQdkPWMD/08cDLj1jcynM8GMgUKzldcUKpWvwaL1MFaNda/pZAwd3DH\nr/s+XHnGKNafNalkCd+n/bWXUkJvVlE3Aakx2hfuNiT2WQ9rb6J7LkPhrWN4X1E4\n1a++vt53qgMYH+jvYJkiKi9mDa+XVZR7XA9tD2xLGC5l0zt7Aaz4xUG9YVxc8/sW\nwP7zi1j187I4XZ9FIhO4+dmbpy+vg4e51xvQfC9/5T5vmfWR\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/example.election.yaml",
    "content": "# Valimiste seadistuse näide\n\nidentifier: TESTCONF\nquestions:\n  - TESTQUESTION\n\nperiod:\n  servicestart:  2017-01-16T08:50:00+02:00\n  electionstart: 2017-01-16T09:00:00+02:00\n  electionstop:  2017-01-18T19:00:00+02:00\n  servicestop:   2017-01-18T19:15:00+02:00\n  verificationstop:   2017-01-18T19:20:00+02:00\n\nvoting:\n  ratelimitstart:   50\n  ratelimitminutes:  5\n\nverification:\n  count:       3\n  minutes:    30\n  latestonly: false\n\nvoterlist:\n  key: !container rr_pub.key\n\nvis:\n  url: https://vis-mock.local/vis3/\n  ca:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIDmzCCAoOgAwIBAgIDOBFTMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAkVF\n      MRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZp\n      Y2F0ZXMxEzARBgNVBAMMClNlcnZpY2UgQ0EwIBcNMjEwNjE3MTIwNTI0WhgPMjEy\n      MTA1MjQxMjA1MjRaMFsxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kx\n      HzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxFzAVBgNVBAMMDnZpcy1t\n      b2NrLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzZW7yXGO\n      SEt4qavm/vNJj9otKf9j8runLJcmoTGR61A5As2gluRnj3z6/U8An/VsX1dUG2qL\n      sJecEXUkvXBSTw2IIyfA6lJNtaov44ytRB4fKpuFx+lb00WmuRf99uC0Tpp5K0Of\n      7D5NO5dgtMXLrdpzuyJD6NP2xrp8Dio5a1TosHcQEVWJvR/dwCr02TmcQjvILOVN\n      vJFr038Dg77NQKojOsag60KRrHcKc1WukgUPu4AJ2VFCmn67rCfp39tusbcndsNs\n      EX7zGNghOk85CMoGWlYfnJm94oAoKUsYtpF1hinfBN/EikXZwJbsVFoSg4mo/te2\n      MfwzK7E5j17JWwIDAQABo2owaDAdBgNVHQ4EFgQUI2yfomatZq3HLyL+DpqeBYO9\n      /1EwHwYDVR0jBBgwFoAUbyNWl77jkIGrjVtW7Eu4+qpDXxYwDgYDVR0PAQH/BAQD\n      AgOoMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQCA\n      RsgmPcL4NaHxQGRxPS2agw2ihLPgkmZcUJhJ0ObnmVYBjCCpYxaNd0vCWBYejChj\n      Q2cblFAuKUtc+pwSC2WDPV9rRwLNOQn/PR2dzeNRP7L0UmNyPcH5pwmltAJLFQqW\n      OdnlXL8ehXSLeIdzlBkqNYZCrj4Ulv4W8cLosjCrmwTZ8OW2E/GSTY7YljsPZZt0\n      uI5Sxz8VtlMJsVh0HWCQ4oAawp6BOgUAGdE9PIo2UlDHyUHBSDXDuZKTGut+yv1T\n      NOq+xK3YM/CRFf/ujIuc1zx+dzkM9l72VJT2BQ6+LKeS5qzduJTG/8dJJFzgNB9p\n      vo7reW+FURRS+CQI633D\n      -----END CERTIFICATE-----\n  min: 15\n\nxroad:\n  ca: !container xroadservice_ca.pem\n\nballot:\n  # This group must be the one used for voter ballot encryption public key\n  # generation, check documentation for all available groups to use\n  encpkeygroup: RFC3526ModPGroup3072\n  encpkey: !container vote_enc_pkey.pem\n\nauth:\n  ticket:\n  tls:\n    roots:\n      - !container TEST_of_EE_Certification_Centre_Root_CA.pem\n    intermediates:\n      - !container TEST_of_ESTEID-SK_2015.pem\n    ocsp:\n      url: http://demo.sk.ee/ocsp\n      responders:\n        - !container TEST_of_SK_OCSP_RESPONDER_2020.pem\n      retry: 2\n\nidentity: pnoee\n\nage:\n  method:   estpic\n  timezone: Europe/Tallinn\n  limit:    18\n\nvote:\n  bdoc:\n    filecount: 4\n    bdocsize: 32768  # 32 KiB\n    filesize: 32768  # 32 KiB\n    roots:\n      - !container TEST_of_EE_Certification_Centre_Root_CA.pem\n    intermediates:\n      - !container TEST_of_ESTEID-SK_2015.pem\n    profile: BES\n\nmid:\n  url: https://tsp.demo.sk.ee/mid-api/\n  relyingpartyuuid: 00000000-0000-0000-0000-000000000000\n  relyingpartyname: DEMO\n  language:    EST\n  authmessage: Mobiil-ID autentimise testimine.\n  signmessage: Mobiil-ID allkirjastamise testimine.\n  messageformat: GSM-7\n  authchallengesize: 64\n  statustimeoutms: 5000\n  roots:\n    - !container TEST_of_EE_Certification_Centre_Root_CA.pem\n  intermediates:\n    - !container TEST_of_ESTEID-SK_2015.pem\n  ocsp:\n    url: http://demo.sk.ee/ocsp\n    responders:\n      - !container TEST_of_SK_OCSP_RESPONDER_2020.pem\n\nsmartid:\n  url: https://sid.demo.sk.ee/smart-id-rp/v2/\n  relyingpartyuuid: 00000000-0000-0000-0000-000000000000\n  relyingpartyname: DEMO\n  certificatelevel: QUALIFIED\n  authinteractionsorder:\n    - type: verificationCodeChoice\n      displayText60: authenticating\n    - type: displayTextAndPIN\n      displayText60: authenticating\n  signinteractionsorder:\n    - type: verificationCodeChoice\n      displayText60: signing\n    - type: displayTextAndPIN\n      displayText60: signing\n  authchallengesize: 64\n  statustimeoutms: 5000\n  roots:\n    - !container TEST_of_EE_Certification_Centre_Root_CA.pem\n  intermediates:\n    - !container TEST_of_ESTEID-SK_2015.pem\n  ocsp:\n    url: http://demo.sk.ee/ocsp\n    responders:\n      - !container TEST_of_SK_OCSP_RESPONDER_2020.pem\n\nqualification:\n  - protocol: tspreg\n    conf:\n      url: http://demo.sk.ee/tsa\n      signers:\n        - !container DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem\n      delaytime: 1\n      retry: 2\n  - protocol: ocsp\n    conf:\n      url: http://demo.sk.ee/ocsp\n      responders:\n        - !container TEST_of_SK_OCSP_RESPONDER_2020.pem\n      retry: 2\n\n# this block defines any statistics that EHS shares with any other third-party\nstats:\n  # this block defines detail statistics that EHS shares with VIS,\n  # you can add 'common' here if implemented, or any other kind\n  detail:\n    # this block defines an automation (scheduler) for detail statistics sharing\n    scheduler:\n      # cron is a scheduler, you can add 'systemd-timer' here if implemented,\n      # only crontab syntax supported for cron: section\n      cron:\n        min: \"*/15\" # minutes of a hour: 00, 15, 30, 45\n        hour: \"*\" # each hour\n        day: \"*\" # each day\n        month: \"*\" # each month\n        weekday: \"*\" # each week day\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/example.technical.yaml",
    "content": "# Tehnilise seadistuse näide\n\ndebug: true\n\nsnidomain: inttest.ivxv.ee\n\nfilter:\n  tls:\n    handshaketimeout: 20\n    ciphersuites:\n      - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n      # Vanemate nutiseadmete tugi.\n      - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\n      - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n  codec:\n    rwtimeout: 10\n    requestsize: 16384  # 16 KiB\n    logrequests: true\n\nnetwork:\n  - id: default\n    services:\n      proxy:\n        - id:          proxy@proxy1.ivxv.ee\n          address:     proxy1.ivxv.ee:443\n      mid:\n        - id:          mid@mid1.ivxv.ee\n          address:     mid1.ivxv.ee:443\n      smartid:\n        - id:          smartid@smartid1.ivxv.ee\n          address:     smartid1.ivxv.ee:443\n      webeid:\n        - id:          webeid@webeid1.ivxv.ee\n          address:     webeid1.ivxv.ee:443\n          origin:      https://ivxv1.mydomain.ivxv.ee:443 # address that client sees\n      choices:\n        - id:          choices@choices1.ivxv.ee\n          address:     choices1.ivxv.ee:443\n      voting:\n        - id:          voting@voting1.ivxv.ee\n          address:     voting1.ivxv.ee:443\n      verification:\n        - id:          verification@verification1.ivxv.ee\n          address:     verification1.ivxv.ee:443\n      sessionstatus:\n        - id:          sessionstatus@sessionstatus1.ivxv.ee\n          address:     sessionstatus1.ivxv.ee:443\n      storage:\n        - id:          storage@storage1.ivxv.ee\n          address:     storage1.ivxv.ee:2379\n          peeraddress: storage1.ivxv.ee:2380\n\nstatus:\n  session:\n    name: session\n    servername: session.status.inttest.ivxv.ee\n    authttl: 60 # ttl in seconds!\n    choicettl: 90\n    votettl: 60\n    verifyttl: 60\n\nlogging:\n  - address: logserver1.ivxv.ee\n    port: 20514\n  - address: logserver2.ivxv.ee\n    port: 514\n\nstorage:\n  protocol: etcd\n  conf:\n    ca: !container etcd_CA.pem\n    conntimeout: 5\n    optimeout: 10\n    size: 2147483648 # bytes\n    snapshotcount: 10000 # etcd revisions\n    heartbeattimeout: 100 # ms\n    electiontimeout: 1000 # ms\n    bootstrap:\n      - storage@storage1.ivxv.ee\n  ordertimeout: 10\n\nbackup: [\"03:00\", \"09:00\", \"15:00\", \"21:00\"]\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/example.trust.yaml",
    "content": "# Usaldusjuure seadistuse näide\n\ncontainer:\n  bdoc:\n    filecount: 50\n    bdocsize: 104857600  # 100 MiB\n    filesize: 104857600  # 100 MiB\n    roots:\n      - !container TEST_of_EE_Certification_Centre_Root_CA.pem\n    intermediates:\n      - !container TEST_of_ESTEID-SK_2015.pem\n    profile: TS\n    ocsp:\n      responders:\n        - !container TEST_of_SK_OCSP_RESPONDER_2020.pem\n    tsp:\n      signers:\n        - !container DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem\n      delaytime: 1\n    tsdelaytime: 60\n\nauthorizations:\n    - ORAV,IVAN,30809010001\n    - ROPKA,KIVIVALVUR,32608320001\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/example.voters.skip.yaml",
    "content": "# Valijate muudatusnimekirja nr. 15 vahelejätmise korralduse näide\n\nelection: TESTCONF\nskip_voter_list: \"https://vis-ehs-api.ria.ee/ehs-election-voters-changeset 2021-07-22T10:12:49Z\"\nchangeset: 15\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/ivxv.properties.real",
    "content": "ca = EE-GovCA2018.pem.crt, ESTEID2018.pem.crt, ESTEID-SK_2015.pem.crt\nocsp = SK_OCSP_RESPONDER_2011.pem.crt\ntsa = SK_TIMESTAMPING_AUTHORITY_2023.pem.crt\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.decrypt.yaml",
    "content": "decrypt:\n  identifier: TESTCONF\n  protocol:\n    recover:\n      threshold: 2\n      parties: 3\n  anonballotbox: TESTCONF-bb-4.json\n  anonballotbox_checksum: TESTCONF-bb-4.json.sha256sum.bdoc\n  candidates: TESTCONF.choices.bdoc\n  districts: TESTCONF.districts.bdoc\n  provable: true\n  prove_invalid: true\n  check_decodable: false\n  out: decout\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.groupgen.yaml",
    "content": "groupgen:\n  paramtype: mod\n  length: 3072\n  init_template: key.init.template.yaml\n  random_source:\n  - random_source_type: DPRNG\n    random_source_path: public_seed_file\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.init.original.yaml",
    "content": "init:\n  identifier: TESTCONF\n  paramtype:\n    mod:\n      p: 5809605995369958062791915965639201402176612226902900533702900882779736177890990861472094774477339581147373410185646378328043729800750470098210924487866935059164371588168047540943981644516632755067501626434556398193186628990071248660819361205119793693985433297036118232914410171876807536457391277857011849897410207519105333355801121109356897459426271845471397952675959440793493071628394122780510124618488232602464649876850458861245784240929258426287699705312584509625419513463605155428017165714465363094021609290561084025893662561222573202082865797821865270991145082200656978177192827024538990239969175546190770645685893438011714430426409338676314743571154537142031573004276428701433036381801705308659830751190352946025482059931306571004727362479688415574702596946457770284148435989129632853918392117997472632693078113129886487399347796982772784615865232621289656944284216824611318709764535152507354116344703769998514148343807\n      g: 2\n  out: initout\n  skiptest: true\n  fastmode: true\n  signaturekeylen: 3072\n  signcn: SIGNATURE\n  signsn: 1\n  enccn: ENCRYPTION\n  encsn: 2\n  required_randomness: 128\n  random_source:\n  - random_source_type: file\n    random_source_path: randomness_file\n  - random_source_type: system\n  - random_source_type: DPRNG\n    random_source_path: seed_file\n  - random_source_type: stream\n    random_source_path: /dev/urandom\n  - random_source_type: user\n    random_source_path: user_entropy.exe\n  genprotocol:\n    desmedt:\n      threshold: 2\n      parties: 3\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.init.yaml",
    "content": "init:\n  identifier: TESTCONF\n  paramtype:\n    mod:\n      p: 58096059953699580627919159656392014021766122269029005337..\n      g: 2\n  out: initout\n  skiptest: true\n  fastmode: true\n  signaturekeylen: 3072\n  signcn: SIGNATURE\n  signsn: 1\n  enccn: ENCRYPTION\n  encsn: 2\n  required_randomness: 128\n  random_source:\n  - random_source_type: file\n    random_source_path: randomness_file\n  - random_source_type: system\n  - random_source_type: DPRNG\n    random_source_path: seed_file\n  - random_source_type: stream\n    random_source_path: /dev/urandom\n  - random_source_type: user\n    random_source_path: user_entropy.exe\n  genprotocol:\n    desmedt:\n      threshold: 2\n      parties: 3\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.testkey.yaml",
    "content": "testkey:\n  identifier: TESTCONF\n  out: initout\n  threshold: 2\n  parties: 3\n  fastmode: false\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/key.util.yaml",
    "content": "util:\n  listreaders: true\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.anonymize.yaml",
    "content": "anonymize:\n  ballotbox: out-3/TESTCONF-bb-3.json\n  ballotbox_checksum: out-3/TESTCONF-bb-3.json.sha256sum.bdoc\n  out: out-4\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.check.yaml",
    "content": "check:\n  ballotbox: TESTCONF.votes.zip\n  ballotbox_checksum: TESTCONF.votes.zip.sha256sum.bdoc\n  signed_ballot_max_size_bytes: 32768\n  districts: TESTCONF.districts.bdoc\n  registrationlist: TESTCONF.registration.zip\n  registrationlist_checksum: TESTCONF.registration.zip.sha256sum.bdoc\n  tskey: TESTCONF.ts.key\n  vlkey: TESTCONF.voterfile.pub.key\n  voterlists_dir: TESTCONF.voterlists\n  voterlists:\n    - path: TESTCONF.voters_1\n      signature: TESTCONF.voters_1.signature\n    - path: TESTCONF.voters_2\n      signature: TESTCONF.voters_2.signature\n  election_start: 2017-05-01T12:00:00+03:00\n  out: out-1\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.checkAndSquash.yaml",
    "content": "checkAndSquash:\n  ballotbox: TESTCONF.votes.zip\n  ballotbox_checksum: TESTCONF.votes.zip.sha256sum.bdoc\n  signed_ballot_max_size_bytes: 32768\n  districts: TESTCONF.districts.bdoc\n  registrationlist: TESTCONF.registration.zip\n  registrationlist_checksum: TESTCONF.registration.zip.sha256sum.bdoc\n  tskey: TESTCONF.ts.key\n  vlkey: TESTCONF.voterfile.pub.key\n  voterlists_dir: TESTCONF.voterlists\n  voterlists:\n    - path: TESTCONF.voters_1\n      signature: TESTCONF.voters_1.signature\n    - path: TESTCONF.voters_2\n      signature: TESTCONF.voters_2.signature\n  enckey: TESTCONF-enc.pub.key\n  election_start: 2017-05-01T12:00:00+03:00\n  out: out-2\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.export.yaml",
    "content": "export:\n  ballotbox: TESTCONF.votes.zip\n  ballotbox_checksum: TESTCONF.votes.zip.sha256sum.bdoc\n  signed_ballot_max_size_bytes: 32768\n  out: out-export\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.revoke.yaml",
    "content": "revoke:\n  ballotbox: out-2/TESTCONF-bb-2.json\n  ballotbox_checksum: out-2/TESTCONF-bb-2.json.sha256sum.bdoc\n  districts: TESTCONF.districts.bdoc\n  revocationlists:\n    - TESTCONF.revoke_1.bdoc\n    - TESTCONF.revoke_2.bdoc\n  out: out-3\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.revokeAndAnonymize.yaml",
    "content": "revokeAndAnonymize:\n  ballotbox: out-2/TESTCONF-bb-2.json\n  ballotbox_checksum: out-2/TESTCONF-bb-2.json.sha256sum.bdoc\n  districts: TESTCONF.districts.bdoc\n  revocationlists:\n    - TESTCONF.revoke_1.bdoc\n    - TESTCONF.revoke_2.bdoc\n  out: out-4\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.squash.yaml",
    "content": "squash:\n  ballotbox: out-1/TESTCONF-bb-1.json\n  ballotbox_checksum: out-1/TESTCONF-bb-1.json.sha256sum.bdoc\n  districts: TESTCONF.districts.bdoc\n  enckey: TESTCONF-enc.pub.key\n  out: out-2\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.stats.yaml",
    "content": "stats:\n  ballotbox: TESTCONF.votes.zip\n  signed_ballot_max_size_bytes: 32768\n  election_day: 2018-11-11\n  period_start: 2018-11-01T10:00:00+03:00\n  period_end:   2018-11-07T22:00:00+03:00\n  districts: TESTCONF.districts.bdoc\n  vlkey: TESTCONF.voterfile.pub.key\n  voterlists:\n    - path: TESTCONF.voters_1\n      signature: TESTCONF.voters_1.signature\n    - path: TESTCONF.voters_2\n      signature: TESTCONF.voters_2.signature\n  out: out-stats\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.statsdiff.yaml",
    "content": "statsdiff:\n  compare: out-stats/TESTCONF-stats.json\n  to: TESTCONF-voting-stats.json\n  diff: TESTCONF-stats-diff.json\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/processor.verify.yaml",
    "content": "verify:\n  file: processor.yaml.bdoc\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/rnd.groupgen.yaml",
    "content": "groupgen:\n\n  [...]\n\n  random_source:\n    - random_source_type: DPRNG\n      random_source_path: public_seed_file\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/rnd.init.yaml",
    "content": "init:\n\n  [...]\n\n  required_randomness: 128\n  random_source:\n  - random_source_type: system\n  - random_source_type: user\n    random_source_path: server.exe\n\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/rr_pub.key",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxOJOUqfOxVHHGplNmSSt\nLYXfpjyRlJrONLu74b85rBjTmr1bBwLRz+LemN9QWJJySKMaB0kShUhYoBDCAnpr\ngChxvnMzh2fsX9eK//dBWj+xbWFtzr2rrKLR8BnbB8SFDH18p1c4Pk3fDZGTME0D\ngmceUhAS83kZb/sZeRBf3ODDf9eWgFZupOOLGMIzwUYlMbYGGDV8dR8tNHKyNgj7\nyWWPlCrqh1vrBqYSDcTZv0B5Rd+aaBwwvTFLc3cqeE2Xy59Rh7bJvTf8IHlPBPRY\neqDqGvpQ/1rQcuqzfqqB6zc1qHUu09nViLc6qJJ3jQ5jQjJ1OREz9u05whEpD8Y1\n9QIDAQAB\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/vote_enc_pkey.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIDMzCCAaEGCSsGAQQBl1UCATCCAZICggGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiK\r\nZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY3\r\n7WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STP\r\nX4NlXSPco62WHGLzViCFUrue1SkHcJaWbWcMNU5KvJgE8XRsCMoYIXwykF5GLjbOO+OedywYDoYD\r\nmyeDouwHoo+1xV3wb0xSyd4ry/aVWBcYOZVJfOqVauUV0iYYmPoFEBVyjlqKqsQtrTMXDQRQejOo\r\nVSGr3xy6ZOz7hQRY2+8KiupxV10GDH2zlw+FpuHkx6v1rozbCTPXHoyU4EolYZ3O49ImGtLua/Ev\r\n+gbZighk2HYCcz7IamRSHysYF3sgDLvhF1d6YV1sdwmIwLrZRuII4k+gdOWrMUPbW/zg/RCOS4LR\r\nIKk60sr//////////wIBAhsISEEtU0VUVVADggGKADCCAYUCggGBANr8jgmDq2SRdr85JlmKb+J4\r\nOuqinXnSyJ/p/S0qqyde8VCzaBe2G7xj/l0zSVf1dj6JJeGzaN2G9DbrchOrix33ctjKNjnsp/kN\r\nRro3xr/hPcnX3xdnywSvJIR8FEx2p1b3EU2AMEnmY8YDJeLYb7M/YqtLAfWd5qFCYsnzapMIgWj6\r\nWfKvx8+h8STzI2Kc3xtHTW6KQrSRdb8yIJUIv5Tt+3Lgyng50twN4n1tynLjkpa5UG0sMIQWHjZV\r\neEUWasqOUKfWugUEswUe7vi8AAL6q8EVxeh0C1VCwbkTdV/GwlI5zuvXdXZtFn9IOMgp3cHZjYNQ\r\neEBFCVGzwzoBr9S3b75A7WAkGV596B7jHTJuBP02nJw4kU+TDr7tpVM2WK4tcZKrW3YNhOBofAt7\r\ne1F2U7gyvwEECkEIc8PnYmh5twwUQv+AF2ABJOnoDR3cBbzqzyBzNSwnyu6kdegyXk8tBJLQ6ddu\r\nGZ6R0YR8V4xT7bCMVDMIM0fi7EmIkicJlQ==\n-----END PUBLIC KEY-----"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/config-examples/xroadservice_ca.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDfzCCAmegAwIBAgIUdYaa1OjZYFHPG7Ku7/Res9nou2AwDQYJKoZIhvcNAQEL\nBQAwTzELMAkGA1UEBhMCRUUxDjAMBgNVBAgMBVRhcnR1MQ8wDQYDVQQKDAZTQ0NF\nSVYxHzAdBgNVBAMMFnhzZXJ2aWNlLmRldmVsLml2eHYuZWUwHhcNMjIxMTMwMTY0\nOTE2WhcNMjMxMTMwMTY0OTE2WjBPMQswCQYDVQQGEwJFRTEOMAwGA1UECAwFVGFy\ndHUxDzANBgNVBAoMBlNDQ0VJVjEfMB0GA1UEAwwWeHNlcnZpY2UuZGV2ZWwuaXZ4\ndi5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMO3JO230yexwypu\nYHfBUgGiTvXTIBScpP1HwiLvBSr9FylD+iaZ890TpGLGMSH83FWgznu4NJmbqhHF\ndlrZICnivng9C7w+/egE0coqExRuR0ECWSLXAkfNVQ82YKPedhdZ3gw88YByFwvy\nEhjD17Fiv5CHZ3Iu3Fa4Lb0U95/0AI45GGgLA2DWB914Lqloc28lyCXb2cQ3OUoM\nCZkjGmsC9BLSXbsiqhU2TqQ1R1vh9/ru4cYR37JtWrU/H7tGwNB/8/AFQ161zZOz\nDXIBv5wEmZ4Rco+pXiQY5Tp3wucagwGdYFFqzxPeiPxRa56Qk5F7zBQyifEZ/ypK\nMCSRO9UCAwEAAaNTMFEwHQYDVR0OBBYEFLdlusaUnlQmzLPFo+fLNewQcPzwMB8G\nA1UdIwQYMBaAFLdlusaUnlQmzLPFo+fLNewQcPzwMA8GA1UdEwEB/wQFMAMBAf8w\nDQYJKoZIhvcNAQELBQADggEBAKTbKmS8Wy5vSiaqlEo+Cs6bl1/RMRjzjc7k51Jk\n0l1uvPBEA2/3ETZAkErqpApfwOD+CGNLPAgoLMH7dSpzyb7Z7i6bjZUcH8CeKT/D\nXZNHun2eBIuEP+sRXdzs+UAwL5Axj5ncemNEjfHLgZaKA4cr/QNkrdxViqzUbsFY\nACjTr5UhqSLZpjRRmT7UNtGB5wdk+F76DIEdjGCAWzpufPTwPDemx301P4qg4R1m\nnS0ErClDR+AiQLTkLB/uzCQmWMhgq/++boyw8Ny2A8I/OphEghsl//NYBlQ+Uz2K\nhtQmzR3L3Q0NE+9vXh8PFuFLv1qtyOpuhEn0/HuU/NzGMcU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/genparam.py",
    "content": "#!/usr/bin/env sage\n\nfrom sage.all import *\n\nn = 3072\np_found = False\nc = 0\n\nwhile not(p_found):\n    if (Mod(c,10000) == 0):\n        print(\"c is: \", c)\n    p = 2**n - 2**(n-64) - 1 + 2**64*(floor(2**(n-130)*pi.n(prec=10000))+c)\n    if is_pseudoprime(p):\n        print(p, \" is prime\")\n        q = (p-1)/2\n        if is_pseudoprime(q):\n            print(p, \" is safe prime\")\n            p_found = True\n    c = c + 1\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/index.rst",
    "content": "..  IVXV seadistuste koostamise juhend\n\nIVXV seadistuste koostamise juhend\n==========================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 2\n   :numbered:\n\n   annotatsioon\n   ylevaade\n   rakendused\n   votmerakendus\n   tootlemisrakendus\n   auditirakendus\n   kogumisteenus\n   valijarakendus\n   kontrollrakendus\n   mixnet\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kogumisteenus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _kogumisteenus:\n\nKogumisteenus\n=============\n\n.. include:: kt-ylevaade.inc\n.. include:: kt-usaldusjuur.inc\n.. include:: kt-tehniline.inc\n.. include:: kt-valimised.inc\n.. include:: kt-valijate-nimekirja-vahelejätmine.inc\n.. include:: kt-volitused.inc\n.. include:: kt-krypto.inc\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kontrollrakendus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _kontroll:\n\nKontrollrakenduse seadistamine\n==============================\n\nKontrollrakenduste seadistus on JSON formaadis.\n\nReaalne sisu võib kahel seadistusel olla samasugune ka siis kui\nvõimalike erinevuste tekkimise jaoks kasutatakse ennetavalt erinevaid\nURLe. Varasemalt on erinevate URLide kasutamist õigustanud nt. iOS\nrakenduste pikem tarnetsükkel, mis nõuab testseadistuste\navalikustamist.\n\nSeadistus koosneb viiest peamisest rühmast\n\n* :token:`versions` - Rakenduse nõutud versioon\n* :token:`texts` - Kasutajaliideses kasutatavad tekstid\n* :token:`errors` - Kasutajaliideses kasutatavad veateated\n* :token:`colors` - Kasutajaliidese värvide koodid\n* :token:`params` - Rakenduse tööks vajalikud parameetrid\n* :token:`elections` - Igale küsimuse identifikaatorile vastav tekst\n  kasutajaliideses\n\nKõiki seadistatavaid väärtusi näeb näidisseadistusest. Kõik väärtused\non kohustuslikud.\n\nVersioonide seadistamine\n------------------------\n\nKontrollrakenduse versioon peab olema suurem või võrdne seadistuses määratud\nversiooniga. Versioonid kuuluvad rühma :token:`versions`:\n\n* :token:`android_version_code` - Android-rakenduse minimaalne versioonikood.\n  Väärtus peab olema positiivne JSON täisarv.\n* :token:`ios_bundle_version` - iOS-rakenduse minimaalne versioonisõne. Väärtus\n  peab olema JSON sõne, mis koosneb punktidega eraldatud positiivsetest\n  täisarvudest.\n\nParameetrite seadistamine\n-------------------------\n\nRakenduse tööks vajalikud parameetrid kuuluvad rühma :token:`params`:\n\n* :token:`verification_url` - Nimekiri kogumisteenuse hostinimedest\n  või IP-aadressidest koos pordiga. Järjekord pole oluline. Väärtus\n  peab olema JSON loend ka ühe URLi puhul.\n* :token:`verification_tls` - Nimekiri kogumisteenuse TLS\n  sertifikaatidest PEM vormingus. Järjekord pole oluline. Väärtus peab\n  olema JSON loend ka ühe sertifikaadi puhul.\n* :token:`help_url` - Abiinfo vaate URL\n* :token:`close_timeout` - Ajaaken, mil on kasutajal võimalik oma\n  valikut näha enne rakenduse sulgumist. Millisekundites.\n* :token:`close_interval` - Intervall, millega uuendatakse\n  :token:`close_timeout` väärtust kasutajaliideses. Millisekundites.\n* :token:`con_timeout_1` - Kogumisteenusega ühenduse saamise esimese\n  katse ajapiirang. Millisekundites.\n* :token:`con_timeout_2` - Kui esimese ringiga ei saadud ühendust\n  ühegi kogumisteenuse instantsiga, proovitakse uuesti selle\n  ajapiiranguga. Millisekundites.\n* :token:`public_key` - Valimiste avalik võti, millega krüpteeritakse\n  valijate hääli. PEM vormingus.\n* :token:`tspreg_service_cert` - Ajatembeldusteenuse sertifikaat PEM\n  vormingus.\n* :token:`ocsp_service_cert` - OCSP-teenuse sertifikaadid PEM\n  vormingus. Järjekord pole oluline. Väärtus peab olema JSON loend ka\n  ühe väärtuse puhul. Kui väli on tühi, siis tuvastatakse OCSP\n  responderi sertifikaat automaatselt.\n* :token:`tspreg_client_cert` - Kogumisteenuse sertifikaat\n  registreerimispäringute tegemiseks PEM vormingus.\n\nTekstide seadistamine\n---------------------\n\nKasutajaliideses kasutatavad tekstid kuuluvad rühma :token:`texts`. Järgmised\ntekstid on parametriseeritavad:\n\n* :token:`lbl_close_timeout` - Kontrollrakenduse sulgemisteade koos\n  loenduriga. Tekst peab sisaldama märgendit XX, mis asendatakse automaatselt\n  rakenduse sulgemiseni jäänud ajaga sekundites.\n\nNäide\n-----\n\n.. literalinclude:: config-examples/android-ios-config.json\n   :language: json\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-krypto.inc",
    "content": "..  IVXV seadistuste koostamise juhend\n\n.. _kt-krypto:\n\nKogumisteenuse krüptovõtmed\n---------------------------\n\nKogumisteenuse andmevahetuse turvamiseks on tarvis luua komplekt\nkrüptograafilisi võtmeid. Komplekti koosseis sõltub kogumisteenuse tehnilistest\nseadistustest.\n\n#. Teenuse krüptovõti ja TLS-sertifikaat - kasutatakse teenuste omavahelise\n   suhtluse turvamiseks kõigi teenuste puhul peale vahendusteenuse;\n\n#. Hääletamisteenuse ajatemplipäringute signeerimisvõti - kasutatakse\n   ajatemplipäringute signeerimiseks juhul, kui ajatempliteenus on\n   registreerimisteenuseks;\n\n#. Mobiil-ID/Smart-ID/Web eID tugiteenuse jagatud krüptimissaladus – kasutatakse sümmeetrilise\n   AES-256 krüptimise jaoks. Krüptimissaladusega krüptib Mobiil-ID tugiteenus\n   hääletajale väljastatava identsustõendi, mille abil hääletaja enda\n   identiteeti teistele teenustele tõendab.\n\n\n\nTeenuse krüptovõtme ja TLS-sertifikaadi genereerimine\n*****************************************************\n\nTeenuse krüptovõti ja TLS-sertifikaat genereeritakse kõigile teenustele peale\nvahendusteenuse. Kõikide teenuste sertifikaadid peavad olema välja antud sama\nsertifitseerimiskeskuse (CA – *Certificate Authority*) poolt.\n\n\nCA sertifikaadi genereerimine\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nSertifitseerimiskeskuse krüptovõtme ja sertifikaadi genereerimine toimub\njärgneva käsuga:\n\n.. code-block:: shell-session\n\n   $ openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -x509 -nodes\n     -days 365 -out ca.pem -keyout ca.key -utf8\n     -subj \"/C=EE/O=Example/OU=IVXV Test Certificates/CN=Service CA\"\n\nKäsu väljundiks on failid :file:`ca.key` (võti) ja :file:`ca.pem` (sertifikaat).\n\n\nTeenuse isendi krüptovõtme ja TLS-sertifikaadi genereerimine\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTeenuse isendi krüptovõtme ja sertifikaadipäringu genereerimine toimub järgneva\nkäsuga:\n\n.. code-block:: shell-session\n\n   $ openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes\n     -out <teenuse-id>-tls.csr -keyout <teenuse-id>-tls.key -utf8\n     -subj \"/C=EE/O=Example/OU=IVXV Test Certificates/CN=<teenuse-id>\"\n\nKäsu väljundiks on failid :file:`<teenuse-id>-tls.key` (võti) ja\n:file:`<teenuse-id>-tls.csr` (sertifikaadipäring).\n\n.. attention::\n\n   Talletusteenuse puhul peab sertifikaadipäringus olema CN väärtuseks teenuse\n   identifikaatori asemel hostinimi: erinevalt teistest teenustest ei kasutata\n   talletusteenuse puhul alternatiivset TLS nime.\n\nTeenuse isendi TLS-sertifikaadi genereerimine toimub järgneva käsuga:\n\n.. code-block:: shell-session\n\n   $ openssl x509 -req -days 365 -CA ca.pem -CAkey ca.key -set_serial 1\n     -extfile service-cert-openssl.cnf -extensions ext_<teenuse-tüüp>\n     -in <teenuse-id>-tls.csr -out <teenuse-id>-tls.pem\n\nKäsu väljundiks on fail :file:`<teenuse-id>-tls.pem`.\n\nSertifikaadi genereerimiseks peab failisüsteemis olema seadistusfail\n:file:`service-cert-openssl.cnf`.\n\n.. literalinclude:: ../../../tests/testdata/testca/service/service-cert-openssl.cnf\n   :caption: Fail ``service-cert-openssl.cnf``\n   :language: ini\n\n\nHääletamisteenuse ajatemplipäringute signeerimisvõtme ja sertifikaadi genereerimine\n***********************************************************************************\n\nHääletamisteenuse registreerimispäringute tegemise võti genereeritakse\njärgneva käsuga:\n\n.. code-block:: shell-session\n\n   $ openssl genrsa -out tspreg.key 2048\n\nKäsu väljundiks on fail :file:`tspreg.key`.\n\nHääletamisteenuse registreerimispäringute tegemise võtme sertifikaat\ngenereeritakse järgneva käsuga:\n\n.. code-block:: shell-session\n\n   $ openssl req -x509 -nodes -days 365 -out tspreg.pem -key tspreg.key -utf8\n     -subj \"/C=EE/O=Example/OU=IVXV Test Certificates/CN=Collector Registration\"\n\nKäsu väljundiks on fail :file:`tspreg.pem`.\n\n.. note::\n\n   Hääletamisteenuse ajatemplipäringute signeerimisvõti on vaja genereerida\n   vaid juhul, kui ajatempliteenust kasutatakse registreerimisteenuseks.\n\n\nMobiil-ID/Smart-ID/Web eID tugiteenusele jagatud krüptimissaladuse genereerimine\n********************************************************************************\n\nJagatud krüptimissaladus genereeritakse järgneva käsuga:\n\n.. code-block:: shell-session\n\n   $ openssl rand -out mobid-shared-secret.key 32\n\nKäsu väljundiks on 32 baidi suurune fail :file:`mobid-shared-secret.key`, mida\nMobiil-ID, Smart-ID ja Web eID teenus hakkab kasutama sümmeetrilise AES-256 krüptimise\njaoks.\n\n.. note::\n\n   Mobiil-ID/Smart-ID/Web eID tugiteenuse jagatud krüptimissaladus on vaja genereerida vaid\n   juhul, kui Mobiil-ID, Smart-ID või Web eID tugiteenus on kasutusel.\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-tehniline.inc",
    "content": "..  IVXV seadistuste koostamise juhend\n\n.. _kt-technical:\n\nKogumisteenuse tehnilise seadistuse koostamine\n----------------------------------------------\n\nTehniline seadistus määrab kogumisteenuse tehnilised parameetrid. Sama tehnilist\nseadistust peaks olema võimalik kasutada erinevate valimiste seadistustega [#]_.\n\n.. [#] Aga mitte samaaegselt: kogumisteenus toetab ainult ühte valimist.\n\nTehnilise seadistuse koostab kogumisteenuse osutaja.\n\nSeadistusfaili nimi peab alati lõppema stringiga :file:`technical.yaml`.\nFailinime võimalik eesliide peab alati lõppema punktiga.\n\n:debug:\n        Tõeväärtus, kas logidesse kirjutatakse silumisteateid.\n\n:snidomain:\n        Kohustuslik väli.\n        Domeen, mida kasutatakse teenuste SNI väärtuse seadistamiseks.\n        Varasemalt on kasutusel olnud väärtus `ivxv.invalid` võib kasutada ka\n        reaalseid domeene, mille nimehaldus on korraldaja kontrolli all nagu nt.\n        `ivxv.ee`\n\n----\n\n:filter:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab ühenduste filtrite seadistusi.\n\n:filter.tls:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab ühenduste TLS-filtri seadistusi.\n\n:filter.tls.handshaketimeout:\n        Kohustuslik väli.\n        Maksimaalne aeg sekundites TLS-kätluse läbiviimiseks.\n\n:filter.tls.ciphersuites:\n        Kohustuslik väli.\n        TLS-protokolli versioonis 1.2 rakendamiseks lubatud šifrikomplektid.\n        Hetkel toetatud valikud on::\n\n            TLS_RSA_WITH_AES_128_CBC_SHA\n            TLS_RSA_WITH_AES_256_CBC_SHA\n            TLS_RSA_WITH_AES_128_GCM_SHA256\n            TLS_RSA_WITH_AES_256_GCM_SHA384\n            TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\n            TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\n            TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\n            TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n            TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\n            TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n            TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n            TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n\n        Kui TLS-kätluse käigus lepitakse kokku TLS-protokolli versioonis 1.3,\n        siis šifrikomplekti seadistada ei saa ning kõik serveri poolt toetatud\n        turvalised šifrid on lubatud. Hetkel on nendeks::\n\n            TLS_AES_128_GCM_SHA256\n            TLS_AES_256_GCM_SHA384\n            TLS_CHACHA20_POLY1305_SHA256\n\n:filter.codec:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab ühenduste koodekfiltri seadistusi.\n\n:filter.codec.rwtimeout:\n        Kohustuslik väli.\n        Maksimaalne aeg sekundites valijalt tervikliku päringu lugemiseks.\n        Maksimaalne aeg sekundites valijale tervikliku päringu kirjutamiseks.\n\n:filter.codec.requestsize:\n        Päringu maksimaalne suurus baitides. Välja puudumise või väärtuse 0\n        korral päringu suurust ei piirata.\n\n:filter.codec.logrequests:\n        Tõeväärtus, kas logidesse kirjutatakse kõik sissetulnud päringud\n        algkujul. Päringu logi talletatakse tavalogist eraldi.\n\n----\n\n:network:\n        Kohustuslik väli.\n        Loetelu kogumisteenuse võrgusegmentidest.\n\n        Kõik kogumisteenuse võrgusegmentide parameetrid määrab kogumisteenuse osutaja.\n\n:network.*.id:\n        Kohustuslik väli.\n        Võrgusegmendi identifikaator.\n\n:network.*.services:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab kogumisteenuse selle võrgusegmendi\n        mikroteenuste seadistust.\n\n        .. todo:: Kuigi kõik selle bloki parameetrid määrab Koguja, kas\n                Korraldaja peaks vähemalt vahendusteenuste väliseid aadresse\n                kinnitama, kuna need on otspunktid, kuhu valija- ja\n                kontrollrakendus peavad ühendama hakkama?\n\n:network.*.services.proxy:\n        Loetelu, mis sisaldab vahendusteenuste isendite seadistust.\n\n:network.*.services.mid:\n        Loetelu, mis sisaldab Mobiil-ID toeteenuste isendite seadistust.\n\n:network.*.services.smartid:\n        Loetelu, mis sisaldab Smart-ID toeteenuste isendite seadistust.\n\n:network.*.services.webeid:\n        Loetelu, mis sisaldab Web eID toeteenuste isendite seadistust.\n\n:network.*.services.sessionstatus:\n        Loetelu, mis sisaldab Session status toeteenuste isendite seadistust.\n\n:network.*.services.choices:\n        Loetelu, mis sisaldab nimekirjateenuste isendite seadistust.\n\n:network.*.services.voting:\n        Loetelu, mis sisaldab hääletamisteenuste isendite seadistust.\n\n:network.*.services.verification:\n        Loetelu, mis sisaldab kontrollteenuste isendite seadistust.\n\n:network.*.services.storage:\n        Loetelu, mis sisaldab talletusteenuste isendite seadistust.\n\n:network.*.services.log:\n        Loetelu, mis sisaldab logikogumisteenuste isendite seadistust.\n\n:network.*.services.backup:\n        Varundusteenuse isendi seadistus.\n\n:network.*.services.*.id:\n        Kohustuslik väli.\n        Mikroteenuse isendi identifikaator.\n\n:network.*.services.*.address:\n        Kohustuslik väli.\n        Mikroteenuse isendi võrguaadress ja -port.\n\n        .. todo:: Sama parameetrit kasutatakse kirjeldamaks seda, millisel\n                aadressil teenus peab kuulama hakkama ning milliselt aadressilt\n                saavad haldusteenus ja vahendusteenus teenusele ligi. Kui\n                sisemine ja välimine aadress hakkavad erinema, siis tuleb see\n                parameeter lüüa kaheks.\n\n:network.*.services.*.peeraddress:\n        Mikroteenuse isenditevahelise suhtluse võrguaadress ja -port.\n        Kasutatakse ainult juhul, kui sama teenust pakkuvad isendid peavad\n        omavahel suhtlema (nt talletusteenus).\n\n        .. todo:: Sama parameetrit kasutatakse kirjeldamaks seda, millisel\n                aadressil teenus peab kuulama hakkama ning milliselt aadressilt\n                saavad teised isendid teenusele ligi. Kui sisemine ja välimine\n                aadress hakkavad erinema, siis tuleb see parameeter lüüa\n                kaheks.\n\n:network.*.services.*.origin:\n        Kohustuslik väli.\n        Mikroteenuse isendi täielik domeeninimi ja -port.\n\n----\n\n:status:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab loetelu staatust raporteeriva serveritest.\n\n:status.name:\n        Kohustuslik väli. Hetkel toetatud ainult ``session``.\n\n        Staatust raporteeriva serveri nimi.\n\n:status.servername:\n        Kohustuslik väli.\n        Staatust raporteeriva serveri SNI.\n\n:status.authttl:\n        Kohustuslik väli.\n        Aeg sekundites. Vaikeväärtus 0.\n\n        Maksimaalne aeg autentimiseks.\n\n:status.choicettl:\n        Kohustuslik väli.\n        Aeg sekundites. Vaikeväärtus 0.\n\n        Maksimaalne aeg valiku tegemiseks.\n\n:status.votettl:\n        Kohustuslik väli.\n        Aeg sekundites. Vaikeväärtus 0.\n\n        Maksimaalne aeg hääle andmiseks.\n\n:status.verifyttl:\n        Kohustuslik väli.\n        Aeg sekundites. Vaikeväärtus 0.\n\n        Maksimaalne aeg hääle verifitseerimiseks.\n\n----\n\n:logging:\n        Alamblokk, mis sisaldab loetelu mikroteenuste kauglogimise serveritest.\n        Siin blokis kirjeldatakse:\n\n        * Logiseire teenus. Alati loetelus esimene. Teenusele saadetakse IVXV\n          logi alates tasemest INFO;\n\n        * Vajadusel ka täiendavad välised logikogujad, kuhu saadetakse täielik\n          logi alates tasemest DEBUG.\n\n        Logimine toimub üle RELP protokolli.\n\n        Kõik logiserverite parameetrid määrab kogumisteenuse osutaja.\n\n:logging.address:\n        Kohustuslik väli.\n        Logiserveri aadress (IP-aadress või hostinimi).\n\n:logging.port:\n        Logiserveri port (täisarv, vaikimisi *20514*).\n\n----\n\n:storage:\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab talletusprotokolli seadistust.\n\n        Kõik talletusprotokolli parameetrid määrab kogumisteenuse osutaja.\n\n:storage.protocol:\n        Kohustuslik väli.\n        Kogumisteenuse talletusprotokoll. Hetkel toetatud ainult ``etcd``.\n\n:storage.conf:\n        Kohustuslik väli.\n        Talletusprotokolli seadistus. Sisu sõltub ``storage.protocol`` parameetri\n        väärtusest.\n\n:storage.conf.ca:\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Talletusteenuste TLS sertifikaatide väljastaja sertifikaat.\n\n:storage.conf.conntimeout:\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Maksimaalne aeg sekundites etcd serveriga ühenduse loomiseks.\n\n:storage.conf.optimeout:\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Maksimaalne aeg sekundites ühe talletusoperatsiooni teostamiseks.\n\n:storage.conf.size:\n        Valikuline väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Andmebaasi suurus baitides.\n\n:storage.conf.electiontimeout:\n        Valikuline väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Aeg millisekundites, mida kasutatakse liidri kättesaadavuse\n        kontrollimiseks.\n\n:storage.conf.heartbeattimeout:\n        Valikuline väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Aeg millisekundites, mida kasutatakse andmebaasi hosti\n        kättesaadavuse kontrollimiseks.\n\n:storage.conf.snapshotcount:\n        Valikuline väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Andmebaasi talletamispäringute arv, mille täitumise korral\n        andmebaasi seis varundatakse.\n\n:storage.conf.bootstrap:\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``storage.protocol`` on ``etcd``.\n\n        Loetelu nende talletusteenuste identifikaatoritest, mis on osa algsest\n        etcd klastrist. Vajalik, et talletusteenuse isend teaks esmasel\n        käivitumisel, kas see loob uut klastrit või liitub olemasolevaga.\n\n        Esmases tehnilises seadistuses peab loetelu kattuma ``network`` blokis\n        loetletud talletusteenuste identifikaatoritega. Hiljem teenuseid\n        lisades või eemaldades ei tohi ``storage.conf.bootstrap`` väärtust\n        uuendada.\n\n:storage.ordertimeout:\n         Aeg sekundites. Vaikeväärtus 5.\n\n         Maksimaalne aeg hääle kirjutamiseks häälte järjekorra tabelisse.\n\n----\n\n:backup:\n        Varunduse parameetrid.\n\n        Loetelu varundamise kellaaegadest vormingus HH:MM.\n\n:file:`example.technical.yaml`:\n\n.. literalinclude:: config-examples/example.technical.yaml\n   :language: yaml\n   :linenos:\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-usaldusjuur.inc",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _kt-trust:\n\nKogumisteenuse usaldusjuure seadistamine\n----------------------------------------\n\nKogumisteenuse usaldusjuur sisaldab andmeid seadistuste (kaasa arvatud\nusaldusjuure enda) allkirjade kontrollimiseks ja nimekirja süsteemi esmastest\nvolitustest.\n\nUsaldusjuure seadistuse koostab valimiste korraldaja.\nSeadistusfaili nimi peab alati lõppema stringiga :file:`trust.yaml`.\nFailinime võimalik eesliide peab alati lõppema punktiga.\n\n.. attention::\n\n   Usaldusjuure seadistuste laadimine lähtestab kogumisteenuse. Seetõttu pole\n   juba seadistatud kogumisteenuse usaldusahela muutmine võimalik. Volituste\n   muutmine on võimalik vastavate korralduste abil.\n\n:container:\n\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab seadistusfailide allkirjade kontrollimise\n        seadistust.\n\n:container.bdoc:\n\n        Alamblokk, mis sisaldab seadistusfailide BDOC-allkirjade kontrollimise\n        seadistust.\n\n:container.bdoc.filecount:\n        Kohustuslik väli. Maksimaalne lubatud failide arv ZIP konteineris, ning\n        kuna tegu on BDOC konteineriga, siis sellele väärtusele tuleb alati liita 3,\n        sest BDOC konteineris on lisaks sisufailidele veel ``signatures0.xml``,\n        ``manifest.xml`` ja ``mimetype`` failid.\n\n:container.bdoc.bdocsize:\n        Kohustuslik väli.\n        BDOC konteineri maksimaalne lubatud suurus baitides.\n\n:container.bdoc.filesize:\n        Kohustuslik väli.\n        BDOC konteineris olevate failide maksimaalne lubatud hõrendatud suurus\n        baitides.\n\n:container.bdoc.roots:\n\n        Kohustuslik väli.\n        Seadistuste allkirjastajate sertifikaatide usaldusjuured.\n\n:container.bdoc.intermediates:\n\n        Seadistuste allkirjastajate sertifikaatide vahesertifikaadid.\n        Usalduse saavutamiseks peab nende sertifikaatide abil olema võimalik\n        luua ahel allkirjastaja sertifikaadist usaldusjuureni.\n\n:container.bdoc.profile:\n\n        Kohustuslik väli.\n        Seadistuste allkirjadelt nõutav BDOC profiil. Toetatud valikud on\n        ``BES`` (põhiprofiil kirjeldatud BDOC spetsifikatsiooni jaotises 5),\n        ``TS`` (ajatemplitega profiil kirjeldatud BDOC\n        spetsifikatsiooni jaotises 6.2).\n\n:container.bdoc.ocsp.responders:\n\n        Kasutatakse ainult juhul kui ``container.bdoc.profile`` on ``TS``.\n\n        Kehtivuskinnitusi väljastanud OCSP responderi sertifikaadid. Kui nende\n        hulgast responderi sertifikaati ei leita, siis otsitakse OCSP vastuses\n        olevate sertifikaatide hulgast selline, mis on antud välja sama\n        väljastaja poolt, mis seadistuste allkirjastaja sertifikaat, ning on\n        lubatud OCSP vastuste signeerimiseks. AIA loogika kasutamise\n        korral võib väli olla tühi.\n\n:container.bdoc.tsp.signers:\n\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``container.bdoc.profile`` on ``TS``.\n\n        Ajatempliteenuse vastuse allkirjastamise sertifikaadid.\n\n:container.bdoc.tsp.delaytime:\n\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``container.bdoc.profile`` on ``TS``.\n\n        Maksimaalne ajanihe ajatempli loomise ja allkirjastamise vahel\n        sekundites.\n\n:container.bdoc.tsdelaytime:\n\n        Kasutatakse ainult juhul kui ``container.bdoc.profile`` on ``TS``.\n\n        Maksimaalne ajanihe ajatempli ja kehtivuskinnituse loomise vahel\n        sekundites. Välja puudumise või väärtuse 0 korral peavad mõlemad olema\n        loodud samal sekundil.\n\n:authorizations:\n\n        Kohustuslik väli.\n        Esmane nimekiri kogumisteenuse halduri volitustega isikutest (vt.\n        :ref:`rollid`), mis rakendatakse süsteemile usaldusjuure laadimisel.\n        Iga isiku kohta on kirje tema ID-kaardi välja ``Common Name`` (CN)\n        väärtusega. Minimaalselt peab sisaldama usaldusjuure signeerinud isiku\n        andmeid.\n\n\nNäide\n*****\n\n:file:`example.trust.yaml`:\n\n.. literalinclude:: config-examples/example.trust.yaml\n   :language: yaml\n   :linenos:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-valijate-nimekirja-vahelejätmine.inc",
    "content": "..  IVXV seadistuste koostamise juhend\n\n.. _kt-voter-list-skip:\n\nValijate nimekirja vahelejätmine\n--------------------------------\n\nValijate nimekirja vahelejätmine tähendab haldusteenuses registreerinud\nvigase valijate nimekirja asendamist tühja nimekirjaga.\n\nProtseduur on vajalik olukorras, kus haldusteenus on Valimiste Infosüsteemist\nalla laadinud ja haldusteenuses registreerinud valijate muudatusnimekirja, mida\npole võimalik kogumisteenusele rakendada (nimekiri on vigane või pole kooskõlas\nteiste kogumisteenuste seadistustega).\n\nValijate nimekirja vahelejätmine korralduse koostab valimiste korraldaja.\n\nSeadistusfaili nimi peab alati lõppema stringiga :file:`.skip.yaml`.\n\n:election:\n\n        Kohustuslik väli.\n        Valimise unikaalne identifikaator.\n\n:skip_voter_list:\n\n        Kohustuslik väli.\n        Vahele jäetava valijate muudatusnimekirja versioon.\n\n:changeset:\n\n        Kohustuslik väli.\n        Vahele jäetava valijate muudatusnimekirja järjekorranumber.\n\nNäide\n*****\n\n:file:`example.voters.skip.yaml`:\n\n.. literalinclude:: config-examples/example.voters.skip.yaml\n   :language: yaml\n   :linenos:\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-valimised.inc",
    "content": "..  IVXV seadistuste koostamise juhend\n\n.. _kt-election:\n\nKogumisteenusele valimise seadistuse koostamine\n-----------------------------------------------\n\nValimise seadistus määrab ühe valimise seadistuse.\n\nValimise seadistuse koostab valimiste korraldaja.\nSeadistusfaili nimi peab alati lõppema stringiga :file:`election.yaml`.\nFailinime võimalik eesliide peab alati lõppema punktiga.\n\n:identifier:\n\n        Kohustuslik väli.\n        Valimise unikaalne identifikaator.\n\n:questions:\n\n        Loetelu, mis sisaldab ühe või enama valimise küsimuse unikaalset\n        identifikaatorit. Unikaalsus peab olema tagatud ainult konkreetse\n        valimise küsimuste hulgas. Kohustuslik väli.\n\n----\n\n:period:\n\n        Kohustuslik väli.\n        E-hääletuse perioodi andmete alamblokk.\n\n:period.servicestart:\n\n        Kohustuslik väli.\n        Kogumisteenuses häälte vastuvõtmise algusaeg. Sellest hetkest alates\n        hakkab kogumisteenus ühendusi teenindama. See aeg peab eelnema valimise\n        algusajale ning on mõeldud enne valimise algust proovihääle andmiseks.\n\n        Enne ``electionstart`` parameetriga määratud aega vastu võetud häälte\n        puhul tagastatakse valijarakendusele hääle esitamise lõpus vastav\n        veateade (hääl jõudis kohale enne valimise algust). Sellised hääled\n        tühistatakse automaatselt töötlemise käigus.\n\n:period.electionstart:\n\n        Kohustuslik väli.\n        E-hääletuse algusaeg. Sellest hetkest alates antud hääled lähevad\n        häälte lugemisel arvesse.\n\n:period.electionstop:\n\n        Kohustuslik väli.\n        E-hääletuse lõpuaeg. Sellest hetkest lõpetatakse valikute nimekirjade\n        väljastamine.\n\n:period.servicestop:\n\n        Kohustuslik väli.\n        E-hääletuse lõppemisaeg. Sellest hetkest lõpetatakse häälte\n        vastuvõtmine ning teenused lõpetavad töö.\n\n----\n\n:voting:\n\n        Hääle esitamise parameetrite alamblokk.\n\n:voting.ratelimitstart:\n\n        Valija poolt esitatud häälte kogus, mille järel rakendub talle\n        hääletamissageduse piirang. Arvesse lähevad ka vigased hääled, kuna\n        muidu saaks nendega süsteemi koormata piirangust hoolimata. Välja\n        puudumise või väärtuse 0 korral rakendub piirang alates esimesest\n        häälest.\n\n:voting.ratelimitminutes:\n\n        Aeg minutites, mis peab jääma kahe hääle esitamise vahele, kui valijale\n        on rakendatud hääletamissageduse piirang. Välja puudumise või väärtuse\n        0 korral on hääletamissageduse piirangud välja lülitatud.\n\n----\n\n:verification:\n\n        Kohustuslik väli.\n        Hääle kontrollimise parameetrite alamblokk.\n\n:verification.count:\n\n        Kohustuslik väli.\n        Suurim ühe hääle lubatud kontrollimiste arv.\n\n:verification.minutes:\n\n        Kohustuslik väli.\n        Hääle kontrollimise perioodi kestus minutites. Pärast hääle\n        andmist on selle perioodi vältel võimalik häält kontrollida.\n\n:verification.latestonly:\n\n        Tõeväärtus, kas kontrollida saab ainult valija viimati antud häält. Kui\n        väärtus on väär või puudu, siis saab kontrollida kõiki valija hääli\n        (teiste piirangute raames).\n\n----\n\n:voterforeignehak:\n\n        Alaliselt välisriigis elavate valijate ringkonnakuuluvuse tuvastamiseks\n        kasutatav EHAK-kood.\n\n        Kui parameeter on määratud, siis alaliselt välisriigis elavad valijad\n        kuuluvad ringkondadesse, kuhu kuulub ka parameetris määratud\n        EHAK-koodile vastav haldusüksus. Täiendavalt peab ringkondade nimekiri\n        sisaldama sellise EHAK-koodiga haldusüksust.\n\n        Kui parameeter on määramata, siis kasutatakse EHAK-koodi \"0000\". Kui\n        parameeter on määramata ning ringkondade nimekiri ei sisalda\n        vaikekoodile vastavat haldusüksust, siis valijate nimekiri ei tohi\n        sisaldada alaliselt välisriigis elavaid valijaid.\n\n:ignorevoterlist:\n\n        Ringkonna identifikaator, mille valikud esitada kõigile valijatele.\n        Kui see väärtus ei ole tühi, siis kogumisteenus ei kasuta valijate\n        nimekirja ning esitab kõigile valijatele väärtusega määratud ringkonna\n        valikud ja lubab hääletada kõigil, kellel õnnestub isikutuvastus ning\n        hääle allkirja kontrollimine.\n\n----\n\n:voterlist:\n\n        Kohustuslik väli.\n        Valijate nimekirjade kontrollimise parameetrid.\n\n:voterlist.key:\n\n        Kohustuslik väli.\n        ECDSA-võtmepaari avalik võti valijate nimekirjade allkirja\n        kontrollimiseks.\n\n:vis:\n        Alamblokk, mis sisaldab Valimiste Infosüsteemi seadistust.\n\n:vis.url:\n\n        Kohustuslik väli.\n        Valimiste Infosüsteemi URL.\n\n:vis.ca:\n\n        Valimiste Infosüsteemi TLS-sertifikaadi usaldusahel.\n\n:vis.min:\n\n        Valimiste Infosüsteemi valijate nimekirja hankimise sagedus\n        minutites (vaikeväärtus on 15).\n\n:xroad:\n        Alamblokk, mis sisaldab X-teega ühenduva teenuse seadistust.\n\n:xroad.ca:\n\n        X-teega teenuse TLS-sertifikaadi usaldusahel.\n\n----\n\n:ballot:\n        Alamblokk, mis sisaldab sedeli verifitseerimise seadistust.\n\n:ballot.encpkeygroup:\n\n\tKohustuslik väli.\n        Abstraktne rühm, millele kuulub Võtmerakenduses genereeritud avalik võti\n        häälte krüpteerimiseks.\n\n        Võimalikud väärtused ``RFC3526ModPGroup2048`` (võtme pikkus on 2048 bitti ehk\n        256 baiti), ``RFC3526ModPGroup3072``, ``RFC3526ModPGroup4096``,\n        ``RFC3526ModPGroup8192``, ``NIST-P256``, ``NIST-P384``, ``NIST-P521`` ja\n        ``Edwards25519``. NB! Palun veendu, et valitud rühm loetelust oli ka\n        kasutusel avaliku võtme genereerimisel Võtmerakenduses (vt Võtmerakenduse\n        konfiguratsiooni faili).\n\n\tAntud rühma kasutatakse krüpteeritud häälte korrektsuse verifitseerimiseks.\n\n        Antud väli on ignoreeritud kui `encpkey` on määratud.\n\n:ballot.encpkey:\n\n        Avalik võti häälte krüpteerimiseks valijarakenduses.\n\n        Võtme rühma kasutatakse krüpteeritud häälte korrektsuse verifitseerimiseks.\n\n----\n\n:auth:\n\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab valija tuvastamise seadistust.\n\n:auth.ticket:\n\n        Alamblokk, mis sisaldab piletipõhise valija tuvastamise seadistust.\n\n        Piletipõhist valija tuvastamist kasutatakse Mobiil-ID/Smart-ID/Web eID puhul,\n        kus ``mid``/``smartid``/``webeid`` teenus tuvastab valija ning väljastab talle\n        pileti, millega teistele teenustele ennast tuvastada.\n\n        See alamblokk on tühi, aga tema olemasolu või puudumine määrab, kas\n        piletipõhine valija tuvastus on lubatud või ei.\n\n:auth.tls:\n\n        Alamblokk, mis sisaldab TLS-põhise valija tuvastamise seadistust.\n\n        TLS-põhist valijatuvastust kasutatakse ID-kaardi puhul.\n\n:auth.tls.roots:\n\n        Kohustuslik väli.\n        Valija TLS-klientsertifikaatide usaldusjuured.\n\n:auth.tls.intermediates:\n\n        Valija TLS-klientsertifikaatide vahesertifikaadid. TLS-autentimiseks\n        peab nende sertifikaatide abil olema võimalik luua ahel valija\n        klientsertifikaadist usaldusjuureni.\n\n:auth.tls.ocsp:\n\n        Alamblokk, mis sisaldab valija TLS-klientsertifikaatide oleku\n        kontrollimise seadistust. Selle bloki puudumisel valija sertifikaatide\n        kehtivust ei kontrollita välisest kehtivuskinnitusteenusest.\n\n:auth.tls.ocsp.url:\n\n        Valija TLS-klientsertifikaatide kehtivuskinnitusteenuse aadress.\n        Kui pole seadistatud, siis kasutatakse kehtivuskinnitusteenuse\n        aadressi otse TLS-klientsertifikaadist.\n\n:auth.tls.ocsp.responders:\n\n        Valija TLS-klientsertifikaatide kehtivuskinnitusteenuse responderi\n        sertifikaadid. Kui nende hulgast responderi sertifikaati ei leita, siis\n        otsitakse vastuses olevate sertifikaatide hulgast selline, mis on antud\n        välja sama väljastaja poolt, mis kontrollitav sertifikaat, ning on\n        lubatud OCSP vastuste signeerimiseks.\n\n:auth.tls.ocsp.retry:\n\n        Valija TLS-klientsertifikaatide oleku kontrolli korduvkatsete arv.\n        Juhul kui sertifikaadi oleku kontroll ebaõnnestub võrgu- või serverivea\n        tõttu, saab seda automaatselt korrata. Välja väärtus määrab katsete\n        arvu, mis tehakse lisaks algsele päringule. Seega kui väärtus on 1,\n        siis tehakse kokku maksimaalselt kaks päringut. Välja puudumise või\n        väärtuse 0 korral automaatseid korduvkatseid ei sooritata.\n\n----\n\n:identity:\n\n        Tuvastatud valija X.500 eraldusnimest unikaalse identifikaatori\n        tuletamise meetod. Hetkel toetatud valikud ``commonname``,\n        ``serialnumber`` ja ``pnoee``.\n\n        Eesti elektrooniliste isikut tõendavate dokumentide korral on\n        ``commonname`` puhul identifikaator kujul \"PERENIMI,EESNIMI,ISIKUKOOD\"\n        ning teiste valikute teise puhul \"ISIKUKOOD\".\n\n        Kui ``serialnumber`` tagastab eraldusnimest ``serialNumber`` välja\n        muutmata kujul, siis ``pnoee`` eemaldab sellelt enne mittekohustusliku\n        \"PNOEE-\" eesliite. Viimane on vastavuses standardi ETSI EN 319 412-1\n        jaotisega 5.1.3 Eesti isikukoodide jaoks, kuid lubab ka standardile\n        mittevastavaid seerianumbreid.\n\n\n----\n\n:age:\n\n        Alamblokk, mis sisaldab valija vanuse kontrolli seadistust. Kui see\n        blokk puudub, siis valija vanust ei kontrollita.\n\n:age.method:\n\n        Kohustuslik väli.\n        Valija sünniaja tuvastamiseks kasutatav meetod. Hetkel toetatud ainult\n        ``estpic``, mis eeldab, et valija unikaalne identifikaator on Eesti\n        isikukood ning eraldab sealt sünniaja.\n\n:age.timezone:\n\n        Kohustuslik väli.\n        IANA ajavööndi nimi, milles valija vanus arvutatakse ehk millises\n        ajavööndis peab valija olema valimisealine.\n\n:age.limit:\n\n        Kohustuslik väli.\n        Valija peab olema vähemalt nii vana, et hääletada. Kui väärtus on 0,\n        siis valija vanust ei kontrollita.\n\n----\n\n:vote:\n\n        Kohustuslik väli.\n        Alamblokk, mis sisaldab häälte allkirjade kontrollimise seadistust.\n\n:vote.bdoc:\n\n        Alamblokk, mis sisaldab häälte BDOC-allkirjade kontrollimise seadistust.\n\n:vote.bdoc.filecount:\n\n        Kohustuslik väli. Maksimaalne lubatud failide arv ZIP konteineris, ning\n        kuna tegu on BDOC konteineriga, siis sellele väärtusele tuleb alati liita 3,\n        sest BDOC konteineris on lisaks sisufailidele veel ``signatures0.xml``,\n        ``manifest.xml`` ja ``mimetype`` failid. Antud parameetri väärtuseks sobib 4\n        (hääl + 3 lisafaili).\n\n:vote.bdoc.bdocsize:\n\n        Kohustuslik väli.\n        BDOC konteineri maksimaalne lubatud suurus baitides.\n\n:vote.bdoc.filesize:\n\n        Kohustuslik väli.\n        BDOC konteineris olevate failide maksimaalne lubatud hõrendatud suurus\n        baitides.\n\n:vote.bdoc.roots:\n\n        Kohustuslik väli.\n        Häälte allkirjastajate sertifikaatide usaldusjuured.\n\n:vote.bdoc.intermediates:\n\n        Häälte allkirjastajate sertifikaatide vahesertifikaadid.\n        Hääle arvesseminekuks peab nende sertifikaatide abil olema võimalik\n        luua ahel allkirjastaja sertifikaadist usaldusjuureni.\n\n:vote.bdoc.profile:\n\n        Kohustuslik väli.\n        Häälte allkirjadelt nõutav BDOC profiil. Toetatud valikud on ``BES``\n        (põhiprofiil kirjeldatud BDOC spetsifikatsiooni jaotises 5) ja\n        ``TS`` (ajatemplitega profiil kirjeldatud BDOC spetsifikatsiooni\n        jaotises 6.2).\n\n        See peaks olema ``BES``, kuna kõikide allkirjastamisvahendite puhul ei\n        ole sissetulev hääl kvalifitseeritud (nt Eesti ID-kaart). Kogumisteenus\n        kvalifitseerib häältel olevad allkirjad ise (vt ``qualification``).\n\n----\n\n:mid:\n\n        Alamblokk, mis sisaldab Mobiil-ID teenusepakkuja seadistust.\n\n:mid.url:\n\n        Kohustuslik väli.\n        Mobiil-ID teenusepakkuja asukoht.\n\n:mid.relyingpartyuuid:\n\n        Kohustuslik väli.\n        Mobiil-ID teenusepakkujaga kokkulepitud kliendi identifikaator.\n\n:mid.relyingpartyname:\n\n        Kohustuslik väli.\n        Mobiil-ID teenusepakkujaga kokkulepitud kliendi nimi.\n\n:mid.language:\n\n        Kohustuslik väli.\n        Mobiil-ID kasutajale kuvatavate sõnumite keel. Võimalikud väärtused\n        ``EST``, ``ENG``, ``RUS`` ja ``LIT``.\n\n:mid.authmessage:\n\n        Kohustuslik väli.\n        Sõnum, mida Mobiil-ID kasutajale kuvada autentimise käigus.\n\n:mid.signmessage:\n\n        Kohustuslik väli.\n        Sõnum, mida Mobiil-ID kasutajale kuvada allkirjastamise käigus.\n\n:mid.messageformat:\n\n        Autentimise ning allkirjastamise käigus kasutajale kuvatava sõnumi\n        vorming. Võimalikud väärtused ``GSM-7`` (Mobiil-ID poolt kasutatav\n        vaikeväärtus) ja ``UCS-2``.\n\n:mid.authchallengesize:\n\n        Autentimise käigus Mobiil-ID teenusele saadetava pretensiooni pikkus.\n        Võimalikud väärtused ``32`` (vaikeväärtus), ``48`` ja ``64``.\n\n:mid.statustimeoutms:\n\n        Parameeter, mis edastatakse autentimise ja allkirjastamise staatuse\n        päringu korral Mobiil-ID teenusele ning millega saab kontrollida, kui\n        kaua ootab Mobiil-ID teenus kasutaja poolse tegevuse lõpptulemust, enne\n        kui vastab, et tegevus on pooleli. Väärtuse puudumisel oodatakse\n        võimalikult vähe: täpne aeg sõltub Mobiil-ID teenusest.\n\n        Antud parameetri abil saab vähendada ühe autentimise või allkirjastamise\n        käigus Mobiil-ID teenusele saadetavate päringute arvu.\n\n:mid.roots:\n\n        Kohustuslik väli.\n        Mobiil-ID sertifikaatide usaldusjuured.\n\n:mid.intermediates:\n\n        Mobiil-ID sertifikaatide vahesertifikaadid. Mobiil-ID autentimiseks\n        peab nende sertifikaatide abil olema võimalik luua ahel Mobiil-ID\n        sertifikaadist usaldusjuureni.\n\n:mid.ocsp:\n\n        Alamblokk, mis sisaldab valija Mobiil-ID sertifikaatide oleku\n        kontrollimise seadistust.\n\n:mid.ocsp.url:\n\n        Valija Mobiil-ID sertifikaatide kehtivuskinnitusteenuse aadress.\n        Kui pole seadistatud, siis kasutatakse kehtivuskinnitusteenuse\n        aadressi otse Mobiil-ID klientsertifikaadist.\n\n:mid.ocsp.responders:\n\n        Mobiil-ID sertifikaatide OCSP responderi sertifikaadid. Kui nende\n        hulgast responderi sertifikaati ei leita, siis otsitakse vastuses\n        olevate sertifikaatide hulgast selline, mis on antud välja sama\n        väljastaja poolt, mis kontrollitav sertifikaat, ning on lubatud OCSP\n        vastuste signeerimiseks.\n\n----\n\n:smartid:\n\n        Alamblokk, mis sisaldab Smart-ID teenusepakkuja seadistust.\n\n:smartid.url:\n\n        Kohustuslik väli.\n        Smart-ID teenusepakkuja asukoht.\n\n:smartid.relyingpartyuuid:\n\n        Kohustuslik väli.\n        Smart-ID teenusepakkujaga kokkulepitud kliendi identifikaator.\n\n:smartid.relyingpartyname:\n\n        Kohustuslik väli.\n        Smart-ID teenusepakkujaga kokkulepitud kliendi nimi.\n\n:smartid.authinteractionsorder:\n\n        Kohustuslik väli.\n        Autentimise käigus Smart-ID kasutajale kuvatavate interaktsioonide järjekord.\n        Järjekorrast valitakse esimene interaktsioon, mida rakendus toetab.\n        Võimalikud  väärtused: ``displayTextAndPIN`` koos ``displayText60``,\n        ``verificationCodeChoice`` koos ``displayText60``,\n        ``confirmationMessage`` koos ``displayText200`` või\n        ``confirmationMessageAndVerificationCodeChoice`` koos ``displayText200``.\n\n:smartid.signinteractionsorder:\n\n        Kohustuslik väli.\n        Allkirjastamise käigus Smart-ID kasutajale kuvatavate interaktsioonide järjekord.\n        Järjekorrast valitakse esimene interaktsioon, mida rakendus toetab.\n        Võimalikud  väärtused: ``displayTextAndPIN`` koos ``displayText60``,\n        ``verificationCodeChoice`` koos ``displayText60``,\n        ``confirmationMessage`` koos ``displayText200`` või\n        ``confirmationMessageAndVerificationCodeChoice`` koos ``displayText200``.\n\n:smartid.authchallengesize:\n\n        Autentimise käigus Smart-ID teenusele saadetava pretensiooni pikkus.\n        Võimalikud väärtused ``32`` (vaikeväärtus), ``48`` ja ``64``.\n\n:smartid.statustimeoutms:\n\n        Autentimise ja allkirjastamise staatuse päringu korral Smart-ID\n        teenusele edastatav parameeter, millega saab kontrollida, kui kaua\n        Smart-ID teenus ootab kasutajapoolse tegevuse lõpptulemust, enne kui\n        vastab, et tegevus on pooleli.\n        Väärtuse puudumisel oodatakse võimalikult vähe: täpne aeg sõltub\n        Smart-ID teenusest.\n\n        Parameeter aitab vähendada autentimise või allkirjastamise käigus\n        Smart-ID teenusele saadetavate päringute arvu.\n\n:smartid.roots:\n\n        Kohustuslik väli.\n        Smart-ID sertifikaatide usaldusjuured.\n\n:smartid.intermediates:\n\n        Smart-ID sertifikaatide vahesertifikaadid. Smart-ID autentimiseks\n        peab nende sertifikaatide abil olema võimalik luua ahel Smart-ID\n        sertifikaadist usaldusjuureni.\n\n:smartid.ocsp:\n\n        Alamblokk, mis sisaldab valija Smart-ID sertifikaatide oleku\n        kontrollimise seadistust.\n\n:smartid.ocsp.url:\n\n        Valija Smart-ID sertifikaatide kehtivuskinnitusteenuse aadress.\n        Kui pole seadistatud, siis kasutatakse kehtivuskinnitusteenuse\n        aadressi otse Smart-ID klientsertifikaadist.\n\n:smartid.ocsp.responders:\n\n        Smart-ID sertifikaatide OCSP responderi sertifikaadid. Kui nende\n        hulgast responderi sertifikaati ei leita, siis otsitakse vastuses\n        olevate sertifikaatide hulgast selline, mis on\n        1) välja antud sama väljastaja poolt, mis kontrollitav sertifikaat;\n        ja\n        2) lubatud OCSP vastuste signeerimiseks.\n\n----\n\n:qualification:\n\n        Loetelu välistest kvalifitseerivatest päringutest, mis tehakse iga\n        hääle kohta, koos seadistustega.\n\n        Siin on kasutatud loetelu protokoll ja seadistus blokkidest selle\n        asemel, et anda igale protokollile oma blokk, kuna kvalifitseerivate\n        päringute järjekord on oluline ning seadistatav.\n\n:qualification.*.protocol:\n\n        Kohustuslik väli.\n        Kvalifitseeriva päringu protokoll. Hetkel toetatud ``ocsp``\n        (harilik OCSP), ``tsp`` (PKIX ajatempel) ja ``tspreg`` (PKIX ajatempel\n        registreerimistõendina).\n\n:qualification.*.conf:\n\n        Kohustuslik väli.\n        Kvalifitseeriva päringu protokolli seadistus. Sisu sõltub\n        ``qualification.*.protocol`` parameetri väärtusest.\n\n:qualification.*.conf.url:\n\n        Valikuline väli ainult ``ocsp`` puhul, sest kui pole seadistatud,\n        kasutatakse kehtivuskinnitusteenuse aadressi otse klientsertifikaadist.\n        Aadress, kuhu kvalifitseeriv päring tehakse.\n\n:qualification.*.conf.responders:\n\n        Kasutatakse ainult juhul kui ``qualification.*.protocol`` on ``ocsp``.\n\n        OCSP responderi sertifikaadid. Kui nende hulgast responderi sertifikaati\n        ei leita, siis otsitakse vastuses olevate sertifikaatide hulgast\n        selline, mis on antud välja sama väljastaja poolt, mis kontrollitav\n        sertifikaat, ning on lubatud OCSP vastuste signeerimiseks. AIA\n        loogika kasutamise korral võib väli jääda tühjaks.\n\n:qualification.*.conf.signers:\n\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``qualification.*.protocol`` on ``tsp``\n        või ``tspreg``.\n\n        Ajatempliteenuse vastuse allkirjastamise sertifikaadid.\n\n:qualification.*.conf.delaytime:\n\n        Kohustuslik väli.\n        Kasutatakse ainult juhul kui ``qualification.*.protocol`` on ``tsp``\n        või ``tspreg``.\n\n        Maksimaalne ajanihe ajatempli loomise ja allkirjastamise vahel\n        sekundites.\n\n:qualification.*.conf.retry:\n\n        Kvalifitseeriva päringu korduvkatsete arv. Juhul kui päring ebaõnnestub\n        võrgu- või serverivea tõttu, saab seda automaatselt korrata. Välja\n        väärtus määrab katsete arvu, mis tehakse lisaks algsele päringule.\n        Seega kui väärtus on 1, siis tehakse kokku maksimaalselt kaks päringut.\n        Välja puudumise või väärtuse 0 korral automaatseid korduvkatseid ei\n        sooritata.\n\nNäide\n*****\n\n:file:`example.election.yaml`:\n\n.. literalinclude:: config-examples/example.election.yaml\n   :language: yaml\n   :linenos:\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-volitused.inc",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _kt-management:\n\nKogumisteenuse volituste kirjeldamine\n-------------------------------------\n\nKasutajatele volituste määramine käib süsteemis kirjeldatud rollide kaudu.\nIgale rollile on määratud komplekt õigusi ja kasutajal on kõik volitused, mis\ntalle seotud rollide kaudu on määratud.\n\nVolituste määramise korraldus määrab ühele kasutajale tema rollid süsteemis.\n\nVolitused koostatakse JSON-vormingus failina, millega määratakse:\n\n#. Korralduse sisu (``action=user-permissions``);\n\n#. Volitatud kasutaja *Common Name* väli tema ID-kaardilt (väli ``cn``);\n\n#. Kasutaja rollide nimekiri komadega eraldatud nimekirjana (väli ``roles``).\n\nFaili vorming:\n\n.. code-block:: JavaScript\n\n   {\n       \"action\": \"user-permissions\",\n       \"cn\": \"<kasutaja-CN\">,\n       \"roles\": \"roll1[,roll2]\"\n   }\n\n\n.. _rollid:\n\nRollid\n******\n\nKogumisteenuses on järgnevad rollid:\n\n#. **Kogumisteenuse haldur** (``admin``) on ette nähtud kogumisteenuse\n   tehniliseks haldamiseks;\n\n#. **Valimiste haldur** (``election-conf-manager``) on ette nähtud valimiste\n   seadistuste kehtestamiseks;\n\n#. **Vaataja** (``viewer``) on ette nähtud haldusteenuse kaudu väljastatavate\n   andmete vaatamiseks;\n\n#. **Õigusteta kasutaja** (``none``). See roll on ette nähtud kasutajakonto\n   kirje hoidmiseks olukorras, kus kasutajale pole ühtegi teist rolli määratud\n   (näiteks pärast lisamist või pärast kõigist teistest rollidest eemaldamist).\n\n.. list-table::\n   Rollide ja volituste maatriks\n   :header-rows: 1\n\n   *  -\n      - ``admin``\n      - ``election-`` ``conf-`` ``manager``\n      - ``viewer``\n      - ``none``\n\n   *  - Üldseisundi ja statistika vaatamine\n      - ✓\n      - ✓\n      - ✓\n      - `-`\n\n   *  - Valimiste seadistuste rakendamine\n      - ✓\n      - ✓\n      - `-`\n      - `-`\n\n   *  - E-valimiskasti allalaadimine\n      - ✓\n      - ✓\n      - `-`\n      - `-`\n\n   *  - Kasutajate haldus\n      - ✓\n      - `-`\n      - `-`\n      - `-`\n\n   *  - Tehnilise seadistuse rakendamine\n      - ✓\n      - `-`\n      - `-`\n      - `-`\n\n   *  - Logide vaatamine\n      - ✓\n      - `-`\n      - `-`\n      - `-`\n\n\nVolituste reeglid\n*****************\n\n* Kasutaja võib olla mitmes rollis korraga;\n\n* Roll annab kasutajale rolliga seotud õigused, ükski roll õigusi ära ei võta.\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/kt-ylevaade.inc",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\nÜlevaade\n--------\n\nKogumisteenuse juhtimine toimub signeeritud korralduste abil. Korraldused\nkoostatakse operaatori poolt tekstiredaktori abil või imporditakse Valimiste\nInfosüsteemist. Korraldused signeeritakse ID-kaardiga.\n\nNimekiri kogumisteenuse haldamise korraldustest:\n\n#. Usaldusjuure seadistus;\n\n#. Tehniline seadistus;\n\n#. Valimiste seadistus;\n\n#. Valikute nimekiri;\n\n#. Ringkondade nimekiri;\n\n#. Valijate nimekiri;\n\n#. Valijate nimekirja vahelejätmine;\n\n#. Kasutajate volituste määramine.\n\nLisaks korraldustele tuleb kogumisteenusele genereerida ka komplekt\nkrüptovõtmeid.\n\n\nKorralduste vorming\n*******************\n\nKogumisteenuse korralduste vorming on enamasti YAML või JSON, valijate\nnimekirja puhul kasutatakse spetsiifilist vormingut.\n\nYAML-vormingus korraldused:\n\n#. Usaldusjuure seadistus;\n\n#. Tehniline seadistus;\n\n#. Valimiste seadistus.\n\nJSON-vormingus korraldused:\n\n#. Valikute nimekiri;\n\n#. Kasutajate volituste määramine.\n\nKohandatud vormingus korraldused:\n\n#. Valijate nimekiri.\n\n\nYAML-vormingus korraldused\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYAML-vormingus seadistustesse on võimalik kaasata väliseid faile. Selleks\nkasutatakse silti ``!container``.\n\nNäide:\n\n.. code-block:: yaml\n\n   # välja \"ext_file\" väärtus loetakse failist \"certificate-file.pem\"\n   ext_file: !container certificate-file.pem\n\n.. caution::\n\n   Väliste failide kaasamisel tuleb arvestada sellega, et seadistused\n   laaditakse süsteemi BDOC-vormingus konteinerites (vt. lõiku\n   :ref:`korralduspaki-vorming`). See seab väliste failide kaasamisele\n   järgmised nõuded:\n\n   * Kasutatavad välised failid peavad olema pakendatud seadistusfailiga\n     samasse konteinerisse;\n\n   * Välised failid peavad asuma seadistusfailiga samas kataloogis.\n\n\n.. _korralduspaki-vorming:\n\nKorralduspaki vorming\n*********************\n\nKorralduspakk on BDOC-vormingus konteiner, milles on korraldusfail ja\nsignatuurid. Üks korralduspakk tohib sisaldada ainult ühte korraldust.\nYAML-vormingus seadistuse (usaldusjuure seadistus, tehniline seadistus ja\nvalimiste seadistus) korral võivad failis olla ka seadistusfaili poolt\nkasutatavad välised failid.\n\n.. vim:syntax=rst:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/mixnet.rst",
    "content": "..  IVXV juhend Verificatumi miksneti ettevalmistamiseks ning kasutamiseks\n\nE-häälte miksimine\n====================================================\n\n.. _mix-install:\n\nMiksneti Verificatum paigaldamine\n---------------------------------\n\nEeldused\n^^^^^^^^\n\nJuhend on kasutamiseks distributsiooniga Ubuntu 20.04 LTS (Bionic Beaver) ja\nsee eeldab, et käske käivitatakse lihtkasutaja õigustest, kellel on õigus\nprivileegide eskaleerimiseks `sudo` käsu abil. Lisaks on eeldatud järgmiste\nfailide olemasolu kasutaja kodukaustas:\n\nGithub repositooriumist (https://github.com/vvk-ehk/intcheck):\n\n* :file:`intcheck.py` - tööriist kataloogide täielikkuse kontrolliks\n\nIVXV tarnefailist:\n\n* :file:`gmpmee.dirsha256sum` - ``gmpmee`` kataloogi räsi;\n\n* :file:`vmgj.dirsha256sum` - ``vmgj`` kataloogi räsi;\n\n* :file:`vcr.dirsha256sum` - ``vcr`` kataloogi räsi;\n\n* :file:`vmn.dirsha256sum` - ``vmn`` kataloogi räsi;\n\n* :file:`ivxv-verificatum-1.10.3-runner.zip` - IVXV adapter Verificatumi\n  kasutamiseks.\n\nValimise korraldaja käest:\n\n* :file:`data/bb-4.json` - anonüümitud e-valimiskast;\n\n* :file:`data/pub.pem` - häälte krüpteerimiseks kasutatud võti.\n\nKataloogis :file:`data/` ei tohi olla ühtegi teist faili.\n\nPärast protsessi lõppu on kataloogis :file:`data/` vajalikud järgnevad failid:\n\n* :file:`shuffled.json` - miksitud e-valimiskast;\n\n* :file:`proof.zip` - korrektse miksimise tõend.\n\n\nVerificatumi ehitamine\n^^^^^^^^^^^^^^^^^^^^^^\n\nEhitamiseks vajalike pakkide paigaldamine::\n\n    sudo apt-get install --no-install-recommends -y autoconf autoconf automake \\\n    build-essential libgmp-dev libtool git openjdk-11-jdk-headless \\\n    python unzip wget\n\nVerificatumi lähtekoodi allalaadimine::\n\n    git clone https://github.com/verificatum/verificatum-gmpmee gmpmee\n    git clone https://github.com/verificatum/vmgj\n    git clone https://github.com/verificatum/vcr\n    git clone https://github.com/verificatum/vmn\n\nLähtekoodist puhaste arhiivide loomine täielikkuse kontrolliks::\n\n    cd gmpmee\n    git checkout 4aafc31\n    rm -rf .git/\n    cd ../vmgj\n    git checkout 8d7d412\n    rm -rf .git/\n    cd ../vcr\n    git checkout af9fd82\n    rm -rf .git/\n    cd ../vmn\n    git checkout bb00543\n    rm -rf .git/\n    cd ..\n\nVerificatumi lähtekoodi täielikkuse kontrollimine::\n\n    chmod +x ./intcheck.py\n    ./intcheck.py verify gmpmee gmpmee.dirsha256sum\n    ./intcheck.py verify vmgj vmgj.dirsha256sum\n    ./intcheck.py verify vcr vcr.dirsha256sum\n    ./intcheck.py verify vmn vmn.dirsha256sum\n\n`gmpmee` ehitamine::\n\n    cd gmpmee/\n    make -f Makefile.build\n    ./configure\n    make\n    sudo make install\n\n`vmgj` ehitamine::\n\n    cd ../vmgj/\n    make -f Makefile.build\n    ./configure\n    make\n    sudo make install\n\n`vcr` ehitamine::\n\n    cd ../vcr/\n    make -f Makefile.build\n    ./configure --enable-vmgj\n    make\n    sudo make install\n\n`vmn` ehitamine::\n\n    cd ../vmn/\n    make -f Makefile.build\n    ./configure\n    make\n    sudo make install\n\n\nIVXV Verificatumi adapteri ja käivitusskripti lahtipakkimine::\n\n    cd ..\n    unzip ivxv-verificatum-1.10.3-runner.zip\n\nVerificatumi teekide kopeerimine adapteri väliste teekide kataloogi::\n\n    cp /usr/local/share/java/verificatum-vmgj-1.2.2.jar mixer/lib/verificatum-vmgj.jar\n    cp /usr/local/share/java/verificatum-vcr-vmgj-3.0.4.jar mixer/lib/verificatum-vcr-vmgj.jar\n    cp /usr/local/share/java/verificatum-vmn-3.0.4.jar mixer/lib/verificatum-vmn.jar\n    cp /usr/local/lib/libgmpmee.so.0.0.0 mixer/lib/libgmpmee.so.0\n    cp /usr/local/lib/libvmgj-1.2.2.so mixer/lib/libvmgj-1.2.2.so\n\n\n\n.. _mix-mix:\n\n\nE-häälte miksimine\n----------------------------------------\n\nVerificatumi miksneti käivitamine::\n\n    cd data\n    ../mixer/bin/mix.py --pubkey pub.pem --ballotbox bb-4.json \\\n    --shuffled shuffled.json --proof-zipfile proof.zip shuffle\n\nVerificatumi miksneti käivitamine koos entroopiaallika eelneva tühjendamisega::\n\n    cd data\n    ../mixer/bin/mix.py --pubkey pub.pem --ballotbox bb-4.json \\\n    --shuffled shuffled.json --proof-zipfile proof.zip --empty-entropy-pool \\\n    shuffle\n\n.. _mix-verify:\n\nMiksimistõendi verifitseerimine\n-------------------------------\n\nVerificatumi adapteri abil saab miksimistõendit ka verifitseerida::\n\n    cd ..\n    mkdir verify\n    cp data/proof.zip verify\n    cd verify\n    ../mixer/bin/mix.py verify --proof-zipfile proof.zip\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/rakendused.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n\n.. _ivxv-rakendused:\n\nIVXV rakendused\n===============\n\n.. _app-install:\n\nRakenduste paigaldamine\n--------------------------------------------------------------------------------\n\nIVXV rakendused on:\n\n* võtmerakendus `key` (:numref:`app-key`),\n* töötlemisrakendus `processor` (:numref:`app-processor`),\n* auditirakendus `auditor` (:numref:`app-auditor`).\n\nIVXV rakendused on arendatud programmeerimiskeeles Java, kasutusel on Java 21.\nRakendused on testitud Windows 11 ja Ubuntu 22.04 platvormil kasutades OpenJDK'd\nvõi Oracle Javat.\n\nRakendused tarnitakse ZIP-vormingus failidena::\n\n  <rakendus>-<tarnenumber>.zip\n\nPeale ZIP-faili lahti pakkimist tekib kataloogipuu::\n\n   <rakendus>-<tarnenumber>\n   |-- bin\n   |   |-- <rakendus>\n   |   |-- <rakendus.bat>\n   |-- lib\n   |   |-- *.jar\n\nKui kataloogitee `<rakendus>-<tarnenumber>/bin` panna keskkonnamuutujasse `PATH`, saab rakendust\nedaspidi käivitada käsurealt::\n\n  $ <rakendus>\n\nRakendusi paigaldades tuleb arvestada, et kesksüsteemi protokollides kirjeldatud\nraportid kasutavad ilma ajavööndita aja vormingut (`yyyymmddhhmmss`).\nAjavööndiga ajamärgendi (näiteks hääletamise aeg kogumisteenuselt saadud\ne-valimiskastis) esitamiseks raportis teisendavad Java rakendused ajamärgendi esmalt\noperatsioonisüsteemi ajavööndisse ning seejärel eemaldavad ajavööndi info.\nSeetõttu tuleb rakendusi käivitavates masinates seadistada ajavöönd selliseks,\nmillise kohalikus ajas soovitakse ajamärgendeid näha.\n\n.. _app-trust:\n\nRakenduste usaldusjuure kirjeldamine\n------------------------------------\n\nRakenduste kasutamine eeldab digitaalselt allkirjastatud seadistuste kasutamist.\nAllkirjade verifitseerimiseks vajalikud sertifikaadid tuleb rakendusele ette\nanda usaldusjuure koosseisus. Usaldusjuur on samuti digitaalselt allkirjastatud.\n\nUsaldusjuure seadistuse koostab valimiste korraldaja.\n\n:ca:\n\n    Komadega eraldatud loetelu konteineris sisalduvatest CA sertifikaatidest ja\n    vahesertifikaatidest.\n\n:ocsp:\n\n    Komadega eraldatud loetelu konteineris sisalduvatest OCSP sertifikaatidest.\n\n:tsa:\n\n    Komadega eraldatud loetelu konteineris sisalduvatest ATO sertifikaatidest.\n\nKõik sertifikaadid antakse PEM vormingus.\n\nRakendusele esitatakse usaldusjuur BDOC konteineris, kus usaldusjuure\nspetsifikatsioon on kirjeldatud failis `ivxv.properties` ning kõik juure\nelemendid on konteinerisse laaditud.\n\n\nNäide\n*****\n\n:file:`ivxv.properties`:\n\n.. literalinclude:: config-examples/ivxv.properties.real\n   :linenos:\n\n\nRakenduste käivitamine\n--------------------------------------------\n\nRakendusi käivitatakse käsurealt, nende toimimist juhitakse käsureaparameetrite\nja digitaalselt allkirjastatud seadistustega. Kõik rakendused väljastavad\nvajadusel abiinfot::\n\n  $ <rakendus> --help\n\n  Rakendus 'rakendus'        - Rakendus\n\n  Kasutamine:\n    <rakendus> <tööriist> --conf <conf> [--params <params>] [--force <force>] [--quiet <quiet>] [--lang <lang>] [--container_threads <container_threads>] [--threads <threads>]\n    <rakendus> <tööriist> -h | --help\n    <rakendus> -h | --help\n\n  Tööriistad:\n    tool_foo         - Tegevuse FOO teostamine\n    tool_bar         - Tegevuse BAR teostamine\n\n  Käsurea argumendid:\n    -h --help             - Abi\n    -c --conf (*)         - Konfiguratsioon\n    -p --params           - Tööriista parameetrid\n    -f --force            - Ära küsi kasutajalt kinnitust\n    -q --quiet            - Vaikne käivitusrežiim\n    --lang                - Keel\n    -ct --container_threads - Allkirjastatud konteinerite teegi poolt kasutatav lõimede arv (<= 0 korral dünaamiline)\n    -t --threads          - Rakenduse poolt paralleeltöötluse korral kasutatav lõimede arv (<= 0 korral dünaamiline)\n  Rakendus lõpetas töö ilma vigadeta\n\nRakenduste kasutamisel tuleb määrata konkreetne tööriist, usaldusjuur ning\nseadistusfail::\n\n  $ <rakendus> tool_foo --conf usaldusjuur.asice --params tool_foo.conf.asice\n\n  Konfiguratsiooni laadimine failist usaldusjuur.asice\n  Konfiguratsiooni allkirja kontrollimine\n  Konfiguratsiooni allkirja on andnud NIMI NIMESTE\n  Konfiguratsiooni allkirja andmise aeg on 24.12.2018 18:00\n  Konfiguratsiooni allkiri on korrektne ja kehtiv\n\n  FOO!\n\n  Rakendus lõpetas töö ilma vigadeta\n\nJuhised rakenduste tööriistade ning nende seadistusfailide koostamise kohta\nantakse järgmistes peatükkides. Käsureaargumendid on kõigil rakendustel samad:\n\n:-h --help:\n    Abiinfo kuvamine kas rakenduse või konkreetse tööriista kohta.\n\n:-c --conf (*):\n    Digitaalselt allkirjastatud fail usaldusjuurega. Kohustuslik parameeter.\n\n:-p --params:\n    Digitaalselt allkirjastatud tööriista parameetrid.\n\n:-f --force:\n    Ära küsi kasutajalt kinnitust.\n\n:-q --quiet:\n    Vaikne käivitusrežiim.\n\n:--lang:\n    Juhul kui rakendus on kompileeritud mitmekeelsena, siis keele valik.\n    Vaikimisi on rakendustes võimaldatud ainult eesti keel.\n\n:-ct --container_threads:\n    Allkirjastatud konteinerite teegi poolt kasutatav lõimede arv. Vaikimisi\n    valitakse lõimede arv teegi poolt dünaamiliselt lähtudes saadaolevate\n    tuumade arvust.\n\n:-t --threads:\n    Rakenduse poolt paralleeltöötluse korral kasutatav lõimede arv. Vaikimisi\n    valitakse lõimede arv rakenduse poolt dünaamiliselt lähtudes saadaolevate\n    tuumade arvust.\n\n\nRakendustest eksisteerivad nii tooteversioonid kui testversioonid.\nTestrakendused on kohaldatud protseduuride efektiivseks testimiseks, kuid ei\nsobi valimiste tegelikuks läbiviimiseks. Näiteks ei võimalda võtmerakenduse\ntestversioon kasutada kiipkaarte. Testversioonid rakendustest kuvavad\nkäivitamisel hoiatuse::\n\n  ********************************************************************\n  *                           !!! HOIATUS !!!                        *\n  *                                                                  *\n  * Rakendus on käivitatud arendusrežiimis ning rakenduse käitumine  *\n  * võib erineda tavarežiimist.                                      *\n  * Rakenduse käivitamiseks tavarežiimis tuleb rakendus ümber        *\n  * kompileerida.                                                    *\n  ********************************************************************\n\nRakenduste käivituskeskkonna parameetrid\n----------------------------------------\n\nSuure e-valimiskasti auditeerimisel, töötlemisel või dekrüpteerimisel, võib olla\ntarvilik suurendada protsessi mälupiirangut.\n\nSeda saab teha kasutades rakendusespetsiifilist keskkonnamuutujat\n``{RAKENDUS}_OPTS``, mis defineerib täiendavad argumendid Java virtuaalmasinale.\n``{RAKENDUS}`` on üks kolmest ``AUDITOR``, ``KEY`` või ``PROCESSOR``. Protsessi\nmälupiirangu suurendamiseks tuleb kasutada argumenti ``-Xmx{N}G``, kus ``{N}``\non mälupiirangu suurus gigabaitides.\n\nNäiteks 10 gigabaidi mälu eraldamiseks töötlemisrakendusele tuleb seada\n``PROCESSOR_OPTS=-Xmx10G``.\n\n\n.. list-table:: Rakenduste mälupiirangu parameetrid\n   :header-rows: 1\n\n   * - Rakendus\n     - Vaikimisi mälupiirang\n     - Keskkonnamuutuja\n   * - Auditirakendus\n     - 8GB\n     - ``AUDITOR_OPTS``\n   * - Töötlemisrakendus\n     - 8GB\n     - ``PROCESSOR_OPTS``\n   * - Võtmerakendus\n     - Puudub\n     - ``KEY_OPTS``\n\n\nRakendused töötavad nii 32-bitise kui 64-bitise Java andmemudeliga, samas\nefektiivseimaks toimimiseks tuleb rakendusi kasutada 64-bitisel platvormil\n64-bitise Java andmemudeliga. Juhul kui rakendus ei suuda käivitamisel 64-bitist\nmudelit tuvastada kuvatakse hoiatus::\n\n  ********************************************************************\n  *                           !!! HOIATUS !!!                        *\n  *                                                                  *\n  * 64-bitise Java andmemudeli tuvastamine ebaõnnestus. Rakendus on  *\n  * vähemefektiivsem. Rakenduse jõudluse suurendamiseks tuleb        *\n  * kasutada 64-bitise andmemudeliga Java keskkonda.                 *\n  ********************************************************************\n\nJuhul kui rakenduse mälupiirang on 4GB või rohkem, ei ole 32-bitise\nandmemudeliga Java võimeline rakendust käivitama. Kuvatakse järgmine veateade::\n\n  Invalid maximum heap size: -Xmx4G\n  The specified size exceeds the maximum representable size.\n  Error: Could not create the Java Virtual Machine.\n  Error: A fatal exception has occurred. Program will exit.\n\n\n\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/spelling_wordlist.txt",
    "content": "\nBeaver\nBionic\nGithub\niOS\nJavat\nOracle\nUbuntu\n\nhosti\nhostinimedest\nhostinimi\n\netcd\nxml\n\nCSV\nIP\nPDF\nURL\n\nBertoni\nDaemen\nPeeters\nVan\nAssche\nSponge\nBased\nPseudo\nRandom\nNumber\nGenerators\n\nKivinen\nKojo\nMore\nModular\nExponential\nDiffie\nHellman\ngroups\nfor\nInternet\nKey\nExchange\n\nDesmedt\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/tootlemisrakendus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _app-processor:\n\nTöötlemisrakendus\n=================\n\nTöötlemisrakendus on käsurearakendus e-valimiskasti kontrollimiseks ja edasiseks\ntöötlemiseks peale e-hääletamise lõppu.\n\nTöötlemisrakenduse põhilised tööriistad on *check*, *squash*, *revoke* ja\n*anonymize*, mis käivitatakse loetletud järjekorras vastavalt ette nähtud\nvalimisprotseduuridele.\nPõhitööriistade sisendi hulgas on alati kas kogumisteenuse või eelmise tööriista\npoolt väljastatud e-valimiskast ja e-valimiskasti digitaalselt allkirjastatud räsi.\nVäljundi hulgas on töötlemisetapi tulemuseks olev e-valimiskast koos allkirjastamata\nräsiga. Kuna rakendused käivitatakse internetiühenduseta arvutis, tuleb\nräsifailid tõsta digitaalseks allkirjastamiseks välisesse seadmesse.\nE-valimiskasti räsi arvutatakse funktsiooniga ``hex(sha256(<fail>))``.\n\nLisaks põhitööriistadele on rakendusel veel neli täiendavat tööriista:\n*export*, *verify*, *stats* ja *statsdiff*.\n\nKõigi tööriistade kasutamine eeldab allkirjastatud usaldusjuure ja konkreetse\ntööriista seadistuste olemasolu.\nFaile väljastavatel tööriistadel tuleb seadistustes määrata väljundkausta\nasukoht. Väljundkausta ei tohi käivitamise ajal olemas olla, selle loob rakendus.\nAlljärgnevalt on kirjeldatud tööriistade seadistusi.\n\n.. _processor-check:\n\nE-valimiskasti töötlemine - verifitseerimine\n--------------------------------------------\n\nKogumisteenusest väljastatud e-valimiskasti verifitseerimiseks kasutatakse tööriista\n*check*. valimiskasti verifitseeritakse usaldusjuure, valijate nimekirjade, ringkondade\nnimekirja ja registreerimisteenuse väljundi vastu.\n\nVerifitseerimise käigus kontrollitakse järgmiseid põhilisi omadusi:\n\n* Ringkondade nimekirja ja valijate nimekirjade andmeterviklus ja\n  kooskõlalisus;\n\n* E-valimiskasti andmeterviklus;\n\n* E-hääletajate valimisõigus e. kuuluvus valijate nimekirja (kontrollitakse\n  juhul kui valijate nimekirjad on seadistustes kirjeldatud);\n\n* E-valimiskastis sisalduvate häälte vastavus digiallkirja vormingule;\n\n* Registreerimisandmete andmeterviklus;\n\n* E-valimiskastis sisalduvate häälte vastavus registreerimisandmetega.\n\nE-valimiskasti verifitseerimine on töömahukas protsess. 4-tuumalise *i7* protsessoriga\narvuti suudab ühe sekundi jooksul töödelda umbes 200 häält. Töötlemise jooksul\nkuvatakse kasutajale edenemisriba, mille alusel on võimalik ennustada\ntöötlemisele kuluvat aega.\n\n:check.ballotbox:\n        Kogumisteenusest väljastatud e-valimiskast.\n\n:check.ballotbox_checksum:\n        Kogumisteenusest väljastatud e-valimiskasti digitaalselt allkirjastatud räsi.\n\n        Kui määramata, siis ei väljastata korrastatud e-valimiskasti järgmisteks\n        etappideks. Kasulik mitte-lõpliku e-valimiskasti valimisaegseks kontrolliks.\n\n:check.signed_ballot_max_size_bytes:\n        Kogumisteenusest väljastatud e-valimiskasti üksiku allkirjastatud hääle\n        maksimaalne lubatud pikkus baitides.\n\n        Kui määramata, siis vaikimisi kasutatakse 32768 baiti.\n\n:check.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri.\n\n:check.registrationlist:\n        Registreerimisteenusest pärit registreerimisandmed. Kui määramata, siis\n        ei kontrollita e-valimiskastis sisalduvate häälte vastavust\n        registreerimisandmetega.\n\n:check.registrationlist_checksum:\n        Registreerimisandmete digitaalselt allkirjastatud räsi. Võib puududa,\n        kui ``registrationlist`` puudub.\n\n:check.tskey:\n        Registreerimispäringute verifitseerimiseks kasutatav kogumisteenuse\n        avalik võti registreerimispäringute tegemise sertifikaadist.\n\n:check.vlkey:\n        Valijate nimekirjade verifitseerimiseks kasutatav avalik võti.\n        Argument on kohustuslik, kui valijate nimekirjad on antud.\n\n:check.voterlists_dir:\n        Valijate nimekirjade loendi kaust. Kui on määramata, siis e-hääletanute\n        hääleõigust ei kontrollita.\n\n:check.voterlists:\n        Valijate nimekirjade loend. Kui on määramata, siis e-hääletanute\n        hääleõigust ei kontrollita.\n\n:check.voterlists.path:\n        Valijate nimekirja fail.\n\n:check.voterlists.signature:\n        Valijate nimekirja allkiri, mis on antud algoritmiga\n        ``ecdsa-with-SHA256``.\n\n:check.districts_mapping:\n        Valijate nimekirjas oleva ringkonna ja jaoskonna teisendusfail\n        (valikuline).\n\n:check.election_start:\n        Hääletamise algusaeg. Sellest varasema hääletusajaga hääli käsitletakse\n        proovihäältena ning need lugemisele ei lähe.\n\n:check.voterforeignehak:\n        Alaliselt välisriigis elavate valijate ringkonnakuuluvuse tuvastamiseks\n        kasutatav EHAK-kood. Vaikeväärtus \"0000\".\n\n:check.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. Tervikluskontrolliga korrastatud e-valimiskast :file:`<valimise id>-bb-1.json`;\n\n        #. Tervikluskontrolliga korrastatud e-valimiskasti räsi\n           :file:`<valimise id>-bb-1.json.sha256sum`;\n\n        #. E-valimiskasti töötlemisvigade raport :file:`ballotbox_errors.txt`;\n\n        #. Valijate nimekirjade töötlemisvigade raport\n           :file:`voterlist_errors.txt`;\n\n        #. *Log1* fail ehk vastuvõetud hääled\n           :file:`<valimise id>.<küsimuse id>.check.log1`.\n\n:file:`processor.check.yaml`:\n\n.. literalinclude:: config-examples/processor.check.yaml\n   :language: yaml\n   :linenos:\n\n.. _processor-squash:\n\nE-valimiskasti töötlemine - korduvhäälte tühistamine\n----------------------------------------------------\n\nKorduvate e-häälte tühistamiseks kasutatakse tööriista *squash*.\nTööriista sisendiks on tööriista *check* poolt koostatud e-valimiskast.\nKorduvhäälte tühistamisel jäetakse alles iga hääletaja kõige hilisema hääl ja\neemaldatakse kõik varasemad hääled.\n\n:squash.ballotbox:\n        Tervikluskontrolliga korrastatud e-valimiskast.\n\n:squash.ballotbox_checksum:\n        Tervikluskontrolliga korrastatud e-valimiskasti digitaalselt allkirjastatud\n        räsi.\n\n:squash.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri.\n\n:squash.enckey:\n        Krüpteerimise avaliku võtme faili asukoht (võtmerakenduse väljund).\n        Võtit kasutatakse krüpteeritud häälte eelkontrolliks, eristamaks\n        päriselt krüpteeritud hääli suvalisest binaarsest prügist.\n\n:squash.out:\n        Tööriista väljundkaust. Sellesse kausta luuakse:\n\n        #. Korduvhäältest puhastatud e-valimiskast :file:`<valimise id>-bb-2.json`;\n\n        #. Korduvhäältest puhastatud e-valimiskasti räsi :file:`<valimise\n           id>-bb-2.json.sha256sum`;\n\n        #. E-hääletanute nimekiri JSON-vormingus :file:`<valimise\n           id>-ivoterlist.json`;\n\n        #. E-hääletanute nimekiri PDF-vormingus :file:`<valimise\n           id>-ivoterlist.pdf`;\n\n        #. Tühistamiste ja ennistamiste aruanne :file:`<valimise\n           id>-revocation-report.csv`;\n\n        #. Tühistamiste ja ennistamiste aruanne ilma isikuandmeteta\n           :file:`<valimise\n           id>-revocation-report.csv.anonymous`;\n\n        #. *Log2* fail ehk tühistatud hääled :file:`<valimise id>.<küsimuse\n           id>.squash.log2`.\n\n:file:`processor.squash.yaml`:\n\n.. literalinclude:: config-examples/processor.squash.yaml\n   :language: yaml\n   :linenos:\n\n\n.. _processor-revoke:\n\nE-valimiskasti töötlemine - häälte tühistamine ja ennistamine jaoskonnainfo põhjal\n----------------------------------------------------------------------------------\n\nHäälte tühistamiseks ja ennistamiseks jaoskonnainfo põhjal kasutatakse tööriista\n*revoke*. Tööriist saab sisendiks tööriista *squash* poolt koostatud e-valimiskasti ning\nrakendab sellele sisendiks antud tühistus- ja ennistusnimekirjad.\n\n:revoke.ballotbox:\n        Korduvhäältest puhastatud e-valimiskast.\n\n:revoke.ballotbox_checksum:\n        Korduvhäältest puhastatud e-valimiskasti digitaalselt allkirjastatud räsi.\n\n:revoke.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri.\n\n:revoke.revocationlists:\n        Tühistus- ja ennistusnimekirjade loend. Võib olla tühi.\n\n:revoke.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. Korduvhääletajate häältest puhastatud e-valimiskast :file:`<valimise\n           id>-bb-3.json`;\n\n        #. Korduvhääletajate häältest puhastatud e-valimiskasti räsi\n           :file:`<valimise id>-bb-3.json.sha256sum`;\n\n        #. Tühistamiste ja ennistamiste aruanne :file:`<valimise\n           id>-revocation-report.csv`;\n\n        #. Tühistamiste ja ennistamiste aruanne ilma isikuandmeteta\n           :file:`<valimise\n           id>-revocation-report.csv.anonymous`;\n\n        #. E-hääletanute nimekiri JSON-vormingus :file:`<valimise\n           id>-ivoterlist.json``;\n\n        #. *Log2* fail e. tühistatud hääled\n           :file:`<valimise id>.<küsimuse id>.revoke.log2`.\n\n:file:`processor.revoke.yaml`:\n\n.. literalinclude:: config-examples/processor.revoke.yaml\n   :language: yaml\n   :linenos:\n\n\n.. _processor-anonymize:\n\nE-valimiskasti töötlemine - anonüümimine\n----------------------------------------\n\nE-valimiskasti anonüümimiseks kasutatakse tööriista *anonymize*.\nTööriist saab sisendiks tööriista *revoke* poolt koostatud e-valimiskasti ning eemaldab\nsellest valijate info.\n\n:anonymize.ballotbox:\n        Korduvhääletajate häältest puhastatud e-valimiskast.\n\n:anonymize.ballotbox_checksum:\n        Korduvhääletajate häältest puhastatud e-valimiskasti digitaalselt allkirjastatud\n        räsi.\n\n:anonymize.out:\n        Tööriista väljundkaust. Sellesse kausta luuakse:\n\n        #. Hääletajate isikuandmetest puhastatud e-valimiskast :file:`<valimis\n           id>-bb-4.json`;\n\n        #. Hääletajate isikuandmetest puhastatud e-valimiskasti räsi :file:`<valimise\n           id>-bb-4.json.sha256sum`;\n\n        #. *Log3* fail e. lugemisele läinud hääled :file:`<valimise\n           id>.<küsimuse id>.anonymize.log3`.\n\n:file:`processor.anonymize.yaml`:\n\n.. literalinclude:: config-examples/processor.anonymize.yaml\n   :language: yaml\n   :linenos:\n\n\nTöötlemisrakenduse täiendavad tööriistad\n----------------------------------------\n\nTööriist *verify*\n*****************\n\n*Verify* on lisavahend, millega saab verifitseerida digitaalselt allkirjastatud\nkonteineri allkirja ning kuvada konteineri andmed.\n\n:verify.file:\n        Verifitseeritav fail.\n\n\n:file:`processor.verify.yaml`:\n\n.. literalinclude:: config-examples/processor.verify.yaml\n   :language: yaml\n   :linenos:\n\n\nTööriist *export*\n*****************\n\n*Export* on lisavahend, millega saab eksportida kogumisteenusest väljastatud\ne-valimiskasti seest täielikke digitaalselt allkirjastatud hääle konteinereid. On\nvõimalik eksportida nii kõiki hääli korraga, kui konkreetse valija hääli.\n\n:export.ballotbox:\n        Kogumisteenusest väljastatud e-valimiskast.\n\n:export.ballotbox_checksum:\n        Kogumisteenusest väljastatud e-valimiskasti digitaalselt allkirjastatud räsi.\n\n:export.signed_ballot_max_size_bytes:\n        Kogumisteenusest väljastatud e-valimiskasti üksiku allkirjastatud hääle\n        maksimaalne lubatud pikkus baitides.\n\n        Kui määramata, siis vaikimisi kasutatakse 32768 baiti.\n\n:export.voter_id:\n        Valija identifikaator (valikuline).\n\n:export.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. E-valimiskasti töötlemisvigade raport :file:`ballotbox_errors.txt`\n           (valikuline);\n\n        #. E-valimiskastist eksporditud häälte digitaalselt allkirjastatud konteinerid.\n\n\n:file:`processor.export.yaml`:\n\n.. literalinclude:: config-examples/processor.export.yaml\n   :language: yaml\n   :linenos:\n\n\nTööriist *stats*\n****************\n\n*Stats* on lisavahend, millega saab arvutada häälte ja hääletajate statistikat\ne-valimiskasti põhjal. Statistikat on võimalik piiritleda ajavahemikuga ning väljundit\non võimalik piiritleda koondandmetega kui ka ringkondade kaupa. NB! Tööriist ei\nkontrolli digitaalallkirju, häälte töötlemiseks tuleb kasutada *check*,\n*squash*, *revoke*, *anonymize* töövoogu.\n\n:stats.ballotbox:\n        E-valimiskast, mille põhjal statistika koostada. Kui faili laiendiks on\n        ``.json``, siis peab see olema olema töödeldud e-valimiskast. Vastasel juhul\n        peab see olema kogumisteenusest väljastatud e-valimiskast.\n\n:stats.signed_ballot_max_size_bytes:\n        Kogumisteenusest väljastatud e-valimiskasti üksiku allkirjastatud hääle\n        maksimaalne lubatud pikkus baitides.\n\n        Kui määramata, siis vaikimisi kasutatakse 32768 baiti.\n\n:stats.election_day:\n        Valimispäev. Kõikide e-hääletanute vanused arvutatakse statistika\n        tarbeks selle kuupäeva suhtes.\n\n:stats.period_start:\n        Statistikaperioodi algusaeg (valikuline). Sellest varasema\n        hääletusajaga hääli statistikasse ei kaasata.\n\n:stats.period_end:\n        Statistikaperioodi lõppaeg (valikuline). Sellest hilisema hääletusajaga\n        hääli statistikasse ei kaasata.\n\n:stats.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri. Vajalik ringkondade\n        kaupa statistika väljastamiseks. Kui on määramata, siis väljastatakse\n        ainult koondstatistika.\n\n:stats.vlkey:\n        Valijate nimekirjade verifitseerimiseks kasutatav avalik võti.\n        Argument on kohustuslik valijate nimekirjade kasutamise korral.\n\n:stats.voterlists:\n        Valijate nimekirjade loend. Vajalik kogumisteenusest väljastatud\n        e-valimiskastist valija ringkonna tuvastamiseks.\n\n        Argument on kohustuslik, kui e-valimiskast on väljastatud kogumisteenusest ja\n        statistikat väljastatakse ringkondade kaupa.\n\n:stats.voterlists.path:\n        Valijate nimekirja fail.\n\n:stats.voterlists.signature:\n        Valijate nimekirja allkiri, mis on antud algoritmiga\n        ``ecdsa-with-SHA256``.\n\n:check.voterforeignehak:\n        Alaliselt välisriigis elavate valijate ringkonnakuuluvuse tuvastamiseks\n        kasutatav EHAK-kood. Vaikeväärtus \"0000\".\n\n:stats.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. E-valimiskasti statistika JSON-vormingus :file:`<valimise id>-stats.json`\n           (:file:`ELECTION-stats.json` kui valimist ei suudeta tuvastada);\n\n        #. E-valimiskasti statistika CSV-vormingus :file:`<valimise id>-stats.csv`\n           (:file:`ELECTION-stats.csv` kui valimist ei suudeta tuvastada);\n\n        #. E-valimiskasti töötlemisvigade raport :file:`ballotbox_errors.txt`\n           (tekib vigade korral);\n\n        #. Valijate nimekirjade töötlemisvigade raport\n           :file:`voterlist_errors.txt` (tekib vigade korral).\n\n\n:file:`processor.stats.yaml`:\n\n.. literalinclude:: config-examples/processor.stats.yaml\n   :language: yaml\n   :linenos:\n\n\nTööriist *statsdiff*\n********************\n\n*Statsdiff* on lisavahend, millega saab arvutada kahe statistikafaili vahet.\nTulemuseks on kolmas statistikafail, mille kõik väärtused on pärit alusfailist,\nkust on lahutatud võrreldava faili väärtused.\n\n:statsdiff.compare:\n        Statistika võrdluse alusfail JSON-vormingus.\n\n:statsdiff.to:\n        Võrreldav statistika fail JSON-vormingus. Võrreldav statistika fail on pärit\n        IVXV logimonitorist. Selleks et kasutada *statsdiff* utiliidi tuleb antud failist\n        eemaldada **time:** ja **meta:** JSON kirjed. Need kirjed ei oma tähtsust\n        statistika võrdlemisel ja on lihtsalt statistika faili genereerimise ajatempel,\n        mis on alati erinev **statsdiff.compare** ja **statsdiff.to** failides.\n\n:statsdiff.diff:\n        Tööriista väljundfail. Sellesse faili salvestatakse statistikate vahe\n        JSON-vormingus.\n\n\n:file:`processor.statsdiff.yaml`:\n\n.. literalinclude:: config-examples/processor.statsdiff.yaml\n   :language: yaml\n   :linenos:\n\n.. _processor-checkAndSquash:\n\nE-valimiskasti töötlemine - verifitseerimine ja korduvhäälte tühistamine\n------------------------------------------------------------------------\n\nAntud tööriist teostab nii verifitseerimist, kui ka korduvhäälte tühistamist.\nRohkem infot teostavate operatsioonide kohta leidub alapeatükkides:\n\n* *E-valimiskasti töötlemine - verifitseerimine*\n* *E-valimiskasti töötlemine - korduvhäälte tühistamine*\n\n:checkAndSquash.ballotbox:\n        Kogumisteenusest väljastatud e-valimiskast.\n\n:checkAndSquash.ballotbox_checksum:\n        Kogumisteenusest väljastatud e-valimiskasti digitaalselt allkirjastatud räsi.\n\n        Kui määramata, siis ei väljastata korrastatud e-valimiskasti järgmisteks\n        etappideks. Kasulik mitte-lõpliku e-valimiskasti valimisaegseks kontrolliks.\n\n:checkAndSquash.signed_ballot_max_size_bytes:\n        Kogumisteenusest väljastatud e-valimiskasti üksiku allkirjastatud hääle\n        maksimaalne lubatud pikkus baitides.\n\n        Kui määramata, siis vaikimisi kasutatakse 32768 baiti.\n\n:checkAndSquash.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri.\n\n:checkAndSquash.registrationlist:\n        Registreerimisteenusest pärit registreerimisandmed. Kui määramata, siis\n        ei kontrollita e-valimiskastis sisalduvate häälte vastavust\n        registreerimisandmetega.\n\n:checkAndSquash.registrationlist_checksum:\n        Registreerimisandmete digitaalselt allkirjastatud räsi. Võib puududa,\n        kui ``registrationlist`` puudub.\n\n:checkAndSquash.tskey:\n        Registreerimispäringute verifitseerimiseks kasutatav kogumisteenuse\n        avalik võti registreerimispäringute tegemise sertifikaadist.\n\n:checkAndSquash.vlkey:\n        Valijate nimekirjade verifitseerimiseks kasutatav avalik võti.\n        Argument on kohustuslik, kui valijate nimekirjad on antud.\n\n:checkAndSquash.voterlists_dir:\n        Valijate nimekirjade loendi kaust. Kui on määramata, siis e-hääletanute\n        hääleõigust ei kontrollita.\n\n:checkAndSquash.voterlists:\n        Valijate nimekirjade loend. Kui on määramata, siis e-hääletanute\n        hääleõigust ei kontrollita.\n\n:checkAndSquash.voterlists.path:\n        Valijate nimekirja fail.\n\n:checkAndSquash.voterlists.signature:\n        Valijate nimekirja allkiri, mis on antud algoritmiga\n        ``ecdsa-with-SHA256``.\n\n:checkAndSquash.districts_mapping:\n        Valijate nimekirjas oleva ringkonna ja jaoskonna teisendusfail\n        (valikuline).\n\n:checkAndSquash.election_start:\n        Hääletamise algusaeg. Sellest varasema hääletusajaga hääli käsitletakse\n        proovihäältena ning need lugemisele ei lähe.\n\n:checkAndSquash.voterforeignehak:\n        Alaliselt välisriigis elavate valijate ringkonnakuuluvuse tuvastamiseks\n        kasutatav EHAK-kood. Vaikeväärtus \"0000\".\n\n:checkAndSquash.enckey:\n        Krüpteerimise avaliku võtme faili asukoht (võtmerakenduse väljund).\n        Võtit kasutatakse krüpteeritud häälte eelkontrolliks, eristamaks\n        päriselt krüpteeritud hääli suvalisest binaarsest prügist.\n\n:checkAndSquash.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. Korduvhäältest puhastatud e-valimiskast :file:`<valimise id>-bb-2.json`;\n\n        #. Korduvhäältest puhastatud e-valimiskasti räsi :file:`<valimise\n           id>-bb-2.json.sha256sum`;\n\n        #. E-hääletanute nimekiri JSON-vormingus :file:`<valimise\n           id>-ivoterlist.json`;\n\n        #. E-hääletanute nimekiri PDF-vormingus :file:`<valimise\n           id>-ivoterlist.pdf`;\n\n        #. Tühistamiste ja ennistamiste aruanne :file:`<valimise\n           id>-revocation-report.csv`;\n\n        #. Tühistamiste ja ennistamiste aruanne ilma isikuandmeteta\n           :file:`<valimise\n           id>-revocation-report.csv.anonymous`;\n\n        #. *Log1* fail ehk vastuvõetud hääled\n           :file:`<valimise id>.<küsimuse id>.log1`.\n\n        #. *Log2* fail ehk tühistatud hääled :file:`<valimise id>.<küsimuse\n           id>.log2`.\n\n        #. E-valimiskasti töötlemisvigade raport :file:`ballotbox_errors.txt`\n           (valikuline);\n\n        #. Valijate nimekirjade töötlemisvigade raport\n           :file:`voterlist_errors.txt` (valikuline);\n\n\n:file:`processor.checkAndSquash.yaml`:\n\n.. literalinclude:: config-examples/processor.checkAndSquash.yaml\n   :language: yaml\n   :linenos:\n\n.. _processor-revokeAndAnonymize:\n\nE-valimiskasti töötlemine - häälte tühistamine, ennistamine jaoskonnainfo põhjal ja anonüümimine\n---------------------------------------------------------------------------------------------------\n\nHäälte tühistamiseks, ennistamiseks jaoskonnainfo põhjal ning anonüümimiseks\nkasutatakse tööriista *revokeAndAnonymize*. Tööriist saab sisendiks tööriista *squash*\nvõi *checkAndSquash* poolt koostatud e-valimiskasti ning rakendab sellele sisendiks antud\ntühistus- ja ennistusnimekirjad.\n\n:revokeAndAnonymize.ballotbox:\n        Korduvhäältest puhastatud e-valimiskast.\n\n:revokeAndAnonymize.ballotbox_checksum:\n        Korduvhäältest puhastatud e-valimiskasti digitaalselt allkirjastatud räsi.\n\n:revokeAndAnonymize.districts:\n        Digitaalselt allkirjastatud ringkondade nimekiri.\n\n:revokeAndAnonymize.revocationlists:\n        Tühistus- ja ennistusnimekirjade loend. Võib olla tühi.\n\n:revokeAndAnonymize.out:\n        Tööriista väljundkaust. Sellesse kausta tekivad:\n\n        #. Korduvhääletajate häältest puhastatud ning anonüümitud e-valimiskast\n           :file:`<valimise id>-bb-4.json`;\n\n        #. Korduvhääletajate häältest puhastatud ning anonüümitud e-valimiskasti räsi\n           :file:`<valimise id>-bb-4.json.sha256sum`;\n\n        #. Tühistamiste ja ennistamiste aruanne :file:`<valimise\n           id>-revocation-report.csv`;\n\n        #. Tühistamiste ja ennistamiste aruanne ilma isikuandmeteta\n           :file:`<valimise\n           id>-revocation-report.csv.anonymous`;\n\n        #. E-hääletanute nimekiri JSON-vormingus :file:`<valimise\n           id>-ivoterlist.json``;\n\n        #. *Log2* fail e. tühistatud hääled\n           :file:`<valimise id>.<küsimuse id>.log2`.\n\n        #. *Log3* fail e. lugemisele läinud hääled :file:`<valimise\n           id>.<küsimuse id>.log3`.\n\n:file:`processor.revokeAndAnonymize.yaml`:\n\n.. literalinclude:: config-examples/processor.revokeAndAnonymize.yaml\n   :language: yaml\n   :linenos:\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/valijarakendus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _valijarakendus:\n\nValijarakenduse seadistamine\n============================\n\nValijarakenduse seadistamist ning pakendamist käsitleb eraldi dokument.\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/votmerakendus.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\n.. _app-key:\n\nVõtmerakendus\n=============\n\nVõtmerakendus `key` koosneb tööriistadest *groupgen*, *init*, *testkey*,\n*decrypt* ja *util*. Kõigi tööriistade kasutamine eeldab allkirjastatud\nusaldusjuure ja konkreetse tööriista seadistuste olemasolu. Alljärgnevalt\nkirjeldame konkreetsete tööriistade seadistusi.\n\n.. _key-groupgen:\n\nHäälte salastamise võtme spetsifikatsiooni valimine\n--------------------------------------------------------------------------------\n\nKasutamaks ElGamali krüptosüsteemi häälte krüpteerimiseks, on oluline häälte\nsalastamise võtme spetsifikatsiooni valimine ehk kasutatavate rühma parameetrite\nvalimine, milles tehakse matemaatilisi operatsioone. Oluline on, et antud\nparameetrid oleksid valitud läbipaistvalt, vältimaks tagauste olemasolu, mille\nabil oleks võimalik ilma salajast võtit omamata krüpteeritud hääli avada.\n\nKuna turvalisuse jaoks peavad rühma parameetrid vastama teatud tingimustele,\nsiis nende valimiseks pole kiiret meetodit. Sobivate rühmaparameetrite\nleidmiseks tuleb juhuslikult valida mingid parameetrid ja kontrollida, kas need\nvastavad antud tingimustele.\n\nRühma parameetrite genereerimise protsessi on võimalik läbipaistvalt läbi viia\nkahel viisil:\n\n  #. Kasutades teadaolevaid defineeritud parameetreid\n  #. Parameetreid avaliku algoritmi alusel deterministlikult genereerides\n\n\nTeadaolevate parameetrite kasutamine\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMitmed standardid ja rakendused on juba defineerinud parameetrid, mida on sobiv\nkasutada ElGamal krüptosüsteemis. Kasutades laialt levinud parameetreid on\nsuurem tõenäosus, et neid on sõltumatult kontrollitud.\n\nÜheks selliseks standardiks on [RFC3526]_, mis kasutab samuti deterministlikku\nparameetrite genereerimist. Antud standardi korral saab kontrollida defineeritud\nparameetrite korrektsust järgneva Sage skriptiga:\n\n.. literalinclude:: genparam.py\n   :language: python\n   :linenos:\n\n\nUute parameetrite deterministlik ja kontrollitav genereerimine\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nElGamal krüptosüsteemi jaoks sobilikke parameetreid saab genereerida kasutades\ntööriista *groupgen*.\n\nVõtmespetsifikatsiooni genereerimine on ajaliselt mahukas tegevus, mis võib\nolenevalt riistvarast kesta tunde. Ühekordselt genereeritud rühm on kasutatav\nmitmetel valimistel.\n\n:groupgen.paramtype: ElGamal'i krüptosüsteemi töö aluseks oleva rühma\n                     tüüp. Toetatud väärtused:\n                     #. ``mod`` - jäägiklassiring ``Zp``\n\n:groupgen.length: ElGamal'i krüptosüsteemi töö aluseks olevat rühma iseloomustav\n                  turvaparameeter. Jäägiklassiringide korral on sobiv väärtus\n                  3072.\n\n:groupgen.init_template: Asukoht, kuhu kirjutatakse rühma parameetrid. Väljund\n                         sobib kasutamiseks võtme genereerimise seadistuse\n                         koostamisel.\n\n:groupgen.random_source: Juhuarvugeneraatori sisendiks kasutatavate allikate\n                         loetelu. Vaata ka :numref:`random-gen`.\n\nElliptkõveraid kasutades on toetatud kõver P-384, mille parameetrite\ngenereerimine ei ole vajalik.\n\nKasutades juhuslike parameetrite leidmiseks juhuarvugeneraatorit, mille\nalgväärtus on üheselt defineeritav ning avalikustatud, võivad kolmandad\nosapooled kontrollida, et avaldatud rühma parameetrid on esimesed sellised\nleitud parameetrid, mis vastavad tingimustele. Näitekonfiguratsioon kasutab\nDPRNG'd avaliku seemnefailiga. Vaata ka :numref:`random-gen`.\n\n:file:`key.groupgen.yaml`:\n\n.. literalinclude:: config-examples/key.groupgen.yaml\n   :language: yaml\n   :linenos:\n\nSellise seadistuse korral loetakse juhuarvugeneraatori algväärtus failist\n``public_seed_file``. Oluline on, et sellisel juhul käivitatakse võtmerakendus\nühelõimelisena::\n\n  $ key groupgen --conf usaldusjuur.asice --params key.groupgen.asice --threads 1\n\n.. _key-init:\n\nHäälte salastamise võtme genereerimine\n--------------------------------------\n\nHäälte salastamise võtme genereerimiseks kasutatakse võtmerakenduse tööriista\n*init*. Võti genereeritakse seadistustes näidatud läviskeemiga ``MofN``, mis\ntähendab, et N võtmehaldurist peavad häälte dekrüpteerimisel osalema vähemalt M\nhaldurit, vastasel juhul ei ole dekrüpteerimine võimalik.\n\n\n:init.identifier: Valimise unikaalne identifikaator.\n\n:init.out:\n\n       Võtmerakenduse tööriista *init* väljundkataloog. Sellesse kataloogi\n       tekivad\n\n       #. PEM vormingus allkirjavõtme sertifikaat (``sign.pem``)\n       #. PEM vormingus krüpteerimisvõtme sertifikaat (``enc.pem``)\n       #. PEM vormingus krüpteerimisvõti (``pub.pem``)\n       #. DER vormingus krüpteerimisvõti (``pub.der``)\n\n:init.skiptest: Võtmeosakute kontrolltestide vahelejätmine.\n\n:init.fastmode: Kaartidele automaatne terminalide määramine. Vaikimise väärtus\n                on tõene.\n\n----\n\n:init.paramtype: ElGamal krüptosüsteemi aluseks oleva rühma parameetrid, mis\n                 ühtlasi määravad võtme turvataseme. Korraga saab olla\n                 kasutusel vaid ühe rühma spetsifikatsioon - kas\n                 jäägiklassiring või elliptkõver\n\n:init.paramtype.ec: Elliptkõverat määravad parameetrid.\n\n:init.paramtype.ec.name: Kasutatava kõvera nimi, hetkel P-384.\n\n:init.paramtype.mod: Jäägiklassiringi määravad parameetrid kümnendesituses.\n                     Parameetrid võib luua võtmerakenduse tööriista *groupgen*\n                     kasutades.\n\n:init.paramtype.mod.p: Jäägiklassiringi moodul.\n\n:init.paramtype.mod.g: Jäägiklassiringi generaator.\n\n----\n\n:init.signaturekeylen: Võtmerakenduse poolt genereeritava allkirjastamise võtme\n                       pikkus.\n\n:init.signcn: Võtmerakenduse poolt loodava allkirjastamise sertifikaadi subjekti\n              nimi (väli *CN*).\n\n:init.signsn: Võtmerakenduse poolt loodava allkirjastamise sertifikaadi\n              järjekorranumber.\n\n:init.enccn: Võtmerakenduse poolt loodava krüpteerimise sertifikaadi subjekti\n             nimi (väli *CN*).\n\n:init.encsn: Võtmerakenduse poolt loodava krüpteerimise sertifikaadi\n             järjekorranumber.\n\n----\n\n:init.required_randomness: Juhuslikkuse allikatest loetava entroopia kohustuslik\n                           hulk baitides.\n\n:init.random_source: Juhuarvugeneraatori sisendiks kasutatavate allikate\n                     loetelu. Vaata ka :numref:`random-gen`.\n\n----\n\n:init.genprotocol: Võtme genereerimiseks kasutatava algoritmi ja läviskeemi\n                   spetsifikatsioon.\n\n:init.genprotocol.desmedt:\n\n      Algoritmi Desmedt korral genereeritakse võti usaldatava osakujagaja poolt\n      ehk võtmerakenduse mälus. Privaatvõtme osakud talletatakse kiipkaartidel.\n\n      Täiendavalt tuleb määrata läviskeemi osaliste arv ja minimaalne kvoorum.\n\n      Kaartide arv 7 - võimalikud kvoorumid 1,2,3,4 - soovitatav kvoorum 4\n      Kaartide arv 8 - võimalikud kvoorumid 1,2,3,4 - soovitatav kvoorum 4\n      Kaartide arv 9 - võimalikud kvoorumid 1,2,3,4,5 - soovitatav kvoorum 5\n\n:init.genprotocol.desmedt.threshold: Läviskeemi M väärtus - kvoorum.\n\n:init.genprotocol.desmedt.parties: Läviskeemi N väärtus.\n\n:file:`key.init.yaml`:\n\n.. literalinclude:: config-examples/key.init.yaml\n   :language: yaml\n   :linenos:\n\n\n\n.. _key-testkey:\n\nHäälte salastamise võtme testimine\n----------------------------------\n\nHäälte salastamise võtme testimine kontrollib võtme rekonstrueerimise võimekust\nselliselt, et iga osak osaleb vähemalt kahes kvoorumis. Testimiseks on vajalik\nkõigi osakute osalemine.\n\n:testkey.identifier: Valimise unikaalne identifikaator.\n\n:testkey.out: Krüpteerimise avaliku võtme asukoha kataloog.\n\n:testkey.threshold: Testimiseks kasutatav lävi, sama mis võtme loomisel\n                    spetsifitseeritud.\n\n:testkey.parties: Testimiseks kasutatav osapoolte arv, sama mis võtme loomisel\n                  spetsifitseeritud.\n\n:testkey.fastmode: Kaartidele automaatne terminalide määramine. Vaikimise\n                   väärtus on tõene.\n\n\n:file:`key.testkey.yaml`:\n\n.. literalinclude:: config-examples/key.testkey.yaml\n   :language: yaml\n   :linenos:\n\n\n.. _key-decrypt:\n\nE-häälte dekrüpteerimine\n--------------------------------------\n\nElektrooniliste häälte dekrüpteerimiseks kasutatakse võtmerakenduse tööriista\n*decrypt*. Dekrüpteerimise õnnestumiseks peab osalema läviskeemi poolt määratud\nkvoorumi jagu võtmehaldureid. Kui rakendati skeemi ``5of9``, siis osaleb\ndekrüpteerimisel täpselt 5 võtmehaldurit. Vähema arvu haldurite korral ei ole\ndekrüpteerimine võimalik.\n\n:decrypt.identifier:\n\n        Valimise unikaalne identifikaator.\n\n----\n\n:decrypt.protocol:\n\n:decrypt.protocol.recover:\n\n      Algoritmi Desmedt korral genereeritakse võti usaldatava osakujagaja poolt\n      ehk võtmerakenduse mälus. Privaatvõtme osakud talletatakse kiipkaartidel.\n\n:decrypt.protocol.recover.threshold:\n\n      Läviskeemi M väärtus - kvoorum, mis spetsifitseeriti võtme loomisel.\n\n:decrypt.protocol.recover.parties:\n\n      Läviskeemi N väärtus, mis spetsifitseeriti võtme loomisel.\n\n----\n\n:decrypt.anonballotbox:\n\n      Töötlemisrakenduse või miksimisrakenduse poolt loodud e-valimiskast anonüümistatud\n      häältega.\n\n:decrypt.anonballotbox_checksum:\n\n      Anonüümistatud häältega e-valimiskasti allkirjastatud SHA256 kontrollsummafail.\n\n:decrypt.questioncount:\n\n      Küsimuste arv anonüümistatud e-valimiskastis. Vaikimisi väärtus on 1.\n\n:decrypt.candidates:\n\n      Valimise valikute nimekiri allkirjastatud kujul.\n\n:decrypt.districts:\n\n      Valimise ringkondade nimekiri allkirjastatud kujul.\n\n:decrypt.provable:\n\n      Valikuline korrektse dekrüpteerimise tõestuse väljastamine. Vaikimisi\n      väärtus on tõene.\n\n:decrypt.prove_invalid:\n\n      Valikuline korrektse dekrüpteerimise tõestuse väljastamine ka\n      ebakorrektsete sedelite kohta. Eeldab dekrüpteerimise tõestuse\n      väljastamist. Vaikimisi väärtus on väär.\n\n:decrypt.check_decodable:\n\n      Krüptogrammide korrektsuse kontrollimine enne dekrüpteerimist. Juhul kui\n      krüptogrammide sisend ei tule usaldatud allikast, siis tuleb kontrollida\n      krüptogrammide korrektsust. Usaldatud allikad on töötlemisrakendus ning\n      miksija. Vaikimisi väärus on väär.\n\n:decrypt.out:\n\n      Võtmerakenduse tööriista *decrypt* väljundkataloog. Eduka dekrüpteerimise\n      korral tekivad siia kausta:\n\n      #. Elektroonilise hääletamise tulemus\n      #. Elektroonilise hääletamise tulemuse signatuur\n      #. Dekrüpteeritud valimiskast\n      #. Dekrüpteeritud valimiskasti signatuur\n      #. Loend kehtetutest sedelitest\n      #. Kehtivate sedelite lugemistõend\n      #. Kehtetute sedelite lugemistõend (valikuline)\n\nDekrüpteeritud valimiskast tundlikke andmeid ei sisalda.\n\n:file:`key.decrypt.yaml`:\n\n.. literalinclude:: config-examples/key.decrypt.yaml\n   :language: yaml\n   :linenos:\n\nPärast dekrüpteerimist on võimalik kontrollida väljastatud elektroonilise\nhääletamise tulemuse signatuuri korrektsust. Selleks tuleb teha järgnevad\nsammud:\n\n1. Eraldada allkirja kontrollimise võti allkirjastamise sertifikaadist::\n\n    openssl x509 -in initout/sign.pem -noout -pubkey > sign.pub\n\n2. Kontrollida hääletamise tulemuse allkirja::\n\n    openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt \\\n    rsa_pss_saltlen:32 -sigopt rsa_mgf1_md:sha256 -verify sign.pub \\\n    -signature decout/TESTCONF.tally.signature decout/TESTCONF.tally\n\nKorrektse allkirja korral kuvatakse väärtust `Verified OK`.\n\nSarnaste sammudega on võimalik kontrollida ka väljastatud dekrüpteeritud\nvalimiskasti signatuuri korrektsust.\n\nVõtmerakenduse täiendavad tööriistad\n------------------------------------\n\n:util.listreaders: Loetle ühendatud kaardilugejad.\n\n:file:`key.util.yaml`:\n\n.. literalinclude:: config-examples/key.util.yaml\n   :language: yaml\n   :linenos:\n\n\n.. _random-gen:\n\nJuhuarvude genereerimine võtmerakenduses\n----------------------------------------\n\nVõtmerakenduse tööriistad *groupgen* ja *init* vajavad oma tööks juhuarve, mille\ngenereerimiseks on võimalik kasutada erinevaid entroopiaallikaid, mis\nvõtmerakenduse poolt üheks allikaks kombineeritakse.\n\nKombineerimisel on oluline, et säiliks sisendite sõltumatus, st. kombineeritud\nväljund ei tohi olla kehvem ühestki sisendist. IVXV raamistikus toimub entroopia\nkombineerimine SHAKE-256 muutuva väljundipikkusega räsifunktsiooni abil (XOF),\nkasutades skeemi nagu on kirjeldatud [BDPA10]_.\n\nLõpliku pikkusega entroopiaallika kasutamisel loetakse kogu väärtus ning antakse\nsee SHAKE-256 sisendiks. Piiramata pikkusega allika lisamisel salvestatakse\nselle viide kombineerija mällu.\n\nPärides kombineerijast töödeldud juhuslikkust, loetakse kõigepealt igast\nsalvestatud entroopia allikast sama palju baite ning antakse see SHAKE-256\nsisendiks. Seejärel kopeeritakse SHAKE-256 isend, muudetakse kopeeritud\nSHAKE-256 režiim lugemisele ning loetakse nõutud baitide jagu väljundit.\n\n\n:random_source: Juhuarvugeneraatori sisendiks kasutatavate allikate loetelu.\n\n:random_source.random_source_type: Juhuarvugeneraatori allika tüüp.\n\n:random_source.random_source_path: Juhuarvugeneraatori allika seadistatav\n                                   asukoht. Argument on valikuline sõltuvalt\n                                   allika tüübist.\n\n----\n\n:random_source_type\\: file: Entroopia lugemine failist.\n\n:random_source_path\\: `randomness_file`: Kasutatav fail.\n\n----\n\n:random_source_type\\: system: Operatsioonisüsteemi poolt pakutav\n                              entroopiaallikas (Linuxil `/dev/urandom`).\n\n----\n\n:random_source_type\\: DPRNG: Deterministlik pseudojuhuslik generaator (DPRNG) on\n                             mõeldud baidijadade genereerimiseks, kasutades\n                             etteantud seemneväärtust. Sama seemneväärtuse\n                             korral genereerib meetod alati sama jada.\n:random_source_path\\: `seed_file`: DPRNG seemneväärtus saadakse viidatud faili\n                                   SHA256 räsides.\n\n----\n\n:random_source_type\\: stream: Entroopia lugemine voogseadmelt.\n\n:random_source_path\\: `/dev/urandom`: Kasutatav seade.\n\n----\n\n:random_source_type\\: user: Välise programmi käest üle sokli juhuslikkust hankiv\n                            entroopiaallikas. Kasutusel on IVXV-spetsiifiline\n                            protokoll.\n\n:random_source_path\\: `user_entropy.exe`: Tee programmini, mis tuleb\n                                          juhuslikkuse hankimiseks käivitada.\n\n\nEntroopiaallikate kombineerimine kirjeldatud viisil võimaldab realiseerida\nerinevaid juhuarvude genereerimise stsenaariumeid. Näiteks häälte salastamise\nvõtme genereerimise korral on vaja tagada võtme konfidentsiaalsus ning olla\nkindel, et genereerimisprotsess ei ole hiljem korratav. Näitekonfiguratsioon\nkasutab välist programmi kasutaja sisendi lugemiseks ning süsteemset\njuhuarvugeneraatorit:\n\n:file:`rnd.init.yaml`:\n\n.. literalinclude:: config-examples/rnd.init.yaml\n   :language: yaml\n   :linenos:\n\n\nRakendus käivitatakse mitmelõimelisena::\n\n  $ key init --conf usaldusjuur.asice --params rnd.init.asice\n\nHäälte salastamise võtme spetsifikatsioon on erinevalt häälte salastamise\nvõtmest avalik ning selle genereerimisel kasutatud juhuslikkuse võib samuti teha\navalikuks. Näitekonfiguratsioon kasutab DPRNG'd avaliku seemnefailiga.\n\n:file:`rnd.groupgen.yaml`:\n\n.. literalinclude:: config-examples/rnd.groupgen.yaml\n   :language: yaml\n   :linenos:\n\nKui eesmärk on genereerimisprotsessi korratavus, tuleb rakendus käivitada\nühelõimelisena::\n\n  $ key groupgen --conf usaldusjuur.asice --params rnd.groupgen.asice --threads 1\n\n\n\n.. [BDPA10] G. Bertoni, J. Daemen, M. Peeters, G. Van Assche: Sponge-Based\n   Pseudo-Random Number Generators. CHES 2010: 33-47\n\n.. [RFC3526] T. Kivinen, M. Kojo: More Modular Exponential (MODP) Diffie-Hellman\n   groups for Internet Key Exchange (IKE). IETF RFC3526, 2003\n"
  },
  {
    "path": "Documentation/et/seadistuste_koostejuhend/ylevaade.rst",
    "content": "..  IVXV kogumisteenuse haldusteenuse kirjeldus\n\nIVXV seadistused valimise korraldamise protsessis\n-------------------------------------------------\n\nIVXV kasutamiseks valimise kontekstis tuleb süsteem ja sellega seotud\nrakendused seadistada nii, et on võimalik valijatelt häälte vastuvõtmine ning\nnende käitlemine vastavalt süsteemile seatud terviklus-, konfidentsiaalsus ja\nkäideldavusnõuetele. Käesolev tehniline dokument annab ülevaate olulisimatest\nseadistustoimingutest ning on mõeldud täiendama elektroonilise hääletamise\nkäsiraamatu poolt kirjeldatud protseduurireeglite täitmist.\n\nSeadistuste koostamiseks vajalikud andmed\n*****************************************\n\nValimise üldparameetrid\n  Valimise üldparameetrid määravad valimise unikaalse identifikaatori\n  kasutamiseks kõigi seotud komponentide poolt, küsimuste arvu ning\n  identifikaatorid, hääletamisperioodi alguse- ja lõpuaja ning hääle\n  kontrollimise seadistuse. Valimise üldparameetrite spetsifikatsiooni\n  käsitletakse käesolevas dokumendis.\n\nAlgne valijate nimekiri\n  Algne valijate nimekiri on kohandatud vormingus fail, mille vorming ning\n  seotud protokollid on defineeritud dokumendis \"IVXV protokollid\". Eesti\n  riiklike valimiste korral tuleb algne valijate nimekiri Rahvastikuregistrist.\n\nValikute nimekiri\n  Valikute nimekiri on JSON vormingus fail, mille vorming ning seotud\n  protokollid on defineeritud dokumendis \"IVXV protokollid\". Eesti riiklike\n  valimiste korral tuleb valikute nimekiri valimiste infosüsteemist.\n\nRingkondade nimekiri\n  Ringkondade nimekiri on JSON vormingus fail, mille vorming ning seotud\n  protokollid on defineeritud dokumendis \"IVXV protokollid\". Eesti riiklike\n  valimiste korral tuleb ringkondade nimekiri valimiste infosüsteemist.\n\nRakenduste usaldusjuur\n  Rakenduste usaldusjuur defineerib sertifitseerimishierarhiad, mille alusel\n  IVXV rakendused verifitseerivad digitaalallkirju. Eesti riiklike valimiste\n  korral määrab usaldusjuure koosseisu Riigi Valimisteenistus. Rakenduste\n  usaldusjuure vormingut käsitletakse peatükis :numref:`ivxv-rakendused`.\n\nKogumisteenuse usaldusjuur\n  Kogumisteenuse usaldusjuur defineerib sertifitseerimishierarhiad, mille\n  alusel IVXV kogumisteenuse komponendid verifitseerivad digitaalallkirju. Eesti\n  riiklike valimiste korral määrab usaldusjuure koosseisu Riigi\n  Valimisteenistus. Kogumisteenuse usaldusjuure vormingut ning seotud protokolle\n  käsitletakse peatükis :numref:`kogumisteenus`.\n\nKogumisteenuse tehniline seadistus\n  Kogumisteenuse tehniline seadistus kirjeldab IVXV mikroteenuste seadistuse\n  ning isendite jaotumise. Eesti riiklike valimiste korral leiab kogumisteenuse\n  osutaja Riigi Valimisteenistus. Tehniline seadistus kooskõlastatakse\n  valimiste omaniku ja kogumisteenuse osutaja vahel. Tehnilist seadistust\n  käsitletakse peatükis :numref:`kt-technical`.\n\nKogumisteenuse võtmed ja sertifikaadid\n  Kogumisteenuse mikroteenused suhtlevad omavahel TLS protokolli vahendusel.\n  Vastavad sertifikaadid tuleb eksportida Valijarakendusse ja\n  Kontrollrakendusse. Kogumisteenusega seotud võtmete loomist käsitletakse\n  peatükis :numref:`kt-krypto`.\n\nHäälte salastamise võtme spetsifikatsioon\n  Häälte salastamise võtme jaoks kasutatav algoritm ning seotud tehnilised\n  parameetrid fikseeritakse enne häälte salastamise võtme genereerimist. Võtme\n  spetsifikatsiooni käsitletakse peatükis :numref:`key-groupgen`.\n\nHääletamisperioodile eelnevad tegevused\n***************************************\n\nEnne hääletamisperioodi algust teostatakse lähtuvalt eelnevatest andmetest\njärgmised tegevused:\n\n#. :numref:`app-install`\n#. :numref:`app-trust`\n#. :numref:`key-groupgen`\n#. :numref:`key-init`\n#. :numref:`key-testkey`\n#. :numref:`kt-trust`\n#. :numref:`kt-technical`\n#. :numref:`kt-election`\n#. Ringkondade nimekirja laadimine Kogumisteenusesse\n#. Valikute nimekirja laadimine Kogumisteenusesse\n#. Valijate nimekirja (algne) laadimine Kogumisteenusesse\n#. :numref:`kt-management`\n#. :numref:`valijarakendus`\n#. :numref:`kontroll`\n\nHääletamisperioodi tegevused\n****************************\n\n#. Valijate nimekirjade (muudatused) laadimine Kogumisteenusesse\n\nHääletamisperioodile järgnevad tegevused\n****************************************\n\nE-valimiskasti töötlemine\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n#. :numref:`processor-check`\n#. :numref:`processor-squash`\n#. :numref:`processor-revoke`\n#. :numref:`processor-anonymize`\n\nHäälte miksimine\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n#. :numref:`mix-install`\n#. :numref:`mix-mix`\n#. :numref:`mix-verify`\n\nHääletamistulemuse väljaselgitamine ja andmeaudit\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n#. :numref:`key-decrypt`\n#. :numref:`auditor-convert`\n#. :numref:`auditor-mix`\n#. :numref:`auditor-decrypt`\n"
  },
  {
    "path": "Documentation/et/votmerakendus/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/votmerakendus/index.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\n======================================================================\nIVXV tehniline dokumentatsioon\n======================================================================\n\n-------------\nVõtmerakendus\n-------------\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 3\n   :numbered:\n\n   sissejuhatus\n   votmeosakute_protokollid\n   viited\n"
  },
  {
    "path": "Documentation/et/votmerakendus/sissejuhatus.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nSissejuhatus\n============\n\nVõtmerakendus on Korraldaja põhitööriist, millega genereeritakse iga hääletamise jaoks häälte salastamise ja häälte avamise võti. Võtmerakenduse abil toimub ka häälte lugemine ja tulemuse väljastamine.\n\nDokumendis spetsifitseeritakse võtmerakenduse tehnilised detailid.\n"
  },
  {
    "path": "Documentation/et/votmerakendus/viited.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nViited\n======\n\n.. [DF89] Desmedt, Y. & Frankel, Y. Brassard, G. (Ed.). Threshold Cryptosystems.\n   Advances in Cryptology - CRYPTO '89, 9th Annual International Cryptology\n   Conference, Santa Barbara, California, USA, August 20-24, 1989,\n   Proceedings, Springer, 1989, 435, 307-315\n.. [Shoup00] Shoup, V. Practical Threshold Signatures Advances in Cryptology -\n   EUROCRYPT 2000, International Conference on the Theory and Application of\n   Cryptographic Techniques, Bruges, Belgium, May 14-18, 2000, Proceeding, 2000,\n   207-220\n.. [RFC8017]  PKCS #1: RSA Cryptography Specifications Version 2.2.\n   https://tools.ietf.org/html/rfc8017\n"
  },
  {
    "path": "Documentation/et/votmerakendus/votmeosakute_protokollid.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nEritüübiliste võtmeosakute protokollide liidestamine\n====================================================\n\nVõtmeosakute genereerimise protokolli liides\n--------------------------------------------\n\nKlass `ee.ivxv.key.protocol.GenerationProtocol` defineerib liidese, mida\nElGamali või RSA võtmeosakute genereerimise protokoll peab täitma. Liides on\njärgnev::\n\n    public interface GenerationProtocol {\n        byte[] generateKey() throws ProtocolException;\n    }\n\nEritüübiliste võtmeosakute genereerimiseks peab implementeerima `generateKey()`\nmeetodi, mis tagastab kodeeritud avaliku võtme X.509 sertifikaadina DER\nformaadis. Protokolli klass peab olema paki `ee.ivxv.key.protocol.generation`\nalampakis.\n\nProtokolli parameetrid tuleb määrata protokolli klassiinstantsi\ninitsialiseerimise ajal.\n\nDekrüpteerimise protokolli liides\n---------------------------------\n\nDekrüpteerimise ja võtmeosakute genereerimise protokoll ei pea olema üksüheses\nseoses, st. võtmeosakute genereerimise protokollile võib vastata mitu\ndekrüpteerimise protokolli. Seega on dekrüpteerimise protokolli liides\ndefineeritud sõltumatult võtmeosakute genereerimise liidesest. Protokoll peab\nimplementeerima `ee.ivxv.key.protocol.DecryptionProtocol` liidese::\n\n    public interface DecryptionProtocol {\n        ElGamalDecryptionProof decryptMessage(byte[] msg) throws ProtocolException;\n    }\n\nMeetod `decryptMessage()` võtab sisendina krüptogrammi DER formaadis ning\nväljastab `ElGamalDecryptionProof` instantsi. Juhul kui protokoll ei toeta\nlugemistõendi väljastamist, siis on vastavad väljad väärtustatud tühiväärtusega\n(`null`).\n\nAnaloogselt võtmeosakute genereerimise protokollile tuleb protokolli\nparameetrid määrata klassiinstantsi initsialiseerimise ajal.\n\nAllkirjastamise protokolli liides\n---------------------------------\n\nLisaks dekrüpteerimise protokollile saab implementeerida ka allkirjastamise\nprotokolli. Sarnaselt dekrüpteerimisprotokollile võib ühele võtmegenereerimise\nmeetodile vastata mitu allkirjastamise protokolli. Protokoll peab\nimplementeerima `ee.ivxv.key.protocol.SigningProtocol` liidese::\n\n    public interface SigningProtocol {\n        byte[] sign(byte[] msg) throws ProtocolException;\n    }\n\nMeetod `sign()` võtab sisendina sõnumi, mida soovitakse allkirjastada, ja\nväljastab RSA-PSS allkirja järgnevate parameetritega:\n\n.. _RSA-PSS parameetrid:\n\n- sõnumi räsifunktsioon: SHA2-256\n- maski genereerimise funktsioon: MGF1, maski räsifunktsioon SHA2-256 ja maski\n  pikkus 32 baiti\n- soola pikkus: 32 baiti\n- sababait: `0xbc`\n\nAnaloogselt võtmeosakute genereerimise protokollile tuleb protokolli\nparameetrid määrata klassiinstantsi initsialiseerimise ajal.\n\nToetatud protokollid\n--------------------\n\nHetkel on teostatud järgnevad võtmeosakute genereerimise protokollid:\n\n* `ee.ivxv.key.protocol.generation.desmedt.DesmedtGeneration`: Võtmeosakud on\n  sellised, et oleks võimalik kasutada [DF89]_ hajutatud\n  dekrüpteerimisprotokolli. Võtmeosakud salvestatakse otse PKCS15 liidest\n  toetavale pääsmikule. Klassiinstantsi initsialiseerimise ajal saab anda\n  järgnevaid argumente:\n\n  + `PKCS15Card[] cards`: järjend objektides mis implementeerivad PKCS15Card\n    liidest (nt. kiipkaardid või tarkvaralised pääsmikud).\n  + `ElGamalParameters params`: ElGamali krüptosüsteemi parameetrid.\n  + `ThresholdParameters tparams`: läviskeemi parameetrid.\n  + `Rnd rnd`: juhuslikkuse sisend võtmeosakute genereerimisel\n  + `byte[] cardShareAID`: võtmeosaku ligipääsuidentifikaator PKCS15\n    pääsmikul. Defineerib, millise ligipääsutunnusega pääseb võtmeosakule\n    ligi.\n  + `byte[] cardShareName`: võtmeosaku identifikaator PKCS15 pääsmikul.\n\n* `ee.ivxv.key.protocol.generation.shoup.ShoupGeneration`: Võtmeosakud on\n  sellised, et oleks võimalik kasutada [Shoup00]_ põhinevat hajutatud\n  allkirjastamisprotokolli. Võtmeosakud salvestatakse otse PKCS15 liidest\n  toetavale pääsmikule. Klassiinstantsi initsialiseerimise ajal saab anda\n  järgnevaid argumente:\n\n  + `PKCS15Card[] cards`: järjend objektidest, mis implementeerivad PKCS15Card\n    liidest (nt. kiipkaardid või tarkvaralised pääsmikud).\n  + `int modLen`: RSA võtme pikkus bittides\n  + `ThresholdParameters tparams`: läviskeemi parameetrid.\n  + `Rnd rnd`: juhuslikkuse sisend võtmeosakute genereerimisel\n  + `byte[] cardShareAID`: võtmeosaku ligipääsuidentifikaator PKCS15\n    pääsmikul. Defineerib, millise ligipääsutunnusega pääseb võtmeosakule\n    ligi.\n  + `byte[] cardShareName`: võtmeosaku identifikaator PKCS15 pääsmikul.\n\nOn teostatud järgnevad dekrüpteerimise protokollid:\n\n* `ee.ivxv.key.protocol.decryption.recover.RecoverDecryption`: Loetakse PKCS15\n  liidest toetavatelt pääsmikelt võtmeosakud, rekonstrueeritakse nende abil\n  operatiivmälus salajane võti ning teostatakse dekrüpteerimine lugemistõendiga.\n  Klassiinstantsi initsialiseerimise ajal saab anda järgnevaid argumente:\n\n  + `PKCS15Card[] cards`: järjend objektides mis implementeerivad PKCS15Card\n    liidest (nt. kiipkaardid või tarkvaralised pääsmikud).\n  + `ThresholdParameters tparams`: läviskeemi parameetrid.\n  + `byte[] cardShareAID`: võtmeosaku ligipääsuidentifikaator PKCS15\n    pääsmikul. Defineerib, millise ligipääsutunnusega pääseb võtmeosakule\n    ligi.\n  + `byte[] cardShareName`: võtmeosaku identifikaator PKCS15 pääsmikul.\n\nOn teostatud järgnevad allkirjastamise protokollid:\n\n* `ee.ivxv.key.protocol.signing.shoup.ShoupSigning`: Loetakse PKCS15 liidest\n  toetavatelt pääsmikelt võtmeosakud, konstrueeritakse mälus allkirjastamise\n  osakud ilma võtit rekonstrueerimata ning kombineeritakse allkirjastamise\n  osakud RSA-PSS allkirjaks. Klassiinstantsi initsialiseerimise ajal saab anda\n  järgnevaid argumente:\n\n  + `PKCS15Card[] cards`: järjend objektides mis implementeerivad PKCS15Card\n    liidest (nt. kiipkaardid või tarkvaralised pääsmikud).\n  + `ThresholdParameters tparams`: läviskeemi parameetrid.\n  + `Rnd rnd`: juhuslikkuse sisend RSA-PSS allkirja soola genereerimisel.\n  + `byte[] cardShareAID`: võtmeosaku ligipääsuidentifikaator PKCS15\n    pääsmikul. Defineerib, millise ligipääsutunnusega pääseb võtmeosakule\n    ligi.\n  + `byte[] cardShareName`: võtmeosaku identifikaator PKCS15 pääsmikul.\n\nProtokollide võtmerakendusega liidestamine\n------------------------------------------\n\nJärgnev kirjeldus käib nii võtmeosakute genereerimise ja dekrüpteerimise\nprotokollide kohta.\n\n.. note:: Praegune kirjeldus on üldine. Kui konfi- ja argumentide parsimine on\n   lõplikult välja töötatud ning protokollid võtmerakendusega liidestatud, siis\n   tuleks järgnevat lõiku täiendada.\n\nLiidestamaks uut protokolli võtmerakendusega, tuleb kõigepealt teostada vastava\nprotokolli liidest täitev klass. Võtmerakendus peab töö alguses seadistuse\ntöötlemise käigus aru saama kas käsureaargumentidest või seadistusfailist,\nmillist protokolli soovitakse kasutada. Seejärel tuleb vastava klassi staatilise\nmeetodi abil ülejäänud käsureaargumentide või seadistusfaili abil\ninitsialiseerida uus protokolliklassi instants. Seejärel tuleb genereerida võti\nvõi dekrüpteerida sõnum.\n\nToetatud protokollide kirjeldused\n---------------------------------\n\nShamiri saladuse jagamise skeem\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOlgu meil salajane väärtus :math:`s = a_0` ja soovime seda jagada :math:`n`\nosapoole vahel selliselt, et vähemalt :math:`t` osapoolt saaksid selle saladuse\nrekonstrueerida.  Selleks valime koefitsiendid :math:`a_1` kuni :math:`a_{t-1}`\nning vaatame polünoomi muutuja :math:`x` suhtes:\n\n.. math::\n    P(x) = a_{t-1}  x^{t-1} + .. + a_1  x + a_0\n\nOlgu :math:`x_1` kuni :math:`x_n` nullist erinevad unikaalsed väärtused\n(üldiselt :math:`1` kuni :math:`n`), sellisel juhul saame osakud :math:`s_i =\nP(x_i)` ning salajase väärtuse :math:`s = P(0)`.\n\nGeomeetriliselt vaadates on :math:`P(x)` polünoom ning osakud punktid sellel\npolünoomil. Põhikoolimatemaatikast teame, et :math:`t-1` järku polünoomi\njoonistamiseks piisab meile :math:`t` punktist (sirge jaoks kaks punkti,\nparabooli jaoks kolm punkti jne.). Salajane väärtus on selle polünoomi väärtus\ny-telje lõikepunktis.\n\nVaadates rekonstrueerimist arvuliselt, mitte geomeetriliselt, saame kasutades\nLagrange interpoleerimise meetodit. Tähist :math:`\\prod` kasutame me mitme\nliikmega korrutise tähistamiseks ja tähist :math:`\\sum` kasutame me mitme\nliikmega summa tähistamiseks.\n\nNüüd, tähistame lisaks :math:`t` osapoolt, kes osalevad salajase väärtuse\nrekonstrueerimisel tähisega :math:`U`. Lagrange interpoleerimise valem ütleb:\n\n.. math::\n    \\overline{P}(x) = \\sum\\limits_{j \\in U} s_j \\frac{\\prod\\limits_{i \\in U, j \\neq i}x-x_i}{\\prod\\limits_{i \\in U, j \\neq i}x_j-x_i}\n\nTõepoolest: fikseerime :math:`j` - paneme tähele, et kui :math:`x = x_j`, siis\nmurru väärtus on :math:`1` (kuna lugejas ja nimetajas olevad kordajad taandavad\nüksteist) ja kui :math:`x \\neq x_j`, kuid :math:`x = x_k`, mingi muu :math:`k\n\\in U` korral, siis murd on :math:`0` (kuna lugejas on :math:`x_k - x_i = 0`\nmingi :math:`i \\in U` korral). Seega:\n\n.. math::\n    \\overline{P}(x_j) = s_j + 0 \\sum_{i \\in U, i \\neq j} s_i = s_j = P(x_j)\n\nKuna osapooled teavad väärtuseid :math:`s_i = P(x_i)` (osakud), siis\nkombineerides ning korrutades need läbi baaspolünoomiga\n\n.. math::\n    L(U,x,j) = \\frac{\\prod\\limits_{i \\in U, j \\neq i}x - x_i}{\\prod\\limits_{i \\in U, j \\neq i}x_j - x_i}\n\nja fikseerides :math:`x = 0`, saame jagatud saladuse.\n\n`ee.ivxv.key.protocol.generation.desmedt.DesmedtGeneration`\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nArvestades, et ElGamali võtmeparameetriks on rühm :math:`G` koos generaatoriga\n:math:`g`, siis salajaseks võtmeks valitakse :math:`x`, mis on ülimalt\n:math:`ord(g)`, st. :math:`g` multiplikatiivne järk rühmas :math:`G`. Vastavaks\navalikuks võtmeks võetakse väärtus :math:`y = g^x`. Salajase võtme komplektiks\nsaab väärtus :math:`(G, g, x)` ja avaliku võtme komplektiks väärtus :math:`(G,\ng, y)`. Rühm :math:`G` valitakse selliselt, et tema järk on mingi algarv\n:math:`p` selliselt, et kehtib :math:`p = 2  q + 1`, kus :math:`q` on samuti\nalgarv. Selliselt juhul kirjeldab :math:`G` väärtust algarv :math:`p`.\n\nAlgebrast teame, et kui :math:`G` järk on :math:`2q + 1`, siis iga selle rühma\nelemendi järk on kas :math:`1`, :math:`2`, :math:`q` või :math:`2q`. Me oleme\nhuvitatud selliselt generaatorist, mille järk on :math:`q` ja mis on ruutjääk,\nkuna see genereerib piisavalt suure alamrühma, mille kõik elemendid on\nruutjäägid. Vastasel juhul võib toimuda ühe biti lekkimine krüpteeritud sõnumi\nkohta. Sellise generaatori leidmiseks vaatame me rühma suvalisi elemente ning\nkontrollime tema järku ning ruutjäägilisust kuni leiame sobiva elemendi. Sellise\nelemendi määrame generaatoriks.\n\nInstants genereerib juhusliku :math:`0<x<q` salajaseks võtmeks, jagab selle\nShamiri ühissalastuse abil argumentidena antud osapoolte vahel. Iga salajase\nvõtme osak kodeeritakse kui ühissalastamata salajase võtme komplekt.\n\nSeejärel arvutatakse :math:`y = g^x` ning tagastatakse kodeeritud avaliku võtme\nkomplekt.\n\n`ee.ivxv.key.protocol.generation.shoup.ShoupGeneration`\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRSA võtmepaar genereeritakse järgnevalt: genereeritakse kaks algarvu :math:`p`\nja :math:`q` bitipikkusega :math:`\\textit{modLen}/2` ning võetakse :math:`n =\npq`. Avalik võti :math:`e` võetakse selliselt et :math:`\\gcd(e, \\phi(n)) = 1`,\nkuid antud protokollis on :math:`e` fikseeritud :math:`e=65537`. Seega tuleb\nvalida :math:`p` ja :math:`q` nii pikalt kui see tingimus kehtib. Salajane võti\n:math:`d` võetakse selliselt, et :math:`de \\equiv 1 \\pmod{\\phi(n)}`, kus\n:math:`\\phi` on Euleri :math:`\\phi`.\n\nArv :math:`\\phi(n)` näitab, kui paljud arvudest :math:`1 \\leq m < n` on sellised\net :math:`\\gcd(m,n) = 1`, kus :math:`\\gcd(a,b)` on kahe arvu :math:`a` ja\n:math:`b` suurim ühistegur. On ilmne, et kui :math:`p` on algarv, siis\n:math:`\\phi(p) = p-1`. Lisaks on lihtne näidata, et kui :math:`p` ja :math:`q`\non algarvud, siis :math:`\\phi(pq) = \\phi(p)\\phi(q)`.\n\nEuleri teoreem ütleb, et kui :math:`a` ja :math:`n` on ühistegurita, siis:\n\n.. math::\n    a^{\\phi(n)} \\equiv 1 \\pmod{n}\n\nSeega, kui sõnumi :math:`m` allkirjastamiseks tehakse :math:`s \\equiv m^d\n\\pmod{n}`, siis verifitseerimiseks kontrollitakse kas :math:`s^e \\equiv m\n\\pmod{n}`. Tõepoolest: :math:`(m^d)^e \\equiv m^{de} \\equiv m^{k\\phi(n)+1} \\equiv\nm^{k\\phi(n)}m \\equiv 1^km \\equiv m \\pmod{n}`.\n\nSalajane võti :math:`d` jagatakse Shamiri salastuse jagamisega osadeks, iga osa\nkodeeritakse kui jagamata salajase võtme komponent ning salvestatakse\nosapoolele. Avalik võtme komplekt kodeeritakse ning tagastatakse.\n\n`ee.ivxv.key.protocol.decryption.recover.RecoverDecryption`\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nProtokoll toimib, rekonstrueerides ElGamali võtme ning dekrüpteerides sellega\nkrüptogramme.\n\nTäpsemalt, olgu :math:`U` indeksid kaartidest, mis moodustavad argumendiks antud\n:math:`\\mathit{cards}` muutuja. Instants loeb salajase võtme komplektid\nkaartidelt, kontrollib võtmekomplektide terviklust (st. rühma :math:`G` ja\ngeneraatori :math:`g` kirjelduse ühesust), dekodeerib igast komplektist salajase\nvõtme :math:`x_i`.\n\nSeejärel arvutatakse salajane võti :math:`x` kasutades Lagrange\ninterpoleerimist:\n\n.. math::\n    x = P(0) = \\sum\\limits_{j\\in U} s_j \\frac{\\prod\\limits_{i\\in U, j \\neq i} -x_i}{\\prod\\limits_{i\\in U, j \\neq i} x_j-x_i}\n\nKrüptogrammi :math:`c=(c_1,c_2)=(my^r,g^r)` dekrüpteerimiseks arvutatakse:\n\n.. math::\n    d = \\frac{c_1}{c_{2}^x}\n\nDekrüpteerimise lugemistõendi jaoks valitakse juhuslik :math:`r` ning\nkonstrueeritakse järgnevad pühendumused:\n\n.. math::\n    a = c_{2}^r \\\\\n    b = g^r\n\nSeejärel arvutatakse Fiat-Shamiri pretensioon järgnevalt, kus `H` on\nräsifunktsioon `SHA2-256` ning `B2I` on meetod, mis teisendab baidijada\ntäisarvuks ühtlaselt vahemikus::\n\n    K = H(\"DECRYPTION\" || y || c || d || a || b)\n    k = B2I(K, q)\n\nNüüd arvutatakse lugemistõendi vastus:\n\n.. math::\n    s = kx + r\n\nKogu lugemistõend on komplekt :math:`(a,b,s)`. Tagastatakse :math:`(d,(a,b,s))`.\n\n`ee.ivxv.key.protocol.signing.shoup.ShoupSigning`\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAntud protokollis ei toimu võtme rekonstrueerimist.\n\nOlgu :math:`U` indeksid kaartidest, mis moodustavad argumendiks antud\n:math:`\\mathit{cards}` muutuja. Klassiinstants loeb salajase võtme komplektid ja\nkontrollib nende terviklust (st. mooduli ja avaliku võtme ühesus). Loetakse\nmällu võtme moodul :math:`n` ja avalik võti :math:`e`. Lisaks dekodeeritakse ja\nloetakse mällu salajased võtmed :math:`d_i`. Allkirja genereerimiseks sõnumile\n:math:`m` rakendatakse sellele EMSA-PSS kodeerimist [RFC8017]_, kus on kasutusel\nvarasemalt defineeritud `RSA-PSS parameetrid`_, saades allkirjastamiseks sõnumi\n:math:`M`.\n\nMe tähistame tähisega :math:`n!` arvu :math:`n` faktoriaali, st. :math:`n! = 1\n\\cdot 2 \\cdot 3 \\cdot \\ldots \\cdot n`. Meenutame, et Lagrange interpolatsiooni\nbaaspolünoom oli:\n\n.. math::\n    L(U,x,j) = \\frac{\\prod\\limits_{i\\in U, j \\neq i} x-x_i}{\\prod\\limits_{i\\in U, j \\neq i} x_j-x_i}\n\nDefineerime modifitseeritud Lagrange baaspolünoomi järgnevalt:\n\n.. math::\n    L'(U,x,j) = n! \\frac{\\prod\\limits_{i\\in U, j \\neq i} x-x_i}{\\prod\\limits_{i\\in U, j \\neq i} x_j- x_i}\n\nKuna me teame, et punktid :math:`1 \\leq x_i,x_j \\leq n`, siis\n:math:`|x_j-x_i|<n`. Seega, korrutades Lagrange baaspolünoomi läbi :math:`n!`,\nsaame, et :math:`L'(U,j)` on alati täisarv.\n\n.. warning: Kuna kehtib `|k|=|-k|`, siis võib mingitel juhtudel faktoriaalist\n   tegurid ära taandada ja saada murdarvu. Me oleme eksperimentaalselt\n   kontrollinud kõiki juhte kuni 15 osapoolega skeemideni ning siis ei teki\n   murdarvu. Rohkemate osapoolte korral tuleb kontrollida murrulisust ja\n   vajadusel muuta protokolli.\n\nAllkirja konstrueerimiseks arvutame:\n\n.. math::\n    s = \\prod\\limits_{j\\in U} {(M^{x_j})}^{L'(U,0,j)} = M^{\\sum\\limits_{j\\in U} x_j L'(U,0,j)} = M^{n!d}\n\nKuna kasutasime modifitseeritud Lagrange interpoleerimist, siis võrreldes\ntavalise RSA allkirjaga on see astendatud :math:`n!`-ga. Bezout' lemmast teame,\net :math:`x` ja :math:`y` korral leiduvad :math:`a` ja :math:`b` selliselt, et\n:math:`ax+by=\\gcd(x,y)`. Veel enam, selliseid :math:`a` ja :math:`b` väärtuseid\non võimalik leida laiendatud Eukleidese algoritmiga suurima ühisteguri\nleidmiseks.\n\nKasutades Eukleidese laiendatud algoritmi, leitakse :math:`a` ja :math:`b`,\nselliselt et :math:`ae+bn!=\\gcd(e,n!)`. Kuna avalik võti :math:`e` on valitud\nalgarv, siis :math:`\\gcd(e,n!)=1`. Arvutame:\n\n.. math::\n    \\sigma = M^as^b\n\nArvestades, et :math:`de = 1 \\pmod{\\phi(n)}`, on see tõesti korrektne allkiri:\n\n.. math::\n    \\sigma^e &= M^{ae}s^{be}      \\\\\n             &= M^{ae}M^{n!dbe} \\\\\n             &= M^{ae}M^{n!bde} \\\\\n             &= M^{ae}M^{n!b}     \\\\\n             &= M^{ae+bn!}         \\\\\n             &= M^{\\gcd(e,n!)}       \\\\\n             &= M\n\nProtokolli instants tagastab :math:`\\sigma` allkirjana.\n"
  },
  {
    "path": "Documentation/et/xteeteenus/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/et/xteeteenus/index.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\n======================================================================\nIVXV tehniline dokumentatsioon\n======================================================================\n\n-------------\nX-tee teenus\n-------------\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 3\n   :numbered:\n\n   sissejuhatus\n   jooksevnimekiri\n   seadistamine"
  },
  {
    "path": "Documentation/et/xteeteenus/jooksevnimekiri.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nE-hääletamiste jooksev nimekiri\n===============================\n\nTeenus vahendab jooksvat nimekirja EHS-ist X-tee-le.\n\n.. figure:: model/xteevotesorder.png\n\n   X-tee turvaserveri suhtlus EHS-iga\n\nTeenuses konfigureeritakse valimissündmused, mille nimekirja on võimalik küsida.\n\nTeenus pakub kolme otspunkti:\n\n1. ``Valimissündmuste loetelu`` - väljastab aktiivsete valimissündmuste loetelu\n\n2. ``Viimane järjenumber`` - väljastab konkreetse valimissündmuse viimase EHS-s registreeritud e-hääletamise järjenumbri.\n\n3. ``e-hääletamiste pakk`` - väljastab EHS-lt valimissündmuse e-hääletamiste paki, alatest e-hääletamisest järjenumbriga.\n"
  },
  {
    "path": "Documentation/et/xteeteenus/model/Makefile",
    "content": "PLANTUML=env -u DISPLAY plantuml\n\nall: xteeivxv.png xteevotesorder.png\n\n%.png: %.pu\n\t$(PLANTUML) $<\n"
  },
  {
    "path": "Documentation/et/xteeteenus/model/xteeivxv.pu",
    "content": "@startuml\n\nleft to right direction\n\n[X-tee teenus(xroad-service)] as xservice\n[X-tee turvaserver] as xsec\n[IVXV1] as ivxv1\n[IVXV2] as ivxv2\n[IVXV3] as ivxv3\n\nxsec --> xservice\nxservice--> ivxv1\nxservice--> ivxv2\nxservice--> ivxv3\n\n@enduml\n"
  },
  {
    "path": "Documentation/et/xteeteenus/model/xteevotesorder.pu",
    "content": "@startuml\n\nleft to right direction\n\npackage \"IVXV\" {\n    [Järjekorrateenus] as votesorder\n}\n\n[X-tee teenus(xroad-service)] as xservice\n[X-tee turvaserver] as xsec\n\nxsec --> xservice\nxservice--> votesorder\n\n@enduml\n"
  },
  {
    "path": "Documentation/et/xteeteenus/seadistamine.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nSeadistamine\n============\n\nTeenuse konfigureerimiseks kasutatakse ``xroad-service.json`` faili.\n\n``server.address`` - Serveri port\n\n``server.batchmaxsize`` - Paki suurus, soovitatud suurus 1000\n\n``server.openapipath`` - OpenAPI faili asukoht. Serveeritakse https://host/openapi.\n\n``server.tls`` - Serveri TLS konfiguratsioon\n\n``xroad.certificate`` -  X-tee turvaserveri sertifikaat\n\n``elections``- Valimissündmuste list\n\n``elections.name``- Valimissündmuse nimi\n\n``elections.address`` - IVXV serveri aadress\n\n``elections.servername`` - Järjekorrateenuse SNI\n\n``elections.rootca`` - IVXV CA sertifikaat\n\n``elections.clientcert`` - Kliendi sertifikaat, kliendi CA tuleb lisada IVXV konfiguratsiooni\n\n``elections.clientkey`` - Kliendi võti\n\nKäivitamine\n===========\n\nTeenus ei lähe iseseisvalt püsti, ning kui \"Seadistamine\" on tehtud, tuleb juurkasutajalt käivitada `systemctl start xroad-service`.\n\nSeiskamine\n==========\n\nKui teenus läheb maha erinevatel põhjustel (masina taaskäivitamine, veaolukorrad, manuaalne seiskamine `systemctl stop xroad-service` abil), siis tuleb korrata \"Käivitamine\" protseduuri et teenust taas püsti ajada.\n"
  },
  {
    "path": "Documentation/et/xteeteenus/sissejuhatus.rst",
    "content": "..  IVXV tehniline dokumentatsioon\n\nSissejuhatus\n============\n\nX-tee teenus on vahendamaks päringuid EHS-i(de) ja X-tee turvaserveri vahel.\n\n\n.. figure:: model/xteeivxv.png\n\n   X-tee teenus turvaserveri ja EHS-ide vahel\n\nDokumendis spetsifitseeritakse teenuse tehnilised detailid.\n"
  },
  {
    "path": "Documentation/ivxv-technical/ivxv-common.cls",
    "content": "% ivxv-common.cls\n\n% Class definition\n\\ProvidesClass{ivxv-common}\n\n% Class options for handling documents in different languages\n\\newcommand{\\@IVXVStringConfidential}{Konfidentsiaalne}\n\\newcommand{\\@IVXVStringDate}{Kuup\\\"aev:}\n\\newcommand{\\@IVXVStringDocumentType}{Tehniline dokument}\n\\newcommand{\\@IVXVStringVersion}{Versioon}\n\\newcommand{\\@IVXVStringPages}{lk}\n\\newcommand{\\@IVXVStringDocument}{Dok}\n\\newcommand{\\@IVXVStringBackofTitlePage}{\\copyright\\ RVT, \\the\\year}\n\\newcommand{\\@IVXVStringCopyrightFooter}{\\copyright\\ RVT, \\the\\year}\n\n\\DeclareOption{estonian}{%\n\t\\renewcommand{\\@IVXVStringConfidential}{Konfidentsiaalne}%\n\t\\renewcommand{\\@IVXVStringDate}{Kuup\\\"aev:}%\n\t\\renewcommand{\\@IVXVStringDocumentType}{Tehniline dokument}%\n\t\\renewcommand{\\@IVXVStringVersion}{Versioon}%\n\t\\renewcommand{\\@IVXVStringPages}{lk}%\n\t\\renewcommand{\\@IVXVStringDocument}{Dok}%\n}\n\\DeclareOption{english}{%\n\t\\renewcommand{\\@IVXVStringConfidential}{Confidential}%\n\t\\renewcommand{\\@IVXVStringDate}{Date:}%\n\t\\renewcommand{\\@IVXVStringDocumentType}{Technical document}%\n\t\\renewcommand{\\@IVXVStringVersion}{Version}%\n\t\\renewcommand{\\@IVXVStringPages}{pages}%\n\t\\renewcommand{\\@IVXVStringDocument}{Doc.}%\n}\n\n\\ProcessOptions\\relax\n\n\n% Base class, which we will modify\n% Add oldfontcommands for newer versions of memoir.\n%\\LoadClass[a4paper, twoside, 12pt, article]{memoir}\n\\LoadClass[a4paper, twoside, 12pt, article, oldfontcommands]{memoir}\n\n\\usepackage{cmap}\n\n% Make the figure and table captions separated by full stops rather than colons.\n\\usepackage{caption}\n\\captionsetup[table]{labelsep=period}\n\\captionsetup[figure]{labelsep=period}\n\n% margins\n\\usepackage[top=1in,bottom=1in,left=1in,right=1in,pdftex]{geometry}\n\n% For figuring out, how many pages there is in the document\n\\usepackage{lastpage}\n\n% We want to do later MakeUppercase for the section heads and when \\label{} is included in the section head, then it will break everything.\n% Textcase package provides more intelligent MakeUppercase command\n\\usepackage[overload]{textcase}\n\n% utf8 fileformat\n\\usepackage[utf8]{inputenc}\n\\usepackage[T1]{fontenc}\n% Times New Roman font for body text\n% Arial font for section headlines. The font can be installed\n% with ftp://tug.org/tex/getnonfreefonts/install-getnonfreefonts utility\n%\\usepackage[scaled]{uarial}\n\\renewcommand*{\\familydefault}{\\sfdefault}\n\n\\newcommand{\\IVXVArial}{\\fontfamily{phv} \\selectfont}\n\n% Commands for setting the document type, number, version, etc.\n\\newcommand{\\IVXVDefineDocumentType}[1]{\\def\\@IVXVStringDocumentType{#1}}\n\\newcommand{\\IVXVDefineDocumentNumber}[1]{\\def\\@IVXVDocumentNumber{#1}}\n\\newcommand{\\IVXVDefineDocumentVersion}[1]{\\def\\@IVXVDocumentVersion{#1}}\n\\newcommand{\\IVXVDefineDocumentSecurity}[1]{\\renewcommand{\\@IVXVStringConfidential}{#1}}\n\\newcommand{\\IVXVDefineAcknowledgement}[1]{\\def\\@IVXVAcknowledgement{#1}}\n\\newcommand{\\IVXVDefineCopyrightFooter}[1]{\\renewcommand{\\@IVXVStringCopyrightFooter}{#1}}\n\\newcommand{\\IVXVDefineBackofTitlePage}[1]{\\renewcommand{\\@IVXVStringBackofTitlePage}{#1}}\n\n% .. and initialize them to empty\n\\IVXVDefineDocumentNumber{}\n\\global\\let\\@IVXVDocumentNumber\\@empty\n\\IVXVDefineDocumentVersion{}\n\\global\\let\\@IVXVDocumentVersion\\@empty\n\\IVXVDefineAcknowledgement{}\n\\global\\let\\@IVXVAcknowledgement\\@empty\n\n% Pagestyle with headers and footers\n\\setlength{\\headwidth}{\\textwidth}\n\\makepagestyle{IVXVFancy}\n\\makerunningwidth{IVXVFancy}{\\headwidth}\n\n% No indent of paragraphs\n\\setlength{\\parindent}{0pt}\n\\setlength{\\parskip}{1.5ex}\n\n% TODO: Single command for this?\n\\makeoddhead{IVXVFancy}{}{}{\n\t\\IVXVArial \t\\tiny \\@IVXVStringConfidential}\n\\makeevenhead{IVXVFancy}{}{}{\n\t\\IVXVArial \\tiny \\@IVXVStringConfidential}\n\n\\makeoddfoot{IVXVFancy}{\n\t\\IVXVArial\n\t{\\tiny\n\t\t{\\bfseries \\hspace{-0.75em}\\@title} \\\\\n\t\t\t\\@date}}\n\t{}\n\t{\n\t\\IVXVArial\n\t{\\tiny\n\t\t\\@IVXVDocumentVersion \\\\\n\t\t\\thepage\\ / \\pageref{LastPage}}}\n\\makeevenfoot{IVXVFancy}{\n\t\\IVXVArial\n\t{\\tiny\n\t\t{\\bfseries \\hspace{-0.75em}\\@title} \\\\\n\t\t\t\\@date}}\n\t{}\n\t{\n\t\\IVXVArial\n\t{\\tiny\n\t\t\\@IVXVDocumentVersion \\\\\n\t\t\\thepage\\ / \\pageref{LastPage}}}\n\n\\makefootrule{IVXVFancy}{\\headwidth}{\\normalrulethickness}{0pt}\n\n% We have multiline headers, therefore less space is available\n% for text.\n\\addtolength{\\footskip}{2mm}\n\n% Enforce pagestyle for pages with new chapters as well\n\\aliaspagestyle{chapter}{IVXVFancy}\n\n% Set the new style as the default\n\\pagestyle{IVXVFancy}\n\n% Titlepage style with headings\n\\makepagestyle{IVXVTitle}\n\\makerunningwidth{IVXVTitle}{\\headwidth}\n\\makeoddhead{IVXVTitle}{\n\t\\begin{minipage}[t]{0.5\\textwidth}\n\t\t\\hspace{-7mm}\n\t\\end{minipage}\n\t}{}{\n\t\\raisebox{2.5mm}{\\IVXVArial \\scriptsize \\bfseries \\@IVXVStringConfidential}\n\t}\n% Chapter and section headline style\n\n\\renewcommand{\\printchapternum}{\\LARGE \\bfseries \\IVXVArial \\thechapter}\n\\renewcommand{\\printchaptertitle}[1]{\\LARGE \\bfseries \\IVXVArial #1}\n\\setlength{\\beforechapskip}{60pt}\n\\setlength{\\afterchapskip}{40pt}\n\n\\let\\stdchapter\\chapter\n\\renewcommand\\chapter{\\clearpage\\ \\stdchapter}\n\n\\setsecheadstyle{\\normalsize \\bfseries \\IVXVArial}\n\\setsubsecheadstyle{\\normalsize \\bfseries \\IVXVArial}\n\\setsubsubsecheadstyle{\\IVXVArial}\n\n\\usepackage{graphicx}\n% title page\n\\makeatletter\n\\renewcommand{\\maketitle}{%\n\t\\thispagestyle{IVXVTitle}\n\t% \\vspace{2cm}\n\n\t\\begin{minipage}[t]{0.5\\textwidth}\n\t\t%\\vspace{-6.5mm}\n\t\t%\\hfill\n\t\t\\vspace{31mm}\n\t\\end{minipage}\n\t\\vspace{4cm}\n\t\\begin{flushleft}\n\t\t\\IVXVArial\n\t\t{\\LARGE \\bfseries \\@title}\n\t\t\\vfill\n\t\t{\\Large \\bfseries\n\t\t\t\\@IVXVStringDocumentType\n\n\t\t\t\\ifx\\@IVXVDocumentVersion\\@empty\n\t\t\t\tDok. versiooninumber defineerimata. Kasuta IVXVDefineDocumentVersion käsku või IVXV-Document-Version stiili.\n\t\t\t\\else\n\t\t\t\t\\@IVXVStringVersion~\\@IVXVDocumentVersion\n\t\t\t\\fi\n\n\t\t\t\\@date\n\n\t\t\t\\pageref{LastPage} \\@IVXVStringPages\\\\[1.5ex]\n\n\t\t\t\\ifx\\@IVXVDocumentNumber\\@empty\n\t\t\t\tDok. hoidla ning järjenumber defineerimata. Kasuta IVXVDefineDocumentNumber käsku või IVXV-Document-Number stiili.\n\t\t\t\\else\n\t\t\t\t\\@IVXVStringDocument~\\@IVXVDocumentNumber\n\t\t\t\\fi\n\t\t}\\par\n\t\t\\vspace{2cm}\n\t\t{\\scriptsize \\@IVXVAcknowledgement}\n\t\\end{flushleft}\n\t\\pagebreak\n%\t\\thispagestyle{empty}\n%\t\\@IVXVStringBackofTitlePage\n%\t\\cleardoublepage\n}\n\\makeatother\n\n% Make dots appear in the table of contents also for chapters\n\\renewcommand{\\cftchapterdotsep}{\\cftdotsep}\n\n% URL formating, URL breaking and linked entries in the PDF\n\n\\usepackage{url}\n%%% XXX remove breakurl\n% \\usepackage{breakurl}\n%%% XXX \\usepackage[pdfborder={0 0 0}, pdftex, pdfpagelayout=TwoPageRight]{hyperref}\n% hyperref and memoir is incompatible, this fixes the problems\n\\usepackage{memhfixc}\n\n% make enumerate and itemize lists tighter\n\\firmlists\n% and change the numbering for nested lists\n\\renewcommand{\\labelenumi}{\\arabic{enumi}.}\n\\renewcommand{\\labelenumii}{\\arabic{enumi}.\\arabic{enumii}}\n\\renewcommand{\\labelenumiii}{\\arabic{enumi}.\\arabic{enumii}.\\arabic{enumiii}}\n\\renewcommand{\\labelenumiv}{\\arabic{enumi}.\\arabic{enumii}.\\arabic{enumiii}.\\arabic{enumiii}.\\arabic{enumiv}}\n% make LaTeX observe right margins at all costs:\n\\sloppy\n"
  },
  {
    "path": "Documentation/ivxv-technical/ivxv-technical.cls",
    "content": "% ivxv-technical.cls\n\n\\ProvidesClass{ivxv-technical}\n\\DeclareOption*{\\PassOptionsToClass{\\CurrentOption}{ivxv-common}}\n\\ProcessOptions\\relax\n\\LoadClass{ivxv-common}\n"
  },
  {
    "path": "Documentation/ivxv-technical/pandoc-extras.sty",
    "content": "% Style for pandoc generated latex output, related with docbook format.\n% It's needed for special formatting and may be only needed for docbook,\n% so probably you don't need this (e.g. for markdown -> latex conversion).\n\n% Highlighting\n\\usepackage{framed}\n\\usepackage{color}\n\\usepackage{fancyvrb}\n\\newcommand{\\VerbBar}{|}\n\\newcommand{\\VERB}{\\Verb[commandchars=\\\\\\{\\}]}\n\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\}}\n\\newenvironment{Shaded}{\\begin{shaded}}{\\end{shaded}}\n\\definecolor{shadecolor}{gray}{0.9}\n\\newcommand{\\KeywordTok}[1]{\\textcolor[rgb]{0.00,0.44,0.13}{\\textbf{{#1}}}}\n\\newcommand{\\DataTypeTok}[1]{\\textcolor[rgb]{0.56,0.13,0.00}{{#1}}}\n\\newcommand{\\DecValTok}[1]{\\textcolor[rgb]{0.25,0.63,0.44}{{#1}}}\n\\newcommand{\\BaseNTok}[1]{\\textcolor[rgb]{0.25,0.63,0.44}{{#1}}}\n\\newcommand{\\FloatTok}[1]{\\textcolor[rgb]{0.25,0.63,0.44}{{#1}}}\n\\newcommand{\\ConstantTok}[1]{\\textcolor[rgb]{0.53,0.00,0.00}{{#1}}}\n\\newcommand{\\CharTok}[1]{\\textcolor[rgb]{0.25,0.44,0.63}{{#1}}}\n\\newcommand{\\SpecialCharTok}[1]{\\textcolor[rgb]{0.25,0.44,0.63}{{#1}}}\n\\newcommand{\\StringTok}[1]{\\textcolor[rgb]{0.25,0.44,0.63}{{#1}}}\n\\newcommand{\\VerbatimStringTok}[1]{\\textcolor[rgb]{0.25,0.44,0.63}{{#1}}}\n\\newcommand{\\SpecialStringTok}[1]{\\textcolor[rgb]{0.73,0.40,0.53}{{#1}}}\n\\newcommand{\\ImportTok}[1]{{#1}}\n\\newcommand{\\CommentTok}[1]{\\textcolor[rgb]{0.38,0.63,0.69}{\\textit{{#1}}}}\n\\newcommand{\\DocumentationTok}[1]{\\textcolor[rgb]{0.73,0.13,0.13}{\\textit{{#1}}}}\n\\newcommand{\\AnnotationTok}[1]{\\textcolor[rgb]{0.38,0.63,0.69}{\\textbf{\\textit{{#1}}}}}\n\\newcommand{\\CommentVarTok}[1]{\\textcolor[rgb]{0.38,0.63,0.69}{\\textbf{\\textit{{#1}}}}}\n\\newcommand{\\OtherTok}[1]{\\textcolor[rgb]{0.00,0.44,0.13}{{#1}}}\n\\newcommand{\\FunctionTok}[1]{\\textcolor[rgb]{0.02,0.16,0.49}{{#1}}}\n\\newcommand{\\VariableTok}[1]{\\textcolor[rgb]{0.10,0.09,0.49}{{#1}}}\n\\newcommand{\\ControlFlowTok}[1]{\\textcolor[rgb]{0.00,0.44,0.13}{\\textbf{{#1}}}}\n\\newcommand{\\OperatorTok}[1]{\\textcolor[rgb]{0.40,0.40,0.40}{{#1}}}\n\\newcommand{\\BuiltInTok}[1]{{#1}}\n\\newcommand{\\ExtensionTok}[1]{{#1}}\n\\newcommand{\\PreprocessorTok}[1]{\\textcolor[rgb]{0.74,0.48,0.00}{{#1}}}\n\\newcommand{\\AttributeTok}[1]{\\textcolor[rgb]{0.49,0.56,0.16}{{#1}}}\n\\newcommand{\\RegionMarkerTok}[1]{{#1}}\n\\newcommand{\\InformationTok}[1]{\\textcolor[rgb]{0.38,0.63,0.69}{\\textbf{\\textit{{#1}}}}}\n\\newcommand{\\WarningTok}[1]{\\textcolor[rgb]{0.38,0.63,0.69}{\\textbf{\\textit{{#1}}}}}\n\\newcommand{\\AlertTok}[1]{\\textcolor[rgb]{1.00,0.00,0.00}{\\textbf{{#1}}}}\n\\newcommand{\\ErrorTok}[1]{\\textcolor[rgb]{1.00,0.00,0.00}{\\textbf{{#1}}}}\n\\newcommand{\\NormalTok}[1]{{#1}}\n"
  },
  {
    "path": "Documentation/podl/.gitignore",
    "content": "venv/\n"
  },
  {
    "path": "Documentation/podl/podl.py",
    "content": "#!/usr/bin/env python3\n\n# IVXV Internet voting framework\n\n\"\"\"PO translator with Deepl\n\nTranslate PO files with Deepl\n\nUsage:\n    podl --input <input> --api <api> [options]\n    podl -h | --help\n    podl -v | --version\n\nOptions:\n    -h --help               Request for help\n    -v --version            Show version information\n    -i --input <input>      Input file or directory\n    -s --source <source>    Source language [default: ET]\n    -t --target <target>    Target language [default: EN-GB]\n    -f --force              Actually use API\n    --fuzzy                 Override fuzzy translations [default: False]\n    -a --api <api>          Deepl API token\n\"\"\"\n\nimport os\nimport deepl\nimport polib\n\nfrom docopt import docopt\nfrom schema import Or, Schema, SchemaError\n\n\nclass Translator:\n\n    def __init__(self, api, source, target, force):\n        self.__translator = deepl.Translator(api)\n        self.__source_lang = source\n        self.__target_lang = target\n        self.__force = force\n\n    def translate(self, source_msg):\n        if self.__force:\n            return str(self.__translator.translate_text(\n                source_msg,\n                source_lang = self.__source_lang,\n                target_lang = self.__target_lang\n            ))\n        return None\n\n\ndef process_file(filename, translator, fuzzy):\n    po = polib.pofile(filename)\n    print(f\"File {filename} is {po.percent_translated()}% translated\")\n    for entry in po.untranslated_entries():\n        if not entry.msgstr:\n            newmsg = translator.translate(entry.msgid)\n            if newmsg is not None:\n                entry.msgstr = newmsg\n                po.save(filename)\n    if fuzzy:\n        for entry in po.fuzzy_entries():\n            newmsg = translator.translate(entry.msgid)\n            if newmsg is not None:\n                entry.msgstr = newmsg\n                po.save(filename)\n    po.save(filename)\n\n\nif __name__ == '__main__':\n    ARGS = docopt(__doc__, version='Deepl PO translator 1.0')\n    SCHEMA = Schema({\n        '--input': Or(os.path.isdir, os.path.isfile),\n        '--source': str,\n        '--target': str,\n        '--api': str,\n        '--force': bool,\n        '--fuzzy': bool,\n        '--help': Or(False),\n        '--version': Or(False)\n    })\n    try:\n        ARGS = SCHEMA.validate(ARGS)\n    except SchemaError as err:\n        exit(err)\n\n    INPUT = ARGS['--input']\n    DEEPL = Translator(\n        ARGS['--api'], ARGS['--source'], ARGS['--target'], ARGS['--force'])\n\n    if os.path.isfile(INPUT):\n        process_file(INPUT, DEEPL, ARGS['--fuzzy'])\n    elif os.path.isdir(INPUT):\n        for root, dirs, files in os.walk(INPUT):\n            for file in files:\n                if file.endswith(\".po\"):\n                    file_path = os.path.join(root, file)\n                    process_file(file_path, DEEPL, ARGS['--fuzzy'])\n    else:\n        pass\n"
  },
  {
    "path": "Documentation/podl/requirements.in",
    "content": "# We depend on pip-tools being installed to venv.\n#\n# Add dependencies to this file\n# pip-compile to update requirements.txt\n# pip-sync to update venv\n\ndocopt==0.6.2\nschema==0.7.7\npolib==1.2.0\ndeepl==1.19.1\n"
  },
  {
    "path": "Documentation/podl/requirements.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile requirements.in\n#\ncertifi==2024.8.30\n    # via requests\ncharset-normalizer==3.4.0\n    # via requests\ndeepl==1.19.1\n    # via -r requirements.in\ndocopt==0.6.2\n    # via -r requirements.in\nidna==3.10\n    # via requests\npolib==1.2.0\n    # via -r requirements.in\nrequests==2.32.3\n    # via deepl\nschema==0.7.7\n    # via -r requirements.in\nurllib3==2.2.3\n    # via requests\n"
  },
  {
    "path": "Documentation/public/arhitektuur/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/public/arhitektuur/etcd.rst",
    "content": "..  IVXV arhitektuur\n\nLisa - ETCD Andmemudel\n======================\n\nETCD on võti-väärtus andmebaas, kus talletatakse e-hääletamise\nsisendnimekirju, e-hääli ning statistikat.\n\nRingkondade nimekiri\n--------------------\n\n.. table:: Ringkondade nimekiri\n   :widths: 30 35 35\n\n   +----------------------------+---------------------------+-----------------------+\n   | **Võti**                   | **Väärtus**               | **Näide**             |\n   +============================+===========================+=======================+\n   | /districts                 | Juurvõti                  |                       |\n   +----------------------------+---------------------------+-----------------------+\n   | /districts/<EHAK-district> | Seab valija               | /districts/05241      |\n   |                            | EHAK-ringkond paarile     |                       |\n   |                            | vastavusse                | 0000.1                |\n   |                            | ringkonnaidentifikaatori, |                       |\n   |                            | mis viitab /choices       |                       |\n   |                            | harusse                   |                       |\n   +----------------------------+---------------------------+-----------------------+\n   | /districts/counties        | Väli counties             | {                     |\n   |                            | ringkondade               |                       |\n   |                            | nimekirjast               | \"0068\": [             |\n   |                            |                           |                       |\n   |                            |                           | \"0809\",               |\n   |                            |                           |                       |\n   |                            |                           | \"0624\"                |\n   |                            |                           |                       |\n   |                            |                           | ],                    |\n   |                            |                           |                       |\n   |                            |                           | \"0784\": [             |\n   |                            |                           |                       |\n   |                            |                           | \"0524\"                |\n   |                            |                           |                       |\n   |                            |                           | ],                    |\n   |                            |                           |                       |\n   |                            |                           | \"0079\": [             |\n   |                            |                           |                       |\n   |                            |                           | \"0796\"                |\n   |                            |                           |                       |\n   |                            |                           | ],                    |\n   |                            |                           |                       |\n   |                            |                           | \"0793\": [             |\n   |                            |                           |                       |\n   |                            |                           | \"0793\"                |\n   |                            |                           |                       |\n   |                            |                           | ]                     |\n   |                            |                           |                       |\n   |                            |                           | }                     |\n   +----------------------------+---------------------------+-----------------------+\n   | /districts/version         | Nimekirja                 | [\"NIMESTE,NIMI,123456 |\n   |                            | allkirjastajad            | 78912                 |\n   |                            |                           | 2019-02-22T13:58:48Z\" |\n   |                            |                           | ]                     |\n   +----------------------------+---------------------------+-----------------------+\n\nValikute nimekiri\n-----------------\n\n.. table:: Valikute nimekiri\n   :widths: 30 35 35\n\n   +------------------------+-----------------------+-----------------------+\n   | **Võti**               | **Väärtus**           | **Näide**             |\n   +========================+=======================+=======================+\n   | /choices               | Juurvõti              |                       |\n   +------------------------+-----------------------+-----------------------+\n   | /choices/<district-id> | Ringkonnale           | /choices/0000.1       |\n   |                        | district-id vastav    |                       |\n   |                        | valikute nimekiri.    | {                     |\n   |                        |                       |                       |\n   |                        |                       | \"Erakond 1\":{         |\n   |                        |                       |                       |\n   |                        |                       | \"0000.101\":\"Nimi      |\n   |                        |                       | Nimeste1\",            |\n   |                        |                       |                       |\n   |                        |                       | \"0000.102\":\"Nimi      |\n   |                        |                       | Nimeste2\",            |\n   |                        |                       |                       |\n   |                        |                       | \"0000.103\":\"Nimi      |\n   |                        |                       | Nimeste3\"             |\n   |                        |                       |                       |\n   |                        |                       | },                    |\n   |                        |                       |                       |\n   |                        |                       | \"Erakond 2\":{         |\n   |                        |                       |                       |\n   |                        |                       | \"0000.104\":\"Nimi      |\n   |                        |                       | Nimeste4\",            |\n   |                        |                       |                       |\n   |                        |                       | \"0000.105\":\"Nimi      |\n   |                        |                       | Nimeste5\"             |\n   |                        |                       |                       |\n   |                        |                       | },                    |\n   |                        |                       |                       |\n   |                        |                       | \"Üksikkandidaadid\":{  |\n   |                        |                       |                       |\n   |                        |                       | \"0000.106\":\"Nimi      |\n   |                        |                       | Nimeste6\"             |\n   |                        |                       |                       |\n   |                        |                       | }                     |\n   |                        |                       |                       |\n   |                        |                       | }                     |\n   +------------------------+-----------------------+-----------------------+\n   | /choices/version       | Nimekirja             | [\"NIMESTE,NIMI,123456 |\n   |                        | allkirjastajad        | 78912                 |\n   |                        |                       | 2019-02-22T13:58:59Z\" |\n   |                        |                       | ]                     |\n   +------------------------+-----------------------+-----------------------+\n\nValijate nimekiri\n-----------------\n\n.. table:: Valijate nimekiri\n   :widths: 30 35 35\n\n   +---------------------------------+-----------------------+-----------------------+\n   | **Võti**                        | **Väärtus**           | **Näide**             |\n   +=================================+=======================+=======================+\n   | /voters                         | Juurvõti              |                       |\n   +---------------------------------+-----------------------+-----------------------+\n   | /voters/<version-id>            | Valijanimekirja       | /voters/1             |\n   |                                 | versiooni juurvõti    |                       |\n   +---------------------------------+-----------------------+-----------------------+\n   | /voters/<version-id>/<voter-id> | Valija                | /voters/1/12345678912 |\n   |                                 | <EHAK-district> antud |                       |\n   |                                 | nimekirjas            | 05241                 |\n   +---------------------------------+-----------------------+-----------------------+\n   | /voters/<version-id>/version    | Valijatenimekirja     | [\"NIMESTE,NIMI,123456 |\n   |                                 | versioon              | 78912                 |\n   |                                 |                       | 2019-02-22T13:58:59Z\" |\n   |                                 |                       | ]                     |\n   +---------------------------------+-----------------------+-----------------------+\n   | /voters/version                 | Aktuaalse             | 1                     |\n   |                                 | valijatenimekirja     |                       |\n   |                                 | versiooni ID          |                       |\n   +---------------------------------+-----------------------+-----------------------+\n   | /voters/previous                | Eelmise               | 0                     |\n   |                                 | valijanimekirja       |                       |\n   |                                 | versiooni ID          |                       |\n   +---------------------------------+-----------------------+-----------------------+\n\nTalletatud e-hääl\n-----------------\n\n.. table:: Talletatud e-hääl\n   :widths: 30 35 35\n\n   +-------------------------+------------------------+-----------------------+\n   | **Võti**                | **Väärtus**            | **Näide**             |\n   +=========================+========================+=======================+\n   | /vote                   | Juurvõti               |                       |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>         | Hääle juurvõti,        | 16 baiti binaarandmed |\n   |                         | unikaalne              |                       |\n   |                         | identifikaator         |                       |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/count   | Hääle kontrollimise    | 0                     |\n   |                         | loendur                |                       |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/ocsp    | Kehtivuskinnitus       | DER kodeeringus OCSP  |\n   |                         |                        | vastus                |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/time    | Hääle talletamise      | 2019-03-03T10:56:28.8 |\n   |                         | kellaaeg               | 99925926Z             |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/tspreg  | Registreerimiskinnitus | DER kodeeringus PKIX  |\n   |                         |                        | ajatempel             |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/type    | Allkirjastatud hääle   | BDOC                  |\n   |                         | konteineri tüüp        |                       |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/version | Hääle andmisel         | 0                     |\n   |                         | kehtinud valijate      |                       |\n   |                         | nimekirja versiooni    |                       |\n   |                         | ID                     |                       |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/vote    | E-hääl allkirjastatud  | BDOC vormingus        |\n   |                         | konteineris            | allkirjastatud hääl   |\n   +-------------------------+------------------------+-----------------------+\n   | /vote/<vote-id>/voter   | Valija isikukood       | 12345678912           |\n   +-------------------------+------------------------+-----------------------+\n\nStatistikaliidesed\n------------------\n\n.. table:: Statistikaliidesed\n   :widths: 30 35 35\n\n   +-------------------------------+-----------------------+-----------------------+\n   | **Võti**                      | **Väärtus**           | **Näide**             |\n   +===============================+=======================+=======================+\n   | /votes                        | Juurvõti              |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order                  | Hääletamisfaktide     |                       |\n   |                               | järjestuse juurvõti   |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order/<seq>            | Konkreetse            | /votes/order/1        |\n   |                               | hääletamisfakti       |                       |\n   |                               | juurvõti              |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order/<seq>/admincode  | Hääletamisfaktiga     | 0796                  |\n   |                               | seotud EHAK           |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order/<seq>/district   | Hääletamisfaktiga     | 10                    |\n   |                               | seotud ringkonna      |                       |\n   |                               | number                |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order/<seq>/voterid    | Hääletaja isikukood   | 12345678901           |\n   |                               |                       |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/order/<seq>/votername  | Hääletaja nimi        | NIMI NIMESTE          |\n   |                               |                       |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/stats                  | Viimase               | 12                    |\n   |                               | hääletamisfakti       |                       |\n   |                               | järjekorranumber      |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /voted                        | Juurvõti              |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /voted/latest                 | Viimati antud häälte  |                       |\n   |                               | indeksi juurvõti      |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /voted/latest/<voter-id>      | Hääletaja poolt       | /voted/latest/1234567 |\n   |                               | viimati antud hääle   | 8901                  |\n   |                               | aeg ja identifikaator |                       |\n   |                               | binaarkujul           | <2019-03-03T12:15:59Z |\n   |                               |                       | ><vote-id>            |\n   +-------------------------------+-----------------------+-----------------------+\n   | /voted/stats                  | Jaoskonnapõhise       |                       |\n   |                               | statistika indeksi    |                       |\n   |                               | juurvõti              |                       |\n   +-------------------------------+-----------------------+-----------------------+\n   | /voted/stats/<voter-id>       | Hääle andmise         | /voted/stats/12345678 |\n   |                               | kellaaeg koos         | 901                   |\n   |                               | jaoskonnainfoga       |                       |\n   |                               |                       | <0796><2019-02-22T14: |\n   |                               |                       | 17:23Z>               |\n   +-------------------------------+-----------------------+-----------------------+\n   | /votes/voter/stats/<voter-id> | Tühi baitide massiiv  | /votes/voter/stats/   |\n   |                               | (kasutatakse väärtuse | 394091044211          |\n   |                               | versiooni, ning mitte |                       |\n   |                               | väärtust ennast)      |                       |\n   +-------------------------------+-----------------------+-----------------------+\n\nHääletamisseansid\n-----------------\n\n.. table:: Hääletamisseansid\n   :widths: 30 35 35\n\n   +-----------------------+-----------------------+---------------------------+\n   | **Võti**              | **Väärtus**           | **Näide**                 |\n   +=======================+=======================+===========================+\n   | /session              | Juurvõti              |                           |\n   +-----------------------+-----------------------+---------------------------+\n   | /session/<session-id> | RPC meetod, mis       | ``/session/0149468d2866`` |\n   |                       | kutsus antud          | ``6fced7d73b32cc16225d``  |\n   |                       | funktsiooni välja +   |                           |\n   |                       | ``x1F`` + kasutaja    |                           |\n   |                       | autentimismeetod      |                           |\n   +-----------------------+-----------------------+---------------------------+\n"
  },
  {
    "path": "Documentation/public/arhitektuur/index.rst",
    "content": "..  IVXV arhitektuur\n\nIVXV arhitektuur\n========================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 3\n   :numbered:\n\n   yldpohimotted\n   kogumisteenus\n   vallasrezhiim\n   tehnoloogiad\n\n   etcd\n   viited\n"
  },
  {
    "path": "Documentation/public/arhitektuur/kogumisteenus.rst",
    "content": "..  IVXV arhitektuur\n\nKogumisteenus\n=============\n\nÜldkirjelduse [ÜK2016]_ põhjal on Kogumisteenus:\n\n.. epigraph::\n\n   Süsteemi keskne komponent, mida käitab Koguja. Teenus abistab Hääletajat\n   e-hääle koostamisel ning registreerib selle enne salvestamist e-valimiskasti.\n   Kogumisteenus kasutab väliseid teenuseid (tuvastamine, allkirjastamine,\n   registreerimine). Kogumisteenusel on peale Koguja enda teisigi haldureid\n   (Korraldaja, Klienditugi), kelle jaoks on Kogumisteenusel eraldi\n   haldusliidesed.\n\nKogumisteenus töötab sidusrežiimis ning vähemalt valija- ja kontrollrakenduse\nsuunalised liidesed on avatud internetile. Seega töötleb Kogumisteenus\npotentsiaalselt ebausaldusväärsest allikast pärit päringuid. Tulenevalt\ntarkvarale seatavast turvatasemest, kõrgkäideldavuse, skaleeritavuse, kihilise\nevitatavuse ning laiendatavuse nõuetest on kogumisteenus omakorda liigendatud\nühte konkreetset teenust osutavateks mikroteenusteks, mida on võimalik\npaindlikult evitada.\n\nKõik kogumisteenuse komponendid programmeeritakse keeles `Go\n<https://golang.org>`_. Keelel Go on:\n\n- staatiline tüüpimine, mis võimaldab tüübivigade avastamist enne programmi\n  käivitamist;\n\n- automaatne mäluhaldus, mis välistab rakenduse vigasest mäluhaldusest\n  tulenevad turvaaugud;\n\n- kompilaator avatud lähtekoodiga;\n\n- ribastamine/rööprapse, mis võimaldab kasutada paralleelsust mitmetuumalistes\n  süsteemides.\n\nKogumisteenuse andmeedastuseks kasutatakse üldjuhul JSON-vormingut, välja\narvatud olukordades, kus välised asjaolud tingivad mõne muu andmevormingu\nkasutamist (näiteks BDOC-vorming põhineb XML'il).\n\nKogumisteenus toetab Riigikogu valimisi, kohaliku omavalitsuse volikogu\nvalimisi, Euroopa parlamendi valimisi ning rahvahääletusi.\n\nKogumisteenuse komponendid arvestavad virtualiseerimistehnoloogiate\nkasutamisega ning kogumisteenust on võimalik evitada nii ühel virtuaalriistvara\ninstantsil, kui ka mikroteenuste kaupa erinevatel instantsidel. Kogumisteenuse\nkomponendid on evitatavad Ubuntu 22.04 LTS (Jammy Jellyfish)\noperatsioonisüsteemil 64-bitisel arhitektuuril.\n\nAndmesäilitus on teostatud kasutades võti-väärtus andmebaasi (etcd).\nTestotstarbel on teostatud ka andmesäilitus failisüsteemi ning mällu, kuid neid\nei ole soovituslik kasutada tootekeskkonnas. Lisaks on kogumisteenusel olemas\nliides uute talletusprotokollide lisamiseks. Lõplik otsus kasutatava lahenduse\nkohta tehakse kogumisteenuse haldurite poolt teenust seadistades.\n\nMikroteenused\n-------------\n\n.. figure:: model/img/collector_microservices.png\n\n   Kogumisteenuse jaotus mikroteenusteks\n\nKogumisteenus on jaotatud põhiteenusteks ja abiteenusteks. Põhiteenused -\nvahendusteenus, nimekirjateenus, hääletamisteenus, kontrollteenus ning\ntalletamisteenus - on arhitektuuri tehnilise lihtsuse mõttes piiritletud ühe\nvalimisega, kuid ühel riistvaral, ühe operatsioonisüsteemi kontekstis võivad\nkäia mitme valimise mikroteenused. Täiendavalt võib kogumisteenuse juures\nkasutada abiteenuseid - tuvastusteenust hääletaja isiku tuvastamiseks ning\nallkirjateenust valijarakenduse poolt hääle allkirjastamise hõlbustamiseks.\n\nTeenuseid on võimalik evitada nii eraldatult kui koos erinevates\nkonfiguratsioonides, mis teeb võimalikuks kihilise arhitektuuri. Lähtudes\nfunktsioonist on otstarbekas hoida Vahendus- ning Talletamisteenused teistest\neraldi.\n\nTeenused kasutavad transpordiprotokollina TLS'i, ühendused on\nvähemalt serveripoolselt autenditud. Rakenduskihi protokoll on JSON-RPC.\n\nKõik teenused tekitavad tegevuslogi, mida säilitatakse nii lokaalselt kui\nlogitakse syslog protokolli vahendusel kesksesse logikogujasse.\n\nVahendusteenuse funktsioon ja tehniline liides\n``````````````````````````````````````````````\n\nVahendusteenuse põhifunktsioon on ühe sisenemispunkti (port 443) pakkumine\nValijarakendusele ja Kontrollrakendusele. Vahendusteenus on dispetšerteenus\nteiste komponentide vahel, mis võimaldab sisemiselt evitada kogumisteenust\nmikroteenustena, ent omada süsteemil ainult ühte sisenemispunkti. Lisaks suudab\nsee dubleeritud evituse puhul täita koormusjaoturi ülesannet.\n\nVahendusteenus ei termineeri TLS-ühendust vaid kasutab sihtpunkti tuvastamiseks\nTLS'i *Server Name Indication* (SNI) laiendust. Kliendid panevad TLS\n``ClientHello`` sõnumisse SNI-laiendi, kus avatekstis määravad, millise\nteenusega soovivad suhelda: vahendusteenus näeb seda, võtab ühendust vastavat\nteenust pakkuva isendiga ja hakkab kliendi ning teenuse vahelisi sõnumeid\nvahendama. Vahendusteenus EI termineeri TLS'i ning ei näe sõnumite sisu.\nVahendusteenusel on andmed kõigi teiste teenuste asukohtadest (aadress:port)\nning teenus vahendab sõnumivahetust kõigi osapoolte vahel.\n\nVahendusteenus on olekuvaba komponent, mida on võimalik horisontaalselt\nskaleerida.\n\nVahendusteenuse teostus\n'''''''''''''''''''''''\n\nVahendusteenuse teostus kasutab vabavaralist HAProxy serverit, mis on\nüldlevinud tarkvaraline koormusjaotur ja proksi. Kuna Vahendusteenus on\nesimene puutepunkt avalikust internetist tulevate ühenduste jaoks, siis on\nmõistlik kasutada tarkvara, mille töökindlus on juba tõestatud.\n\nKuigi HAProxyt kasutatakse tihti HTTP-režiimis, kus see analüüsib liiklust,\nsiis vahendusteenuse rollis on see TCP-režiimis ning ei näe vahendatava\nkrüpteeritud TLS-kanali sisse.\n\nIVXV seadistusest genereeritakse HAProxy seadistusfail, mis sisaldab teiste\nteenuste asukohti, ning ühenduste vahendamise ülesanne jääb viimase kanda.\nLisaks on võimalik HAProxyt ka seadistada ühenduste sagedusi piirama\nlähteaadressi või mõne muu nimetaja põhjal. See aga jääb süsteemihalduri\nülesandeks.\n\nKuigi HAProxy on võimeline ise teostama koormusjaoturi ülesannet, on seda\nvõimalik evitada ka teiste, potentsiaalselt riistvaraliste koormusjaoturite\ntaha, kus see jääb täitma ainult SNI põhjal vahendamise ülesannet.\n\nHAProxy lähtekood on avalik ja sobiva litsentsiga ning pakendatud\nkogumisteenuse alusplatvormi ametlikus hoidlas (vt. :ref:`tehnoloogiad`).\n\nNimekirjateenuse funktsioon ja tehniline liides\n```````````````````````````````````````````````\n\nNimekirjateenuse põhifunktsioon on valikute nimekirjade vahendamine\nValijarakendusele. Nimekirjateenusesse jõuab informatsioon tuvastatud valija\nkohta ning Nimekirjateenus väljastab valija ringkonnale vastava valikute\nnimekirja Talletamisteenusest Valijarakendusse.\n\nNimekirjateenus on olekuvaba komponent, mida on võimalik horisontaalselt\nskaleerida.\n\nKontrollteenuse funktsioon ja tehniline liides\n``````````````````````````````````````````````\n\nKontrollteenuse põhifunktsioon on kontrollpäringute töötlemine ning\nkontrollitava hääle väljastamine Talletamisteenusest Kontrollrakendusse.\n\nKontrollteenus on olekuvaba komponent, mida on võimalik horisontaalselt\nskaleerida.\n\nHääletamisteenuse funktsioon ja tehniline liides\n````````````````````````````````````````````````\n\nHääletamisteenuse põhifunktsioon on hääletamispäringute töötlemine.\nHääletamisteenus verifitseerib sissetuleva hääle, registreerib selle\nRegistreerimisteenuses ning talletab Talletamisteenusesse.\n\nHääletamisteenus on olekuvaba komponent, mida on võimalik horisontaalselt\nskaleerida.\n\nTalletamisteenuse funktsioon ja tehniline liides\n`````````````````````````````````````````````````\n\nTalletamisteenuse põhifunktsioon on valikute ja valijanimekirjade ning\nhäälte pikaajaline talletamine.\n\nTalletamisteenuse horisontaalseks skaleerimiseks kasutatakse\nhajustalletamist võimaldavat säilitustehnoloogiat.\n\nTalletamisteenuse teostus\n'''''''''''''''''''''''''\n\nTalletamisteenus ei ole teadlik IVXV protokollist ega talletatavate andmete\nspetsiifikast, vaid on üldkasutatav võti-väärtus andmebaas binaarandmete\nsäilitamiseks. Kogu teadmus talletatavate andmete struktuurist ja võtmete\nhierarhiast on teistes, Talletamisteenust kasutatavates teenustes, mis\nkäituvad nii-öelda \"tarkade\" klientidena.\n\nSelline lähenemine lubab ilma suurema vaevata kasutada Talletamisteenusena\nükskõik millist üldlevinud võti-väärtus andmebaasi: ainsateks ülesanneteks on\nIVXV seadistuse teisendamine andmebaasi jaoks sobilikku vormingusse ning\nteenuse käivitamine. Andmebaasi tarkvara peab võimaldama vaid võtme järgi\ntalletamist ja lugemist, võtmete prefiksi järgi loetlemist ning atomaarset\nvõrdle-ja-vaheta (*compare-and-swap*) operatsiooni.\n\nTalletamisteenus on kogumisteenuse töökiiruse oluliseks määrajaks, mistõttu\nmõjutab seda teenust pakkuv riistvara kogu süsteemi jõudlust ning see tuleks\nvastavalt kasutatavale andmebaasile dimensioneerida.\n\nHetkel ainus tooteks mõeldud Talletamisteenuse teostus kasutab hajusat\nvõti-väärtus andmebaasi etcd. Selle puhul tuleks järgida etcd autorite\n`riistvara soovitusi\n<https://coreos.com/etcd/docs/latest/op-guide/hardware.html>`_.\n\nTuvastusteenuse funktsioon ja tehniline liides\n``````````````````````````````````````````````\n\nTuvastusteenuse põhifunktsioon on valija identiteedi tuvastamine.\nTuvastusteenus on vajalik näiteks Mobiil-ID autentimise korral.\n\nWeb-eID abiteenuse teostus\n''''''''''''''''''''''''''\n\nIVXV koosseisu kuulub Web-eID abiteenus, mis realiseerib Tuvastusteenuse\nID-kaardi kasutamiseks Web-eID raamistikus.\n\nEduka Web-eID isikutuvastuse korral väljastab abiteenus Valijarakendusele\npileti, mille abil on võimalik teistele teenustele valija identiteeti\nkinnitada. Iga piletiga saab hääletada ainult ühe korra.\n\nWeb-eID ei paku allkirjateenust, hääle allkirjastamine toimub Valija seadmes\nlokaalselt analoogselt klassikalisele ID-kaardiga\nautentimisele/allkirjastamisele.\n\nWeb-eID abiteenus on olekuvaba komponent. Tänu sellele on võimalik Web-eID\nabiteenust horisontaalselt skaleerida.\n\n\nAllkirjateenuse funktsioon ja tehniline liides\n``````````````````````````````````````````````\n\nAllkirjateenuse funktsioon on Valijarakenduse toetamine hääle\nallkirjastamisel. Allkirjateenus on vajalik näiteks Mobiil-ID allkirjastamise\nkorral.\n\nMobiil-ID abiteenuse teostus\n''''''''''''''''''''''''''''\n\nIVXV koosseisu kuulub Mobiil-ID abiteenus, mis käitub Mobiil-ID jaoks nii\nTuvastusteenusena kui ka Allkirjateenusena. Valijarakendus esitab IVXV\npäringud Mobiil-ID abiteenusele, mis teisendab need Mobiil-ID päringuteks ning\nedastab Mobiil-ID teenusepakkujale.\n\nEduka Mobiil-ID isikutuvastuse korral väljastab abiteenus Valijarakendusele\npileti, mille abil on võimalik teistele teenustele valija identiteeti\nkinnitada. Iga piletiga saab hääletada ainult ühe korra.\n\nAllkirjastamise korral saadab Valijarakendus Mobiil-ID abiteenusele vaid\nallkirjastatava hääle räsi ning kasutab vastuseks saadud signatuuri samamoodi\nkui ID-kaardiga loodud signatuuri.\n\nMobiil-ID abiteenus sisaldab küll olekut pooleliolevate tuvastusseansside\nkohta, aga muus osas on tegu olekuvaba komponendiga. Tänu sellele on võimalik\nMobiil-ID abiteenust horisontaalselt skaleerida.\n\nSmart-ID abiteenuse teostus\n'''''''''''''''''''''''''''\n\nIVXV koosseisu kuulub Smart-ID abiteenus, mis käitub Smart-ID jaoks nii\nTuvastusteenusena kui ka Allkirjateenusena. Valijarakendus esitab IVXV\npäringud Smart-ID abiteenusele, mis teisendab need Smart-ID päringuteks ning\nedastab Smart-ID teenusepakkujale.\n\nEduka Smart-ID isikutuvastuse korral väljastab abiteenus Valijarakendusele\npileti, mille abil on võimalik teistele teenustele valija identiteeti\nkinnitada. Iga piletiga saab hääletada ainult ühe korra.\n\nAllkirjastamise korral saadab Valijarakendus Smart-ID abiteenusele vaid\nallkirjastatava hääle räsi ning kasutab vastuseks saadud signatuuri samamoodi\nkui ID-kaardiga loodud signatuuri.\n\nSmart-ID abiteenus sisaldab küll olekut pooleliolevate tuvastusseansside\nkohta, aga muus osas on tegu olekuvaba komponendiga. Tänu sellele on võimalik\nSmart-ID abiteenust horisontaalselt skaleerida.\n\n\nHääletamisfaktide järjekorrateenus\n``````````````````````````````````\n\nHääletamisfaktide järjekorrateenuse põhifunktsiooniks on hääletamisfaktide\nedastamine Valimiste Infosüsteemile X-tee abiteenuse vahendusel.\n\nKogumisteenuse mikroteenuste evitamine\n``````````````````````````````````````\n\nKogumisteenuse mikroteenused sõltuvad välistest pakkidest minimaalselt.\nVajalikud sõltuvused on:\n\n#. SSH-server haldustegevuste läbiviimiseks (seda kasutab mikroteenuste\n   haldamiseks haldusteenus).\n\n#. rsyslog logide kogumiseks logikogumisteenustesse.\n\nKogumisteenuse mikroteenused pakendatakse deb-vormingus, neid on võimalik\nevitada ka docker'i-laadsete konteineritena.\n\nVälised teenused ja laiendatavus\n--------------------------------\n\n.. figure:: model/img/collector_extension.png\n\n   Kogumisteenuse laiendusmoodulid ja välised teenused\n\nKogumisteenuse mikroteenused kasutavad laiendusmooduleid teostamaks erinevaid\nmehhanisme valija tuvastamiseks, digiallkirjade verifitseerimiseks ja\ntäiendamiseks, sealhulgas hääle registreerimiseks. Laiendusmoodulid võivad\nteostuse võimaldamiseks kasutada väliseid teenuseid. Mikroteenuste\nlaiendatavuse huvides on defineeritud Go API, mille alusel saab teostada\nka täiendavaid mooduleid. Hetkel on teostatud järgmised moodulid:\n\n- Autentimine TLS-sertifikaadiga (ID-kaart);\n\n- Autentimine Tuvastusteenuse piletiga (Mobiil-ID, Smart-ID, Web-eID);\n\n- BDOC verifitseerimine;\n\n- Kehtivuskinnitusteenus OCSP;\n\n- Ajatempliteenus RFC 3161;\n\n- Registreerimisteenus OCSP;\n\n- Registreerimisteenus RFC 3161.\n\nIVXV krüptograafilises protokollis on kesksel kohal Registreerimisteenus, mis\nosaleb samuti häälte pikaajalisel talletamisel.\n\nRegistreerimisteenuse funktsioon\n````````````````````````````````\n\nRegistreerimisteenuse põhifunktsioon on võtta Hääletamisteenuselt vastu\nallkirjastatud registreerimispäringuid, kinnitada need omapoolse allkirjastatud\nvastusega ning säilitada hilisemaks auditeerimiseks vähemalt hääletamisperioodi\nlõpuni.\n\nAuditeerimisel tekkivate võimalike erisuste lahendamiseks on oluline, et\n\n- Registreerimisteenus on võimeline tõestama, et igale tema poolt väljastatud\n  kinnitusele eelnes Talletamisteenuse poolne registreerimispäring;\n\n- Talletamisteenus on võimeline tõestama, et iga tema poolt talletatud hääle\n  kohta on olemas Registreerimisteenuse kinnitus.\n\nPiisav protokoll sellise tõendamistaseme saavutamiseks on, kus mõlemal\nosapoolel on olemas võtmepaar allkirjastamiseks, päringud ja vastused on\nallkirjastatud ning kumbki pool peab registrit teise poole teadete üle. Selline\nprotokoll on realiseeritav näiteks OCSP-põhise Registreerimisteenuse korral.\nSamas võib esineda juhtumeid, kus näiteks registreerimispäringute\nallkirjastamine ei ole standardsete vahenditega võimalik (RFC 3161 põhine\nregistreerimine). Sellisel juhul tuleb registreerimisteenusele vajalik\ntõendusmaterjal anda muude organisatsioonilis-tehniliste vahenditega.\n\nRegistreerimisteenusel on praegu kaks erinevat teostust:\n\n#. OCSP-liides eeldab Eestis rakendatava OCSP-põhise ajamärgendamisteenuse\n   kasutamist, kus allkirjastatud OCSP-päringu nonsiks on Hääletamisteenuse\n   poolt pandud hääle räsi. Päring on allkirjastatud standardsete OCSP\n   vahenditega;\n\n#. RFC 3161 liides, mille korral ebastandardse lahendusena pannakse\n   ajatemplipäringu nonsiks Hääletamisteenuse poolt allkirjastatud hääle räsi.\n\n\nKogumisteenuse laiendusmoodulite lisamine\n`````````````````````````````````````````\n\nKogumisteenuse API defineerib kuute tüüpi laiendusmooduleid:\n\n#. isikutuvastus (Go pakk ``ivxv.ee/auth``, näiteks ``tls``);\n\n#. tuvastatud isiku sertifikaadist valija identifikaatori tuletamine (Go pakk\n   ``ivxv.ee/identity``, näiteks ``serialnumber``);\n\n#. valija identifikaatorist vanuse tuletamine (Go pakk ``ivxv.ee/age``, näiteks\n   ``estpic``);\n\n#. allkirjastatud konteineri verifitseerimine (Go pakk ``ivxv.ee/container``,\n   näiteks ``bdoc``);\n\n#. allkirja kvalifitseerimine (Go pakk ``ivxv.ee/q11n``, näiteks ``tspreg``);\n\n#. andmetalletusprotokoll (Go pakk ``ivxv.ee/storage``, näiteks ``etcd``).\n\nUue mooduli lisamiseks tuleb moodulpakki lisada mooduli identifikaator ning\nmooduli teostusega alampakk. Alampaki alglaadimisel tuleb mooduli\nregistreerimiseks kutsuda välja moodulpaki ``Register`` funktsioon.\n\nUue mooduli kasutamiseks tuleb selle identifikaator lisada seadistusse vastava\nmoodulitüübi seadistuse juurde koos alammooduli seadistusega. Laiendusmoodulile\nantakse ette tema identifikaatoriga viidatud seadistusblokk, mida see\nmooduli-siseselt edasi töötleb.\n\nMoodulpakid ja nende moodulitelt nõutavad liidesed on täpsemalt kirjeldatud\nvastavates lähtekoodifailides. Samuti on iga mooduli kohta olemas vähemalt üks\nteostus, mida saab kasutada eeskujuna.\n\n\nMonitooring\n-----------\n\n.. figure:: model/img/monitoring.png\n\n   Monitooringulahendus\n\nLogimine\n````````\n\nIga mikroteenuse poolt genereeritav logi defineeritakse süstemaatiliselt,\nlähtudes protokollikirjeldusest ning teenuse osutamise olekudiagrammist.\nLogitakse minimaalselt:\n\n* iga päringu kättesaamise fakt ning töötlemise algus;\n\n* töötlemise üleandmine välisele komponendile;\n\n* töötlemisjärje naasmine komponenti;\n\n* päringu töötlemise lõpp ning tulemus;\n\n* täiendavalt oluliste etappide läbimine protsessi olekumudelis.\n\nLogimisel järgitakse järgmisi põhimõtteid:\n\n* Logimiseks kasutatakse rsyslog teenust, mis registreerib logiteate\n  kirjutamise hetke millisekundi täpsusega;\n\n* Iga seansi alustamisel genereerib süsteem unikaalse identifikaatori, mida\n  klientrakendus kasutab oma päringutel kesksüsteemi poole pöördumiseks;\n\n* Kõik ühe seansi alla kuuluvad logikirjed sisaldavad sama\n  seansiidentifikaatorit;\n\n* Logikirje on unikaalselt identifitseeritav;\n\n* Iga logitava teate juures on võimalik unikaalse tunnuse abil üksüheselt\n  tuvastada teate tekkimise koht monitooritavas süsteemis;\n\n* Logikirje on JSON vormingus, automaatse monitooringu jaoks on masinloetavus\n  primaarne ning inimloetavus sekundaarne;\n\n* Logisse minev info saneeritakse (urlencode) ja sellele rakendatakse pikkuse\n  piirangut (piirang terve logiteate ja samuti parameetri kaupa);\n\n* Süsteemiperimeetrist väljastpoolt pärinevat infot logitakse ainult\n  saneerituna ja ainult etteantud pikkuses.\n\nKuna logimine toimub rsyslog vahendusel, on võimalik Guardtime mooduli\nkasutamine logide tervikluse tagamiseks.\n\n\nÜldstatistika\n`````````````\n\nJärgmise statistika jälgimiseks kasutatakse staatilist veebiliidest:\n\n* edukalt kogutud hääled/hääletajate hulk;\n\n* hääletajate jagunemine sugude, vanusegruppide, operatsioonisüsteemide ning\n  autentimisvahendite kaupa;\n\n* edukalt kontrollitud häälte/hääletajate hulk;\n\n* korduvhääletamiste statistika;\n\n* hääletajate jagunemine riigiti IP-aadressi põhjal.\n\n\nDetailstatistika\n````````````````\n\nDetailstatistika agregeeritakse logide põhjal kasutades SCCEIV\nlogianalüsaatorit, mis  analüüsib rakenduste tegevuslogi eeldefineeritud\nprofiili suhtes ning võimaldab seansi-/veatüübipõhist analüüsi.\n\nDetailstatistika on kättesaadav üle HTTPS-liidese.\n\n\n.. _kogumisteenuse-haldus:\n\nHaldus\n------\n\nKogumisteenuse haldamine toimub digitaalallkirjastatud seadistuspakkide abil.\n\nKogumisteenus pakub seadistuspakkide laadimiseks kahte liidest:\n\n* Käsurealiides – rakendus verifitseerib allkirja, valideerib korralduste\n  vormingut, kooskõlalisust ja sobivust kogumisteenuse seisundi suhtes.\n  Korralduse rakendamine toimub eraldi utiliidi abil.\n\n* Veebiliides – veebiliides vahendab seadistuspaki käsurealiidesele ja tagastab\n  kasutajale info laadimise tulemuse kohta. Eduka laadimise korral toimub\n  automaatselt ja samadel põhimõtetel ka seadistuspaki rakendamine.\n\nVeebiliidese funktsioonideks on:\n\n* Kogumisteenuse mikroteenuste seisundi jälgimine;\n\n* Valimiste nimekirjade haldus;\n\n* Statistika kuvamine e-hääletamise kulgemise kohta;\n\n* Haldusteenuse kasutajate haldus;\n\n* Kogumisteenuse halduse logi kuvamine.\n\nKõik rakendusele antud korraldused säilitatakse - ka need mida ei rakendatud.\nVigaseid (mittevalideeruvaid) korraldusi ei säilitata.\n\nKogumisteenuse haldusteenus sooritab järgmisi tegevusi automaatselt:\n\n#. Valijate nimekirjade muudatuste laadimine Valimiste Infosüsteemist;\n\n#. Hääletamise statistika kogumine hääletusteenusest ja eksportimine Valimiste\n   Infosüsteemi;\n\n#. Talletatud häälte, logide ning seadistuste varundamine varundusteenusesse.\n\n\nHaldusteenuse komponendid\n`````````````````````````\n\n.. figure:: model/img/ms-management-service-components.png\n\n   Kogumisteenuse haldusteenuse komponendid\n\n#. **Halduse veebiserver** on süsteemse kasutaja ``www-data``\n   õigustes töötav Apache server, mille ülesanded on:\n\n   #. Kasutajatelt tulevate HTTPS-päringute esmane teenindamine:\n\n      #. Haldusteenuse usaldusväärsuse tõestamine (TLS-sertifikaat);\n\n      #. Kasutajate autentimine;\n\n   #. Valmisgenereeritud veebilehtede ja andmefailide serveerimine\n      andmehoidlast.\n\n   #. Üldiste taustaandmete päringu vastuse varustamine sisseloginud kasutaja\n      andmetega (WSGI).\n\n   #. Üleslaaditavate korralduste esmane valideerimine ja vahendamine\n      haldusdeemonile ning haldusdeemoni sellekohaste vastuste vahendamine\n      kliendile (WSGI).\n\n#. **Haldusdeemon** on kasutajakonto ``ivxv-admin`` õigustes töötav ja\n   kohalikul (``localhost``) liidesel kuulav veebiserver mille ülesanded on:\n\n   #. Üleslaaditavate korralduste valideerimine;\n\n   #. Üleslaaditavate korralduste vahetu rakendamine (kasutajate haldus);\n\n   #. Üleslaaditavate korralduste salvestamine hilisemaks rakendamiseks\n      (seadistuse ja valimisnimekirjade rakendamiseks teenusele);\n\n   #. E-valimiskasti allalaadimise vahendamine.\n\n#. **Agentdeemon** on kasutajakonto ``ivxv-admin``\n   õigustes töötav deemon, mille ülesanded on:\n\n   #. Andmete kogumine ja registreerimine:\n\n      #. Teadaolevate mikroteenuste seisund;\n\n      #. Tegevusmonitooringu statistika allalaadimine;\n\n#. **Andmehoidla** on failisüsteemis asuv kataloog, kuhu haldusteenuse\n   komponendid hoiavad kogutud ja genereeritud andmeid (vaata üksikasjalist\n   kirjeldust ``IVXV kogumisteenuse haldusjuhendi`` lisadest);\n\nVälised komponendid, millega haldusteenus kokku puutub:\n\n#. **Kogumisteenuse alamteenused** - paigaldamine, seadistamine ja seisundi\n   andmete kogumine toimub agentdeemoni kaudu (SSH-ühendus teenuse masinasse);\n\n#. **Seireserver** - üldstatistika andmete allalaadimine haldusteenuses\n   kuvamiseks;\n\n.. figure:: model/img/ms-upload-command.png\n\n   Korralduste laadimine haldusteenusesse\n\n\nKogumisteenuse seisundid\n------------------------\n\nKogumisteenuse seisund kajastab teenuse kõigi alamteenuste seisundit,\nkasutuselolevate väliste teenuste seisundit ja eelneva põhjal tuletatud\nüldseisundit. Kogumisteenuse üldseisundi tuvastamisega tegeleb haldusteenus.\n\nÜldseisundi olekud on:\n\n#. **Paigaldamata** - alates haldusteenuse paigaldamisest kuni kõigi\n   alamteenuste paigaldamiseni;\n\n#. **Paigaldatud** - kõik alamteenused on paigaldatud, neile on rakendatud\n   tehnilised seadistused ja teenuse toimimiseks vajalikud krüptovõtmed.\n   Valimiste seadistust pole rakendatud (kuid see võib olla laaditud\n   haldusteenusesse);\n\n#. **Seadistatud** - kogumisteenus on seadistatud ja töökorras, sellega on\n   võimalik häälte kogumist läbi viia ja e-valimiskasti väljastada.\n\n#. **Osaline tõrge** - kogumisteenus on seadistatud ja osaliselt töökorras,\n   mõned alamteenused pole töökorras, kuid see ei takista kogumisteenuse\n   toimimist.\n\n#. **Tõrge** - kogumisteenuse oluline sõlm pole töökorras, teenuse nõuetekohane\n   osutamine pole võimalik.\n\n.. figure:: model/img/ms-collector-status.png\n   :scale: 50%\n\n   Kogumisteenuse olekudiagramm. Olekud vastavalt värvusele: kollane -\n   seadistamisel, punane - viga, roheline - töökorras.\n\n\nKogumisteenuse alamteenuste seisundid\n`````````````````````````````````````\n\n.. figure:: model/img/ms-service-status.png\n   :scale: 50%\n\n   Haldusteenuse poolt registreeritud alamteenuse olekudiagramm. Olekud\n   vastavalt värvusele: kollane - seadistamisel, punane - viga, roheline -\n   töökorras.\n\n\nKogumisteenuse seisundi muutused\n````````````````````````````````\n\nKogumisteenuse seisund on jälgitav alates haldusteenuse edukast\npaigaldamisest, algne seisund on **Paigaldamata**.\n\n\nPaigaldamata\n''''''''''''\n\nToimub usaldusjuure ja tehnilise seadistuse rakendamine kogumisteenusele:\n\n#. Seadistuste laadimine kogumisteenusesse;\n\n#. Tehnilises seadistuses kirjeldatud alamteenuste paigaldus;\n\n#. Usaldusjuure ja tehniliste seadistuste rakendamine alamteenustele;\n\nSeadistuste eduka rakendamise tulemusena saab süsteemi uueks seisundiks\n**Paigaldatud**.\n\n\nPaigaldatud\n'''''''''''\n\nKogumisteenuse seadistused on rakendatud kõigile alamteenustele, valimiste\nseadistused pole rakendatud. Toimub valimiste seadistuse laadimine\nhaldusteenusesse ja rakendamine alamteenustele.\n\nValimiste seadistuse eduka rakendamise korral saab süsteemi uueks seisundiks\n**Seadistatud**.\n\n\nSeadistatud\n'''''''''''\n\nKõik kogumisteenuse alamteenused on seadistatud ja töökorras. Haldusteenusel on\nkõikidest alamteenustest värsked seisundiraportid. Süsteemiga on võimalik\nhääletust läbi viia ja e-valimiskasti väljastada.\n\nKui süsteemis tuvastatakse tõrge, saab süsteemi uueks **Osaline tõrge**.\n\n**Seadistatud** olekust ei pöörduta enam kunagi tagasi olekutesse\n**paigaldamata** või **paigaldatud**, kuigi uute alamteenuste lisamisel (kuni\nneed on olekus **paigaldamata/paigaldatud**) oleks vastavad tingimused\ntäidetud.\n\n\nOsaline tõrge\n'''''''''''''\n\nSüsteem on seadistatud ja osaliselt töökorras, mõned süsteemi dubleeritud osad\npole töökorras, kuid see ei takista süsteemil toimimast.\n\nRikke süvenemisel piirini, kus süsteem pole võimeline teenust osutama, saab\nsüsteemi uueks olekuks **Tõrge**. Kõigi rikete kõrvaldamise järel saab\nsüsteemi uueks olekuks **Seadistatud**.\n\n\nTõrge\n'''''\n\nSeadistatud süsteemil on tuvastatud rike, mis takistab teenuse osutamist.\n\nRikete kõrvaldamisel olukorrani, kus süsteemiga on võimalik teenust osutada,\nsaab süsteemi uueks olekuks **Osaline tõrge**.\n\n\nEemaldatud\n''''''''''\n\nTeenus on konfiguratsioonist eemaldatud.\n\n\nValijate nimekirjade olekud haldusteenuses\n------------------------------------------\n\nValijate nimekirja olek võib olla:\n\n#. **Rakendamise ootel** - nimekiri on laaditud haldusteenusesse;\n\n#. **Rakendatud** - nimekiri on rakendatud kogumisteenusele;\n\n#. **Vigane** - nimekiri on märgitud vigaseks, haldusteenus uusi valijate\n   nimekirjade muudatusi ei laadi;\n\n#. **Vahele jäetud** - vigane nimekiri on märgitud vahelejätmiseks.\n\n.. figure:: model/img/ms-voter-list-status.png\n\n   Valijate nimekirja olekudiagramm\n\nSiirdeprotsessid:\n\n#. Nimekirja laadimine haldusteenusesse:\n\n   Algnimekirja laadib kogumisteenuse operaator, nimekirja olekuks saab\n   **Rakendamise ootel**;\n\n   Muudatusnimekirja laadib haldusteenus. Vastavalt valideerimise tulemusele\n   saab nimekirja olekuks kas **Rakendamise ootel** või **Vigane**;\n\n#. **Rakendamine kogumisteenusele**: viib läbi haldusteenus **rakendamise\n   ootel** olekus nimekirjaga. Õnnestumisel määratakse nimekirja olekuks\n   **Rakendatud**, vea korral **Vigane**;\n\n#. **Vahelejätmine**: operaator määrab olekuga **Vigane** nimekirjale oleku\n   **Vahele jäetud**.\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/etcd.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 15:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../etcd.rst:4\nmsgid \"Lisa - ETCD Andmemudel\"\nmsgstr \"Appendix - ETCD datamodel\"\n\n#: ../../etcd.rst:6\nmsgid \"\"\n\"ETCD on võti-väärtus andmebaas, kus talletatakse e-hääletamise \"\n\"sisendnimekirju, e-hääli ning statistikat.\"\nmsgstr \"\"\n\"ETCD is key-value database used for storing input lists, votes and \"\n\"statistics of online voting\"\n\n#: ../../etcd.rst:10 ../../etcd.rst:12\nmsgid \"Ringkondade nimekiri\"\nmsgstr \"List of districts\"\n\n#: ../../etcd.rst:16 ../../etcd.rst:70 ../../etcd.rst:123 ../../etcd.rst:155\n#: ../../etcd.rst:196 ../../etcd.rst:258\nmsgid \"**Võti**\"\nmsgstr \"**Key**\"\n\n#: ../../etcd.rst:16 ../../etcd.rst:70 ../../etcd.rst:123 ../../etcd.rst:155\n#: ../../etcd.rst:196 ../../etcd.rst:258\nmsgid \"**Väärtus**\"\nmsgstr \"**Value**\"\n\n#: ../../etcd.rst:16 ../../etcd.rst:70 ../../etcd.rst:123 ../../etcd.rst:155\n#: ../../etcd.rst:196 ../../etcd.rst:258\nmsgid \"**Näide**\"\nmsgstr \"**Example**\"\n\n#: ../../etcd.rst:18\nmsgid \"/districts\"\nmsgstr \"/districts\"\n\n#: ../../etcd.rst:18 ../../etcd.rst:72 ../../etcd.rst:125 ../../etcd.rst:157\n#: ../../etcd.rst:198 ../../etcd.rst:224 ../../etcd.rst:260\nmsgid \"Juurvõti\"\nmsgstr \"Rootkey\"\n\n#: ../../etcd.rst:20\nmsgid \"/districts/<EHAK-district>\"\nmsgstr \"/districts/<EHAK-district>\"\n\n#: ../../etcd.rst:20\nmsgid \"\"\n\"Seab valija EHAK-ringkond paarile vastavusse ringkonnaidentifikaatori, \"\n\"mis viitab /choices harusse\"\nmsgstr \"\"\n\"Connects voter's EHAK-district pair with district identifier referring to\"\n\" the /choices branch\"\n\n#: ../../etcd.rst:20\nmsgid \"/districts/05241\"\nmsgstr \"/districts/05241\"\n\n#: ../../etcd.rst:22\nmsgid \"0000.1\"\nmsgstr \"0000.1\"\n\n#: ../../etcd.rst:27\nmsgid \"/districts/counties\"\nmsgstr \"/districts/counties\"\n\n#: ../../etcd.rst:27\nmsgid \"Väli counties ringkondade nimekirjast\"\nmsgstr \"Field counties from the district list\"\n\n#: ../../etcd.rst:27 ../../etcd.rst:76\nmsgid \"{\"\nmsgstr \"{\"\n\n#: ../../etcd.rst:29\nmsgid \"\\\"0068\\\": [\"\nmsgstr \"\\\"0068\\\": [\"\n\n#: ../../etcd.rst:31\nmsgid \"\\\"0809\\\",\"\nmsgstr \"\\\"0809\\\",\"\n\n#: ../../etcd.rst:33\nmsgid \"\\\"0624\\\"\"\nmsgstr \"\\\"0624\\\"\"\n\n#: ../../etcd.rst:35 ../../etcd.rst:41 ../../etcd.rst:47\nmsgid \"],\"\nmsgstr \"],\"\n\n#: ../../etcd.rst:37\nmsgid \"\\\"0784\\\": [\"\nmsgstr \"\\\"0784\\\": [\"\n\n#: ../../etcd.rst:39\nmsgid \"\\\"0524\\\"\"\nmsgstr \"\\\"0524\\\"\"\n\n#: ../../etcd.rst:43\nmsgid \"\\\"0079\\\": [\"\nmsgstr \"\\\"0079\\\": [\"\n\n#: ../../etcd.rst:45\nmsgid \"\\\"0796\\\"\"\nmsgstr \"\\\"0796\\\"\"\n\n#: ../../etcd.rst:49\nmsgid \"\\\"0793\\\": [\"\nmsgstr \"\\\"0793\\\": [\"\n\n#: ../../etcd.rst:51\nmsgid \"\\\"0793\\\"\"\nmsgstr \"\\\"0793\\\"\"\n\n#: ../../etcd.rst:53\nmsgid \"]\"\nmsgstr \"]\"\n\n#: ../../etcd.rst:55 ../../etcd.rst:106 ../../etcd.rst:108\nmsgid \"}\"\nmsgstr \"}\"\n\n#: ../../etcd.rst:57\nmsgid \"/districts/version\"\nmsgstr \"/districts/version\"\n\n#: ../../etcd.rst:57 ../../etcd.rst:110\nmsgid \"Nimekirja allkirjastajad\"\nmsgstr \"Signers of lists\"\n\n#: ../../etcd.rst:57\nmsgid \"[\\\"NIMESTE,NIMI,123456 78912 2019-02-22T13:58:48Z\\\" ]\"\nmsgstr \"[\\\"NIMESTE,NIMI,123456 78912 2019-02-22T13:58:48Z\\\" ]\"\n\n#: ../../etcd.rst:64 ../../etcd.rst:66\nmsgid \"Valikute nimekiri\"\nmsgstr \"Choices list\"\n\n#: ../../etcd.rst:72\nmsgid \"/choices\"\nmsgstr \"/choices\"\n\n#: ../../etcd.rst:74\nmsgid \"/choices/<district-id>\"\nmsgstr \"/choices/<district-id>\"\n\n#: ../../etcd.rst:74\nmsgid \"Ringkonnale district-id vastav valikute nimekiri.\"\nmsgstr \"Choices list corresponding to district-id\"\n\n#: ../../etcd.rst:74\nmsgid \"/choices/0000.1\"\nmsgstr \"/choices/0000.1\"\n\n#: ../../etcd.rst:78\nmsgid \"\\\"Erakond 1\\\":{\"\nmsgstr \"\\\"Party 1\\\":{\"\n\n#: ../../etcd.rst:80\nmsgid \"\\\"0000.101\\\":\\\"Nimi Nimeste1\\\",\"\nmsgstr \"\\\"0000.101\\\":\\\"Nimi Nimeste1\\\",\"\n\n#: ../../etcd.rst:83\nmsgid \"\\\"0000.102\\\":\\\"Nimi Nimeste2\\\",\"\nmsgstr \"\\\"0000.102\\\":\\\"Nimi Nimeste2\\\",\"\n\n#: ../../etcd.rst:86\nmsgid \"\\\"0000.103\\\":\\\"Nimi Nimeste3\\\"\"\nmsgstr \"\\\"0000.103\\\":\\\"Nimi Nimeste3\\\"\"\n\n#: ../../etcd.rst:89 ../../etcd.rst:99\nmsgid \"},\"\nmsgstr \"},\"\n\n#: ../../etcd.rst:91\nmsgid \"\\\"Erakond 2\\\":{\"\nmsgstr \"\\\"Party 2\\\":{\"\n\n#: ../../etcd.rst:93\nmsgid \"\\\"0000.104\\\":\\\"Nimi Nimeste4\\\",\"\nmsgstr \"\\\"0000.104\\\":\\\"Nimi Nimeste4\\\",\"\n\n#: ../../etcd.rst:96\nmsgid \"\\\"0000.105\\\":\\\"Nimi Nimeste5\\\"\"\nmsgstr \"\\\"0000.105\\\":\\\"Nimi Nimeste5\\\"\"\n\n#: ../../etcd.rst:101\nmsgid \"\\\"Üksikkandidaadid\\\":{\"\nmsgstr \"\\\"Independent candidates\\\":{\"\n\n#: ../../etcd.rst:103\nmsgid \"\\\"0000.106\\\":\\\"Nimi Nimeste6\\\"\"\nmsgstr \"\\\"0000.106\\\":\\\"Nimi Nimeste6\\\"\"\n\n#: ../../etcd.rst:110\nmsgid \"/choices/version\"\nmsgstr \"/choices/version\"\n\n#: ../../etcd.rst:110 ../../etcd.rst:134\nmsgid \"[\\\"NIMESTE,NIMI,123456 78912 2019-02-22T13:58:59Z\\\" ]\"\nmsgstr \"[\\\"NIMESTE,NIMI,123456 78912 2019-02-22T13:58:59Z\\\" ]\"\n\n#: ../../etcd.rst:117 ../../etcd.rst:119\nmsgid \"Valijate nimekiri\"\nmsgstr \"Voterlist\"\n\n#: ../../etcd.rst:125\nmsgid \"/voters\"\nmsgstr \"/voters\"\n\n#: ../../etcd.rst:127\nmsgid \"/voters/<version-id>\"\nmsgstr \"/voters/<version-id>\"\n\n#: ../../etcd.rst:127\nmsgid \"Valijanimekirja versiooni juurvõti\"\nmsgstr \"Rootkey of voterlist version\"\n\n#: ../../etcd.rst:127\nmsgid \"/voters/1\"\nmsgstr \"/voters/1\"\n\n#: ../../etcd.rst:130\nmsgid \"/voters/<version-id>/<voter-id>\"\nmsgstr \"/voters/<version-id>/<voter-id>\"\n\n#: ../../etcd.rst:130\nmsgid \"Valija <EHAK-district> antud nimekirjas\"\nmsgstr \"Voter <EHAK-district> in the list\"\n\n#: ../../etcd.rst:130\nmsgid \"/voters/1/12345678912\"\nmsgstr \"/voters/1/12345678912\"\n\n#: ../../etcd.rst:132\nmsgid \"05241\"\nmsgstr \"05241\"\n\n#: ../../etcd.rst:134\nmsgid \"/voters/<version-id>/version\"\nmsgstr \"/voters/<version-id>/version\"\n\n#: ../../etcd.rst:134\nmsgid \"Valijatenimekirja versioon\"\nmsgstr \"Voterlist version\"\n\n#: ../../etcd.rst:139\nmsgid \"/voters/version\"\nmsgstr \"/voters/version\"\n\n#: ../../etcd.rst:139\nmsgid \"Aktuaalse valijatenimekirja versiooni ID\"\nmsgstr \"ID of latest voterlist\"\n\n#: ../../etcd.rst:139\nmsgid \"1\"\nmsgstr \"1\"\n\n#: ../../etcd.rst:143\nmsgid \"/voters/previous\"\nmsgstr \"/voters/previous\"\n\n#: ../../etcd.rst:143\nmsgid \"Eelmise valijanimekirja versiooni ID\"\nmsgstr \"ID of previous voterlist\"\n\n#: ../../etcd.rst:143 ../../etcd.rst:163 ../../etcd.rst:178\nmsgid \"0\"\nmsgstr \"0\"\n\n#: ../../etcd.rst:149 ../../etcd.rst:151\nmsgid \"Talletatud e-hääl\"\nmsgstr \"Stored i-vote\"\n\n#: ../../etcd.rst:157\nmsgid \"/vote\"\nmsgstr \"/vote\"\n\n#: ../../etcd.rst:159\nmsgid \"/vote/<vote-id>\"\nmsgstr \"/vote/<vote-id>\"\n\n#: ../../etcd.rst:159\nmsgid \"Hääle juurvõti, unikaalne identifikaator\"\nmsgstr \"Rootkey of a vote, unique identifier\"\n\n#: ../../etcd.rst:159\nmsgid \"16 baiti binaarandmed\"\nmsgstr \"16 bytes binary\"\n\n#: ../../etcd.rst:163\nmsgid \"/vote/<vote-id>/count\"\nmsgstr \"/vote/<vote-id>/count\"\n\n#: ../../etcd.rst:163\nmsgid \"Hääle kontrollimise loendur\"\nmsgstr \"Vote verification counter\"\n\n#: ../../etcd.rst:166\nmsgid \"/vote/<vote-id>/ocsp\"\nmsgstr \"/vote/<vote-id>/ocsp\"\n\n#: ../../etcd.rst:166\nmsgid \"Kehtivuskinnitus\"\nmsgstr \"Proof-of-validity\"\n\n#: ../../etcd.rst:166\nmsgid \"DER kodeeringus OCSP vastus\"\nmsgstr \"DER encoded OCSP response\"\n\n#: ../../etcd.rst:169\nmsgid \"/vote/<vote-id>/time\"\nmsgstr \"/vote/<vote-id>/time\"\n\n#: ../../etcd.rst:169\nmsgid \"Hääle talletamise kellaaeg\"\nmsgstr \"Time of the vote storage\"\n\n#: ../../etcd.rst:169\nmsgid \"2019-03-03T10:56:28.8 99925926Z\"\nmsgstr \"2019-03-03T10:56:28.8 99925926Z\"\n\n#: ../../etcd.rst:172\nmsgid \"/vote/<vote-id>/tspreg\"\nmsgstr \"/vote/<vote-id>/tspreg\"\n\n#: ../../etcd.rst:172\nmsgid \"Registreerimiskinnitus\"\nmsgstr \"Registration confirmation\"\n\n#: ../../etcd.rst:172\nmsgid \"DER kodeeringus PKIX ajatempel\"\nmsgstr \"DER encoded PKIX timestamp\"\n\n#: ../../etcd.rst:175\nmsgid \"/vote/<vote-id>/type\"\nmsgstr \"/vote/<vote-id>/type\"\n\n#: ../../etcd.rst:175\nmsgid \"Allkirjastatud hääle konteineri tüüp\"\nmsgstr \"Type of signed container\"\n\n#: ../../etcd.rst:175\nmsgid \"BDOC\"\nmsgstr \"BDOC\"\n\n#: ../../etcd.rst:178\nmsgid \"/vote/<vote-id>/version\"\nmsgstr \"/vote/<vote-id>/version\"\n\n#: ../../etcd.rst:178\nmsgid \"Hääle andmisel kehtinud valijate nimekirja versiooni ID\"\nmsgstr \"ID of the voterlist valid at the time of voting\"\n\n#: ../../etcd.rst:183\nmsgid \"/vote/<vote-id>/vote\"\nmsgstr \"/vote/<vote-id>/vote\"\n\n#: ../../etcd.rst:183\nmsgid \"E-hääl allkirjastatud konteineris\"\nmsgstr \"I-vote in signed container\"\n\n#: ../../etcd.rst:183\nmsgid \"BDOC vormingus allkirjastatud hääl\"\nmsgstr \"Vote in BDOC format\"\n\n#: ../../etcd.rst:186\nmsgid \"/vote/<vote-id>/voter\"\nmsgstr \"/vote/<vote-id>/voter\"\n\n#: ../../etcd.rst:186\nmsgid \"Valija isikukood\"\nmsgstr \"Voter personal code\"\n\n#: ../../etcd.rst:186\nmsgid \"12345678912\"\nmsgstr \"12345678912\"\n\n#: ../../etcd.rst:190 ../../etcd.rst:192\nmsgid \"Statistikaliidesed\"\nmsgstr \"Statisticinterfaces\"\n\n#: ../../etcd.rst:198\nmsgid \"/votes\"\nmsgstr \"/votes\"\n\n#: ../../etcd.rst:200\nmsgid \"/votes/order\"\nmsgstr \"/votes/order\"\n\n#: ../../etcd.rst:200\nmsgid \"Hääletamisfaktide järjestuse juurvõti\"\nmsgstr \"Rootkey of votesorder\"\n\n#: ../../etcd.rst:203\nmsgid \"/votes/order/<seq>\"\nmsgstr \"/votes/order/<seq>\"\n\n#: ../../etcd.rst:203\nmsgid \"Konkreetse hääletamisfakti juurvõti\"\nmsgstr \"Rootkey of specific vote\"\n\n#: ../../etcd.rst:203\nmsgid \"/votes/order/1\"\nmsgstr \"/votes/order/1\"\n\n#: ../../etcd.rst:207\nmsgid \"/votes/order/<seq>/admincode\"\nmsgstr \"/votes/order/<seq>/admincode\"\n\n#: ../../etcd.rst:207\nmsgid \"Hääletamisfaktiga seotud EHAK\"\nmsgstr \"EHAK associated with vote\"\n\n#: ../../etcd.rst:207\nmsgid \"0796\"\nmsgstr \"0796\"\n\n#: ../../etcd.rst:210\nmsgid \"/votes/order/<seq>/district\"\nmsgstr \"/votes/order/<seq>/district\"\n\n#: ../../etcd.rst:210\nmsgid \"Hääletamisfaktiga seotud ringkonna number\"\nmsgstr \"District associated with vote\"\n\n#: ../../etcd.rst:210\nmsgid \"10\"\nmsgstr \"10\"\n\n#: ../../etcd.rst:214\nmsgid \"/votes/order/<seq>/voterid\"\nmsgstr \"/votes/order/<seq>/voterid\"\n\n#: ../../etcd.rst:214\nmsgid \"Hääletaja isikukood\"\nmsgstr \"Personal code\"\n\n#: ../../etcd.rst:214\nmsgid \"12345678901\"\nmsgstr \"12345678901\"\n\n#: ../../etcd.rst:217\nmsgid \"/votes/order/<seq>/votername\"\nmsgstr \"/votes/order/<seq>/votername\"\n\n#: ../../etcd.rst:217\nmsgid \"Hääletaja nimi\"\nmsgstr \"Voter name\"\n\n#: ../../etcd.rst:217\nmsgid \"NIMI NIMESTE\"\nmsgstr \"NIMI NIMESTE\"\n\n#: ../../etcd.rst:220\nmsgid \"/votes/stats\"\nmsgstr \"/votes/stats\"\n\n#: ../../etcd.rst:220\nmsgid \"Viimase hääletamisfakti järjekorranumber\"\nmsgstr \"Sequence number of last vote\"\n\n#: ../../etcd.rst:220\nmsgid \"12\"\nmsgstr \"12\"\n\n#: ../../etcd.rst:224\nmsgid \"/voted\"\nmsgstr \"/voted\"\n\n#: ../../etcd.rst:226\nmsgid \"/voted/latest\"\nmsgstr \"/voted/latest\"\n\n#: ../../etcd.rst:226\nmsgid \"Viimati antud häälte indeksi juurvõti\"\nmsgstr \"Rootkey of last vote index\"\n\n#: ../../etcd.rst:229\nmsgid \"/voted/latest/<voter-id>\"\nmsgstr \"/voted/latest/<voter-id>\"\n\n#: ../../etcd.rst:229\nmsgid \"Hääletaja poolt viimati antud hääle aeg ja identifikaator binaarkujul\"\nmsgstr \"The time and identifier of the last vote in binary\"\n\n#: ../../etcd.rst:229\nmsgid \"/voted/latest/1234567 8901\"\nmsgstr \"/voted/latest/1234567 8901\"\n\n#: ../../etcd.rst:232\nmsgid \"<2019-03-03T12:15:59Z ><vote-id>\"\nmsgstr \"<2019-03-03T12:15:59Z ><vote-id>\"\n\n#: ../../etcd.rst:235\nmsgid \"/voted/stats\"\nmsgstr \"/voted/stats\"\n\n#: ../../etcd.rst:235\nmsgid \"Jaoskonnapõhise statistika indeksi juurvõti\"\nmsgstr \"Station statistics rootkey\"\n\n#: ../../etcd.rst:239\nmsgid \"/voted/stats/<voter-id>\"\nmsgstr \"/voted/stats/<voter-id>\"\n\n#: ../../etcd.rst:239\nmsgid \"Hääle andmise kellaaeg koos jaoskonnainfoga\"\nmsgstr \"Time of voting with station info\"\n\n#: ../../etcd.rst:239\nmsgid \"/voted/stats/12345678 901\"\nmsgstr \"/voted/stats/12345678 901\"\n\n#: ../../etcd.rst:242\nmsgid \"<0796><2019-02-22T14: 17:23Z>\"\nmsgstr \"<0796><2019-02-22T14: 17:23Z>\"\n\n#: ../../etcd.rst:245\nmsgid \"/votes/voter/stats/<voter-id>\"\nmsgstr \"/votes/voter/stats/<voter-id>\"\n\n#: ../../etcd.rst:245\nmsgid \"\"\n\"Tühi baitide massiiv (kasutatakse väärtuse versiooni, ning mitte väärtust\"\n\" ennast)\"\nmsgstr \"Empty array - we use the version of the value, not value itself\"\n\n#: ../../etcd.rst:245\nmsgid \"/votes/voter/stats/ 394091044211\"\nmsgstr \"/votes/voter/stats/ 394091044211\"\n\n#: ../../etcd.rst:252 ../../etcd.rst:254\nmsgid \"Hääletamisseansid\"\nmsgstr \"Voting sessions\"\n\n#: ../../etcd.rst:260\nmsgid \"/session\"\nmsgstr \"/session\"\n\n#: ../../etcd.rst:262\nmsgid \"/session/<session-id>\"\nmsgstr \"/session/<session-id>\"\n\n#: ../../etcd.rst:262\nmsgid \"\"\n\"RPC meetod, mis kutsus antud funktsiooni välja + ``x1F`` + kasutaja \"\n\"autentimismeetod\"\nmsgstr \"RPC method called + ``x1F`` + authentication method\"\n\n#: ../../etcd.rst:262\nmsgid \"``/session/0149468d2866`` ``6fced7d73b32cc16225d``\"\nmsgstr \"``/session/0149468d2866`` ``6fced7d73b32cc16225d``\"\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/index.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:35+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\n#: ../../index.rst:4\nmsgid \"IVXV arhitektuur\"\nmsgstr \"IVXV architecture\"\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/kogumisteenus.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 15:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../kogumisteenus.rst:4\nmsgid \"Kogumisteenus\"\nmsgstr \"Collection service\"\n\n#: ../../kogumisteenus.rst:6\nmsgid \"Üldkirjelduse [ÜK2016]_ põhjal on Kogumisteenus:\"\nmsgstr \"Based on the general description [ÜK2016]_, the Collection Service is:\"\n\n#: ../../kogumisteenus.rst:10\nmsgid \"\"\n\"Süsteemi keskne komponent, mida käitab Koguja. Teenus abistab Hääletajat \"\n\"e-hääle koostamisel ning registreerib selle enne salvestamist \"\n\"e-valimiskasti. Kogumisteenus kasutab väliseid teenuseid (tuvastamine, \"\n\"allkirjastamine, registreerimine). Kogumisteenusel on peale Koguja enda \"\n\"teisigi haldureid (Korraldaja, Klienditugi), kelle jaoks on \"\n\"Kogumisteenusel eraldi haldusliidesed.\"\nmsgstr \"\"\n\"The central component of the system, operated by the Collector. The \"\n\"service assists the Voter in compiling the e-Vote and registers the vote before\"\n\" storing it in the digital ballotbox. The Collection Service makes use of\"\n\" external services (authentication, signing, registration). In addition \"\n\"to the Collector, the Collection Service has other administrators \"\n\"(Organiser, Customer Support) for which the Collection Service has \"\n\"separate administrative interfaces.\"\n\n#: ../../kogumisteenus.rst:17\nmsgid \"\"\n\"Kogumisteenus töötab sidusrežiimis ning vähemalt valija- ja \"\n\"kontrollrakenduse suunalised liidesed on avatud internetile. Seega \"\n\"töötleb Kogumisteenus potentsiaalselt ebausaldusväärsest allikast pärit \"\n\"päringuid. Tulenevalt tarkvarale seatavast turvatasemest, \"\n\"kõrgkäideldavuse, skaleeritavuse, kihilise evitatavuse ning laiendatavuse\"\n\" nõuetest on kogumisteenus omakorda liigendatud ühte konkreetset teenust \"\n\"osutavateks mikroteenusteks, mida on võimalik paindlikult evitada.\"\nmsgstr \"\"\n\"The collection service works in online mode and at least the interfaces \"\n\"to the voter and the verification application are open to the internet. \"\n\"Thus, the Collection Service processes queries from potentially \"\n\"unreliable sources. In turn, due to the security level, high \"\n\"availability, scalability, deployment and extensibility requirements of \"\n\"the software, the Collection Service is broken down into microservices \"\n\"providing one specific service, which can be flexibly extensible.\"\n\n#: ../../kogumisteenus.rst:25\nmsgid \"\"\n\"Kõik kogumisteenuse komponendid programmeeritakse keeles `Go \"\n\"<https://golang.org>`_. Keelel Go on:\"\nmsgstr \"\"\n\"All collection service components are programmed in `Go \"\n\"<https://golang.org>`_. The language Go is:\"\n\n#: ../../kogumisteenus.rst:28\nmsgid \"\"\n\"staatiline tüüpimine, mis võimaldab tüübivigade avastamist enne programmi\"\n\" käivitamist;\"\nmsgstr \"\"\n\"static typing, which allows typing errors to be detected before the \"\n\"program is run;\"\n\n#: ../../kogumisteenus.rst:31\nmsgid \"\"\n\"automaatne mäluhaldus, mis välistab rakenduse vigasest mäluhaldusest \"\n\"tulenevad turvaaugud;\"\nmsgstr \"\"\n\"automatic memory management, which eliminates security holes caused by \"\n\"incorrect memory management in the application;\"\n\n#: ../../kogumisteenus.rst:34\nmsgid \"kompilaator avatud lähtekoodiga;\"\nmsgstr \"open source compiler;\"\n\n#: ../../kogumisteenus.rst:36\nmsgid \"\"\n\"ribastamine/rööprapse, mis võimaldab kasutada paralleelsust \"\n\"mitmetuumalistes süsteemides.\"\nmsgstr \"goroutines, allowing parallelism to be used in multi-CPU systems.\"\n\n#: ../../kogumisteenus.rst:39\nmsgid \"\"\n\"Kogumisteenuse andmeedastuseks kasutatakse üldjuhul JSON-vormingut, välja\"\n\" arvatud olukordades, kus välised asjaolud tingivad mõne muu \"\n\"andmevormingu kasutamist (näiteks BDOC-vorming põhineb XML'il).\"\nmsgstr \"\"\n\"The JSON format will generally be used for the data transmission of the \"\n\"collection service, except in situations where external circumstances \"\n\"dictate the use of another data format (for example, the BDOC format is \"\n\"based on XML).\"\n\n#: ../../kogumisteenus.rst:43\nmsgid \"\"\n\"Kogumisteenus toetab Riigikogu valimisi, kohaliku omavalitsuse volikogu \"\n\"valimisi, Euroopa parlamendi valimisi ning rahvahääletusi.\"\nmsgstr \"\"\n\"The collection service supports elections to the Riigikogu, local council\"\n\" elections, European Parliament elections and referendums.\"\n\n#: ../../kogumisteenus.rst:46\nmsgid \"\"\n\"Kogumisteenuse komponendid arvestavad virtualiseerimistehnoloogiate \"\n\"kasutamisega ning kogumisteenust on võimalik evitada nii ühel \"\n\"virtuaalriistvara instantsil, kui ka mikroteenuste kaupa erinevatel \"\n\"instantsidel. Kogumisteenuse komponendid on evitatavad Ubuntu 22.04 LTS \"\n\"(Jammy Jellyfish) operatsioonisüsteemil 64-bitisel arhitektuuril.\"\nmsgstr \"\"\n\"The collection service components take into account the use of \"\n\"virtualisation technologies and the collection service can be hosted on a\"\n\" single virtual hardware instance or on a microservice-by-microservice \"\n\"basis on different instances. The collection service components are \"\n\"executable on Ubuntu 22.04 LTS (Jammy Jellyfish) operating system on \"\n\"64-bit architecture.\"\n\n#: ../../kogumisteenus.rst:52\nmsgid \"\"\n\"Andmesäilitus on teostatud kasutades võti-väärtus andmebaasi (etcd). \"\n\"Testotstarbel on teostatud ka andmesäilitus failisüsteemi ning mällu, \"\n\"kuid neid ei ole soovituslik kasutada tootekeskkonnas. Lisaks on \"\n\"kogumisteenusel olemas liides uute talletusprotokollide lisamiseks. \"\n\"Lõplik otsus kasutatava lahenduse kohta tehakse kogumisteenuse haldurite \"\n\"poolt teenust seadistades.\"\nmsgstr \"\"\n\"Data storage is performed using a key-value database (etcd). For testing \"\n\"purposes, data storage on the file system and in memory has also been \"\n\"implemented, but is not recommended for use in the production \"\n\"environment. In addition, the collection service has an interface for \"\n\"adding new storage protocols. The final decision on the solution to be \"\n\"used will be made by the collection administrators when configuring the \"\n\"service.\"\n\n#: ../../kogumisteenus.rst:59\nmsgid \"Mikroteenused\"\nmsgstr \"Microservices\"\n\n#: ../../kogumisteenus.rst:63\nmsgid \"Kogumisteenuse jaotus mikroteenusteks\"\nmsgstr \"Breakdown of collection service into microservices\"\n\n#: ../../kogumisteenus.rst:65\nmsgid \"\"\n\"Kogumisteenus on jaotatud põhiteenusteks ja abiteenusteks. Põhiteenused -\"\n\" vahendusteenus, nimekirjateenus, hääletamisteenus, kontrollteenus ning \"\n\"talletamisteenus - on arhitektuuri tehnilise lihtsuse mõttes piiritletud \"\n\"ühe valimisega, kuid ühel riistvaral, ühe operatsioonisüsteemi kontekstis\"\n\" võivad käia mitme valimise mikroteenused. Täiendavalt võib \"\n\"kogumisteenuse juures kasutada abiteenuseid - tuvastusteenust hääletaja \"\n\"isiku tuvastamiseks ning allkirjateenust valijarakenduse poolt hääle \"\n\"allkirjastamise hõlbustamiseks.\"\nmsgstr \"\"\n\"The collection service is broken down into basic and helper services. The\"\n\" basic services - dispatcher, choice-list, voting, verification and \"\n\"storage - are limited to a single election for the sake of technical \"\n\"simplicity of the architecture, but multiple election microservices can \"\n\"run on a single hardware, in the context of a single operating system. In\"\n\" addition, the collection service may use helper services - an \"\n\"authentication service to authenticate the voter, and a signature service\"\n\" to facilitate the signing of the vote by the voting application.\"\n\n#: ../../kogumisteenus.rst:73\nmsgid \"\"\n\"Teenuseid on võimalik evitada nii eraldatult kui koos erinevates \"\n\"konfiguratsioonides, mis teeb võimalikuks kihilise arhitektuuri. Lähtudes\"\n\" funktsioonist on otstarbekas hoida Vahendus- ning Talletamisteenused \"\n\"teistest eraldi.\"\nmsgstr \"\"\n\"Services can be provided both separately and together in different \"\n\"configurations, enabling a layered architecture. Based on the function, \"\n\"it makes sense to keep the dispatcher and storage services separate from \"\n\"each other.\"\n\n#: ../../kogumisteenus.rst:78\nmsgid \"\"\n\"Teenused kasutavad transpordiprotokollina TLS'i, ühendused on vähemalt \"\n\"serveripoolselt autenditud. Rakenduskihi protokoll on JSON-RPC.\"\nmsgstr \"\"\n\"The services use TLS as transport protocol, all connections are at least \"\n\"server-side authenticated. The application layer protocol is JSON-RPC.\"\n\n#: ../../kogumisteenus.rst:81\nmsgid \"\"\n\"Kõik teenused tekitavad tegevuslogi, mida säilitatakse nii lokaalselt kui\"\n\" logitakse syslog protokolli vahendusel kesksesse logikogujasse.\"\nmsgstr \"\"\n\"All services generate an activity log, which is both stored locally and \"\n\"logged via the syslog protocol to a central log collector.\"\n\n#: ../../kogumisteenus.rst:85\nmsgid \"Vahendusteenuse funktsioon ja tehniline liides\"\nmsgstr \"Dispatcher service function and technical interface\"\n\n#: ../../kogumisteenus.rst:87\nmsgid \"\"\n\"Vahendusteenuse põhifunktsioon on ühe sisenemispunkti (port 443) \"\n\"pakkumine Valijarakendusele ja Kontrollrakendusele. Vahendusteenus on \"\n\"dispetšerteenus teiste komponentide vahel, mis võimaldab sisemiselt \"\n\"evitada kogumisteenust mikroteenustena, ent omada süsteemil ainult ühte \"\n\"sisenemispunkti. Lisaks suudab see dubleeritud evituse puhul täita \"\n\"koormusjaoturi ülesannet.\"\nmsgstr \"\"\n\"The main function of the Dispatcher Service is to provide a single entry \"\n\"point (port 443) to the Voting Application and the Verification \"\n\"Application. The Dispatcher Service is dispatching between other \"\n\"components, allowing the collection service to be provided internally as \"\n\"microservices, but with only one entry point on the system. In addition, \"\n\"it is able to act as a load balancer in the case of distributed \"\n\"deployment.\"\n\n#: ../../kogumisteenus.rst:93\nmsgid \"\"\n\"Vahendusteenus ei termineeri TLS-ühendust vaid kasutab sihtpunkti \"\n\"tuvastamiseks TLS'i *Server Name Indication* (SNI) laiendust. Kliendid \"\n\"panevad TLS ``ClientHello`` sõnumisse SNI-laiendi, kus avatekstis \"\n\"määravad, millise teenusega soovivad suhelda: vahendusteenus näeb seda, \"\n\"võtab ühendust vastavat teenust pakkuva isendiga ja hakkab kliendi ning \"\n\"teenuse vahelisi sõnumeid vahendama. Vahendusteenus EI termineeri TLS'i \"\n\"ning ei näe sõnumite sisu. Vahendusteenusel on andmed kõigi teiste \"\n\"teenuste asukohtadest (aadress:port) ning teenus vahendab sõnumivahetust \"\n\"kõigi osapoolte vahel.\"\nmsgstr \"\"\n\"The dispatcher service does not terminate the TLS connection but uses the\"\n\" TLS *Server Name Indication* (SNI) extension to identify the \"\n\"destination. Clients put an SNI extension in the TLS ``ClientHello`` \"\n\"message, where they specify in the plain text which service they wish to \"\n\"communicate with: the dispatcher service sees this, contacts the host \"\n\"providing the corresponding service and starts dispatching messages \"\n\"between the client and the service. The dispatcher service does NOT \"\n\"terminate the TLS and does not see the content of the messages. The \"\n\"dispatcher service has information about the locations of all other \"\n\"services (address:port) and mediates the messaging between all parties.\"\n\n#: ../../kogumisteenus.rst:102\nmsgid \"\"\n\"Vahendusteenus on olekuvaba komponent, mida on võimalik horisontaalselt \"\n\"skaleerida.\"\nmsgstr \"\"\n\"The dispatcher service is a stateless component that can be scaled \"\n\"horizontally.\"\n\n#: ../../kogumisteenus.rst:106\nmsgid \"Vahendusteenuse teostus\"\nmsgstr \"Implementation of the dispatcher service\"\n\n#: ../../kogumisteenus.rst:108\nmsgid \"\"\n\"Vahendusteenuse teostus kasutab vabavaralist HAProxy serverit, mis on \"\n\"üldlevinud tarkvaraline koormusjaotur ja proksi. Kuna Vahendusteenus on \"\n\"esimene puutepunkt avalikust internetist tulevate ühenduste jaoks, siis \"\n\"on mõistlik kasutada tarkvara, mille töökindlus on juba tõestatud.\"\nmsgstr \"\"\n\"The dispatcher service is implemented using the freeware HAProxy server, \"\n\"which is a ubiquitous software load balancer and proxy. As the Dispatcher\"\n\" Service is the first point of contact for connections from the public \"\n\"Internet, it makes sense to use software that is already proven to be \"\n\"reliable.\"\n\n#: ../../kogumisteenus.rst:113\nmsgid \"\"\n\"Kuigi HAProxyt kasutatakse tihti HTTP-režiimis, kus see analüüsib \"\n\"liiklust, siis vahendusteenuse rollis on see TCP-režiimis ning ei näe \"\n\"vahendatava krüpteeritud TLS-kanali sisse.\"\nmsgstr \"\"\n\"While HAProxy is often used in HTTP mode, where it analyses traffic, in \"\n\"the role of a proxy service it is in TCP mode and does not see into the \"\n\"encrypted TLS channel being proxied.\"\n\n#: ../../kogumisteenus.rst:117\nmsgid \"\"\n\"IVXV seadistusest genereeritakse HAProxy seadistusfail, mis sisaldab \"\n\"teiste teenuste asukohti, ning ühenduste vahendamise ülesanne jääb \"\n\"viimase kanda. Lisaks on võimalik HAProxyt ka seadistada ühenduste \"\n\"sagedusi piirama lähteaadressi või mõne muu nimetaja põhjal. See aga jääb\"\n\" süsteemihalduri ülesandeks.\"\nmsgstr \"\"\n\"The IVXV configuration generates a HAProxy configuration file containing \"\n\"the locations of the other services, and leaves the connection brokering \"\n\"to the latter. In addition, it is also possible to configure HAProxy to \"\n\"limit connections based on the source address or some other denominator. \"\n\"This, however, is left to the system administrator.\"\n\n#: ../../kogumisteenus.rst:123\nmsgid \"\"\n\"Kuigi HAProxy on võimeline ise teostama koormusjaoturi ülesannet, on seda\"\n\" võimalik evitada ka teiste, potentsiaalselt riistvaraliste \"\n\"koormusjaoturite taha, kus see jääb täitma ainult SNI põhjal vahendamise \"\n\"ülesannet.\"\nmsgstr \"\"\n\"Although HAProxy is capable of performing the load balancer task itself, \"\n\"it can also be exported behind other, potentially hardware load \"\n\"balancers, where it remains to perform only the task of dispatching on \"\n\"the basis of SNI.\"\n\n#: ../../kogumisteenus.rst:127\nmsgid \"\"\n\"HAProxy lähtekood on avalik ja sobiva litsentsiga ning pakendatud \"\n\"kogumisteenuse alusplatvormi ametlikus hoidlas (vt. :ref:`tehnoloogiad`).\"\nmsgstr \"\"\n\"The HAProxy source code is public and suitably licensed and packaged in \"\n\"the official repository of the underlying collection service platform \"\n\"(see :ref:`tehnoloogiad`).\"\n\n#: ../../kogumisteenus.rst:131\nmsgid \"Nimekirjateenuse funktsioon ja tehniline liides\"\nmsgstr \"Functionality and technical interface of the choice-list service\"\n\n#: ../../kogumisteenus.rst:133\nmsgid \"\"\n\"Nimekirjateenuse põhifunktsioon on valikute nimekirjade vahendamine \"\n\"Valijarakendusele. Nimekirjateenusesse jõuab informatsioon tuvastatud \"\n\"valija kohta ning Nimekirjateenus väljastab valija ringkonnale vastava \"\n\"valikute nimekirja Talletamisteenusest Valijarakendusse.\"\nmsgstr \"\"\n\"The main function of the choice-list service is to provide choice list to\"\n\" the Voting application. The Choice List Service receives information \"\n\"about the authenticated voter and issues the corresponding list of \"\n\"choices to the voter's precinct to the Voting Application.\"\n\n#: ../../kogumisteenus.rst:138\nmsgid \"\"\n\"Nimekirjateenus on olekuvaba komponent, mida on võimalik horisontaalselt \"\n\"skaleerida.\"\nmsgstr \"\"\n\"The choice-list service is a stateless component that can be scaled \"\n\"horizontally.\"\n\n#: ../../kogumisteenus.rst:142\nmsgid \"Kontrollteenuse funktsioon ja tehniline liides\"\nmsgstr \"Verification service functionality and technical interface\"\n\n#: ../../kogumisteenus.rst:144\nmsgid \"\"\n\"Kontrollteenuse põhifunktsioon on kontrollpäringute töötlemine ning \"\n\"kontrollitava hääle väljastamine Talletamisteenusest Kontrollrakendusse.\"\nmsgstr \"\"\n\"The main function of the Verification Service is to process verification \"\n\"requests and output the vote to be verified from the Storage Service to \"\n\"the Verification Application.\"\n\n#: ../../kogumisteenus.rst:147\nmsgid \"\"\n\"Kontrollteenus on olekuvaba komponent, mida on võimalik horisontaalselt \"\n\"skaleerida.\"\nmsgstr \"\"\n\"The verification service is a stateless component that can be scaled \"\n\"horizontally.\"\n\n#: ../../kogumisteenus.rst:151\nmsgid \"Hääletamisteenuse funktsioon ja tehniline liides\"\nmsgstr \"Voting service function and technical interface\"\n\n#: ../../kogumisteenus.rst:153\nmsgid \"\"\n\"Hääletamisteenuse põhifunktsioon on hääletamispäringute töötlemine. \"\n\"Hääletamisteenus verifitseerib sissetuleva hääle, registreerib selle \"\n\"Registreerimisteenuses ning talletab Talletamisteenusesse.\"\nmsgstr \"\"\n\"The main function of the voting service is the processing of voting \"\n\"requests. The Voting Service verifies the incoming vote, registers it in \"\n\"the Registration Service and stores it in the Storage Service.\"\n\n#: ../../kogumisteenus.rst:157\nmsgid \"\"\n\"Hääletamisteenus on olekuvaba komponent, mida on võimalik horisontaalselt\"\n\" skaleerida.\"\nmsgstr \"\"\n\"The voting service is a stateless component that can be scaled \"\n\"horizontally.\"\n\n#: ../../kogumisteenus.rst:161\nmsgid \"Talletamisteenuse funktsioon ja tehniline liides\"\nmsgstr \"Functionality and technical interface of the storage service\"\n\n#: ../../kogumisteenus.rst:163\nmsgid \"\"\n\"Talletamisteenuse põhifunktsioon on valikute ja valijanimekirjade ning \"\n\"häälte pikaajaline talletamine.\"\nmsgstr \"\"\n\"The main function of the storage service is the long-term storage of  \"\n\"lists of choices, lists of voters and votes.\"\n\n#: ../../kogumisteenus.rst:166\nmsgid \"\"\n\"Talletamisteenuse horisontaalseks skaleerimiseks kasutatakse \"\n\"hajustalletamist võimaldavat säilitustehnoloogiat.\"\nmsgstr \"\"\n\"Storage technology that enables distributed storage is used for \"\n\"horizontal scaling of the storage service.\"\n\n#: ../../kogumisteenus.rst:170\nmsgid \"Talletamisteenuse teostus\"\nmsgstr \"Implementation of storage service\"\n\n#: ../../kogumisteenus.rst:172\nmsgid \"\"\n\"Talletamisteenus ei ole teadlik IVXV protokollist ega talletatavate \"\n\"andmete spetsiifikast, vaid on üldkasutatav võti-väärtus andmebaas \"\n\"binaarandmete säilitamiseks. Kogu teadmus talletatavate andmete \"\n\"struktuurist ja võtmete hierarhiast on teistes, Talletamisteenust \"\n\"kasutatavates teenustes, mis käituvad nii-öelda \\\"tarkade\\\" klientidena.\"\nmsgstr \"\"\n\"The storage service is not aware of the IVXV protocol or the specifics of\"\n\" the data being stored, but is a public key-value database for storing \"\n\"binary data. All the knowledge about the structure of the stored data and\"\n\" the hierarchy of keys is in the other services using the Storage \"\n\"Service, which behave as \\\"smart\\\" clients.\"\n\n#: ../../kogumisteenus.rst:178\nmsgid \"\"\n\"Selline lähenemine lubab ilma suurema vaevata kasutada \"\n\"Talletamisteenusena ükskõik millist üldlevinud võti-väärtus andmebaasi: \"\n\"ainsateks ülesanneteks on IVXV seadistuse teisendamine andmebaasi jaoks \"\n\"sobilikku vormingusse ning teenuse käivitamine. Andmebaasi tarkvara peab \"\n\"võimaldama vaid võtme järgi talletamist ja lugemist, võtmete prefiksi \"\n\"järgi loetlemist ning atomaarset võrdle-ja-vaheta (*compare-and-swap*) \"\n\"operatsiooni.\"\nmsgstr \"\"\n\"This approach allows any common key-value database to be used as a \"\n\"Storage Service without much effort: the only tasks are to convert the \"\n\"IVXV configuration into a database-appropriate format and to start the \"\n\"service. The database software only needs to support key-by-key storage \"\n\"and read, key-by-prefix read, and atomic *compare-and-swap* operation.\"\n\n#: ../../kogumisteenus.rst:185\nmsgid \"\"\n\"Talletamisteenus on kogumisteenuse töökiiruse oluliseks määrajaks, \"\n\"mistõttu mõjutab seda teenust pakkuv riistvara kogu süsteemi jõudlust \"\n\"ning see tuleks vastavalt kasutatavale andmebaasile dimensioneerida.\"\nmsgstr \"\"\n\"The storage service is an important determinant of the performance of the\"\n\" collection service, so the hardware that provides this service affects \"\n\"the performance of the whole system and should be dimensioned according \"\n\"to the database used.\"\n\n#: ../../kogumisteenus.rst:189\nmsgid \"\"\n\"Hetkel ainus tooteks mõeldud Talletamisteenuse teostus kasutab hajusat \"\n\"võti-väärtus andmebaasi etcd. Selle puhul tuleks järgida etcd autorite \"\n\"`riistvara soovitusi <https://coreos.com/etcd/docs/latest/op-\"\n\"guide/hardware.html>`_.\"\nmsgstr \"\"\n\"Currently, the only Storage Service implementation for the production \"\n\"uses a distributed key-value database etcd. For this, the `hardware \"\n\"recommendations <https://coreos.com/etcd/docs/latest/op-\"\n\"guide/hardware.html>`_ of the etcd authors  should be followed.\"\n\n#: ../../kogumisteenus.rst:195\nmsgid \"Tuvastusteenuse funktsioon ja tehniline liides\"\nmsgstr \"Authentication service function and technical interface\"\n\n#: ../../kogumisteenus.rst:197\nmsgid \"\"\n\"Tuvastusteenuse põhifunktsioon on valija identiteedi tuvastamine. \"\n\"Tuvastusteenus on vajalik näiteks Mobiil-ID autentimise korral.\"\nmsgstr \"\"\n\"The main function of the authentication service is to identify and \"\n\"authenticate the voter. For example, the authentication service must be \"\n\"implemented for Mobile-ID authentication.\"\n\n#: ../../kogumisteenus.rst:201\nmsgid \"Web-eID abiteenuse teostus\"\nmsgstr \"Implementation of the Web-eID authentication service\"\n\n#: ../../kogumisteenus.rst:203\nmsgid \"\"\n\"IVXV koosseisu kuulub Web-eID abiteenus, mis realiseerib Tuvastusteenuse \"\n\"ID-kaardi kasutamiseks Web-eID raamistikus.\"\nmsgstr \"\"\n\"IVXV includes Web-eID authentication service for using ID-card in the \"\n\"Web-eID framework\"\n\n#: ../../kogumisteenus.rst:206\nmsgid \"\"\n\"Eduka Web-eID isikutuvastuse korral väljastab abiteenus Valijarakendusele\"\n\" pileti, mille abil on võimalik teistele teenustele valija identiteeti \"\n\"kinnitada. Iga piletiga saab hääletada ainult ühe korra.\"\nmsgstr \"\"\n\"In case of a successful Web-eID authentication, the service issues a \"\n\"ticket to the Voting Application, which can be used to confirm the \"\n\"voter's identity to other services. Each ticket can be used to vote only \"\n\"once.\"\n\n#: ../../kogumisteenus.rst:210\nmsgid \"\"\n\"Web-eID ei paku allkirjateenust, hääle allkirjastamine toimub Valija \"\n\"seadmes lokaalselt analoogselt klassikalisele ID-kaardiga \"\n\"autentimisele/allkirjastamisele.\"\nmsgstr \"\"\n\"Web-eID does not provide signature service. The signing of the vote takes\"\n\" place in the Voting Application in the same way as with ID-card.\"\n\n#: ../../kogumisteenus.rst:214\nmsgid \"\"\n\"Web-eID abiteenus on olekuvaba komponent. Tänu sellele on võimalik Web-\"\n\"eID abiteenust horisontaalselt skaleerida.\"\nmsgstr \"\"\n\"The Web-eID service is a stateless component that can be scaled \"\n\"horizontally.\"\n\n#: ../../kogumisteenus.rst:219\nmsgid \"Allkirjateenuse funktsioon ja tehniline liides\"\nmsgstr \"Signature service functionality and technical interface\"\n\n#: ../../kogumisteenus.rst:221\nmsgid \"\"\n\"Allkirjateenuse funktsioon on Valijarakenduse toetamine hääle \"\n\"allkirjastamisel. Allkirjateenus on vajalik näiteks Mobiil-ID \"\n\"allkirjastamise korral.\"\nmsgstr \"\"\n\"The function of the Signature Service is to support the Voting \"\n\"Application in signing a vote. For example, the signature service is \"\n\"required for Mobile-ID signing.\"\n\n#: ../../kogumisteenus.rst:226\nmsgid \"Mobiil-ID abiteenuse teostus\"\nmsgstr \"Implementation of the Mobile-ID service\"\n\n#: ../../kogumisteenus.rst:228\nmsgid \"\"\n\"IVXV koosseisu kuulub Mobiil-ID abiteenus, mis käitub Mobiil-ID jaoks nii\"\n\" Tuvastusteenusena kui ka Allkirjateenusena. Valijarakendus esitab IVXV \"\n\"päringud Mobiil-ID abiteenusele, mis teisendab need Mobiil-ID päringuteks\"\n\" ning edastab Mobiil-ID teenusepakkujale.\"\nmsgstr \"\"\n\"IVXV includes the Mobile-ID Service, which acts as both an Authentication\"\n\" Service and a Signature Service for Mobile-ID. The Voting Application \"\n\"submits the IVXV requests to the Mobile-ID Service, which converts them \"\n\"into Mobile-ID requests and forwards them to the Mobile-ID Service \"\n\"Provider.\"\n\n#: ../../kogumisteenus.rst:233\nmsgid \"\"\n\"Eduka Mobiil-ID isikutuvastuse korral väljastab abiteenus \"\n\"Valijarakendusele pileti, mille abil on võimalik teistele teenustele \"\n\"valija identiteeti kinnitada. Iga piletiga saab hääletada ainult ühe \"\n\"korra.\"\nmsgstr \"\"\n\"In case of a successful Mobile-ID authentication, the help service issues\"\n\" a ticket to the Voting Application, which can be used to confirm the \"\n\"voter's identity to other services. Each ticket can be used to vote only \"\n\"once.\"\n\n#: ../../kogumisteenus.rst:237\nmsgid \"\"\n\"Allkirjastamise korral saadab Valijarakendus Mobiil-ID abiteenusele vaid \"\n\"allkirjastatava hääle räsi ning kasutab vastuseks saadud signatuuri \"\n\"samamoodi kui ID-kaardiga loodud signatuuri.\"\nmsgstr \"\"\n\"In the case of a signature, the Voting Application sends only the hash of\"\n\" the vote to be signed to the Mobile ID Service and uses the signature \"\n\"received in response in the same way as the signature created with an ID-\"\n\"card.\"\n\n#: ../../kogumisteenus.rst:241\nmsgid \"\"\n\"Mobiil-ID abiteenus sisaldab küll olekut pooleliolevate tuvastusseansside\"\n\" kohta, aga muus osas on tegu olekuvaba komponendiga. Tänu sellele on \"\n\"võimalik Mobiil-ID abiteenust horisontaalselt skaleerida.\"\nmsgstr \"\"\n\"The Mobile-ID service does contain a status for pending authentication \"\n\"sessions, but otherwise it is a stateless component. This makes it \"\n\"possible to horizontally scale the Mobile-ID Service.\"\n\n#: ../../kogumisteenus.rst:246\nmsgid \"Smart-ID abiteenuse teostus\"\nmsgstr \"Implementation of Smart-ID service\"\n\n#: ../../kogumisteenus.rst:248\nmsgid \"\"\n\"IVXV koosseisu kuulub Smart-ID abiteenus, mis käitub Smart-ID jaoks nii \"\n\"Tuvastusteenusena kui ka Allkirjateenusena. Valijarakendus esitab IVXV \"\n\"päringud Smart-ID abiteenusele, mis teisendab need Smart-ID päringuteks \"\n\"ning edastab Smart-ID teenusepakkujale.\"\nmsgstr \"\"\n\"IVXV includes the Smart-ID Service, which acts as both an Authentication \"\n\"Service and a Signature Service for Smart-ID. The Voting Application \"\n\"submits the IVXV requests to the Smart-ID Service, which converts them \"\n\"into Smart-ID requests and forwards them to the Smart-ID Service \"\n\"Provider.\"\n\n#: ../../kogumisteenus.rst:253\nmsgid \"\"\n\"Eduka Smart-ID isikutuvastuse korral väljastab abiteenus \"\n\"Valijarakendusele pileti, mille abil on võimalik teistele teenustele \"\n\"valija identiteeti kinnitada. Iga piletiga saab hääletada ainult ühe \"\n\"korra.\"\nmsgstr \"\"\n\"In case of a successful Smart-ID authentication, the help service issues \"\n\"a ticket to the Voting Application, which can be used to confirm the \"\n\"voter's identity to other services. Each ticket can be used to vote only \"\n\"once.\"\n\n#: ../../kogumisteenus.rst:257\nmsgid \"\"\n\"Allkirjastamise korral saadab Valijarakendus Smart-ID abiteenusele vaid \"\n\"allkirjastatava hääle räsi ning kasutab vastuseks saadud signatuuri \"\n\"samamoodi kui ID-kaardiga loodud signatuuri.\"\nmsgstr \"\"\n\"In the case of signing, the Voting App sends only the hash of the vote to\"\n\" be signed to the Smart-ID Service and uses the signature received in \"\n\"response in the same way as the signature created with an ID-card.\"\n\n#: ../../kogumisteenus.rst:261\nmsgid \"\"\n\"Smart-ID abiteenus sisaldab küll olekut pooleliolevate tuvastusseansside \"\n\"kohta, aga muus osas on tegu olekuvaba komponendiga. Tänu sellele on \"\n\"võimalik Smart-ID abiteenust horisontaalselt skaleerida.\"\nmsgstr \"\"\n\"The Smart-ID service does contain a status for pending authentication \"\n\"sessions, but otherwise it is a stateless component. This makes it \"\n\"possible to horizontally scale the Smart-ID Service.\"\n\n#: ../../kogumisteenus.rst:267\nmsgid \"Hääletamisfaktide järjekorrateenus\"\nmsgstr \"Votesorder service\"\n\n#: ../../kogumisteenus.rst:269\nmsgid \"\"\n\"Hääletamisfaktide järjekorrateenuse põhifunktsiooniks on \"\n\"hääletamisfaktide edastamine Valimiste Infosüsteemile X-tee abiteenuse \"\n\"vahendusel.\"\nmsgstr \"\"\n\"Main function of the votesorder service is to provide information about \"\n\"votes received to the Election Management System over X-road service\"\n\n#: ../../kogumisteenus.rst:273\nmsgid \"Kogumisteenuse mikroteenuste evitamine\"\nmsgstr \"Deployment of collection microservices\"\n\n#: ../../kogumisteenus.rst:275\nmsgid \"\"\n\"Kogumisteenuse mikroteenused sõltuvad välistest pakkidest minimaalselt. \"\n\"Vajalikud sõltuvused on:\"\nmsgstr \"\"\n\"Collection microservices rely minimally on external packages. The \"\n\"necessary dependencies are:\"\n\n#: ../../kogumisteenus.rst:278\nmsgid \"\"\n\"SSH-server haldustegevuste läbiviimiseks (seda kasutab mikroteenuste \"\n\"haldamiseks haldusteenus).\"\nmsgstr \"\"\n\"SSH server for administrative activities (used by the management service \"\n\"for microservices).\"\n\n#: ../../kogumisteenus.rst:281\nmsgid \"rsyslog logide kogumiseks logikogumisteenustesse.\"\nmsgstr \"rsyslog for log collection services.\"\n\n#: ../../kogumisteenus.rst:283\nmsgid \"\"\n\"Kogumisteenuse mikroteenused pakendatakse deb-vormingus, neid on võimalik\"\n\" evitada ka docker'i-laadsete konteineritena.\"\nmsgstr \"\"\n\"The collection microservices are packaged in deb format and can also be \"\n\"delivered in docker-like containers.\"\n\n#: ../../kogumisteenus.rst:287\nmsgid \"Välised teenused ja laiendatavus\"\nmsgstr \"External services and scalability\"\n\n#: ../../kogumisteenus.rst:291\nmsgid \"Kogumisteenuse laiendusmoodulid ja välised teenused\"\nmsgstr \"Collection Service Extensions and External Services\"\n\n#: ../../kogumisteenus.rst:293\nmsgid \"\"\n\"Kogumisteenuse mikroteenused kasutavad laiendusmooduleid teostamaks \"\n\"erinevaid mehhanisme valija tuvastamiseks, digiallkirjade \"\n\"verifitseerimiseks ja täiendamiseks, sealhulgas hääle registreerimiseks. \"\n\"Laiendusmoodulid võivad teostuse võimaldamiseks kasutada väliseid \"\n\"teenuseid. Mikroteenuste laiendatavuse huvides on defineeritud Go API, \"\n\"mille alusel saab teostada ka täiendavaid mooduleid. Hetkel on teostatud \"\n\"järgmised moodulid:\"\nmsgstr \"\"\n\"Collection microservices use plug-ins to perform various mechanisms for \"\n\"voter authentication, digital signature verification and qualification, \"\n\"including vote registration. The extension modules may use external \"\n\"services to enable the implementation. For the extensibility of the \"\n\"microservices, a Go API has been defined to allow the execution of \"\n\"additional modules. The following modules are currently implemented:\"\n\n#: ../../kogumisteenus.rst:300\nmsgid \"Autentimine TLS-sertifikaadiga (ID-kaart);\"\nmsgstr \"Authentication with TLS certificate (ID card);\"\n\n#: ../../kogumisteenus.rst:302\nmsgid \"Autentimine Tuvastusteenuse piletiga (Mobiil-ID, Smart-ID, Web-eID);\"\nmsgstr \"\"\n\"Authentication with an Authentication Service Ticket (Mobile-ID, Smart-\"\n\"ID, Web-eID);\"\n\n#: ../../kogumisteenus.rst:304\nmsgid \"BDOC verifitseerimine;\"\nmsgstr \"BDOC verification;\"\n\n#: ../../kogumisteenus.rst:306\nmsgid \"Kehtivuskinnitusteenus OCSP;\"\nmsgstr \"Certificate validity service OCSP;\"\n\n#: ../../kogumisteenus.rst:308\nmsgid \"Ajatempliteenus RFC 3161;\"\nmsgstr \"Timestamp Service RFC 3161;\"\n\n#: ../../kogumisteenus.rst:310\nmsgid \"Registreerimisteenus OCSP;\"\nmsgstr \"Registration service OCSP;\"\n\n#: ../../kogumisteenus.rst:312\nmsgid \"Registreerimisteenus RFC 3161.\"\nmsgstr \"Registration service RFC 3161.\"\n\n#: ../../kogumisteenus.rst:314\nmsgid \"\"\n\"IVXV krüptograafilises protokollis on kesksel kohal Registreerimisteenus,\"\n\" mis osaleb samuti häälte pikaajalisel talletamisel.\"\nmsgstr \"\"\n\"Central to the IVXV cryptographic protocol is the Registration Service, \"\n\"which is also involved in the long-term storage of votes.\"\n\n#: ../../kogumisteenus.rst:318\nmsgid \"Registreerimisteenuse funktsioon\"\nmsgstr \"Registration service function\"\n\n#: ../../kogumisteenus.rst:320\nmsgid \"\"\n\"Registreerimisteenuse põhifunktsioon on võtta Hääletamisteenuselt vastu \"\n\"allkirjastatud registreerimispäringuid, kinnitada need omapoolse \"\n\"allkirjastatud vastusega ning säilitada hilisemaks auditeerimiseks \"\n\"vähemalt hääletamisperioodi lõpuni.\"\nmsgstr \"\"\n\"The main function of the Registration Service is to receive signed \"\n\"registration requests from the Voting Service, validate them with a \"\n\"signed response from the Voting Service, and store them for later audit \"\n\"at least until the end of the voting period.\"\n\n#: ../../kogumisteenus.rst:325\nmsgid \"Auditeerimisel tekkivate võimalike erisuste lahendamiseks on oluline, et\"\nmsgstr \"\"\n\"In order to address possible differences in auditing, it is important \"\n\"that.\"\n\n#: ../../kogumisteenus.rst:327\nmsgid \"\"\n\"Registreerimisteenus on võimeline tõestama, et igale tema poolt \"\n\"väljastatud kinnitusele eelnes Talletamisteenuse poolne \"\n\"registreerimispäring;\"\nmsgstr \"\"\n\"The Registration service is able to prove that each confirmation issued \"\n\"by it was preceded by a registration request from the Storage;\"\n\n#: ../../kogumisteenus.rst:330\nmsgid \"\"\n\"Talletamisteenus on võimeline tõestama, et iga tema poolt talletatud \"\n\"hääle kohta on olemas Registreerimisteenuse kinnitus.\"\nmsgstr \"\"\n\"The Storage is able to prove that there is a confirmation from the \"\n\"Registration Service for each vote it has stored.\"\n\n#: ../../kogumisteenus.rst:333\nmsgid \"\"\n\"Piisav protokoll sellise tõendamistaseme saavutamiseks on, kus mõlemal \"\n\"osapoolel on olemas võtmepaar allkirjastamiseks, päringud ja vastused on \"\n\"allkirjastatud ning kumbki pool peab registrit teise poole teadete üle. \"\n\"Selline protokoll on realiseeritav näiteks OCSP-põhise \"\n\"Registreerimisteenuse korral. Samas võib esineda juhtumeid, kus näiteks \"\n\"registreerimispäringute allkirjastamine ei ole standardsete vahenditega \"\n\"võimalik (RFC 3161 põhine registreerimine). Sellisel juhul tuleb \"\n\"registreerimisteenusele vajalik tõendusmaterjal anda muude \"\n\"organisatsioonilis-tehniliste vahenditega.\"\nmsgstr \"\"\n\"A sufficient protocol to achieve this level of confirmation is where both\"\n\" parties have a key pair to sign, queries and responses are signed, and \"\n\"each party keeps a record of the other party's messages. Such a protocol \"\n\"is feasible, for example, in the case of an OCSP-based Registration \"\n\"Service. However, there may be cases where, for example, signing of \"\n\"registration requests is not possible by standard means (RFC 3161-based \"\n\"registration). In such cases, the necessary evidence for the Registration\"\n\" Service must be provided by other organisational/technical means.\"\n\n#: ../../kogumisteenus.rst:342\nmsgid \"Registreerimisteenusel on praegu kaks erinevat teostust:\"\nmsgstr \"The registration service currently has two different implementations:\"\n\n#: ../../kogumisteenus.rst:344\nmsgid \"\"\n\"OCSP-liides eeldab Eestis rakendatava OCSP-põhise ajamärgendamisteenuse \"\n\"kasutamist, kus allkirjastatud OCSP-päringu nonsiks on Hääletamisteenuse \"\n\"poolt pandud hääle räsi. Päring on allkirjastatud standardsete OCSP \"\n\"vahenditega;\"\nmsgstr \"\"\n\"The OCSP interface requires the use of the OCSP-based timemarking service\"\n\" implemented in Estonia, where the nonce of a signed OCSP request is the \"\n\"hash of the vote cast by the Voting Service. The query is signed using \"\n\"standard OCSP tools;\"\n\n#: ../../kogumisteenus.rst:349\nmsgid \"\"\n\"RFC 3161 liides, mille korral ebastandardse lahendusena pannakse \"\n\"ajatemplipäringu nonsiks Hääletamisteenuse poolt allkirjastatud hääle \"\n\"räsi.\"\nmsgstr \"\"\n\"RFC 3161 interface, where as a non-standard solution the nonce of a \"\n\"timestamp request is set to a hash of the vote signed by the Vote \"\n\"Service.\"\n\n#: ../../kogumisteenus.rst:354\nmsgid \"Kogumisteenuse laiendusmoodulite lisamine\"\nmsgstr \"Adding collection service extension modules\"\n\n#: ../../kogumisteenus.rst:356\nmsgid \"Kogumisteenuse API defineerib kuute tüüpi laiendusmooduleid:\"\nmsgstr \"The Collection API defines six types of extension modules:\"\n\n#: ../../kogumisteenus.rst:358\nmsgid \"isikutuvastus (Go pakk ``ivxv.ee/auth``, näiteks ``tls``);\"\nmsgstr \"authentication (Go package ``ivxv.ee/auth``, for example ``tls``);\"\n\n#: ../../kogumisteenus.rst:360\nmsgid \"\"\n\"tuvastatud isiku sertifikaadist valija identifikaatori tuletamine (Go \"\n\"pakk ``ivxv.ee/identity``, näiteks ``serialnumber``);\"\nmsgstr \"\"\n\"derivation of the voter identifier from the certificate of the \"\n\"authenticated person (Go package ``ivxv.ee/identity``, for example \"\n\"``serialnumber``);\"\n\n#: ../../kogumisteenus.rst:363\nmsgid \"\"\n\"valija identifikaatorist vanuse tuletamine (Go pakk ``ivxv.ee/age``, \"\n\"näiteks ``estpic``);\"\nmsgstr \"\"\n\"deriving the age of a voter from the voter's identifier (Go package \"\n\"``ivxv.ee/age``, for example ``estpic``);\"\n\n#: ../../kogumisteenus.rst:366\nmsgid \"\"\n\"allkirjastatud konteineri verifitseerimine (Go pakk \"\n\"``ivxv.ee/container``, näiteks ``bdoc``);\"\nmsgstr \"\"\n\"verification of a signed container (Go package ``ivxv.ee/container``, for\"\n\" example ``bdoc``);\"\n\n#: ../../kogumisteenus.rst:369\nmsgid \"allkirja kvalifitseerimine (Go pakk ``ivxv.ee/q11n``, näiteks ``tspreg``);\"\nmsgstr \"\"\n\"signature qualification (Go package ``ivxv.ee/q11n``, for example \"\n\"``tspreg``);\"\n\n#: ../../kogumisteenus.rst:371\nmsgid \"andmetalletusprotokoll (Go pakk ``ivxv.ee/storage``, näiteks ``etcd``).\"\nmsgstr \"storage protocol (Go package ``ivxv.ee/storage``, for example ``etcd``).\"\n\n#: ../../kogumisteenus.rst:373\nmsgid \"\"\n\"Uue mooduli lisamiseks tuleb moodulpakki lisada mooduli identifikaator \"\n\"ning mooduli teostusega alampakk. Alampaki alglaadimisel tuleb mooduli \"\n\"registreerimiseks kutsuda välja moodulpaki ``Register`` funktsioon.\"\nmsgstr \"\"\n\"In order to add a new module, the module identifier and the sub-package \"\n\"with the module implementation must be added to the module package. The \"\n\"``Register`` function of the module package must be called to register \"\n\"the module when the sub-package is initially loaded.\"\n\n#: ../../kogumisteenus.rst:377\nmsgid \"\"\n\"Uue mooduli kasutamiseks tuleb selle identifikaator lisada seadistusse \"\n\"vastava moodulitüübi seadistuse juurde koos alammooduli seadistusega. \"\n\"Laiendusmoodulile antakse ette tema identifikaatoriga viidatud \"\n\"seadistusblokk, mida see mooduli-siseselt edasi töötleb.\"\nmsgstr \"\"\n\"In order to use a new module, its identifier must be added to the \"\n\"configuration of the corresponding module type, together with the \"\n\"configuration of the sub-module. The extension module shall be provided \"\n\"with a configuration block referenced by its identifier, which it shall \"\n\"further process within the module.\"\n\n#: ../../kogumisteenus.rst:382\nmsgid \"\"\n\"Moodulpakid ja nende moodulitelt nõutavad liidesed on täpsemalt \"\n\"kirjeldatud vastavates lähtekoodifailides. Samuti on iga mooduli kohta \"\n\"olemas vähemalt üks teostus, mida saab kasutada eeskujuna.\"\nmsgstr \"\"\n\"The module packages and the interfaces required from these modules are \"\n\"described in more detail in the source code. There is also at least one \"\n\"implementation of each module that can be used as an example.\"\n\n#: ../../kogumisteenus.rst:388\nmsgid \"Monitooring\"\nmsgstr \"Monitoring\"\n\n#: ../../kogumisteenus.rst:392\nmsgid \"Monitooringulahendus\"\nmsgstr \"Monitoring solution\"\n\n#: ../../kogumisteenus.rst:395\nmsgid \"Logimine\"\nmsgstr \"Logging\"\n\n#: ../../kogumisteenus.rst:397\nmsgid \"\"\n\"Iga mikroteenuse poolt genereeritav logi defineeritakse süstemaatiliselt,\"\n\" lähtudes protokollikirjeldusest ning teenuse osutamise olekudiagrammist.\"\n\" Logitakse minimaalselt:\"\nmsgstr \"\"\n\"Each log generated by a microservice is systematically defined based on \"\n\"the protocol description and the service provision status diagram. Logs \"\n\"shall be kept to a minimum:\"\n\n#: ../../kogumisteenus.rst:401\nmsgid \"iga päringu kättesaamise fakt ning töötlemise algus;\"\nmsgstr \"the fact of receipt of each request and the start of processing;\"\n\n#: ../../kogumisteenus.rst:403\nmsgid \"töötlemise üleandmine välisele komponendile;\"\nmsgstr \"transfer of processing to an external component;\"\n\n#: ../../kogumisteenus.rst:405\nmsgid \"töötlemisjärje naasmine komponenti;\"\nmsgstr \"the processing stage return component;\"\n\n#: ../../kogumisteenus.rst:407\nmsgid \"päringu töötlemise lõpp ning tulemus;\"\nmsgstr \"the end of query processing and the result;\"\n\n#: ../../kogumisteenus.rst:409\nmsgid \"täiendavalt oluliste etappide läbimine protsessi olekumudelis.\"\nmsgstr \"additional key steps in the process status model.\"\n\n#: ../../kogumisteenus.rst:411\nmsgid \"Logimisel järgitakse järgmisi põhimõtteid:\"\nmsgstr \"Logging is based on the following principles:\"\n\n#: ../../kogumisteenus.rst:413\nmsgid \"\"\n\"Logimiseks kasutatakse rsyslog teenust, mis registreerib logiteate \"\n\"kirjutamise hetke millisekundi täpsusega;\"\nmsgstr \"\"\n\"Logging is done using the rsyslog service, which records the time of \"\n\"writing a log message to the nearest millisecond;\"\n\n#: ../../kogumisteenus.rst:416\nmsgid \"\"\n\"Iga seansi alustamisel genereerib süsteem unikaalse identifikaatori, mida\"\n\" klientrakendus kasutab oma päringutel kesksüsteemi poole pöördumiseks;\"\nmsgstr \"\"\n\"At the start of each session, the system generates a unique identifier \"\n\"that the client application uses in its queries to access the central \"\n\"system;\"\n\n#: ../../kogumisteenus.rst:419\nmsgid \"\"\n\"Kõik ühe seansi alla kuuluvad logikirjed sisaldavad sama \"\n\"seansiidentifikaatorit;\"\nmsgstr \"All the logs belonging to one session contain the same session identifier;\"\n\n#: ../../kogumisteenus.rst:422\nmsgid \"Logikirje on unikaalselt identifitseeritav;\"\nmsgstr \"The log record is uniquely identifiable;\"\n\n#: ../../kogumisteenus.rst:424\nmsgid \"\"\n\"Iga logitava teate juures on võimalik unikaalse tunnuse abil üksüheselt \"\n\"tuvastada teate tekkimise koht monitooritavas süsteemis;\"\nmsgstr \"\"\n\"For each logged message it is possible to uniquely identify, by means of \"\n\"a unique identifier, where the message originated in the monitored \"\n\"system;\"\n\n#: ../../kogumisteenus.rst:427\nmsgid \"\"\n\"Logikirje on JSON vormingus, automaatse monitooringu jaoks on \"\n\"masinloetavus primaarne ning inimloetavus sekundaarne;\"\nmsgstr \"\"\n\"The log record is in JSON format, with machine readability primary and \"\n\"human readability secondary for automatic monitoring;\"\n\n#: ../../kogumisteenus.rst:430\nmsgid \"\"\n\"Logisse minev info saneeritakse (urlencode) ja sellele rakendatakse \"\n\"pikkuse piirangut (piirang terve logiteate ja samuti parameetri kaupa);\"\nmsgstr \"\"\n\"The information that goes into the log is sanitized (urlencode) and a \"\n\"length limit is applied (limit per whole log message and also per \"\n\"parameter);\"\n\n#: ../../kogumisteenus.rst:433\nmsgid \"\"\n\"Süsteemiperimeetrist väljastpoolt pärinevat infot logitakse ainult \"\n\"saneerituna ja ainult etteantud pikkuses.\"\nmsgstr \"\"\n\"Information coming from outside the system perimeter is logged only as \"\n\"redundant and only at a predefined length.\"\n\n#: ../../kogumisteenus.rst:436\nmsgid \"\"\n\"Kuna logimine toimub rsyslog vahendusel, on võimalik Guardtime mooduli \"\n\"kasutamine logide tervikluse tagamiseks.\"\nmsgstr \"\"\n\"Since logging is done via rsyslog, it is possible to use the Guardtime \"\n\"module to ensure log integrity.\"\n\n#: ../../kogumisteenus.rst:441\nmsgid \"Üldstatistika\"\nmsgstr \"General statistics\"\n\n#: ../../kogumisteenus.rst:443\nmsgid \"Järgmise statistika jälgimiseks kasutatakse staatilist veebiliidest:\"\nmsgstr \"A static web interface is used to monitor the following statistics:\"\n\n#: ../../kogumisteenus.rst:445\nmsgid \"edukalt kogutud hääled/hääletajate hulk;\"\nmsgstr \"number of votes successfully collected/votes cast;\"\n\n#: ../../kogumisteenus.rst:447\nmsgid \"\"\n\"hääletajate jagunemine sugude, vanusegruppide, operatsioonisüsteemide \"\n\"ning autentimisvahendite kaupa;\"\nmsgstr \"\"\n\"breakdown of voters by gender, age group, operating system and \"\n\"authentication means;\"\n\n#: ../../kogumisteenus.rst:450\nmsgid \"edukalt kontrollitud häälte/hääletajate hulk;\"\nmsgstr \"the number of successfully verified votes/votes;\"\n\n#: ../../kogumisteenus.rst:452\nmsgid \"korduvhääletamiste statistika;\"\nmsgstr \"statistics on re-votes;\"\n\n#: ../../kogumisteenus.rst:454\nmsgid \"hääletajate jagunemine riigiti IP-aadressi põhjal.\"\nmsgstr \"breakdown of voters by country, based on IP address.\"\n\n#: ../../kogumisteenus.rst:458\nmsgid \"Detailstatistika\"\nmsgstr \"Detailed statistics\"\n\n#: ../../kogumisteenus.rst:460\nmsgid \"\"\n\"Detailstatistika agregeeritakse logide põhjal kasutades SCCEIV \"\n\"logianalüsaatorit, mis  analüüsib rakenduste tegevuslogi eeldefineeritud \"\n\"profiili suhtes ning võimaldab seansi-/veatüübipõhist analüüsi.\"\nmsgstr \"\"\n\"Detailed statistics are aggregated from logs using the SCCEIV log \"\n\"analyser, which analyses the activity logs of applications against a \"\n\"predefined profile and allows for a session/vehicle type analysis.\"\n\n#: ../../kogumisteenus.rst:464\nmsgid \"Detailstatistika on kättesaadav üle HTTPS-liidese.\"\nmsgstr \"Detailed statistics are available over HTTPS.\"\n\n#: ../../kogumisteenus.rst:470\nmsgid \"Haldus\"\nmsgstr \"Administration\"\n\n#: ../../kogumisteenus.rst:472\nmsgid \"\"\n\"Kogumisteenuse haldamine toimub digitaalallkirjastatud seadistuspakkide \"\n\"abil.\"\nmsgstr \"\"\n\"The collection service is managed by means of digitally signed \"\n\"configuration packs.\"\n\n#: ../../kogumisteenus.rst:474\nmsgid \"Kogumisteenus pakub seadistuspakkide laadimiseks kahte liidest:\"\nmsgstr \"\"\n\"The collection service provides two interfaces for loading configuration \"\n\"packages:\"\n\n#: ../../kogumisteenus.rst:476\nmsgid \"\"\n\"Käsurealiides – rakendus verifitseerib allkirja, valideerib korralduste \"\n\"vormingut, kooskõlalisust ja sobivust kogumisteenuse seisundi suhtes. \"\n\"Korralduse rakendamine toimub eraldi utiliidi abil.\"\nmsgstr \"\"\n\"Command line interface - the application verifies the signature, \"\n\"validates the format, consistency and appropriateness of the orders \"\n\"against the status of the collection service. The execution of the order \"\n\"is done through a separate utility.\"\n\n#: ../../kogumisteenus.rst:480\nmsgid \"\"\n\"Veebiliides – veebiliides vahendab seadistuspaki käsurealiidesele ja \"\n\"tagastab kasutajale info laadimise tulemuse kohta. Eduka laadimise korral\"\n\" toimub automaatselt ja samadel põhimõtetel ka seadistuspaki rakendamine.\"\nmsgstr \"\"\n\"Web interface - the web interface passes the configuration package to the\"\n\" command line interface and returns information about the result of the \"\n\"loading to the user. In case of a successful load, the implementation of \"\n\"the configuration package is automatic and follows the same principles.\"\n\n#: ../../kogumisteenus.rst:484\nmsgid \"Veebiliidese funktsioonideks on:\"\nmsgstr \"The features of the web interface are:\"\n\n#: ../../kogumisteenus.rst:486\nmsgid \"Kogumisteenuse mikroteenuste seisundi jälgimine;\"\nmsgstr \"Monitoring the status of collection microservices;\"\n\n#: ../../kogumisteenus.rst:488\nmsgid \"Valimiste nimekirjade haldus;\"\nmsgstr \"Management of voter lists;\"\n\n#: ../../kogumisteenus.rst:490\nmsgid \"Statistika kuvamine e-hääletamise kulgemise kohta;\"\nmsgstr \"Displaying statistics on the progress of e-voting;\"\n\n#: ../../kogumisteenus.rst:492\nmsgid \"Haldusteenuse kasutajate haldus;\"\nmsgstr \"Management of users of the management service;\"\n\n#: ../../kogumisteenus.rst:494\nmsgid \"Kogumisteenuse halduse logi kuvamine.\"\nmsgstr \"Display the collection service management log.\"\n\n#: ../../kogumisteenus.rst:496\nmsgid \"\"\n\"Kõik rakendusele antud korraldused säilitatakse - ka need mida ei \"\n\"rakendatud. Vigaseid (mittevalideeruvaid) korraldusi ei säilitata.\"\nmsgstr \"\"\n\"All orders given to the application are kept - even those that were not \"\n\"applied. Incorrect (non-validated) orders are not retained.\"\n\n#: ../../kogumisteenus.rst:499\nmsgid \"Kogumisteenuse haldusteenus sooritab järgmisi tegevusi automaatselt:\"\nmsgstr \"\"\n\"The collection management service automatically performs the following \"\n\"actions:\"\n\n#: ../../kogumisteenus.rst:501\nmsgid \"Valijate nimekirjade muudatuste laadimine Valimiste Infosüsteemist;\"\nmsgstr \"\"\n\"Downloading changes to electoral rolls from the Election Information \"\n\"System;\"\n\n#: ../../kogumisteenus.rst:503\nmsgid \"\"\n\"Hääletamise statistika kogumine hääletusteenusest ja eksportimine \"\n\"Valimiste Infosüsteemi;\"\nmsgstr \"\"\n\"Collection of voting statistics from the voting service and export to the\"\n\" Election Information System;\"\n\n#: ../../kogumisteenus.rst:506\nmsgid \"Talletatud häälte, logide ning seadistuste varundamine varundusteenusesse.\"\nmsgstr \"Backing up saved voices, logs and settings to a backup service.\"\n\n#: ../../kogumisteenus.rst:510\nmsgid \"Haldusteenuse komponendid\"\nmsgstr \"Components of a management service\"\n\n#: ../../kogumisteenus.rst:514\nmsgid \"Kogumisteenuse haldusteenuse komponendid\"\nmsgstr \"Components of a collection management service\"\n\n#: ../../kogumisteenus.rst:516\nmsgid \"\"\n\"**Halduse veebiserver** on süsteemse kasutaja ``www-data`` õigustes \"\n\"töötav Apache server, mille ülesanded on:\"\nmsgstr \"\"\n\"The **Administrative Web Server** is an Apache server running as ``www-\"\n\"data`` for the system user, whose functions are:\"\n\n#: ../../kogumisteenus.rst:519\nmsgid \"Kasutajatelt tulevate HTTPS-päringute esmane teenindamine:\"\nmsgstr \"Serving HTTPS requests from users on a first-come, first-served basis:\"\n\n#: ../../kogumisteenus.rst:521\nmsgid \"Haldusteenuse usaldusväärsuse tõestamine (TLS-sertifikaat);\"\nmsgstr \"Proof of trustworthiness of the management service (TLS certificate);\"\n\n#: ../../kogumisteenus.rst:523\nmsgid \"Kasutajate autentimine;\"\nmsgstr \"User authentication;\"\n\n#: ../../kogumisteenus.rst:525\nmsgid \"\"\n\"Valmisgenereeritud veebilehtede ja andmefailide serveerimine \"\n\"andmehoidlast.\"\nmsgstr \"Serving pre-built web pages and data files from the repository.\"\n\n#: ../../kogumisteenus.rst:528\nmsgid \"\"\n\"Üldiste taustaandmete päringu vastuse varustamine sisseloginud kasutaja \"\n\"andmetega (WSGI).\"\nmsgstr \"\"\n\"Supplying the response to a general background query with the logged-in \"\n\"user's data (WSGI).\"\n\n#: ../../kogumisteenus.rst:531\nmsgid \"\"\n\"Üleslaaditavate korralduste esmane valideerimine ja vahendamine \"\n\"haldusdeemonile ning haldusdeemoni sellekohaste vastuste vahendamine \"\n\"kliendile (WSGI).\"\nmsgstr \"\"\n\"Initial validation and transmission of uploaded instructions to the \"\n\"management daemon and transmission of the management daemon's responses \"\n\"to the client (WSGI).\"\n\n#: ../../kogumisteenus.rst:535\nmsgid \"\"\n\"**Haldusdeemon** on kasutajakonto ``ivxv-admin`` õigustes töötav ja \"\n\"kohalikul (``localhost``) liidesel kuulav veebiserver mille ülesanded on:\"\nmsgstr \"\"\n\"The **administrative daemon** is a web server running under ``ivxv-\"\n\"admin`` privileges and listening on the local (``localhost``) interface \"\n\"with the following tasks:\"\n\n#: ../../kogumisteenus.rst:538\nmsgid \"Üleslaaditavate korralduste valideerimine;\"\nmsgstr \"Validation of downloadable orders;\"\n\n#: ../../kogumisteenus.rst:540\nmsgid \"Üleslaaditavate korralduste vahetu rakendamine (kasutajate haldus);\"\nmsgstr \"Immediate implementation of uploading instructions (user management);\"\n\n#: ../../kogumisteenus.rst:542\nmsgid \"\"\n\"Üleslaaditavate korralduste salvestamine hilisemaks rakendamiseks \"\n\"(seadistuse ja valimisnimekirjade rakendamiseks teenusele);\"\nmsgstr \"\"\n\"Saving of uploaded orders for later implementation (application of configurations and \"\n\"voter lists for the service);\"\n\n#: ../../kogumisteenus.rst:545\nmsgid \"E-valimiskasti allalaadimise vahendamine.\"\nmsgstr \"Facilitating the downloading of the digital ballot box.\"\n\n#: ../../kogumisteenus.rst:547\nmsgid \"\"\n\"**Agentdeemon** on kasutajakonto ``ivxv-admin`` õigustes töötav deemon, \"\n\"mille ülesanded on:\"\nmsgstr \"\"\n\"The **Agentdeemon** is a daemon with ``ivxv-admin`` privileges, whose \"\n\"tasks are:\"\n\n#: ../../kogumisteenus.rst:550\nmsgid \"Andmete kogumine ja registreerimine:\"\nmsgstr \"Data collection and recording:\"\n\n#: ../../kogumisteenus.rst:552\nmsgid \"Teadaolevate mikroteenuste seisund;\"\nmsgstr \"Status of known microservices;\"\n\n#: ../../kogumisteenus.rst:554\nmsgid \"Tegevusmonitooringu statistika allalaadimine;\"\nmsgstr \"Download statistics of activity;\"\n\n#: ../../kogumisteenus.rst:556\nmsgid \"\"\n\"**Andmehoidla** on failisüsteemis asuv kataloog, kuhu haldusteenuse \"\n\"komponendid hoiavad kogutud ja genereeritud andmeid (vaata üksikasjalist \"\n\"kirjeldust ``IVXV kogumisteenuse haldusjuhendi`` lisadest);\"\nmsgstr \"\"\n\"**Remote repository** is the directory in the file system where the \"\n\"components of the management service store the collected and generated \"\n\"data (see detailed description in the annexes of the ``IVXV Collection \"\n\"Management Guide``);\"\n\n#: ../../kogumisteenus.rst:560\nmsgid \"Välised komponendid, millega haldusteenus kokku puutub:\"\nmsgstr \"External components with which the management service comes into contact:\"\n\n#: ../../kogumisteenus.rst:562\nmsgid \"\"\n\"**Kogumisteenuse alamteenused** - paigaldamine, seadistamine ja seisundi \"\n\"andmete kogumine toimub agentdeemoni kaudu (SSH-ühendus teenuse \"\n\"masinasse);\"\nmsgstr \"\"\n\"**Collection Service Subservices** - installation, configuration and \"\n\"status data collection is done via agent daemon (SSH connection to the \"\n\"service machine);\"\n\n#: ../../kogumisteenus.rst:565\nmsgid \"\"\n\"**Seireserver** - üldstatistika andmete allalaadimine haldusteenuses \"\n\"kuvamiseks;\"\nmsgstr \"\"\n\"**Monitoring** - download of general statistics data for display in the \"\n\"administrative service;\"\n\n#: ../../kogumisteenus.rst:570\nmsgid \"Korralduste laadimine haldusteenusesse\"\nmsgstr \"Uploading orders to the management service\"\n\n#: ../../kogumisteenus.rst:574\nmsgid \"Kogumisteenuse seisundid\"\nmsgstr \"Collection service conditions\"\n\n#: ../../kogumisteenus.rst:576\nmsgid \"\"\n\"Kogumisteenuse seisund kajastab teenuse kõigi alamteenuste seisundit, \"\n\"kasutuselolevate väliste teenuste seisundit ja eelneva põhjal tuletatud \"\n\"üldseisundit. Kogumisteenuse üldseisundi tuvastamisega tegeleb \"\n\"haldusteenus.\"\nmsgstr \"\"\n\"The state of the collection service reflects the state of all sub-\"\n\"services of the service, the state of the external services in use and \"\n\"the overall state derived from the previous buffer. The identification of\"\n\" the overall status of the collection service is handled by the \"\n\"management service.\"\n\n#: ../../kogumisteenus.rst:580\nmsgid \"Üldseisundi olekud on:\"\nmsgstr \"The overall status is:\"\n\n#: ../../kogumisteenus.rst:582\nmsgid \"\"\n\"**Paigaldamata** - alates haldusteenuse paigaldamisest kuni kõigi \"\n\"alamteenuste paigaldamiseni;\"\nmsgstr \"\"\n\"**Uninstalled** - from the installation of the management service to the \"\n\"installation of all sub-services;\"\n\n#: ../../kogumisteenus.rst:585\nmsgid \"\"\n\"**Paigaldatud** - kõik alamteenused on paigaldatud, neile on rakendatud \"\n\"tehnilised seadistused ja teenuse toimimiseks vajalikud krüptovõtmed. \"\n\"Valimiste seadistust pole rakendatud (kuid see võib olla laaditud \"\n\"haldusteenusesse);\"\nmsgstr \"\"\n\"**Installed** - all sub-services have been installed, with the technical \"\n\"settings and cryptographic keys necessary for the service to work. The \"\n\"configuration for elections has not been applied (but may be loaded into \"\n\"the management service);\"\n\n#: ../../kogumisteenus.rst:590\nmsgid \"\"\n\"**Seadistatud** - kogumisteenus on seadistatud ja töökorras, sellega on \"\n\"võimalik häälte kogumist läbi viia ja e-valimiskasti väljastada.\"\nmsgstr \"\"\n\"**Set up** - the collection service is set up and operational, it is able\"\n\" to vote and issue digital ballot box.\"\n\n#: ../../kogumisteenus.rst:593\nmsgid \"\"\n\"**Osaline tõrge** - kogumisteenus on seadistatud ja osaliselt töökorras, \"\n\"mõned alamteenused pole töökorras, kuid see ei takista kogumisteenuse \"\n\"toimimist.\"\nmsgstr \"\"\n\"**Partial failure** - the collection service is configured and partially \"\n\"operational, some of the sub-services are not operational but this does \"\n\"not prevent the collection service from working.\"\n\n#: ../../kogumisteenus.rst:597\nmsgid \"\"\n\"**Tõrge** - kogumisteenuse oluline sõlm pole töökorras, teenuse \"\n\"nõuetekohane osutamine pole võimalik.\"\nmsgstr \"\"\n\"**Failure** - an essential node of the collection service is not in \"\n\"working order, the proper provision of the service is not possible.\"\n\n#: ../../kogumisteenus.rst:603\nmsgid \"\"\n\"Kogumisteenuse olekudiagramm. Olekud vastavalt värvusele: kollane - \"\n\"seadistamisel, punane - viga, roheline - töökorras.\"\nmsgstr \"\"\n\"Collection service status diagram. LEDs according to colour: yellow - in \"\n\"configuration, red - error, green - operational.\"\n\n#: ../../kogumisteenus.rst:608\nmsgid \"Kogumisteenuse alamteenuste seisundid\"\nmsgstr \"Collection sub-service statuses\"\n\n#: ../../kogumisteenus.rst:613\nmsgid \"\"\n\"Haldusteenuse poolt registreeritud alamteenuse olekudiagramm. Olekud \"\n\"vastavalt värvusele: kollane - seadistamisel, punane - viga, roheline - \"\n\"töökorras.\"\nmsgstr \"\"\n\"Status diagram of a sub-service registered by the management service. \"\n\"LEDs according to colour: yellow - in configuration, red - error, green -\"\n\" operational.\"\n\n#: ../../kogumisteenus.rst:619\nmsgid \"Kogumisteenuse seisundi muutused\"\nmsgstr \"Changes in the status of the collection service\"\n\n#: ../../kogumisteenus.rst:621\nmsgid \"\"\n\"Kogumisteenuse seisund on jälgitav alates haldusteenuse edukast \"\n\"paigaldamisest, algne seisund on **Paigaldamata**.\"\nmsgstr \"\"\n\"The status of the collection service is traceable from the successful \"\n\"installation of the management service, the initial status is \"\n\"**Uninstalled**.\"\n\n#: ../../kogumisteenus.rst:626\nmsgid \"Paigaldamata\"\nmsgstr \"Not yet installed\"\n\n#: ../../kogumisteenus.rst:628\nmsgid \"Toimub usaldusjuure ja tehnilise seadistuse rakendamine kogumisteenusele:\"\nmsgstr \"\"\n\"Implementation of the trust root and technical setup for the collection \"\n\"service:\"\n\n#: ../../kogumisteenus.rst:630\nmsgid \"Seadistuste laadimine kogumisteenusesse;\"\nmsgstr \"Uploading settings to the collection service;\"\n\n#: ../../kogumisteenus.rst:632\nmsgid \"Tehnilises seadistuses kirjeldatud alamteenuste paigaldus;\"\nmsgstr \"Installation of the sub-services described in the technical configuration;\"\n\n#: ../../kogumisteenus.rst:634\nmsgid \"Usaldusjuure ja tehniliste seadistuste rakendamine alamteenustele;\"\nmsgstr \"Implementation of trust root and technical settings for sub-services;\"\n\n#: ../../kogumisteenus.rst:636\nmsgid \"\"\n\"Seadistuste eduka rakendamise tulemusena saab süsteemi uueks seisundiks \"\n\"**Paigaldatud**.\"\nmsgstr \"\"\n\"Successful implementation of the settings will result in a new system \"\n\"status of **Installed**.\"\n\n#: ../../kogumisteenus.rst:641\nmsgid \"Paigaldatud\"\nmsgstr \"Installed\"\n\n#: ../../kogumisteenus.rst:643\nmsgid \"\"\n\"Kogumisteenuse seadistused on rakendatud kõigile alamteenustele, \"\n\"valimiste seadistused pole rakendatud. Toimub valimiste seadistuse \"\n\"laadimine haldusteenusesse ja rakendamine alamteenustele.\"\nmsgstr \"\"\n\"Collection service settings are applied to all sub-services, election \"\n\"settings are not applied. Election settings are being loaded into the \"\n\"management service and applied to sub-services.\"\n\n#: ../../kogumisteenus.rst:647\nmsgid \"\"\n\"Valimiste seadistuse eduka rakendamise korral saab süsteemi uueks \"\n\"seisundiks **Seadistatud**.\"\nmsgstr \"\"\n\"If the configuration is successfully implemented, the system will have a \"\n\"new status of **Set up**.\"\n\n#: ../../kogumisteenus.rst:652\nmsgid \"Seadistatud\"\nmsgstr \"Set up\"\n\n#: ../../kogumisteenus.rst:654\nmsgid \"\"\n\"Kõik kogumisteenuse alamteenused on seadistatud ja töökorras. \"\n\"Haldusteenusel on kõikidest alamteenustest värsked seisundiraportid. \"\n\"Süsteemiga on võimalik hääletust läbi viia ja e-valimiskasti väljastada.\"\nmsgstr \"\"\n\"All sub-services of the collection service are configured and \"\n\"operational. The management service has up-to-date status reports for all\"\n\" sub-services. The system is able to conduct voting and issue digital \"\n\"ballot box.\"\n\n#: ../../kogumisteenus.rst:658\nmsgid \"Kui süsteemis tuvastatakse tõrge, saab süsteemi uueks **Osaline tõrge**.\"\nmsgstr \"\"\n\"If a fault is detected in the system, the system will be reset to \"\n\"**Partial fault**.\"\n\n#: ../../kogumisteenus.rst:660\nmsgid \"\"\n\"**Seadistatud** olekust ei pöörduta enam kunagi tagasi olekutesse \"\n\"**paigaldamata** või **paigaldatud**, kuigi uute alamteenuste lisamisel \"\n\"(kuni need on olekus **paigaldamata/paigaldatud**) oleks vastavad \"\n\"tingimused täidetud.\"\nmsgstr \"\"\n\"The **Set-up** state will never return to the **Uninstalled** or \"\n\"**Installed** states, even if new sub-services are added (as long as they\"\n\" are in the **Uninstalled/Installed** state).\"\n\n#: ../../kogumisteenus.rst:667\nmsgid \"Osaline tõrge\"\nmsgstr \"Partial failure\"\n\n#: ../../kogumisteenus.rst:669\nmsgid \"\"\n\"Süsteem on seadistatud ja osaliselt töökorras, mõned süsteemi dubleeritud\"\n\" osad pole töökorras, kuid see ei takista süsteemil toimimast.\"\nmsgstr \"\"\n\"The system is configured and partially operational, some duplicated parts\"\n\" of the system are not operational, but this does not prevent the system \"\n\"from working.\"\n\n#: ../../kogumisteenus.rst:672\nmsgid \"\"\n\"Rikke süvenemisel piirini, kus süsteem pole võimeline teenust osutama, \"\n\"saab süsteemi uueks olekuks **Tõrge**. Kõigi rikete kõrvaldamise järel \"\n\"saab süsteemi uueks olekuks **Seadistatud**.\"\nmsgstr \"\"\n\"If the failure reaches the point where the system is unable to provide \"\n\"service, the system will be set to a new status of **Failure**. When all \"\n\"faults have been cleared, the system will be set to **Set up**.\"\n\n#: ../../kogumisteenus.rst:678\nmsgid \"Tõrge\"\nmsgstr \"Failure\"\n\n#: ../../kogumisteenus.rst:680\nmsgid \"Seadistatud süsteemil on tuvastatud rike, mis takistab teenuse osutamist.\"\nmsgstr \"\"\n\"A fault has been detected in the configured system, preventing the \"\n\"service from being provided.\"\n\n#: ../../kogumisteenus.rst:682\nmsgid \"\"\n\"Rikete kõrvaldamisel olukorrani, kus süsteemiga on võimalik teenust \"\n\"osutada, saab süsteemi uueks olekuks **Osaline tõrge**.\"\nmsgstr \"\"\n\"When the fault is rectified to a state where the system can provide \"\n\"service, the system will be set to a new status of **Partial failure**.\"\n\n#: ../../kogumisteenus.rst:687\nmsgid \"Eemaldatud\"\nmsgstr \"Removed\"\n\n#: ../../kogumisteenus.rst:689\nmsgid \"Teenus on konfiguratsioonist eemaldatud.\"\nmsgstr \"The service has been removed from the configuration.\"\n\n#: ../../kogumisteenus.rst:693\nmsgid \"Valijate nimekirjade olekud haldusteenuses\"\nmsgstr \"Statuses of electoral rolls in the administrative service\"\n\n#: ../../kogumisteenus.rst:695\nmsgid \"Valijate nimekirja olek võib olla:\"\nmsgstr \"The status of the voters list may be:\"\n\n#: ../../kogumisteenus.rst:697\nmsgid \"**Rakendamise ootel** - nimekiri on laaditud haldusteenusesse;\"\nmsgstr \"**Pending** - the list has been uploaded to the management service;\"\n\n#: ../../kogumisteenus.rst:699\nmsgid \"**Rakendatud** - nimekiri on rakendatud kogumisteenusele;\"\nmsgstr \"**Applied** - the list is applied to the collection service;\"\n\n#: ../../kogumisteenus.rst:701\nmsgid \"\"\n\"**Vigane** - nimekiri on märgitud vigaseks, haldusteenus uusi valijate \"\n\"nimekirjade muudatusi ei laadi;\"\nmsgstr \"\"\n\"**Invalid** - the list is marked as invalid, the administrative service \"\n\"does not create new voter list changes;\"\n\n#: ../../kogumisteenus.rst:704\nmsgid \"**Vahele jäetud** - vigane nimekiri on märgitud vahelejätmiseks.\"\nmsgstr \"**Skipped** - an invalid list is marked for skipping.\"\n\n#: ../../kogumisteenus.rst:708\nmsgid \"Valijate nimekirja olekudiagramm\"\nmsgstr \"Voters list status diagram\"\n\n#: ../../kogumisteenus.rst:710\nmsgid \"Siirdeprotsessid:\"\nmsgstr \"Transition processes:\"\n\n#: ../../kogumisteenus.rst:712\nmsgid \"Nimekirja laadimine haldusteenusesse:\"\nmsgstr \"Uploading the list to the management service:\"\n\n#: ../../kogumisteenus.rst:714\nmsgid \"\"\n\"Algnimekirja laadib kogumisteenuse operaator, nimekirja olekuks saab \"\n\"**Rakendamise ootel**;\"\nmsgstr \"\"\n\"The initial list will be loaded by the collection service operator, the \"\n\"status of the list will be **Pending**;\"\n\n#: ../../kogumisteenus.rst:717\nmsgid \"\"\n\"Muudatusnimekirja laadib haldusteenus. Vastavalt valideerimise tulemusele\"\n\" saab nimekirja olekuks kas **Rakendamise ootel** või **Vigane**;\"\nmsgstr \"\"\n\"The change list is loaded by the management service. Depending on the \"\n\"result of the validation, the status of the list will be either \"\n\"**Pending** or **Viable**;\"\n\n#: ../../kogumisteenus.rst:720\nmsgid \"\"\n\"**Rakendamine kogumisteenusele**: viib läbi haldusteenus **rakendamise \"\n\"ootel** olekus nimekirjaga. Õnnestumisel määratakse nimekirja olekuks \"\n\"**Rakendatud**, vea korral **Vigane**;\"\nmsgstr \"\"\n\"**Upgrading to collection service**: carries out a management service \"\n\"with a list in the **pending** state. If successful, the status of the \"\n\"list is set to **Applied**, if not successful, the status is set to \"\n\"**Invalid**;\"\n\n#: ../../kogumisteenus.rst:724\nmsgid \"\"\n\"**Vahelejätmine**: operaator määrab olekuga **Vigane** nimekirjale oleku \"\n\"**Vahele jäetud**.\"\nmsgstr \"\"\n\"**Skip**: the operator assigns the status **Invalid** to the list with \"\n\"the status **Skipped**.\"\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/tehnoloogiad.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 15:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../tehnoloogiad.rst:6\nmsgid \"Kasutatavad tehnoloogiad\"\nmsgstr \"Technologies used\"\n\n#: ../../tehnoloogiad.rst:9\nmsgid \"Kogumisteenuse programmeerimiskeel\"\nmsgstr \"Collection service programming language\"\n\n#: ../../tehnoloogiad.rst:11\nmsgid \"\"\n\"Kogumisteenuse tuumikfunktsionaalsus on programmeeritud keeles Go, mis \"\n\"vastab järgmistele hanke nõuetele:\"\nmsgstr \"\"\n\"The core functionality of the collection service is programmed in the Go \"\n\"language, which meets the following procurement requirements:\"\n\n#: ../../tehnoloogiad.rst:14\nmsgid \"Staatiline tüüpimine;\"\nmsgstr \"Static typing;\"\n\n#: ../../tehnoloogiad.rst:16\nmsgid \"Automaatne mäluhaldus;\"\nmsgstr \"Automatic memory management;\"\n\n#: ../../tehnoloogiad.rst:18\nmsgid \"Kompilaator avatud lähtekoodiga;\"\nmsgstr \"Open source compiler;\"\n\n#: ../../tehnoloogiad.rst:20\nmsgid \"Ribastamine (rööprapse).\"\nmsgstr \"Parallel processing (co-routines).\"\n\n#: ../../tehnoloogiad.rst:22\nmsgid \"Kogumisteenuse haldusteenus on programmeeritud keeles Python.\"\nmsgstr \"The collection management service is programmed in Python.\"\n\n#: ../../tehnoloogiad.rst:26\nmsgid \"Rakenduste programmeerimiskeel\"\nmsgstr \"Programming language for applications\"\n\n#: ../../tehnoloogiad.rst:28\nmsgid \"\"\n\"Rakendused on programmeeritud keeles Java, mis vastab hanke nõuetele \"\n\"keele laia leviku ja jätkusuutlikkuse kohta.\"\nmsgstr \"\"\n\"The applications are programmed in Java, which fulfils the requirements \"\n\"of the call for tender for the wide distribution and sustainability of \"\n\"the language.\"\n\n#: ../../tehnoloogiad.rst:33\nmsgid \"Projekti sõltuvused\"\nmsgstr \"Project dependencies\"\n\n#: ../../tehnoloogiad.rst:35\nmsgid \"\"\n\"Projektis kasutatavad kolmandate osapoolte komponendid koos nende \"\n\"motiveeritud kasutamisvajadusega on üles loetletud järgnevates tabelites.\"\n\" Eraldi tabelid on raamistiku pakendamiseks ja töötamiseks ning \"\n\"raamistiku arenduseks ja testimiseks.\"\nmsgstr \"\"\n\"The third-party components used in the project, together with their \"\n\"motivated use, are listed in the tables below. Separate tables are \"\n\"provided for the packaging and operation of the framework and for the \"\n\"development and testing of the framework.\"\n\n#: ../../tehnoloogiad.rst:40\nmsgid \"\"\n\"Kõik IVXV projektis kasutatavad välised teegid asuvad ``ivxv-\"\n\"external.git`` hoidlas või on saadaval platvormil, kus rakendus tööle \"\n\"hakkab.\"\nmsgstr \"\"\n\"All external teegs used in the IVXV project are located in the ``ivxv-\"\n\"external.git`` repository or are available on the platform where the \"\n\"application will run.\"\n\n#: ../../tehnoloogiad.rst:43\nmsgid \"Kõik kogumisteenuses kasutatavad komponendid on avatud lähtekoodiga.\"\nmsgstr \"All components used in the collection service are open source.\"\n\n#: ../../tehnoloogiad.rst:46\nmsgid \"IVXV raamistiku tööks kasutatavad kolmandate osapoolte komponendid\"\nmsgstr \"Third party components used for the operation of the IVXV framework\"\n\n#: ../../tehnoloogiad.rst:50 ../../tehnoloogiad.rst:222\n#: ../../tehnoloogiad.rst:268\nmsgid \"Nimi\"\nmsgstr \"Name\"\n\n#: ../../tehnoloogiad.rst:51 ../../tehnoloogiad.rst:223\n#: ../../tehnoloogiad.rst:269\nmsgid \"Versioon\"\nmsgstr \"Version\"\n\n#: ../../tehnoloogiad.rst:52 ../../tehnoloogiad.rst:224\n#: ../../tehnoloogiad.rst:270\nmsgid \"Litsents (SPDX)\"\nmsgstr \"Licence\"\n\n#: ../../tehnoloogiad.rst:53 ../../tehnoloogiad.rst:225\n#: ../../tehnoloogiad.rst:271\nmsgid \"Kasutusvajadus\"\nmsgstr \"Requirement\"\n\n#: ../../tehnoloogiad.rst:55\nmsgid \"`Bootstrap <http://getbootstrap.com>`_\"\nmsgstr \"`Bootstrap <http://getbootstrap.com>`_\"\n\n#: ../../tehnoloogiad.rst:56\nmsgid \"3.4.1, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:57 ../../tehnoloogiad.rst:62\n#: ../../tehnoloogiad.rst:67 ../../tehnoloogiad.rst:72\n#: ../../tehnoloogiad.rst:87 ../../tehnoloogiad.rst:97\n#: ../../tehnoloogiad.rst:132 ../../tehnoloogiad.rst:137\n#: ../../tehnoloogiad.rst:153 ../../tehnoloogiad.rst:158\n#: ../../tehnoloogiad.rst:163 ../../tehnoloogiad.rst:168\n#: ../../tehnoloogiad.rst:178 ../../tehnoloogiad.rst:213\n#: ../../tehnoloogiad.rst:244 ../../tehnoloogiad.rst:254\nmsgid \"MIT\"\nmsgstr \"MIT\"\n\n#: ../../tehnoloogiad.rst:58 ../../tehnoloogiad.rst:214\nmsgid \"Kogumisteenuse haldusteenuse kasutajaliidese kujundus\"\nmsgstr \"Collection management service interface design\"\n\n#: ../../tehnoloogiad.rst:60\nmsgid \"Bouncy Castle\"\nmsgstr \"Bouncy Castle\"\n\n#: ../../tehnoloogiad.rst:61\nmsgid \"1.78.1, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:63\nmsgid \"ASN1 käsitlemine, andmetüübi BigInteger abifunktsioonid\"\nmsgstr \"ASN1 handling, BigInteger data type support functions\"\n\n#: ../../tehnoloogiad.rst:65\nmsgid \"`Bottle <https://bottlepy.org/>`_\"\nmsgstr \"`Bottle <https://bottlepy.org/>`_\"\n\n#: ../../tehnoloogiad.rst:66\nmsgid \"0.13.2, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:68\nmsgid \"Raamistik kogumisteenuse haldusteenuse veebiliidese teostamiseks\"\nmsgstr \"\"\n\"Framework for the implementation of the web interface for the collection \"\n\"management service\"\n\n#: ../../tehnoloogiad.rst:70\nmsgid \"CAL10N\"\nmsgstr \"CAL10N\"\n\n#: ../../tehnoloogiad.rst:71\nmsgid \"0.8.1, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:73\nmsgid \"Mitmekeelsuse tugi, tõlkefailide valideerimine\"\nmsgstr \"Multilingual support, validation of translation files\"\n\n#: ../../tehnoloogiad.rst:75\nmsgid \"Digidoc 4j\"\nmsgstr \"Digidoc 4j\"\n\n#: ../../tehnoloogiad.rst:76\nmsgid \"5.3.1, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:77\nmsgid \"LGPL-2.1-only\"\nmsgstr \"LGPL-2.1-only\"\n\n#: ../../tehnoloogiad.rst:78\nmsgid \"BDoc konteinerite käsitlemine\"\nmsgstr \"BDoc container handling\"\n\n#: ../../tehnoloogiad.rst:80\nmsgid \"Apache Commons (collections4 4.4)\"\nmsgstr \"Apache Commons (collections4 4.4)\"\n\n#: ../../tehnoloogiad.rst:81\nmsgid \"Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:82 ../../tehnoloogiad.rst:92\n#: ../../tehnoloogiad.rst:102 ../../tehnoloogiad.rst:112\n#: ../../tehnoloogiad.rst:122 ../../tehnoloogiad.rst:173\n#: ../../tehnoloogiad.rst:208 ../../tehnoloogiad.rst:239\n#: ../../tehnoloogiad.rst:280\nmsgid \"Apache-2.0\"\nmsgstr \"Apache 2.0\"\n\n#: ../../tehnoloogiad.rst:83\nmsgid \"Digidoc 4j ja PDFBox sõltuvused\"\nmsgstr \"Digidoc 4j and PDFBox dependencies\"\n\n#: ../../tehnoloogiad.rst:85\nmsgid \"`Docopt <http://docopt.org/>`_\"\nmsgstr \"`Docopt <http://docopt.org/>`_\"\n\n#: ../../tehnoloogiad.rst:86\nmsgid \"0.6.2, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:88\nmsgid \"Kogumisteenuse haldusutiliitide käsurealiidese teostus\"\nmsgstr \"\"\n\"Command line interface for collection service management tool\"\n\n#: ../../tehnoloogiad.rst:90\nmsgid \"`Fasteners <https://github.com/harlowja/fasteners>`_\"\nmsgstr \"`Fasteners <https://github.com/harlowja/fasteners>`_\"\n\n#: ../../tehnoloogiad.rst:91\nmsgid \"0.19, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:93\nmsgid \"Kogumisteenuse haldusteenuse protsesside lukustus\"\nmsgstr \"Locking processes for the collection management service\"\n\n#: ../../tehnoloogiad.rst:95\nmsgid \"`gin-gonic <https://github.com/gin-gonic>`_\"\nmsgstr \"`gin-gonic <https://github.com/gin-gonic>`_\"\n\n#: ../../tehnoloogiad.rst:96\nmsgid \"1.9.1, Go\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:98\nmsgid \"Veebiraamistik x-tee liidese jaoks\"\nmsgstr \"Web-framework for X-road service interface\"\n\n#: ../../tehnoloogiad.rst:100\nmsgid \"`etcd <https://coreos.com/etcd>`_\"\nmsgstr \"`etcd <https://coreos.com/etcd>`_\"\n\n#: ../../tehnoloogiad.rst:101\nmsgid \"3.5.9, Go\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:103\nmsgid \"Talletusteenusena kasutatav hajus võti-väärtus andmebaas\"\nmsgstr \"Distributed key-value database used as a storage service\"\n\n#: ../../tehnoloogiad.rst:105\nmsgid \"Glassfish JAXB\"\nmsgstr \"Glassfish JAXB\"\n\n#: ../../tehnoloogiad.rst:106\nmsgid \"4.0.5, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:107 ../../tehnoloogiad.rst:203\n#: ../../tehnoloogiad.rst:229\nmsgid \"BSD-3-Clause\"\nmsgstr \"BSD 3 Clause\"\n\n#: ../../tehnoloogiad.rst:108\nmsgid \"Java XML teek\"\nmsgstr \"Java XML library\"\n\n#: ../../tehnoloogiad.rst:110\nmsgid \"Gradle\"\nmsgstr \"Gradle\"\n\n#: ../../tehnoloogiad.rst:111\nmsgid \"8.11, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:113\nmsgid \"Java rakenduste ehitamise raamistik\"\nmsgstr \"Framework for building Java applications\"\n\n#: ../../tehnoloogiad.rst:115\nmsgid \"`HAProxy <http://www.haproxy.org/>`_\"\nmsgstr \"`HAProxy <http://www.haproxy.org/>`_\"\n\n#: ../../tehnoloogiad.rst:116\nmsgid \"2.4.24\"\nmsgstr \"2.4.24\"\n\n#: ../../tehnoloogiad.rst:117\nmsgid \"GPL-2.0-or-later\"\nmsgstr \"GPL-2.0-or-later\"\n\n#: ../../tehnoloogiad.rst:118\nmsgid \"Vahendusteenusena kasutatav TCP-proksi\"\nmsgstr \"TCP proxy used as an intermediary service\"\n\n#: ../../tehnoloogiad.rst:120\nmsgid \"Jackson\"\nmsgstr \"Jackson\"\n\n#: ../../tehnoloogiad.rst:121\nmsgid \"2.18.1, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:123\nmsgid \"JSON vormingus failide lugemine ja kirjutamine\"\nmsgstr \"Read and write JSON format files\"\n\n#: ../../tehnoloogiad.rst:125\nmsgid \"Jinja2\"\nmsgstr \"Jinja2\"\n\n#: ../../tehnoloogiad.rst:126\nmsgid \"3.1.4, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:127 ../../tehnoloogiad.rst:188\n#: ../../tehnoloogiad.rst:285\nmsgid \"BSD\"\nmsgstr \"BSD\"\n\n#: ../../tehnoloogiad.rst:128\nmsgid \"Jinja mallide kasutamine haldusteenuses\"\nmsgstr \"Using Jinja templates in collection management service\"\n\n#: ../../tehnoloogiad.rst:130\nmsgid \"`jQuery <https://jquery.org/>`_\"\nmsgstr \"`jQuery <https://jquery.org/>`_\"\n\n#: ../../tehnoloogiad.rst:131\nmsgid \"3.7.1, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:133 ../../tehnoloogiad.rst:159\n#: ../../tehnoloogiad.rst:164 ../../tehnoloogiad.rst:169\nmsgid \"Kogumisteenuse haldusteenuse kasutajaliides\"\nmsgstr \"Collection management service interface\"\n\n#: ../../tehnoloogiad.rst:135\nmsgid \"jsonschema\"\nmsgstr \"jsonschema\"\n\n#: ../../tehnoloogiad.rst:136\nmsgid \"4.23.0, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:138\nmsgid \"JSON valideerimine haldusteenuses\"\nmsgstr \"JSON validation in collection management service\"\n\n#: ../../tehnoloogiad.rst:140\nmsgid \"Logback\"\nmsgstr \"Logback\"\n\n#: ../../tehnoloogiad.rst:141\nmsgid \"1.5.12, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:142 ../../tehnoloogiad.rst:147\nmsgid \"EPL-1.0 or LGPL-v2.1-only\"\nmsgstr \"EPL-1.0 or LGPL-v2.1-only\"\n\n#: ../../tehnoloogiad.rst:143\nmsgid \"Logimise API teostus\"\nmsgstr \"Implementation of logging API\"\n\n#: ../../tehnoloogiad.rst:145\nmsgid \"Logback JSON\"\nmsgstr \"Logback JSON\"\n\n#: ../../tehnoloogiad.rst:146\nmsgid \"0.1.5, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:148\nmsgid \"\"\n\"Logback logija laiendus JSON vormingus logikirjete koostamiseks Jackson \"\n\"teegi abil\"\nmsgstr \"\"\n\"Logback logger extension for generating JSON format log entries using \"\n\"Jackson library.\"\n\n#: ../../tehnoloogiad.rst:151\nmsgid \"`Logrus <https://github.com/sirupsen/logrus>`_\"\nmsgstr \"`Logrus <https://github.com/sirupsen/logrus>`_\"\n\n#: ../../tehnoloogiad.rst:152\nmsgid \"1.9.3, Go\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:154\nmsgid \"Logimisraamistik x-tee liidese jaoks\"\nmsgstr \"Logframework for X-road service interface\"\n\n#: ../../tehnoloogiad.rst:156\nmsgid \"`metisMenu <https://github.com/onokumus/metisMenu>`_\"\nmsgstr \"`metisMenu <https://github.com/onokumus/metisMenu>`_\"\n\n#: ../../tehnoloogiad.rst:157\nmsgid \"1.1.3, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:161\nmsgid \"`FontAwesome <https://github.com/FortAwesome/Font-Awesome>`_\"\nmsgstr \"`FontAwesome <https://github.com/FortAwesome/Font-Awesome>`_\"\n\n#: ../../tehnoloogiad.rst:162\nmsgid \"6.7.2, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:166\nmsgid \"`DataTables <https://github.com/DataTables/DataTablesSrc>`_\"\nmsgstr \"`DataTables <https://github.com/DataTables/DataTablesSrc>`_\"\n\n#: ../../tehnoloogiad.rst:167\nmsgid \"2.3.2, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:171\nmsgid \"PDFBox\"\nmsgstr \"PDFBox\"\n\n#: ../../tehnoloogiad.rst:172\nmsgid \"3.0.3, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:174\nmsgid \"PDF vormingus raportite genereerimise tugi Java rakendustele\"\nmsgstr \"Support for generating PDF reports for Java applications\"\n\n#: ../../tehnoloogiad.rst:176\nmsgid \"`PyYAML <http://pyyaml.org/>`_\"\nmsgstr \"`PyYAML <http://pyyaml.org/>`_\"\n\n#: ../../tehnoloogiad.rst:177\nmsgid \"6.0.2, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:179\nmsgid \"Kogumisteenuse seadistusfailide töötlemise tugi haldusteenusele\"\nmsgstr \"\"\n\"Support for processing configuration files for the collection service to \"\n\"the management service\"\n\n#: ../../tehnoloogiad.rst:181\nmsgid \"python-crontab\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:182\nmsgid \"3.3.0, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:183\nmsgid \"LGPLv3\"\nmsgstr \"LGPLv3\"\n\n#: ../../tehnoloogiad.rst:184\nmsgid \"Crontab haldusteenuses\"\nmsgstr \"Crontab in collection management service\"\n\n#: ../../tehnoloogiad.rst:186\nmsgid \"python-dateutil\"\nmsgstr \"python-dateutil\"\n\n#: ../../tehnoloogiad.rst:187\nmsgid \"2.9.0, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:189\nmsgid \"Kuupäevad ja kellaajad haldusteenuses\"\nmsgstr \"Dates and times in collection management service\"\n\n#: ../../tehnoloogiad.rst:191\nmsgid \"python-debian\"\nmsgstr \"python-debian\"\n\n#: ../../tehnoloogiad.rst:192\nmsgid \"0.1.49, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:193\nmsgid \"GPLv2\"\nmsgstr \"GPLv2\"\n\n#: ../../tehnoloogiad.rst:194\nmsgid \"Debian pakkide lugemine haldusteenuses\"\nmsgstr \"Managing DEB files in collection management service\"\n\n#: ../../tehnoloogiad.rst:196\nmsgid \"pyopenssl\"\nmsgstr \"pyopenssl\"\n\n#: ../../tehnoloogiad.rst:197\nmsgid \"24.2.1, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:198\nmsgid \"Apache\"\nmsgstr \"Apache\"\n\n#: ../../tehnoloogiad.rst:199\nmsgid \"OpenSSL kasutus haldusteenuses\"\nmsgstr \"OpenSSL in collection management service\"\n\n#: ../../tehnoloogiad.rst:201\nmsgid \"`Schematics <https://github.com/schematics/schematics>`_\"\nmsgstr \"`Schematics <https://github.com/schematics/schematics>`_\"\n\n#: ../../tehnoloogiad.rst:202\nmsgid \"2.1.1, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:204\nmsgid \"Kogumisteenuse seadistusfailide valideerimise tugi haldusteenusele\"\nmsgstr \"\"\n\"Support for validation of collection service configuration files for the \"\n\"management service\"\n\n#: ../../tehnoloogiad.rst:206\nmsgid \"SnakeYAML\"\nmsgstr \"SnakeYAML\"\n\n#: ../../tehnoloogiad.rst:207\nmsgid \"2.3, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:209\nmsgid \"YAML vormingus andmete lugemine\"\nmsgstr \"Reading data in YAML format\"\n\n#: ../../tehnoloogiad.rst:211\nmsgid \"\"\n\"`SB Admin 2 <https://github.com/BlackrockDigital/startbootstrap-sb-\"\n\"admin-2>`_\"\nmsgstr \"\"\n\"`SB Admin 2 <https://github.com/BlackrockDigital/startbootstrap-sb-\"\n\"admin-2>`_\"\n\n#: ../../tehnoloogiad.rst:212\nmsgid \"3.3.7+1, JavaScript\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:217\nmsgid \"IVXV raamistiku testide kasutatavad kolmandate osapoolte komponendid\"\nmsgstr \"Third-party components to be used for IVXV Framework tests\"\n\n#: ../../tehnoloogiad.rst:227\nmsgid \"Hamcrest\"\nmsgstr \"Hamcrest\"\n\n#: ../../tehnoloogiad.rst:228\nmsgid \"3.0, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:230\nmsgid \"Loetavam assert-meetodite kasutamine Java üksuste testides\"\nmsgstr \"More readable use of assert methods in Java unit tests\"\n\n#: ../../tehnoloogiad.rst:232\nmsgid \"JUnit\"\nmsgstr \"JUnit\"\n\n#: ../../tehnoloogiad.rst:233\nmsgid \"5.10.0, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:234\nmsgid \"EPL-1.0\"\nmsgstr \"EPL-1.0\"\n\n#: ../../tehnoloogiad.rst:235\nmsgid \"Java testimisraamistik\"\nmsgstr \"Java testing framework\"\n\n#: ../../tehnoloogiad.rst:237\nmsgid \"JUnitParams\"\nmsgstr \"JUnitParams\"\n\n#: ../../tehnoloogiad.rst:238\nmsgid \"1.1.1, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:240\nmsgid \"Testide parametriseerimise tugi\"\nmsgstr \"Test parameterisation support\"\n\n#: ../../tehnoloogiad.rst:242\nmsgid \"Mockito\"\nmsgstr \"Mockito\"\n\n#: ../../tehnoloogiad.rst:243\nmsgid \"5.14.2, Java\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:245\nmsgid \"Testitava koodi sõltuvuste mockimise tugi\"\nmsgstr \"Support for mocking dependencies in code under test\"\n\n#: ../../tehnoloogiad.rst:247\nmsgid \"libdigidocpp-tools\"\nmsgstr \"libdigidocpp-tools\"\n\n#: ../../tehnoloogiad.rst:248\nmsgid \"3.14.5 .1404\"\nmsgstr \"3.14.5 .1404\"\n\n#: ../../tehnoloogiad.rst:249\nmsgid \"LGPL-2.1-or-later\"\nmsgstr \"LGPL-2.1-or-later\"\n\n#: ../../tehnoloogiad.rst:250\nmsgid \"Testandmete genereerimine\"\nmsgstr \"Test data generation\"\n\n#: ../../tehnoloogiad.rst:252\nmsgid \"PyTest\"\nmsgstr \"PyTest\"\n\n#: ../../tehnoloogiad.rst:253\nmsgid \"7.4.2, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:255\nmsgid \"Üksuste testimise tugi Pythonile\"\nmsgstr \"Unit testing support for Python\"\n\n#: ../../tehnoloogiad.rst:257\nmsgid \"Requests\"\nmsgstr \"Requests\"\n\n#: ../../tehnoloogiad.rst:258\nmsgid \"2.32.3, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:259\nmsgid \"Apache 2.0\"\nmsgstr \"Apache 2.0\"\n\n#: ../../tehnoloogiad.rst:260\nmsgid \"HTTP päringute moodul Pythoni testidele\"\nmsgstr \"HTTP request module for Python tests\"\n\n#: ../../tehnoloogiad.rst:263\nmsgid \"\"\n\"IVXV raamistiku arendamiseks ja/või testimiseks kasutatavad kolmandate \"\n\"osapoolte tööriistad\"\nmsgstr \"Third-party tools used to develop and/or test the IVXV framework\"\n\n#: ../../tehnoloogiad.rst:273\nmsgid \"`Behave <https://github.com/behave/behave>`_\"\nmsgstr \"`Behave <https://github.com/behave/behave>`_\"\n\n#: ../../tehnoloogiad.rst:274\nmsgid \"1.2.6, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:275\nmsgid \"BSD-2-Clause\"\nmsgstr \"BSD 2 Clause\"\n\n#: ../../tehnoloogiad.rst:276\nmsgid \"Regressioonitestide käivitaja (*Behavior-driven development*)\"\nmsgstr \"Regression test trigger (*Behavior-driven development*)\"\n\n#: ../../tehnoloogiad.rst:278\nmsgid \"`Docker <http://www.docker.com/>`_\"\nmsgstr \"`Docker <http://www.docker.com/>`_\"\n\n#: ../../tehnoloogiad.rst:279\nmsgid \"18.06 (või uuem)\"\nmsgstr \"18.06 (or newer)\"\n\n#: ../../tehnoloogiad.rst:281\nmsgid \"Regressioonitestide läbiviimise keskkond - tarkvarakonteinerid\"\nmsgstr \"Regression testing environment - software containers\"\n\n#: ../../tehnoloogiad.rst:283\nmsgid \"`Sphinx <http://www.sphinx-doc.org/>`_\"\nmsgstr \"`Sphinx <http://www.sphinx-doc.org/>`_\"\n\n#: ../../tehnoloogiad.rst:284\nmsgid \"7.2.5, Python\"\nmsgstr \"\"\n\n#: ../../tehnoloogiad.rst:286\nmsgid \"Dokumentatsiooni genereerimine\"\nmsgstr \"Generating documentation\"\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/vallasrezhiim.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 19:48+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../vallasrezhiim.rst:4\nmsgid \"Rakendused\"\nmsgstr \"Applications\"\n\n#: ../../vallasrezhiim.rst:8\nmsgid \"Üldpõhimõtted\"\nmsgstr \"General principles\"\n\n#: ../../vallasrezhiim.rst:10\nmsgid \"\"\n\"Kõik rakendused on käsurealiidesega rakendused, mis on pakendatud töötama\"\n\" operatsioonisüsteemi Windows 10 (või uuem) keskkonnas. Komponentide \"\n\"kasutajaliidesed on ühekeelsed. Komponendid tarnitakse eestikeelsetena, \"\n\"nende tõlkimine on võimalik tõlkefaili abil.\"\nmsgstr \"\"\n\"All applications are command-line applications, packaged to work in \"\n\"Windows 10 (or later) operating system environment. Component user \"\n\"interfaces are monolingual. The components are delivered in Estonian, and\"\n\" can be translated using a translation file.\"\n\n#: ../../vallasrezhiim.rst:15\nmsgid \"Rakendused on programmeeritud Java keeles.\"\nmsgstr \"The applications are developed in Java programming language.\"\n\n#: ../../vallasrezhiim.rst:17\nmsgid \"\"\n\"Väliste infosüsteemidega suhtlevad rakendused kasutavad maksimaalselt \"\n\"olemasolevaid liideseid/andmestruktuure.\"\nmsgstr \"\"\n\"Applications interacting with external information systems make maximum \"\n\"use of existing interfaces/data structures.\"\n\n#: ../../vallasrezhiim.rst:20\nmsgid \"\"\n\"Rakendused saavad oma sisendi rakenduste seadistustest ja seadistustes \"\n\"näidatud failidest failisüsteemis ning salvestavad oma väljundi kasutaja \"\n\"näidatud kausta failisüsteemis. Failid võivad paikneda ka operatiiv-\"\n\"mälukettal.\"\nmsgstr \"\"\n\"Applications get their input from the application settings and the files \"\n\"in the file system indicated in the settings, and store their output in \"\n\"the folder in the file system indicated by the user. Files may also be \"\n\"located on the memory-disk.\"\n\n#: ../../vallasrezhiim.rst:24\nmsgid \"\"\n\"Relevantsed rakendused toetavad ElGamal krüptosüsteemi \"\n\"täisarvujäägikorpustel ning P-384 elliptkõveral. Lugemistõend on \"\n\"realiseeritud Schnorri nullteadmustõestusel põhineval protokollil.\"\nmsgstr \"\"\n\"Relevant applications support the ElGamal cryptosystem on prime order \"\n\"fields and the P-384 elliptic curve. The decryption proof is implemented \"\n\"using the Schnorr zero-knowledge protocol.\"\n\n#: ../../vallasrezhiim.rst:30\nmsgid \"Rakenduste abimoodulid\"\nmsgstr \"Applications support modules\"\n\n#: ../../vallasrezhiim.rst:32\nmsgid \"\"\n\"Valimise liides on rakenduste jaoks unifitseeritud, see võimaldab \"\n\"erinevate valimistüüpide realiseerimist moodulitena. Digiallkirja \"\n\"verifitseerimise funktsionaalsus on loodud `digidoc4j <https://github.com\"\n\"/open-eid/digidoc4j>`_ teegi abil. Abimoodulite kasutamist alljärgnevatel\"\n\" skeemidel eraldi välja ei tooda.\"\nmsgstr \"\"\n\"The election interface is unified for applications, allowing different \"\n\"types of elections to be implemented as modules. The digital signature \"\n\"verification functionality has been created using the `digidoc4j \"\n\"<https://github.com/open-eid/digidoc4j>`_ library. The use of the \"\n\"auxiliary modules is not explicitly described in the following diagrams.\"\n\n#: ../../vallasrezhiim.rst:39\nmsgid \"Rakenduste seadistamine\"\nmsgstr \"Setting up applications\"\n\n#: ../../vallasrezhiim.rst:41\nmsgid \"\"\n\"Rakendused seadistatakse kas digitaalallkirjastatud \"\n\"konfiguratsioonipakiga või käsureavõtmetega. Käsureavõtmed ei toeta \"\n\"hierarhilise struktuuriga seadistuste sisestamist. Seadistused \"\n\"konfiguratsioonipakis kirjeldatakse YAML-keeles:\"\nmsgstr \"\"\n\"The applications are configured either with a digitally signed \"\n\"configuration package or with command line parameters. The latter do not \"\n\"support the entry of hierarchically structured configurations. The \"\n\"settings in the configuration package shall be described in YAML:\"\n\n#: ../../vallasrezhiim.rst:88\nmsgid \"Sisendite kooskõlalisuse kontroll\"\nmsgstr \"Input consistency check\"\n\n#: ../../vallasrezhiim.rst:90\nmsgid \"\"\n\"Kõik rakendused teostavad konfiguratsioonile sisendite kooskõlalisuse \"\n\"kontrolli vastavalt nende poolt kasutatavale konfiguratsioonile:\"\nmsgstr \"\"\n\"All applications perform configuration consistency checks on the inputs \"\n\"according to the configuration they use:\"\n\n#: ../../vallasrezhiim.rst:93\nmsgid \"sertifikaatide konfiguratsiooni laadimine;\"\nmsgstr \"loading certificate configuration;\"\n\n#: ../../vallasrezhiim.rst:95\nmsgid \"konfiguratsiooni digiallkirja verifitseerimine;\"\nmsgstr \"Digital signature verification of the configuration;\"\n\n#: ../../vallasrezhiim.rst:97\nmsgid \"ringkondade nimekirja verifitseerimine;\"\nmsgstr \"verification of the list of districts;\"\n\n#: ../../vallasrezhiim.rst:99\nmsgid \"ringkondade nimekirja kooskõlalisuse kontroll;\"\nmsgstr \"checking the consistency of the list of districts;\"\n\n#: ../../vallasrezhiim.rst:101\nmsgid \"ringkondade nimekirja laadimine;\"\nmsgstr \"loading the list of districts;\"\n\n#: ../../vallasrezhiim.rst:103\nmsgid \"valikute nimekirja verifitseerimine;\"\nmsgstr \"verification of the list of choices;\"\n\n#: ../../vallasrezhiim.rst:105\nmsgid \"valikute nimekirja kooskõlalisuse kontroll;\"\nmsgstr \"checking the consistency of the list of choices;\"\n\n#: ../../vallasrezhiim.rst:107\nmsgid \"valikute nimekirja laadimine;\"\nmsgstr \"loading the list of choices;\"\n\n#: ../../vallasrezhiim.rst:109\nmsgid \"valijate nimekirjade verifitseerimine;\"\nmsgstr \"verification of voter lists;\"\n\n#: ../../vallasrezhiim.rst:111\nmsgid \"valijate nimekirjade kooskõlalisus kontroll;\"\nmsgstr \"a check on the consistency of voter lists;\"\n\n#: ../../vallasrezhiim.rst:113\nmsgid \"valijate nimekirjade laadimine.\"\nmsgstr \"loading the voter lists.\"\n\n#: ../../vallasrezhiim.rst:117\nmsgid \"Võtmerakendus\"\nmsgstr \"Key application\"\n\n#: ../../vallasrezhiim.rst:121\nmsgid \"Võtmerakenduse liidesed\"\nmsgstr \"Key application interfaces\"\n\n#: ../../vallasrezhiim.rst:123\nmsgid \"\"\n\"Võtmerakendusega genereeritakse iga hääletamise jaoks häälte salastamise \"\n\"ja häälte avamise võti, samuti toimub selle abil häälte lugemine ja \"\n\"tulemuse väljastamine.\"\nmsgstr \"\"\n\"The key application generates a key for encrypting and decrypting votes, \"\n\"as well as counting votes and issuing the result.\"\n\n#: ../../vallasrezhiim.rst:127\nmsgid \"\"\n\"Võtmerakendus kasutab [DesmedtF89]_ läviskeemi, mis põhineb usaldataval \"\n\"osakujagajal ning rakendab Shamiri osakujagamist, mis on \"\n\"informatsiooniteoreetiliselt turvaline :math:`t < M` osapoole korral, kus\"\n\" M on lävipiir.\"\nmsgstr \"\"\n\"The key application uses the [DesmedtF89]_ threshold scheme based on a \"\n\"trusted dealer and implements Shamir secret-sharing, which is \"\n\"information-theoretically secure for :math:`t < M`, where M is the \"\n\"threshold.\"\n\n#: ../../vallasrezhiim.rst:132\nmsgid \"\"\n\"Võtmeosakud genereeritakse operatiivmälus ning talletatakse \"\n\"PKCS15-liidese vahendusel kiipkaardile.\"\nmsgstr \"\"\n\"The key shares are generated in the operational memory and stored on the \"\n\"smart card via the PKCS15 interface.\"\n\n#: ../../vallasrezhiim.rst:135\nmsgid \"Võtmerakenduse sisend võtme genereerimisel on:\"\nmsgstr \"The input to the key application when generating a key is:\"\n\n#: ../../vallasrezhiim.rst:137 ../../vallasrezhiim.rst:161\nmsgid \"Võtmepaari identifikaator;\"\nmsgstr \"Key pair identifier;\"\n\n#: ../../vallasrezhiim.rst:139\nmsgid \"\"\n\"Krüptosüsteemi ElGamal spetsifikatsioon – täisarvujäägikorpus või P-384 \"\n\"elliptkõver ning võtmepikkus;\"\nmsgstr \"\"\n\"Specification of the ElGamal cryptosystem - prime order field or P-384 \"\n\"elliptic curve and key length;\"\n\n#: ../../vallasrezhiim.rst:142\nmsgid \"\"\n\"M-N läviskeemi spetsifikatsioon, mis peab vastama reeglile :math:`N >= 2 \"\n\"* M - 1`;\"\nmsgstr \"\"\n\"Specification of the M-N threshold scheme, which must satisfy the rule \"\n\":math:`N >= 2 * M - 1`;\"\n\n#: ../../vallasrezhiim.rst:145\nmsgid \"N PKCS15-ühilduvat kiipkaarti;\"\nmsgstr \"N PKCS15-compatible smart card;\"\n\n#: ../../vallasrezhiim.rst:147\nmsgid \"Võtmerakenduse väljund võtme genereerimisel on:\"\nmsgstr \"The output of the key application when generating a key is:\"\n\n#: ../../vallasrezhiim.rst:149\nmsgid \"Isesigneeritud sertifikaat;\"\nmsgstr \"Self-signed certificate;\"\n\n#: ../../vallasrezhiim.rst:151\nmsgid \"N võtmeosakut talletatuna kiipkaartidel;\"\nmsgstr \"N key shares stored on smart cards;\"\n\n#: ../../vallasrezhiim.rst:153 ../../vallasrezhiim.rst:174\nmsgid \"Rakenduse detailne tegevuslogi;\"\nmsgstr \"Detailed activity log of the application;\"\n\n#: ../../vallasrezhiim.rst:155 ../../vallasrezhiim.rst:176\nmsgid \"Rakenduse detailne vealogi.\"\nmsgstr \"A detailed error log for the application.\"\n\n#: ../../vallasrezhiim.rst:157\nmsgid \"Võtmerakenduse sisend häälte lugemisel on:\"\nmsgstr \"The key application input for vote counting is:\"\n\n#: ../../vallasrezhiim.rst:159\nmsgid \"Miksitud hääled;\"\nmsgstr \"Mixed ballots;\"\n\n#: ../../vallasrezhiim.rst:163\nmsgid \"M võtmeosakut vastavalt läviskeemi spetsifikatsioonile.\"\nmsgstr \"M key shares according to the threshold scheme specification.\"\n\n#: ../../vallasrezhiim.rst:165\nmsgid \"Võtmerakenduse väljund häälte lugemisel on:\"\nmsgstr \"The output of the key application for counting votes is:\"\n\n#: ../../vallasrezhiim.rst:167\nmsgid \"Signeeritud hääletamistulemus;\"\nmsgstr \"Signed results;\"\n\n#: ../../vallasrezhiim.rst:169\nmsgid \"Kehtetute häälte loend;\"\nmsgstr \"List of invalid votes;\"\n\n#: ../../vallasrezhiim.rst:171\nmsgid \"\"\n\"Lugemistõend (Schnorri nullteadmustõestusel põhinev protokoll vastavalt \"\n\"hankedokumentides viidatule);\"\nmsgstr \"\"\n\"Decryption proof (Schnorr's zero knowledge protocol as referred to in the\"\n\" contract documents);\"\n\n#: ../../vallasrezhiim.rst:179\nmsgid \"Töötlemisrakendus\"\nmsgstr \"Processing application\"\n\n#: ../../vallasrezhiim.rst:181\nmsgid \"\"\n\"Töötlemisrakendusega verifitseeritakse, tühistatakse ning anonüümitakse \"\n\"hääletamisperioodil kogutud hääli vastavalt Üldkirjelduse jaotisele 7.6.\"\nmsgstr \"\"\n\"The processing application verifies, revokes and anonymises the votes \"\n\"collected during the voting period in accordance with Section 7.6 of the \"\n\"General Description.\"\n\n#: ../../vallasrezhiim.rst:184\nmsgid \"Töötlemisrakenduse sisendid on:\"\nmsgstr \"The inputs to the processing application are:\"\n\n#: ../../vallasrezhiim.rst:186\nmsgid \"kogumisteenuse poolt talletatud elektroonilised hääled;\"\nmsgstr \"electronic votes recorded by the collection service;\"\n\n#: ../../vallasrezhiim.rst:188\nmsgid \"registreerimisteenuse poolt väljastatud ajamärgendid;\"\nmsgstr \"time stamps issued by the registration service;\"\n\n#: ../../vallasrezhiim.rst:190\nmsgid \"valijate nimekirjad;\"\nmsgstr \"voter lists;\"\n\n#: ../../vallasrezhiim.rst:192\nmsgid \"ringkondade nimekiri;\"\nmsgstr \"list of districts;\"\n\n#: ../../vallasrezhiim.rst:194\nmsgid \"tühistusnimekirjad;\"\nmsgstr \"revocation lists;\"\n\n#: ../../vallasrezhiim.rst:196\nmsgid \"ennistusnimekirjad.\"\nmsgstr \"restoration lists.\"\n\n#: ../../vallasrezhiim.rst:198\nmsgid \"Töötlemisrakenduse väljundid on:\"\nmsgstr \"The outputs of the processing application are:\"\n\n#: ../../vallasrezhiim.rst:200\nmsgid \"rakenduse detailne tegevuslogi;\"\nmsgstr \"detailed activity log of the application;\"\n\n#: ../../vallasrezhiim.rst:202\nmsgid \"rakenduse detailne vealogi;\"\nmsgstr \"detailed error log of the application;\"\n\n#: ../../vallasrezhiim.rst:204\nmsgid \"e-hääletanute nimekiri PDF-vormingus, vastavalt töötlemise etapile;\"\nmsgstr \"the list of e-voters in PDF format, according to the stage of processing;\"\n\n#: ../../vallasrezhiim.rst:206\nmsgid \"\"\n\"e-hääletanute nimekiri masintöödeldaval kujul, vastavalt töötlemise \"\n\"etapile;\"\nmsgstr \"\"\n\"the list of e-voters in a machine-processable format, according to the \"\n\"stage of processing;\"\n\n#: ../../vallasrezhiim.rst:208\nmsgid \"anonüümitud hääled.\"\nmsgstr \"anonymised ballots.\"\n\n#: ../../vallasrezhiim.rst:210\nmsgid \"\"\n\"Lisaks varem defineeritud liidestele ja sõltuvustele kasutab \"\n\"töötlemisrakendus kolmanda osapoole teeki PDF'ide väljastamise \"\n\"funktsionaalsuse teostamiseks.\"\nmsgstr \"\"\n\"In addition to the interfaces and dependencies defined earlier, the \"\n\"processing application uses a third-party library to perform PDF output \"\n\"functionality.\"\n\n#: ../../vallasrezhiim.rst:215\nmsgid \"Töötlemisrakenduse liidesed\"\nmsgstr \"Processing application interfaces\"\n\n#: ../../vallasrezhiim.rst:218\nmsgid \"Elektrooniliste häälte täielik töötlemine\"\nmsgstr \"Complete processing of electronic voices\"\n\n#: ../../vallasrezhiim.rst:220\nmsgid \"\"\n\"Elektrooniliste häälte täielik töötlemine on tegevus, mille käigus \"\n\"töötlemisrakendus võrdleb Kogumisteenuse poolt talletatud häälte hulka \"\n\"registreerimisteenuse poolt talletatud häälte hulgaga, kontrollib \"\n\"talletatud häälte vastavust valimiste konfiguratsioonile, tuvastab \"\n\"loendamisele minevad hääled ning anonüümib need Võtmerakendusele üle \"\n\"andmiseks.\"\nmsgstr \"\"\n\"Complete processing of electronic ballots is the process whereby the \"\n\"processing application compares the number of ballots stored by the \"\n\"Collection Service with the number of ballots stored by the Registration \"\n\"Service, verifies that the stored ballots match the election \"\n\"configuration, identifies the ballots to be counted and anonymizes them \"\n\"for delivery to the Key Application.\"\n\n#: ../../vallasrezhiim.rst:226 ../../vallasrezhiim.rst:254\nmsgid \"rakenduse seadistuste laadimine;\"\nmsgstr \"loading app settings;\"\n\n#: ../../vallasrezhiim.rst:228 ../../vallasrezhiim.rst:256\nmsgid \"elektrooniliste häälte digitaalallkirjade verifitseerimine;\"\nmsgstr \"verification of digital signatures of electronic votes;\"\n\n#: ../../vallasrezhiim.rst:230\nmsgid \"registreerimisteenuse kinnituste verifitseerimine;\"\nmsgstr \"verification of registration service confirmations;\"\n\n#: ../../vallasrezhiim.rst:232\nmsgid \"ajatemplite verifitseerimine;\"\nmsgstr \"verification of time stamps;\"\n\n#: ../../vallasrezhiim.rst:234\nmsgid \"iga valija kohta viimase kehtiva hääle tuvastamine;\"\nmsgstr \"identifying the last valid vote for each voter;\"\n\n#: ../../vallasrezhiim.rst:236\nmsgid \"algse elektrooniliselt hääletanute nimekirja väljastamine PDF-vormingus;\"\nmsgstr \"issuing the original list of electronic voters in PDF format;\"\n\n#: ../../vallasrezhiim.rst:238\nmsgid \"tühistus- ja ennistusnimekirjade verifitseerimine;\"\nmsgstr \"verification of revocation and restoration lists;\"\n\n#: ../../vallasrezhiim.rst:240\nmsgid \"tühistus- ja ennistusnimekirjade kooskõlalisuse kontroll;\"\nmsgstr \"checking the consistency of cancellation and reinstatement lists;\"\n\n#: ../../vallasrezhiim.rst:242\nmsgid \"tühistus- ja ennistusnimekirjade rakendamine;\"\nmsgstr \"the implementation of revocation and restoration lists;\"\n\n#: ../../vallasrezhiim.rst:244\nmsgid \"\"\n\"miksimisele minevate häälte nimekirja koostamine, krüptogrammide \"\n\"eraldamine digitaalallkirjadest;\"\nmsgstr \"\"\n\"drawing up a list of votes to be mixed, separating ciphertexts from \"\n\"digital signatures;\"\n\n#: ../../vallasrezhiim.rst:247\nmsgid \"\"\n\"lõpliku elektrooniliselt hääletanute nimekirja väljastamine masinloetavas\"\n\" vormingus.\"\nmsgstr \"issuing the final list of electronic voters in machine-readable format.\"\n\n#: ../../vallasrezhiim.rst:252\nmsgid \"Elektrooniliselt hääletanute nimekirja genereerimine\"\nmsgstr \"Generating a list of voters electronically\"\n\n#: ../../vallasrezhiim.rst:258\nmsgid \"algse elektrooniliselt hääletanute nimekirja väljastamine PDF-vormingus.\"\nmsgstr \"issuing the original list of electronic voters in PDF format.\"\n\n#: ../../vallasrezhiim.rst:262\nmsgid \"Auditirakendus\"\nmsgstr \"Audit application\"\n\n#: ../../vallasrezhiim.rst:266\nmsgid \"Auditirakenduse liidesed\"\nmsgstr \"Audit application interfaces\"\n\n#: ../../vallasrezhiim.rst:268\nmsgid \"\"\n\"Auditirakendusega (joonis 9) verifitseeritakse matemaatiliselt häälte \"\n\"kokkulugemise korrektsust ning miksimise kasutamisel ka miksimise \"\n\"korrektsust.\"\nmsgstr \"\"\n\"The audit application (Figure 9) mathematically verifies the correctness \"\n\"of the voice aggregation and, when mixing is used, the correctness of the\"\n\" mixing.\"\n\n#: ../../vallasrezhiim.rst:271\nmsgid \"Auditirakenduse sisendid on;\"\nmsgstr \"The inputs to the audit application are;\"\n\n#: ../../vallasrezhiim.rst:273\nmsgid \"anonüümitud hääled;\"\nmsgstr \"anonymised ballots;\"\n\n#: ../../vallasrezhiim.rst:275\nmsgid \"miksitud hääled;\"\nmsgstr \"mixed ballots;\"\n\n#: ../../vallasrezhiim.rst:277\nmsgid \"Verificatumi miksimistõend;\"\nmsgstr \"Verificatum mixing proof;\"\n\n#: ../../vallasrezhiim.rst:279\nmsgid \"hääletamistulemus.\"\nmsgstr \"the tally.\"\n\n#: ../../vallasrezhiim.rst:281\nmsgid \"\"\n\"Auditirakenduse väljund on rakenduse detailne tegevuslogi, mis sisaldab \"\n\"ka hinnangut auditi tervikliku õnnestumise kohta. Vajadusel väljastatakse\"\n\" ka rakenduse detailne vealogi.\"\nmsgstr \"\"\n\"The output of the audit application is a detailed log of the \"\n\"application's activities, including an assessment of the overall success \"\n\"of the audit. If required, a detailed error log of the application is \"\n\"also output.\"\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/viited.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 18:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../viited.rst:4\nmsgid \"\"\n\"Desmedt, Y. & Frankel, Y. Brassard, G. (Ed.) Threshold Cryptosystems \"\n\"Advances in Cryptology - CRYPTO '89, 9th Annual International Cryptology \"\n\"Conference, Santa Barbara, California, USA, August 20-24, 1989, \"\n\"Proceedings, Springer, 1989, 435, 307-315.\"\nmsgstr \"\"\n\"Desmedt, Y. & Frankel, Y. Brassard, G. (Ed.) Threshold Cryptosystems \"\n\"Advances in Cryptology - CRYPTO '89, 9th Annual International Cryptology \"\n\"Conference, Santa Barbara, California, USA, August 20-24, 1989, \"\n\"Proceedings, Springer, 1989, 435, 307-315.\"\n\n#: ../../viited.rst:9\nmsgid \"\"\n\"Sven Heiberg, Tarvi Martens, Priit Vinkel, Jan Willemson, Improving the \"\n\"verifiability of the Estonian Internet Voting scheme. In Robert Krimmer, \"\n\"Melanie Volkamer, Jordi Barrat, Josh Benaloh, Nicole Goodman, Peter Y.A. \"\n\"Ryan, Oliver Spycher, Vanessa Teague, Gregor Wenda (Eds.), The \"\n\"International Conference on Electronic Voting E-Vote-ID 2016, 18-21 \"\n\"October 2016, Lochau/Bregenz, Austria, TUT Press, pp. 213-229, ISBN \"\n\"978-9949-83-022-0\"\nmsgstr \"\"\n\"Sven Heiberg, Tarvi Martens, Priit Vinkel, Jan Willemson, Improving the \"\n\"verifiability of the Estonian Internet Voting scheme. In Robert Krimmer, \"\n\"Melanie Volkamer, Jordi Barrat, Josh Benaloh, Nicole Goodman, Peter Y.A. \"\n\"Ryan, Oliver Spycher, Vanessa Teague, Gregor Wenda (Eds.), The \"\n\"International Conference on Electronic Voting E-Vote-ID 2016, 18-21 \"\n\"October 2016, Lochau/Bregenz, Austria, TUT Press, pp. 213-229, ISBN \"\n\"978-9949-83-022-0\"\n\n#: ../../viited.rst:16\nmsgid \"\"\n\"Tehniline kirjeldus. Elektroonilise hääletamise infosüsteemi arenduse \"\n\"hange, Vabariigi Valimiskomisjon, 2016\"\nmsgstr \"\"\n\"Technical description. Tender for the development of an electronic voting\"\n\" information system, National Electoral Committee, 2016\"\n\n#: ../../viited.rst:19\nmsgid \"\"\n\"Elektroonilise hääletamise üldraamistik ja selle kasutamine Eesti \"\n\"riiklikel valimistel. Elektroonilise Hääletamise Komisjon, Tallinn 2016\"\nmsgstr \"\"\n\"General framework for electronic voting and its use in Estonian national \"\n\"elections. Electronic Voting Commission, Tallinn 2016\"\n"
  },
  {
    "path": "Documentation/public/arhitektuur/locales/en/LC_MESSAGES/yldpohimotted.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 15:28+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../yldpohimotted.rst:4\nmsgid \"Sissejuhatus\"\nmsgstr \"Introduction\"\n\n#: ../../yldpohimotted.rst:6\nmsgid \"\"\n\"Elektroonilise hääletamise infosüsteem IVXV on loodud lähtuvalt \"\n\"e-hääletamise raamistikust [ÜK2016]_ ja riigihanke 171780 tehnilisest \"\n\"kirjeldusest [TK2016]_. Käesolevas dokumendis kirjeldatakse IVXV \"\n\"arhitektuurset lahendust. Elektroonilise hääletamise infosüsteem koosneb \"\n\"vallasrežiimirakendustest ning sidusrežiimikomponentidest. Täiendavalt \"\n\"sõltub infosüsteem välistest infosüsteemidest ning mõjutab vahetult \"\n\"elektrooniliseks hääletamiseks ja hääle kontrollimiseks kasutatavaid \"\n\"komponente.\"\nmsgstr \"\"\n\"The IVXV electronic voting information system has been developed on the \"\n\"basis of the e-voting framework [ÜK2016]_ and the technical specification\"\n\" of public procurement 171780 [TK2016]_. This document describes the \"\n\"architectural design of IVXV. The eVoting Information System consists of \"\n\"the offline applications and the online components. In addition, the \"\n\"information system depends on external information systems and directly \"\n\"affects the components used for electronic voting and vote verification.\"\n\n#: ../../yldpohimotted.rst:14\nmsgid \"\"\n\"Arhitektuuridokument kirjeldab IVXV komponente, nende omavahelisi \"\n\"liideseid ja liideseid väliste süsteemidega ning komponentide poolt \"\n\"realiseeritavaid protokolle.\"\nmsgstr \"\"\n\"The architecture document describes the IVXV components, their interfaces\"\n\" with each other and with external systems, and the protocols implemented\"\n\" by the components.\"\n\n#: ../../yldpohimotted.rst:18\nmsgid \"IVXV kontseptsioon\"\nmsgstr \"IVXV Concept\"\n\n#: ../../yldpohimotted.rst:20\nmsgid \"\"\n\"Üldine, kuid terviklik ülevaade elektroonilise hääletamise raamistiku \"\n\"(\\\"IVXV\\\") tehnilisest ja organisatsioonilisest poolest ning selle \"\n\"rakendamisest Eesti riiklikel valimistel on antud e-hääletamise \"\n\"raamistiku üldkirjelduses [ÜK2016]_.\"\nmsgstr \"\"\n\"A general but comprehensive overview of the technical and organisational \"\n\"aspects of the electronic voting framework (\\\"IVXV\\\") and its \"\n\"implementation in Estonian national elections is given in the general \"\n\"description of the e-voting framework [ÜK2016]_.\"\n\n#: ../../yldpohimotted.rst:25\nmsgid \"\"\n\"IVXV infosüsteemina teostab \\\"ümbrikuskeemil\\\" põhinevat e-hääletamise \"\n\"protokolli. IVXV toimib hääletamiseelsel etapil, hääletamisetapil, \"\n\"töötlusetapil ning lugemisetapil ja pakub vahendeid elektroonilise \"\n\"hääletamise protsessis osalemiseks Korraldajale, Lugejale, Hääletajale, \"\n\"Kogujale, Töötlejale, Miksijale, Audiitorile, Klienditoele, Valijate \"\n\"nimekirja koostajale ja täiendajale.\"\nmsgstr \"\"\n\"As an information system, IVXV implements an e-voting protocol based on \"\n\"an \\\"double-envelope scheme\\\". IVXV operates at the pre-voting stage, the\"\n\" voting stage, the processing stage and the counting stage, and provides \"\n\"the tools for the participation in the electronic voting process of the \"\n\"Organiser, Tallier, Voter, Collector, Processor, Registrar, Auditor, \"\n\"Client Support and Voter List manager.\"\n\n#: ../../yldpohimotted.rst:32\nmsgid \"\"\n\"Infosüsteemi komponendid on Kogumisteenus, Töötlemisrakendus, \"\n\"Võtmerakendus ning Auditirakendus. Infosüsteemiga on tihedalt seotud \"\n\"Valijarakendus, Kontrollrakendus ning Miksimisrakendus.\"\nmsgstr \"\"\n\"The components of the information system are the Collection Service, the \"\n\"Processing Application, the Key Application and the Audit Application. \"\n\"Closely linked to the Information System are the Voting Application, the \"\n\"Verification Application and the Mixing Application.\"\n\n#: ../../yldpohimotted.rst:36\nmsgid \"\"\n\"Infosüsteem kasutab oma töös väliseid teenuseid - Tuvastusteenus, \"\n\"Allkirjastamisteenus, Registreerimisteenus, Valimiste Infosüsteem ning \"\n\"X-tee.\"\nmsgstr \"\"\n\"The information system uses external services - Authentication Service, \"\n\"Signature Service, Registration Service, Election Management System and \"\n\"X-Road.\"\n\n#: ../../yldpohimotted.rst:40\nmsgid \"IVXV krüptograafiline protokoll\"\nmsgstr \"IVXV Cryptographic Protocol\"\n\n#: ../../yldpohimotted.rst:42\nmsgid \"\"\n\"Elektroonilise hääletamise turvalisuse, verifitseeritavuse ning \"\n\"hääletamise salajasuse, hääletamise korrektsuse ja hääletaja sõltumatuse \"\n\"saavutamiseks on rangelt kirjeldatud elektroonilise hääletamise \"\n\"krüptograafiline protokoll [HMVW16]_. Protokoll annab vajaliku ja piisava\"\n\" ülevaate IVXV ülesehitusest ning selle turvaaspektidest. IVXV \"\n\"komponendid realiseerivad krüptograafilise protokolli alamosi.\"\nmsgstr \"\"\n\"In order to achieve the security, verifiability and secrecy of electronic\"\n\" voting, the correctness of the voting and the independence of the voter,\"\n\" the cryptographic protocol for electronic voting [HMVW16]_ is strictly \"\n\"described. The protocol provides a necessary and sufficient overview of \"\n\"the IVXV architecture and its security aspects. The components of IVXV \"\n\"implement sub-components of the cryptographic protocol.\"\n\n#: ../../yldpohimotted.rst:50\nmsgid \"Notatsioon\"\nmsgstr \"Notation\"\n\n#: ../../yldpohimotted.rst:52\nmsgid \"\"\n\"Arhitektuurse lahenduse visandi illustreerimiseks kasutatakse dokumendis \"\n\"UML-skeeme, kus eristame värvide ja märgenditega ``<<>>`` kodeeritult \"\n\"olemite – tegijad, liidesed, komponendid – järgmisi aspekte:\"\nmsgstr \"\"\n\"To illustrate a sketch of the architectural solution, the document uses \"\n\"UML diagrams, where we distinguish the following aspects of entities - \"\n\"actors, interfaces, components - coded with colours and ``<<>>``:\"\n\n#: ../../yldpohimotted.rst:56\nmsgid \"\"\n\"Märgend ``<<IVXV>>`` (Kollane) – infosüsteemi liides või komponent \"\n\"defineeritakse/realiseeritakse konkreetse pakkumuse raames tehtavate \"\n\"tööde käigus\"\nmsgstr \"\"\n\"Mark ``<<IVXV>>`` (Yellow) - The information system interface or \"\n\"component will be defined/realised during the works of a specific tender.\"\n\n#: ../../yldpohimotted.rst:60\nmsgid \"\"\n\"Märgend ``<<Väline>>`` (Punane) – infosüsteem sõltub mingi \"\n\"funktsionaalsuse realiseerimisel kolmanda osapoole komponendist või \"\n\"olemasolevast liidesest, mille ümberdefineerimine eeldab ka kolmandate \"\n\"osapoolte tööd.\"\nmsgstr \"\"\n\"``<<<External>>`` (Red) - The information system depends on a third-party\"\n\" component or an existing interface for the implementation of some \"\n\"functionality, the redefinition of which also requires third-party work.\"\n\n#: ../../yldpohimotted.rst:64\nmsgid \"\"\n\"Märgend ``<<VVK>>`` (Pruun) – sarnane eelmisele, kuid liidese/komponendi \"\n\"omanikuks on VVK.\"\nmsgstr \"\"\n\"Flag ``<<<NEC>>`` (Brown) - similar to the previous one, but the \"\n\"interface/component is owned by NEC.\"\n\n#: ../../yldpohimotted.rst:67\nmsgid \"\"\n\"Märgend ``<<Määratlemata>>`` (Must) – infosüsteemi jaoks oluline liides \"\n\"on määratlemata.\"\nmsgstr \"\"\n\"Flag ``<<Undefined>>`` (Black) - Interface relevant to the information \"\n\"system is undefined.\"\n\n#: ../../yldpohimotted.rst:72\nmsgid \"Näiteskeem\"\nmsgstr \"Example scheme\"\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/Makefile",
    "content": "\n# Source diagrams are all in different .pu files\nSRC_DIAG=example ms-collector-status ms-management-service-components \\\n\t\t ms-service-status-changes ms-service-status ms-upload-command \\\n\t\t ms-voter-list-status\n\nPARENT=development\n\n# Sub-diagrams are generated from $(PARENT).pu\nSUB_DIAG=processing collector_microservices collector_extension monitoring \\\n\t\t app_modules key audit\n\n\ninclude ../../../common-model.mk\n\n\n# Set environment variables for subdiagrams\ncollector_extension.env:\n\t$(eval DIAGRAM_DEF=-DCOLLECTOR_EXTENSION)\n\ncollector_microservices.env:\n\t$(eval DIAGRAM_DEF=-DCOLLECTOR_MICROSERVICES)\n\nprocessing.env:\n\t$(eval DIAGRAM_DEF=-DPROCESSING_APPLICATION)\n\nkey.env:\n\t$(eval DIAGRAM_DEF=-DKEY_APPLICATION)\n\naudit.env:\n\t$(eval DIAGRAM_DEF=-DAUDIT_APPLICATION)\n\nmonitoring.env:\n\t$(eval DIAGRAM_DEF=-DMONITORING)\n\napp_modules.env:\n\t$(eval DIAGRAM_DEF=-DAPP_MODULES)\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/actors.pu",
    "content": "\n@startuml\n\n  :Omanik: as Omanik <<Human>>\n  actor Hääletaja <<Human>>\n  actor Töötleja <<Human>>\n  actor Andmeaudiitor <<Human>>\n  actor Klienditugi <<Human>>\n  actor Kogumisteenus <<Server>>\n  actor Registreerimisteenus <<Server>>\n  actor Autentimisteenus <<Server>>\n  actor Allkirjateenus <<Server>>\n\n\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/collector-if.pu",
    "content": "@startuml\n\nleft to right direction\n\n[Kogumisteenus] as collector\n\n\npackage \"Keskkonnateenused\" {\n    [Registreerimisteenus] as tsa\n    [Autentimisteenus] as auth\n    [Allkirjateenus] as sign\n}\n\n[Nimekirjad] as voterlist\n[Varundamine] as backup\n\n\nactor Omanik as owner\nactor Töötleja as offline\n\nactor Klienditeenindaja as helpdesk\n\nactor Hääletaja as voter\n\nactor :Kogumisteenuse admin: as admin\nactor :Kogumisteenuse kaugadmin: as radmin\n\n\ninterface \"Valimise defineerimine\" as i_define\ninterface \"Ümbrikute väljastamine\" as i_export\ninterface \"Hääletamine\" as i_vote\ninterface \"Kontrollimine\" as i_verify\ninterface \"Valikute nimekiri\" as i_cls\ninterface \"Käsurida\" as i_localadmin\ninterface \"Kaughaldus\" as i_remoteadmin\ninterface \"Monitooring\" as i_monitor\ninterface \"Helpdesk\" as i_helpdesk\n\ninterface \"Varundamine\" as i_backup\ninterface \"Nimekirjade uuendused\" as i_voterlist\n\ncollector -down- i_cls\ncollector -down- i_vote\ncollector -down- i_verify\nvoter -up-> i_vote\nvoter -up-> i_verify\nvoter -up-> i_cls\n\n\ncollector -up- i_localadmin\ncollector -up- i_helpdesk\ncollector -up- i_monitor\ncollector -up- i_remoteadmin\n\nadmin -down-> i_localadmin\nhelpdesk -down-> i_helpdesk\nradmin -down-> i_monitor\nradmin -left-> i_remoteadmin\n\n\ncollector -right- i_define\ncollector -right- i_export\nowner -left-> i_define\noffline -left-> i_export\nowner -up-> i_remoteadmin\n\n\nvoterlist -right- i_voterlist\nbackup -right- i_backup\n\n\ncollector .left.> i_backup\ncollector .left.> i_voterlist\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/collector-in.pu",
    "content": "@startuml\n\nleft to right direction\n\npackage \"Kogumisteenus\" {\n    [Storage] as collector\n    [Log] as log\n    [Eligibility] as elig\n    [Verification] as verif\n    [Admin] as adminc\n\n\ninterface \"Valimise defineerimine\" as i_define\ninterface \"Ümbrikute väljastamine\" as i_export\ninterface \"Hääletamine\" as i_vote\ninterface \"Kontrollimine\" as i_verify\ninterface \"Valikute nimekiri\" as i_cls\ninterface \"Käsurida\" as i_localadmin\ninterface \"Kaughaldus\" as i_remoteadmin\ninterface \"Monitooring\" as i_monitor\ninterface \"Helpdesk\" as i_helpdesk\n\n\n\n}\n\nelig -- collector\nverif -- collector\n\nverif ..> log\nelig ..> log\ncollector ..> log\n\npackage \"Keskkonnateenused\" {\n    [Registreerimisteenus] as tsa\n    [Autentimisteenus] as auth\n    [Allkirjateenus] as sign\n}\n\n[Nimekirjad] as voterlist\n[Varundamine] as backup\n\n\ncollector ..> tsa\n\ni_vote ..> sign\ni_cls ..> auth\n\ninterface \"Varundamine\" as i_backup\ninterface \"Nimekirjade uuendused\" as i_voterlist\n\nelig -up- i_cls\nelig -up- i_vote\nverif -up- i_verify\n\n\ncollector -down- i_localadmin\nlog -down- i_helpdesk\nlog -down- i_monitor\nadminc -down- i_remoteadmin\n\n\n\nadminc -down- i_define\nadminc -down- i_export\n\n\nvoterlist -right- i_voterlist\nbackup -right- i_backup\n\n\ncollector .left.> i_backup\nelig .left.> i_voterlist\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/development.pu",
    "content": "@startuml\n\n!ifdef MONITORING\n    !define COLLECTOR\n    !define OWNER\n!endif\n\n!ifdef KEY_APPLICATION\n    !define OWNER\n    !define GENERIC_APP\n!endif\n\n!ifdef AUDIT_APPLICATION\n    !define EXTERNAL_MODULES\n    !define GENERIC_APP\n!endif\n\n!ifdef PROCESSING_APPLICATION\n    !define GENERIC_APP\n    !define EXTERNAL_MODULES\n!endif\n\n!ifdef APP_MODULES\n    !define EXTERNAL_MODULES\n    !define HELPER_MODULES\n!endif\n\n!ifdef COLLECTOR_MICROSERVICES\n    !define COLLECTOR\n    !define SERVICES\n    !define VOTER\n!endif\n\n!ifdef COLLECTOR_EXTENSION\n    !define COLLECTOR\n    !define EXTENSION_MODULES\n    !define EXTERNAL_INTERFACES\n!endif\n\n\n!include skin.inc\n\n!ifdef GENERIC_APP\n    interface \"TR_APP_LOG()\" <<TR_IVXV()>> as i_app_log\n!endif\n\n!ifdef PROCESSING_APPLICATION\n    interface \"TR_IVOTERS_PDF()\" <<TR_UNSPECIFIED()>> as i_ivoters_pdf\n    !define ENCRYPTED_VOTES\n!endif\n\n\n\n!ifdef KEY_APPLICATION\n    interface \"TR_KEY_SHARE()\" <<TR_IVXV()>> as i_key_share\n    interface \"TR_INVALID_VOTES()\" <<TR_IVXV()>> as i_invalid_votes\n    interface \"TR_CERTIFICATE()\" <<TR_EXTERNAL()>> as i_certificate\n    !define AUDITABLE\n!endif\n\n\n!ifdef AUDIT_APPLICATION\n    interface \"TR_MIXING_PROOF()\" <<TR_UNSPECIFIED()>> as i_mixing_proof\n    !define AUDITABLE\n!endif\n\n\n!ifdef AUDITABLE\n    interface \"TR_DECRYPTION_PROOF()\" <<TR_IVXV()>> as i_decryption_proof\n    interface \"TR_VOTING_RESULT()\" <<TR_EXTERNAL()>> as i_voting_result\n    interface \"TR_MIXED_VOTES()\" <<TR_IVXV()>> as i_mixed_votes\n    !define ENCRYPTED_VOTES\n!endif\n\n\n!ifdef ENCRYPTED_VOTES\n    interface \"TR_ENCRYPTED_VOTES()\" <<TR_IVXV()>> as i_encrypted_votes\n!endif\n\n'interface \"TR_CONF_BUNDLE()\" <<TR_IVXV()>> as i_conf_bundle\n\n\n/'\n '\n '  ACTORS\n '\n '/\n\n!ifdef OWNER\n    actor \"TR_A_OWNER()\" <<TR_NEC()>> as a_owner\n!endif\n\n!ifdef MONITORING\n    actor \"TR_A_HELPDESK()\" <<TR_EXTERNAL()>> as a_helpdesk\n    actor \"TR_A_SYSADMIN()\" <<TR_EXTERNAL()>> as a_sysadmin\n!endif\n\n!ifdef KEY_APPLICATION\n    actor \"TR_A_KEYMANAGER()\" <<TR_NEC()>> as a_keymanager\n    a_keymanager ..> i_key_share\n!endif\n\n!ifdef PROCESSING_APPLICATION\n    actor \"TR_A_OFFLINE_PROCESSOR()\" <<TR_NEC()>> as a_offline_processor\n!endif\n\n!ifdef AUDIT_APPLICATION\n    actor \"TR_A_AUDITOR()\" <<TR_EXTERNAL()>> as a_auditor\n!endif\n\n/'\n '\n '  EXTERNAL SERVICES\n '\n '/\n\n!ifdef EXTERNAL_INTERFACES\ninterface \"TR_SK_OCSP()\" <<TR_EXTERNAL()>> as i_sk_ocsp\ninterface \"TR_SK_AUTH()\" <<TR_EXTERNAL()>> as i_sk_auth\ninterface \"TR_RFC_3161()\" <<TR_EXTERNAL()>> as i_rfc_3161\ninterface \"TR_MID()\" <<TR_EXTERNAL()>> as i_mid\n!endif\n\n\n!ifdef PROCESSING_APPLICATION\ninterface \"TR_REGISTERED_VOTES()\" <<TR_UNSPECIFIED()>> as i_registered_votes\n!endif\n\n!ifdef EXTERNAL_SERVICES\n[TR_SK_OCSP()] <<TR_EXTERNAL()>> as sk_ocsp\nsk_ocsp -up- i_sk_ocsp\nsk_ocsp -down- i_registered_votes\n\n[TR_RFC_3161()] <<TR_EXTERNAL()>> as rfc_3161\nrfc_3161 -up- i_rfc_3161\nrfc_3161 -down- i_registered_votes\n\n\n[TR_DDOC_SERVICE()] <<TR_EXTERNAL()>> as ddoc_service\nddoc_service -up- i_mid\n\n!endif\n\n!ifdef MONITORING\n[TR_ELECTION_WWW()] <<TR_NEC()>> as election_www\n[TR_ZABBIX_EXT()] <<TR_EXTERNAL()>> as zabbix_ext\n'[TR_MESSENTE()] <<TR_EXTERNAL()>> as messente\n'interface \"TR_MESSENTE()\" <<TR_EXTERNAL()>> as i_messente\n\n    interface \"TR_MONITOR()\" <<TR_EXTERNAL()>> as i_monitor\n    interface \"TR_HELPDESK()\" <<TR_EXTERNAL()>> as i_helpdesk\n'messente -up- i_messente\n\n!endif\n\n\n\n\n\n\n!ifdef PROCESSING_APPLICATION\ninterface \"TR_VIS_VOTERS()\" <<TR_EXTERNAL()>> as i_vis_voters\ninterface \"TR_VIS_REVOKE()\" <<TR_EXTERNAL()>> as i_vis_revoke\ninterface \"TR_POP_VOTERS()\" <<TR_EXTERNAL()>> as i_pop_voters\ninterface \"TR_VIS_DIST()\" <<TR_EXTERNAL()>> as i_vis_dist\n!endif\n\n\n'[TR_VIS()] <<TR_EXTERNAL()>> as vis\n'interface \"TR_VIS_RESULTS()\" <<TR_EXTERNAL()>> as i_vis_results\n'vis -up- i_vis_results\n'interface \"TR_VIS_CANDS()\" <<TR_EXTERNAL()>> as i_vis_cands\n'vis - i_vis_cands\n'vis -up- i_vis_dist\n\n\n!ifdef VIS\nvis -up- i_vis_voters\nvis -up- i_vis_revoke\n\n[TR_POP()] <<TR_EXTERNAL()>> as pop\npop -up- i_pop_voters\n!endif\n\n\n!ifdef EXTERNAL_MODULES\n[TR_JDIGIDOC()] <<TR_EXTERNAL()>> as jdigidoc\ninterface \"TR_JAVA()\" <<TR_EXTERNAL()>> as i_java\ni_java -right- jdigidoc\n!endif\n\n\n!ifdef HELPER_MODULES\npackage \"TR_P_HELPERS()\" as p_helpers {\n\n    interface \"TR_ELEC()\" <<TR_IVXV()>> as elec\n    [TR_RK_ELEC()] <<TR_IVXV()>> as rk_elec\n    [TR_RH_ELEC()] <<TR_IVXV()>> as rh_elec\n    [TR_EP_ELEC()] <<TR_IVXV()>> as ep_elec\n    [TR_KOV_ELEC()] <<TR_IVXV()>> as kov_elec\n\n    rk_elec -up- elec\n    rh_elec -up- elec\n    ep_elec -up- elec\n    kov_elec -up- elec\n}\n!endif\n\n\n\n\n!ifdef PROCESSING_APPLICATION\ninterface \"TR_EXPORT_VOTES()\" <<TR_IVXV()>> as i_export_votes\n!endif\n\n\n!ifdef COLLECTOR\n\npackage \"TR_P_COLLECTOR()\" as p_collector {\n\n\n\n\n\n!ifdef SUPPORT\n        [TR_ADMIN_APP()] <<TR_IVXV()>> as admin_app\n'        admin_app - i_export_votes\n        interface \"TR_LOCALADMIN()\" <<TR_IVXV()>> as i_localadmin\n        interface \"TR_REMOTEADMIN()\" <<TR_IVXV()>> as i_remoteadmin\n        admin_app -up- i_localadmin\n        admin_app -left- i_remoteadmin\n\n\n        [TR_BACKUP()] <<TR_IVXV()>> as backup\n\n\n\n       interface \"TR_BACKUP()\" <<TR_IVXV()>> as i_backup\n       backup - i_backup\n!endif\n\n!ifdef MONITORING\n\n    interface \"TR_LOG_INT()\" <<TR_IVXV()>> as i_log_int\n    interface \"TR_ZABBIX()\" <<TR_IVXV()>> as i_zabbix\n\n    rectangle \"TR_R_MONITORING()\" as r_monitoring {\n        [TR_IVXV_MONITOR()] <<TR_EXTERNAL()>> as ivxv_monitor\n\n        database \"TR_MONITOR_DB()\" as monitor_db {\n            }\n\n        interface \"TR_LOG_EXT()\" <<TR_IVXV()>> as i_log_ext\n'        interface \"TR_LOG_SMTP()\" <<TR_IVXV()>> as i_log_smtp\n'        interface \"TR_LOG_SMS()\" <<TR_IVXV()>> as i_log_sms\n        ivxv_monitor -down- i_log_int\n'        ivxv_monitor -down- i_log_smtp\n'        ivxv_monitor -down- i_log_sms\n        ivxv_monitor -up- i_log_ext\n        ivxv_monitor -up- i_zabbix\n        ivxv_monitor -left-> monitor_db\n\n        zabbix_ext <-left- i_monitor\n        zabbix_ext <-left- i_helpdesk\n'        i_log_sms -> i_messente\n\n    }\n\n    a_helpdesk -down-> i_helpdesk\n    a_owner -down-> i_monitor\n    a_sysadmin -down-> i_monitor\n\n    election_www --> i_log_ext\n    zabbix_ext --> i_zabbix\n\n!endif\n\n\n!ifdef EXTENSION_MODULES\n\n    interface \"TR_API_INT()\" <<TR_IVXV()>> as i_api_int\n\n    rectangle \"TR_R_MID_AUTH()\" as r_mid_auth {\n        [TR_MID_AUTH()] <<TR_IVXV()>> as mid_auth\n        [TR_MID_SIGN()] <<TR_IVXV()>> as mid_sign\n\n        interface \"TR_MID()\" <<TR_IVXV()>> as i_mid\n\n        mid_auth -up- i_mid\n        mid_sign -up- i_mid\n\n        mid_auth -down-> i_mid\n        mid_sign -down-> i_mid\n    }\n\n    rectangle \"TR_R_EXTENSION()\" as r_extension {\n        [TR_TLS()] <<TR_IVXV()>> as tls\n        [TR_TICKET()] <<TR_IVXV()>> as ticket\n        [TR_BDOC()] <<TR_IVXV()>> as bdoc\n        [TR_OCSP()] <<TR_IVXV()>> as ocsp\n        [TR_TSP()] <<TR_IVXV()>> as tsp\n        [TR_REG()] <<TR_IVXV()>> as reg\n\n        interface \"TR_AUTH()\" <<TR_IVXV()>> as i_auth\n        interface \"TR_VERIFY()\" <<TR_IVXV()>> as i_verify\n        interface \"TR_Q11N()\" <<TR_IVXV()>> as i_q11n\n\n        tls -up- i_auth\n        ticket -up- i_auth\n        bdoc -up- i_verify\n        ocsp -up- i_q11n\n        tsp -up- i_q11n\n        reg -up- i_q11n\n\n        tls -down-> i_sk_auth\n        ocsp -down-> i_sk_ocsp\n        tsp -down-> i_rfc_3161\n        reg -down-> i_sk_ocsp\n        reg -down-> i_rfc_3161\n    }\n\n    i_api_int -down-> i_mid\n    i_api_int -down-> i_auth\n    i_api_int -down-> i_verify\n    i_api_int -down-> i_q11n\n!endif\n\n!ifdef SERVICES\n    rectangle \"TR_R_STORAGE()\" as r_storage {\n        [TR_STORAGE()] <<TR_IVXV()>> as storage\n\n        database \"TR_STORAGE_DB()\" as storage_db {\n            }\n\n        database \"TR_CONF_DB_STORAGE()\" as conf_db_storage {\n            }\n\n\n        storage -> storage_db\n\n        interface \"TR_TLS_STORAGE()\" <<TR_IVXV()>> as i_tls_storage\n        i_tls_storage -- storage\n\n    }\n\n\n\n    rectangle \"TR_R_CLS()\" as r_cls {\n        interface \"TR_TLS_CLS()\" <<TR_IVXV()>> as i_tls_cls\n        [TR_CLS()] <<TR_IVXV()>> as cls\n\n        database \"TR_CONF_DB_CLS()\" as conf_db_cls {\n            }\n        cls --> i_tls_storage\n\n        cls -up- i_tls_cls\n    }\n\n    rectangle \"TR_R_VERIFY()\" as r_verify {\n        interface \"TR_TLS_VERIFY()\" <<TR_IVXV()>> as i_tls_verify\n        [TR_VERIFY()] <<TR_IVXV()>> as verify\n\n        database \"TR_CONF_DB_VERIFY()\" as conf_db_verify {\n            }\n        verify --> i_tls_storage\n\n        verify -up- i_tls_verify\n    }\n\n    rectangle \"TR_R_VOTING()\" as r_voting {\n        interface \"TR_TLS_VOTE()\" <<TR_IVXV()>> as i_tls_vote\n        [TR_VOTE()] <<TR_IVXV()>> as vote\n\n        database \"TR_CONF_DB_VOTE()\" as conf_db_vote {\n            }\n        vote --> i_tls_storage\n\n        vote -up- i_tls_vote\n    }\n\n    rectangle \"TR_R_DISPATCH()\" as r_dispatch {\n        interface \"TR_HTTPS_EXT()\" <<TR_IVXV()>> as i_https_ext\n        interface \"TR_TLS_INT()\" <<TR_IVXV()>> as i_tls_int\n        [TR_FORWARDER()] <<TR_IVXV()>> as forwarder\n\n        database \"TR_CONF_DB_FORWARDER()\" as conf_db_forwarder {\n            }\n\n        i_tls_int --> i_tls_cls\n        i_tls_int --> i_tls_vote\n        i_tls_int --> i_tls_verify\n\n        forwarder -up- i_https_ext\n        forwarder -down-> i_tls_int\n    }\n\n!endif\n\n}\n\n!endif\n\n\n/'\n '\n ' VÕTMERAKENDUS\n '\n '/\n\n!ifdef KEY_APPLICATION\npackage \"TR_P_KEY()\" as p_key {\n    [TR_KEY_APP()] <<TR_IVXV()>> as key_app\n\n    interface \"TR_KEY_IN()\" <<TR_IVXV()>> as i_key_in\n    interface \"TR_KEY_OUT()\" <<TR_IVXV()>> as i_key_out\n    interface \"TR_PKCS15()\" <<TR_IVXV()>> as i_pkcs15\n    interface \"TR_KEY_GUI()\" <<TR_IVXV()>> as i_key_gui\n\n    key_app -down-> i_pkcs15\n    key_app -left-> i_key_in\n    key_app -up-> i_key_out\n    key_app -right-> i_key_gui\n\n\n    i_pkcs15 -- i_key_share\n    i_key_in ..> i_mixed_votes\n    i_key_out .up.> i_voting_result\n    i_key_out .up.> i_decryption_proof\n    i_key_out .up.> i_app_log\n    i_key_out .up.> i_certificate\n    i_key_out .up.> i_invalid_votes\n}\n\na_owner -> i_key_gui\n!endif\n\n\n/'\n '\n ' TÖÖTLEMISRAKENDUS\n '\n '/\n\n!ifdef PROCESSING_APPLICATION\npackage \"TR_P_PROCESSOR()\" as p_processor {\n    [TR_OFFLINE_PROCESSOR()] <<TR_IVXV()>> as offline_processor\n    [TR_PDF_API()] <<TR_EXTERNAL()>> as pdf_api\n\n    offline_processor -> pdf_api\n\n    interface \"TR_PROCESSOR_IN()\" <<TR_IVXV()>> as i_processor_in\n    interface \"TR_PROCESSOR_OUT()\" <<TR_IVXV()>> as i_processor_out\n    interface \"TR_PROCESSOR_GUI()\" <<TR_IVXV()>> as i_processor_gui\n    offline_processor -down-> i_processor_in\n    offline_processor -left- i_processor_gui\n    offline_processor -up-> i_processor_out\n    offline_processor -right-> i_java\n\n    i_processor_in ..> i_registered_votes\n    i_processor_in ..> i_vis_revoke\n    i_processor_in ..> i_vis_dist\n    i_processor_in ..> i_pop_voters\n    i_processor_in ..> i_export_votes\n\n    i_processor_out .up.> i_vis_voters\n    i_processor_out .up.> i_ivoters_pdf\n    i_processor_out .up.> i_encrypted_votes\n    i_processor_out .up.> i_app_log\n}\n\na_offline_processor -> i_processor_gui\n!endif\n\n/'\n '\n ' AUDITIRAKENDUS\n '\n '/\n\n!ifdef AUDIT_APPLICATION\npackage \"TR_P_AUDIT()\" as p_audit {\n    [TR_OFFLINE_AUDIT()] <<TR_IVXV()>> as offline_audit\n    [TR_DECRYPTION_VERIFIER()] <<TR_IVXV()>> as decryption_verifier\n    interface \"TR_MIXING_VERIFIER()\" <<TR_IVXV()>> as i_mixing_verifier\n    interface \"TR_AUDIT_IN()\" <<TR_IVXV()>> as i_audit_in\n    interface \"TR_AUDIT_OUT()\" <<TR_IVXV()>> as i_audit_out\n    interface \"TR_AUDIT_GUI()\" <<TR_IVXV()>> as i_audit_gui\n\n    offline_audit --> decryption_verifier\n    offline_audit --> i_mixing_verifier\n\n    offline_audit -down-> i_audit_in\n    offline_audit -left- i_audit_gui\n    offline_audit -up-> i_audit_out\n    offline_audit -right-> i_java\n\n    i_audit_in ..> i_encrypted_votes\n    i_audit_in ..> i_mixed_votes\n    i_audit_in ..> i_decryption_proof\n    i_audit_in ..> i_mixing_proof\n    i_audit_in ..> i_voting_result\n\n\n    i_audit_out ..> i_app_log\n\n}\n\na_auditor -> i_audit_gui\n!endif\n\n/'\n '\n ' SEADISTUSRAKENDUS\n '\n '/\n\n!ifdef CONFIG_APPLICATION\npackage \"TR_P_CONFIG()\" as p_config {\n    [TR_CONF_APP()] <<TR_IVXV()>> as conf_app\n\n    database \"TR_CONF_DB()\" as conf_db {\n    }\n\n    conf_app -> conf_db\n\n    interface \"TR_CONF_IN()\" <<TR_IVXV()>> as i_conf_in\n    interface \"TR_CONF_OUT()\" <<TR_IVXV()>> as i_conf_out\n\n    interface \"TR_SMTP()\" <<TR_IVXV()>> as i_smtp\n    interface \"TR_CONF_GUI()\" <<TR_IVXV()>> as i_conf_gui\n\n    conf_app -up- i_smtp\n    conf_app -down- i_conf_in\n    conf_app -down- i_conf_out\n    conf_app -down- i_conf_gui\n\n    i_conf_out --> i_conf_bundle\n    i_conf_out --> i_app_log\n\n    i_conf_in --> i_vis_dist\n    i_conf_in --> i_vis_cands\n    i_conf_in --> i_pop_voters\n}\n\na_owner --> i_conf_gui\n!endif\n\n\n\n\n\n\n\n/'\nactor \"TR_A_ADMIN()\" <<TR_NEC()>> as admin\nowner -left-> i_remoteadmin\nadmin -down-> i_localadmin\nadmin -down-> i_backup\n'/\n\n!ifdef VOTER\nactor \"TR_A_VOTER()\" <<TR_EXTERNAL()>> as voter\ninterface \"TR_VOTE()\" <<TR_NEC()>> as i_vote\ninterface \"TR_INDVERIFY()\" <<TR_NEC()>> as i_indverify\ni_https_ext -up- i_vote\ni_https_ext -up- i_indverify\nvoter -down-> i_vote\nvoter -down-> i_indverify\n!endif\n\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/elec-vote.pu",
    "content": "@startuml\n\ntitle \"Hääletamisfaas\"\n\nactor Valija as Voter\nparticipant VER\nparticipant VA\n\nparticipant Auth\nparticipant Sign\nparticipant CAVA\nparticipant TSA\n\nbox \"Collector Service\"\n  participant AuthProxy\n  participant SignProxy\n  participant VerProxy\n  participant CLS\n  participant VSS\nend box\n\n== Hääletamine ==\n\nVA -> CLS: Valija identifikaator, id\n\nalt Võimalik autentimine\n  CLS --> VA\n  VA --> AuthProxy\n  AuthProxy --> Auth\n  Auth --> Voter\n  AuthProxy --> VA\n  VA --> CLS: AUTH(id)\nend\n\nCLS -> VA: Valikute nimekiri, C_id\n\nVA -> Voter: Valikute kuvamine, C_id\nVoter -> VA: Valiku tegemine, choice_id\n\nVA -> VA: r = GEN()\nVA -> VA: ballot_id = ENC(choice_id;r)\n\nalt Allkirjastamine lokaalselt\n  VA --> VA: vote_id = SIGN(ballot_id, id)\nelse Allkirjastamine läbi proksi\n  VA --> SignProxy\n  SignProxy --> Sign\n  Sign --> Voter\n  SignProxy --> VA: vote_id = SIGN(ballot_id, id)\nend\n\nVA -> VSS: vote_id\n\nVSS -> CAVA: id\nCAVA -> VSS: val_id = Valid(id)\n\nVSS -> TSA: vote_id\nTSA -> VSS: tok_id = Timestamp(vote_id)\n\nVSS -> VSS: vote_ref = Store(vote_id, val_id, tok_id)\n\nVSS -> VA: tok_id, vote_ref\n\nVA -> VA: VerTimestamp(tok_id)\nVA -> Voter: vote_ref, r\n\n== Hääle kontroll ==\n\nVoter -> VER: vote_ref, r\n\nVER -> VerProxy: vote_ref\nVerProxy -> VSS: vote_ref\nVSS -> VerProxy: vote_id, tok_id, val_id\nVerProxy -> VER: vote_id, tok_id, val_id\nVER -> VER: id, choice = VerifyVote(vote_id, tok_id, val_id)\nVER -> Voter: id, choice\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/election.pu",
    "content": "@startuml\n\nactor Voter\nparticipant VER\nparticipant VA\n\nparticipant Auth\nparticipant Sign\n\nbox \"Collector Service\"\n  participant AuthProxy\n  participant SignProxy\n  participant CLS\n  participant VSS\nend box\n\nparticipant CAVA\nparticipant TSA\n\nparticipant CONF\nparticipant WORKER\nparticipant MIX\nparticipant DECRYPT\nactor Owner\nactor Auditor\n\n\n== Setup phase ==\n\n== Voting phase ==\n\nVA -> CLS: Valija identifikaator, id\n\nalt Võimalik autentimine\n  CLS --> VA\n  VA --> AuthProxy\n  AuthProxy --> Auth\n  Auth --> Voter\n  Voter --> Auth\n  Auth --> AuthProxy\n  AuthProxy --> VA\n  VA --> CLS: AUTH(id)\nend\n\nCLS -> VA: Valikute nimekiri, C_id\n\nVA -> Voter: Valikute kuvamine, C_id\nVoter -> VA: Valiku tegemine, choice_id\n\nVA -> VA: r = GEN()\nVA -> VA: ballot_id = ENC(choice_id;r)\n\nalt Allkirjastamine lokaalselt\n  VA --> VA: vote_id = SIGN(ballot_id, id)\nelse Allkirjastamine läbi proksi\n  VA --> SignProxy\n  SignProxy --> Sign\n  Sign --> Voter\n  Voter --> Sign\n  Sign --> SignProxy\n  SignProxy --> VA: vote_id = SIGN(ballot_id, id)\nend\n\nVA -> VSS: vote_id\n\nVSS -> CAVA: id\nCAVA -> VSS: val_id = Valid(id)\n\nVSS -> TSA: vote_id\nTSA -> VSS: tok_id = Timestamp(vote_id)\n\nVSS -> VSS: vote_ref = Store(vote_id, val_id, tok_id)\n\nVSS -> VA: tok_id, vote_ref\n\n\n== Tallying phase ==\n\n== Auditing phase ==\n\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/en/lang.pu",
    "content": "\n' SKIN.INC\n\n!define TR_EXTERNAL() External\n!define TR_UNSPECIFIED() Unspecified\n!define TR_NEC() NEC\n!define TR_IVXV() IVXV\n\n\n' EXAMPLE.PU\n\n!define TR_STAKEHOLDER() Stakeholder\n\n!define TR_APPLICATION() Application\n!define TR_SERVICE() Service\n\n!define TR_INTERFACE() Interface\n!define TR_UNSPECIFIED_INTERFACE() Unspecified interface\n\n!define TR_COMPONENT() Component\n!define TR_IVXV_COMPONENT() IVXV Component\n!define TR_EXTERNAL_COMPONENT() External component\n!define TR_DATABASE() Database\n\n\n' DEVELOPMENT.PU\n\n!define TR_MONITOR_DB() Logs\n!define TR_STORAGE_DB() Lists and votes\n!define TR_CONF_DB_CLS() Settings\n!define TR_CONF_DB_FORWARDER() Settings\n!define TR_CONF_DB_STORAGE() Settings\n!define TR_CONF_DB_VERIFY() Settings\n!define TR_CONF_DB_VOTE() Settings\n!define TR_CONF_DB() Settings\n!define TR_P_HELPERS() Helper modules\n!define TR_P_AUDIT() Audit application\n!define TR_P_COLLECTOR() Collection service\n!define TR_P_CONFIG() Configuration application\n!define TR_P_PROCESSOR() Processor\n!define TR_P_KEY() Key application\n!define TR_R_VOTING() Voting service\n!define TR_R_VERIFY() Verification service\n!define TR_R_EXTENSION() Extension modules\n!define TR_R_MID_AUTH() Mobiil-ID helper service\n!define TR_R_MONITORING() Monitoring\n!define TR_R_CLS() Choicelist service\n!define TR_R_STORAGE() Storage\n!define TR_R_DISPATCH() Dispatcher\n\n!define TR_ADMIN_APP() Management\n!define TR_BACKUP() Backup\n!define TR_BDOC() BDOC\n!define TR_CLS() Choicelist service\n!define TR_CONF_APP() Configuration app\n!define TR_DDOC_SERVICE() Mobile ID Service\n!define TR_DECRYPTION_VERIFIER() Decryption proof verifier\n!define TR_ELECTION_WWW() Election WWW\n!define TR_EP_ELEC() Europarliament\n!define TR_FORWARDER() Dispatcher\n!define TR_IVXV_MONITOR() Logmonitor\n!define TR_JDIGIDOC() DigiDoc4J\n!define TR_KEY_APP() Key app\n!define TR_KOV_ELEC() Local government\n!define TR_MESSENTE() Messente SMS\n!define TR_MID_AUTH() Authentication\n!define TR_MID_SIGN() Signing\n!define TR_OCSP() OCSP\n!define TR_OFFLINE_AUDIT() Audit\n!define TR_OFFLINE_PROCESSOR() Processor\n!define TR_PDF_API() PDF API\n!define TR_POP() Population register\n!define TR_REG() Registering service\n!define TR_RFC_3161() RFC3161 Registering service\n!define TR_RH_ELEC() Referendum\n!define TR_RK_ELEC() Riigikogu\n!define TR_SK_OCSP() OCSP Registering service\n!define TR_STORAGE() Storage\n!define TR_TICKET() Authentication, ticket\n!define TR_TLS() Authentication, TLS\n!define TR_TSP() Timestamp\n!define TR_VERIFY() Verification service\n!define TR_VIS() VIS\n!define TR_VOTE() Voting\n!define TR_ZABBIX_EXT() External monitoring\n\n!define TR_A_ADMIN() Local admin\n!define TR_A_AUDITOR() Auditor\n!define TR_A_HELPDESK() Helpdesk\n!define TR_A_KEYMANAGER() Keymanager\n!define TR_A_OFFLINE_PROCESSOR() Processor\n!define TR_A_OWNER() Election owner\n!define TR_A_SYSADMIN() System admin\n!define TR_A_VOTER() Voter\n\n!define TR_API_INT() IVXV API\n!define TR_APP_LOG() Application log\n!define TR_AUDIT_GUI() GUI\n!define TR_AUDIT_IN() FS Import\n!define TR_AUDIT_OUT() FS Export\n!define TR_AUTH() Authentication\n!define TR_BACKUP() Backup\n!define TR_CERTIFICATE() Certificate\n!define TR_CONF_BUNDLE() Configuration bundle\n!define TR_CONF_GUI() GUI\n!define TR_CONF_IN() FS Import\n!define TR_CONF_OUT() FS Export\n!define TR_DECRYPTION_PROOF() Decryption proof\n!define TR_ELEC() Election interface\n!define TR_ENCRYPTED_VOTES() Anonymized votes\n!define TR_EXPORT_VOTES() Votes export\n!define TR_HELPDESK() Helpdesk\n!define TR_HTTPS_EXT() HTTPS\n!define TR_INDVERIFY() Verification application\n!define TR_INVALID_VOTES() List of invalid votes\n!define TR_IVOTERS_PDF() List of i-voters, PDF\n!define TR_JAVA() DigiDoc4J\n!define TR_KEY_GUI() GUI\n!define TR_KEY_IN() FS Import\n!define TR_KEY_OUT() FS Export\n!define TR_KEY_SHARE() Key share\n!define TR_LOCALADMIN() Command line administration\n!define TR_LOG_EXT() HTTPS\n!define TR_LOG_INT() RELP-JSON\n!define TR_LOG_SMS() SMS\n!define TR_LOG_SMTP() SMTP\n!define TR_MESSENTE() Messente\n!define TR_MID() Mobile ID Service\n!define TR_MIXED_VOTES() Shuffled ballots\n!define TR_MIXING_PROOF() Proof of shuffle\n!define TR_MIXING_VERIFIER() Shuffle verifier\n!define TR_MONITOR() Monitoring\n!define TR_PKCS15() PKCS15\n!define TR_POP_VOTERS() Voterlist\n!define TR_PROCESSOR_GUI() GUI\n!define TR_PROCESSOR_IN() FS Import\n!define TR_PROCESSOR_OUT() FS Export\n!define TR_Q11N() Qualification\n!define TR_REGISTERED_VOTES() Registered votes\n!define TR_REMOTEADMIN() Remote administration\n!define TR_RFC_3161() RFC3161\n!define TR_SK_AUTH() Authentication OCSP\n!define TR_SK_OCSP() OCSP\n!define TR_SMTP() Urgent command (SMTP)\n!define TR_TLS_CLS() Choices\n!define TR_TLS_INT() TLS-SNI\n!define TR_TLS_STORAGE() TLS\n!define TR_TLS_VERIFY() Verification\n!define TR_TLS_VOTE() Voting\n!define TR_VERIFY() Verification\n!define TR_VIS_CANDS() List of choices\n!define TR_VIS_DIST() List of districts\n!define TR_VIS_RESULTS() Tally import\n!define TR_VIS_REVOKE() Revokation/restoration list\n!define TR_VIS_VOTERS() List of i-voters\n!define TR_VOTE() Voting application\n!define TR_VOTING_RESULT() Voting result\n!define TR_ZABBIX() Zabbix\n\n' MS\n\n!define TR_MS_STATE_TITLE() Collection service states\n\n\n!define TR_MS_S_UNINSTALLED() Uninstalled\n!define TR_MS_S_INSTALLED() Installed\n!define TR_MS_S_SETUP() Set up\n!define TR_MS_S_FAILURE() Failure\n!define TR_MS_S_PARTIAL_FAILURE() Partial failure\n\n\n!define TR_MS_S_UNINSTALLED_TXT() - Management service is installed, \\\n\\n- Collector services are uninstalled.\n\n!define TR_MS_S_INSTALLED_TXT() All collector services are installed \\\n\\nand technical configuration applied. \\\n\\nElection configuration has not been applied.\n\n!define TR_MS_S_SETUP_TXT() Election configuration has been applied \\\n\\nto the Collection service \\\n\\nit is possible to vote \\\n\\nand export digital ballot box\n\n!define TR_MS_S_FAILURE_TXT() At least one collector service has failed. \\\n\\nCollection service is unfunctional\n\n!define TR_MS_S_PARTIAL_FAILURE_TXT() At least one replicated collector service \\\n\\nhas failed, Collection service is functional.\n\n\n!define TR_MS_TRANS_INSTALL() Installation of services\n!define TR_MS_TRANS_SETUP() Application of election configuration\n!define TR_MS_TRANS_FAIL() Failure detection\n!define TR_MS_TRANS_RECOVERY() Recovery detection\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/et/lang.pu",
    "content": "\n' SKIN.INC\n\n!define TR_EXTERNAL() Väline\n!define TR_UNSPECIFIED() Määratlemata\n!define TR_NEC() VVK\n!define TR_IVXV() IVXV\n\n\n' EXAMPLE.PU\n\n!define TR_STAKEHOLDER() Osapool\n\n!define TR_APPLICATION() Rakendus\n!define TR_SERVICE() Teenus\n\n!define TR_INTERFACE() Liides\n!define TR_UNSPECIFIED_INTERFACE() Määratlemata liides\n\n!define TR_COMPONENT() Komponent\n!define TR_IVXV_COMPONENT() IVXV komponent\n!define TR_EXTERNAL_COMPONENT() Väline komponent\n!define TR_DATABASE() Andmebaas\n\n\n' DEVELOPMENT.PU\n\n!define TR_MONITOR_DB() Logid\n!define TR_STORAGE_DB() Nimekirjad ja hääled\n!define TR_CONF_DB_CLS() Seaded\n!define TR_CONF_DB_FORWARDER() Seaded\n!define TR_CONF_DB_STORAGE() Seaded\n!define TR_CONF_DB_VERIFY() Seaded\n!define TR_CONF_DB_VOTE() Seaded\n!define TR_CONF_DB() Seadistused\n!define TR_P_HELPERS() Abimoodulid\n!define TR_P_AUDIT() Auditirakendus\n!define TR_P_COLLECTOR() Kogumisteenus\n!define TR_P_CONFIG() Seadistusrakendus\n!define TR_P_PROCESSOR() Töötlemisrakendus\n!define TR_P_KEY() Võtmerakendus\n!define TR_R_VOTING() Hääletamisteenus\n!define TR_R_VERIFY() Kontrollteenus\n!define TR_R_EXTENSION() Laiendusmoodulid\n!define TR_R_MID_AUTH() Mobiil-ID abiteenus\n!define TR_R_MONITORING() Monitooring\n!define TR_R_CLS() Nimekirjateenus\n!define TR_R_STORAGE() Talletamisteenus\n!define TR_R_DISPATCH() Vahendusteenus\n\n!define TR_ADMIN_APP() Haldus\n!define TR_BACKUP() Varundamine\n!define TR_BDOC() BDOC\n!define TR_CLS() Nimekirjateenus\n!define TR_CONF_APP() Seadistusrakendus\n!define TR_DDOC_SERVICE() Mobile ID Service\n!define TR_DECRYPTION_VERIFIER() Lugemistõendi kontroll\n!define TR_ELECTION_WWW() Valimiste veebiserver\n!define TR_EP_ELEC() Europarlament\n!define TR_FORWARDER() Vahendusteenus\n!define TR_IVXV_MONITOR() Logianalüsaator\n!define TR_JDIGIDOC() DigiDoc4J\n!define TR_KEY_APP() Võtmerakendus\n!define TR_KOV_ELEC() Kohaliku omavalitsuse volikogu\n!define TR_MESSENTE() Messente SMS\n!define TR_MID_AUTH() Autentimine\n!define TR_MID_SIGN() Allkirjastamine\n!define TR_OCSP() Kehtivuskinnitus\n!define TR_OFFLINE_AUDIT() Audit\n!define TR_OFFLINE_PROCESSOR() Töötlemisrakendus\n!define TR_PDF_API() PDF API\n!define TR_POP() Rahvastikuregister\n!define TR_REG() Registreerimine\n!define TR_RFC_3161() RFC3161 Registreerimisteenus\n!define TR_RH_ELEC() Rahvahääletus\n!define TR_RK_ELEC() Riigikogu\n!define TR_SK_OCSP() OCSP Registreerimisteenus\n!define TR_STORAGE() Talletamisteenus\n!define TR_TICKET() Autentimine, pilet\n!define TR_TLS() Autentimine, TLS\n!define TR_TSP() Ajatempel\n!define TR_VERIFY() Kontrollteenus\n!define TR_VIS() VIS\n!define TR_VOTE() Hääletamisteenus\n!define TR_ZABBIX_EXT() Väline monitooringuserver\n\n!define TR_A_ADMIN() Admin\n!define TR_A_AUDITOR() Audiitor\n!define TR_A_HELPDESK() Klienditugi\n!define TR_A_KEYMANAGER() Võtmehaldur\n!define TR_A_OFFLINE_PROCESSOR() Töötleja\n!define TR_A_OWNER() Korraldaja\n!define TR_A_SYSADMIN() Süs.admin\n!define TR_A_VOTER() Valija\n\n!define TR_API_INT() IVXV API\n!define TR_APP_LOG() Rakenduse logi\n!define TR_AUDIT_GUI() GUI\n!define TR_AUDIT_IN() FS Import\n!define TR_AUDIT_OUT() FS Export\n!define TR_AUTH() Tuvastamine\n!define TR_BACKUP() Varundamine\n!define TR_CERTIFICATE() Sertifikaat\n!define TR_CONF_BUNDLE() Konfiguratsioonifail\n!define TR_CONF_GUI() GUI\n!define TR_CONF_IN() FS Import\n!define TR_CONF_OUT() FS Export\n!define TR_DECRYPTION_PROOF() Lugemistõend\n!define TR_ELEC() Valimise liides\n!define TR_ENCRYPTED_VOTES() Anonüümitud hääled\n!define TR_EXPORT_VOTES() Häälte eksport\n!define TR_HELPDESK() Helpdesk\n!define TR_HTTPS_EXT() HTTPS\n!define TR_INDVERIFY() Kontrollrakendus\n!define TR_INVALID_VOTES() Kehtetute häälte loend\n!define TR_IVOTERS_PDF() E-hääletanute nimekiri PDF\n!define TR_JAVA() DigiDoc4J\n!define TR_KEY_GUI() GUI\n!define TR_KEY_IN() FS Import\n!define TR_KEY_OUT() FS Export\n!define TR_KEY_SHARE() Võtmeosak\n!define TR_LOCALADMIN() Käsurida\n!define TR_LOG_EXT() HTTPS\n!define TR_LOG_INT() RELP-JSON\n!define TR_LOG_SMS() SMS\n!define TR_LOG_SMTP() SMTP\n!define TR_MESSENTE() Messente\n!define TR_MID() Mobiil-ID abiteenus\n!define TR_MIXED_VOTES() Miksitud hääled\n!define TR_MIXING_PROOF() Miksimistõend\n!define TR_MIXING_VERIFIER() Miksimistõendi kontroll\n!define TR_MONITOR() Monitooring\n!define TR_PKCS15() PKCS15\n!define TR_POP_VOTERS() Valijate nimekiri\n!define TR_PROCESSOR_GUI() GUI\n!define TR_PROCESSOR_IN() FS Import\n!define TR_PROCESSOR_OUT() FS Export\n!define TR_Q11N() Täiendamine\n!define TR_REGISTERED_VOTES() Registreeritud hääled\n!define TR_REMOTEADMIN() Kaughaldus\n!define TR_RFC_3161() RFC3161\n!define TR_SK_AUTH() Autentimise OCSP\n!define TR_SK_OCSP() OCSP\n!define TR_SMTP() Kiirkorraldus (SMTP)\n!define TR_TLS_CLS() Valikud\n!define TR_TLS_INT() TLS-SNI\n!define TR_TLS_STORAGE() TLS\n!define TR_TLS_VERIFY() Kontrollimine\n!define TR_TLS_VOTE() Hääletamine\n!define TR_VERIFY() Verifitseerimine\n!define TR_VIS_CANDS() Kandidaatide nimekiri\n!define TR_VIS_DIST() Ringkondade nimekiri\n!define TR_VIS_RESULTS() Tulemuste sisestamine\n!define TR_VIS_REVOKE() Tühistus-ennistusnimekiri\n!define TR_VIS_VOTERS() E-hääletanute nimekiri\n!define TR_VOTE() Valijarakendus\n!define TR_VOTING_RESULT() Hääletamistulemus\n!define TR_ZABBIX() Zabbix\n\n\n'MS\n\n!define TR_MS_STATE_TITLE() Kogumisteenuse olek\n\n\n!define TR_MS_S_UNINSTALLED() Paigaldamata\n!define TR_MS_S_INSTALLED() Paigaldatud\n!define TR_MS_S_SETUP() Seadistatud\n!define TR_MS_S_FAILURE() Tõrge\n!define TR_MS_S_PARTIAL_FAILURE() Osaline tõrge\n\n\n!define TR_MS_S_UNINSTALLED_TXT() - Haldusteenus on paigaldatud, \\\n\\n- Alamteenused on paigaldamata.\n\n!define TR_MS_S_INSTALLED_TXT() Kõik alamteenused on paigaldatud \\\n\\nja tehniliselt seadistatud. \\\n\\nValimiste seadistust pole rakendatud.\n\n!define TR_MS_S_SETUP_TXT() Kogumisteenusele on rakendatud \\\n\\nvalimiste seadistused, teenusega \\\n\\non võimalik hääletust läbi viia \\\n\\nja e-valimiskasti väljastada.\n\n!define TR_MS_S_FAILURE_TXT() Üks või mitu alamteenust ei tööta, \\\n\\nkogumisteenuse oluline sõlm \\\n\\npole töökorras.\n\n!define TR_MS_S_PARTIAL_FAILURE_TXT() Üks või mitu dubleeritud \\\n\\nalamteenust ei tööta, \\\n\\nkogumisteenus töötab.\n\n\n!define TR_MS_TRANS_INSTALL() Teenuste \\n paigaldamine\n!define TR_MS_TRANS_SETUP() Valimiste \\n seadistuse \\n rakendamine \\n teenustele\n!define TR_MS_TRANS_FAIL() Teenuse \\n tõrkeoleku \\n tuvastamine\n!define TR_MS_TRANS_RECOVERY() Kõikide \\n teenuste \\n korrasoleku \\n tuvastamine\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/example.pu",
    "content": "@startuml\n\n!include skin.inc\n\nactor \"TR_STAKEHOLDER()\" <<TR_NEC()>> as a_owner\n\n\npackage TR_APPLICATION() {\n\n    interface \"TR_INTERFACE()\" <<TR_IVXV()>> as liides_1\n    interface \"TR_UNSPECIFIED_INTERFACE()\" <<TR_UNSPECIFIED()>> as liides_3\n    [TR_COMPONENT()] <<TR_IVXV()>> as komponent_1\n\n    komponent_1 - liides_1\n    komponent_1 -up- liides_3\n}\n\na_owner --> liides_3\n\npackage TR_SERVICE() {\n\n    interface \"TR_INTERFACE()\" <<TR_IVXV()>> as liides_2\n    [TR_IVXV_COMPONENT()] <<TR_IVXV()>> as komponent_2\n    [TR_EXTERNAL_COMPONENT()] <<TR_EXTERNAL()>> as komponent_3\n\n    komponent_2 --> komponent_3\n    komponent_2 - liides_2\n\n    database \"TR_DATABASE()\" as andmebaas {\n    }\n\n    komponent_2 --> andmebaas\n\n}\n\nliides_1 --> liides_2\n\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/general.pu",
    "content": "\n\n\n\nOsapooled\n\nOmanik\nHääletaja\nTöötleja\nAudiitor\n\n\nKlienditugi\n\n\nCollector\nATO\nVA-STO\nAuth\nSign\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-collector-status.pu",
    "content": "@startuml\n\nTitle TR_MS_STATE_TITLE()\n\nskinparam state {\n  BackgroundColor<<OK>> LightGreen\n  BackgroundColor<<Error>> LightCoral\n}\n\nstate \"TR_MS_S_UNINSTALLED()\" as s_uninstalled\ns_uninstalled : TR_MS_S_UNINSTALLED_TXT()\n\nstate \"TR_MS_S_INSTALLED()\" as s_installed <<OK>>\ns_installed : TR_MS_S_INSTALLED_TXT()\n\nstate \"TR_MS_S_SETUP()\" as s_setup <<OK>>\ns_setup : TR_MS_S_SETUP_TXT()\n\nstate \"TR_MS_S_FAILURE()\" as s_failure <<Error>>\ns_failure : TR_MS_S_FAILURE_TXT()\n\nstate \"TR_MS_S_PARTIAL_FAILURE()\" as s_partial_failure\ns_partial_failure : TR_MS_S_PARTIAL_FAILURE_TXT()\n\n\n[*] --> s_uninstalled\n\ns_uninstalled --> s_installed : TR_MS_TRANS_INSTALL()\n\ns_installed --> s_setup : TR_MS_TRANS_SETUP()\n\ns_setup --> s_partial_failure : TR_MS_TRANS_FAIL()\ns_setup --> [*]\n\ns_partial_failure --> s_setup : TR_MS_TRANS_RECOVERY()\ns_partial_failure --> s_failure\ns_partial_failure --> [*]\n\ns_failure --> s_partial_failure\ns_failure --> [*]\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-management-service-components.en.pu",
    "content": "@startuml\n\n!include skin.inc\n\nTitle Components of the Collection Service Management Service\n\n[User Web Browser] as User\n\nframe IVXV {\n  [Statistics Server]\n  interface \"HTTP\" as StatisticsHTTP\n  [Statistics Server] -down- StatisticsHTTP\n\n  frame \"Collection Service\" {\n    node \"Management Service\" {\n      [Utilities]\n      [Web Server \\n (Apache)] as WebServer\n      interface HTTPS\n      WebServer -u- HTTPS\n      [User] -> HTTPS\n\n      [Management Daemon]\n      interface \"HTTP\" as ManagementDaemonHTTP\n      ManagementDaemonHTTP -- [Management Daemon]\n      [Agent Daemon]\n      database \"Data Catalog\" {\n        [Service \\n data files] as DataFiles\n      }\n    }\n\n    [Microservice]\n    interface \"SSH\" as ServiceSSH\n    [Microservice] -l- ServiceSSH\n  }\n}\n\n/' External Sources '/\nStatisticsHTTP -> [Agent Daemon]\n\n/' Management Service '/\n[Agent Daemon] --> [Data Files] : Writing \\n collected \\n data\n[Management Daemon] --> [Data Files] : 1. Adding and \\n modifying users \\n 2. Saving digital \\n ballot-box\n\n[Data Files] --> [Web Server] : Reading \\n generated \\n data\n\n[Web Server] --> [Data Files] : Loading commands\n[Web Server] --> ManagementDaemonHTTP : Implementing \\n commands\n\n[Data Files] --> [Web Server] : Loading digital \\n ballot-box\n\n/' Service Servers '/\nServiceSSH --> [Management Daemon] : Loading digital \\n ballot-box \\n from storage service\n[Utilities] --> ServiceSSH : 1. Installing service \\n 2. Configuring service \\n 3. Applying list\n[Agent Daemon] --> ServiceSSH : Service monitoring\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-management-service-components.et.pu",
    "content": "@startuml\n\n!include skin.inc\n\nTitle Kogumisteenuse haldusteenuse komponendid\n\n[Kasutaja veebisirvija] as Kasutaja\n\nframe IVXV {\n  [Statistikaserver]\n  interface \"HTTP\" as StatistikaHTTP\n  [Statistikaserver] -down- StatistikaHTTP\n\n  frame Kogumisteenus {\n    node \"Haldusteenus\" {\n      [Utiliidid]\n      [Veebiserver \\n (Apache)] as Veebiserver\n      interface HTTPS\n      Veebiserver -u- HTTPS\n      [Kasutaja] -> HTTPS\n\n      [Haldusdeemon]\n      interface \"HTTP\" as HaldusdeemonHTTP\n      HaldusdeemonHTTP -- [Haldusdeemon]\n      [Agentdeemon]\n      database \"Andmekataloog\" {\n        [Haldusteenuse \\n andmefailid] as Andmefailid\n      }\n    }\n\n    [Mikroteenus]\n    interface \"SSH\" as TeenusSSH\n    [Mikroteenus] -l- TeenusSSH\n  }\n}\n\n/' Välised allikad '/\nStatistikaHTTP -> [Agentdeemon]\n\n/' Haldusteenus '/\n[Agentdeemon] --> [Andmefailid] : Kogutud \\n andmete \\n kirjutamine\n[Haldusdeemon] --> [Andmefailid] : 1. Kasutaja \\n lisamine ja \\n muutmine \\n 2. e-valimiskasti \\n salvestamine\n\n[Andmefailid] --> [Veebiserver] : Genereeritud \\n andmete \\n lugemine\n\n[Veebiserver] --> [Andmefailid] : Korralduste \\n laadimine\n[Veebiserver] --> HaldusdeemonHTTP : Korralduste \\n rakendamine\n\n[Andmefailid] --> [Veebiserver] : E-valimiskasti \\n laadimine\n\n/' Teenusserverid '/\nTeenusSSH --> [Haldusdeemon] : E-valimiskasti \\n laadimine \\n talletusteenusest\n[Utiliidid] --> TeenusSSH : 1. Teenuse paigaldus \\n 2. Teenuse seadistamine \\n 3. Nimekirja rakendamine\n[Agentdeemon] --> TeenusSSH : Teenuse seire\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-service-status-changes.en.pu",
    "content": "@startuml\n\nTitle Collector Service Component State Change on Management Server\n\npartition Installation {\n  :NEW;\n\n    while (Can the service state be queried under the service account?) is (no)\n      if (Can the service package state be queried under the root account of the service machine?) then (yes)\n        :Install the service package;\n        :Create access to the service account;\n      else (no)\n        :Set new state to FAULTY;\n        stop\n      endif\n    endwhile (yes)\n\n    while (Can access be granted to the root account of the service machine?) is (yes)\n      :Remove access to the root account of the service machine;\n    endwhile (no)\n}\n\npartition Configuration {\n  :INSTALLED;\n    while (Are the latest configurations applied to the service?) is (no)\n      :Load configurations into the service machine;\n      :Apply configurations to the service;\n    endwhile (yes)\n}\n\npartition Execution {\n  :CONFIGURED;\n    while (Is it possible to query the service state?) is (yes)\n      :Load the latest data into the management service;\n      :Wait;\n      if (Is the service disabled by the administrator?) then (yes)\n        :Set new state to DISABLED;\n        stop\n      else (no)\n      endif\n    endwhile (no)\n}\n\n:FAULTY;\n\n@enduml\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-service-status-changes.et.pu",
    "content": "@startuml\n\nTitle Koguja teenusekomponendi olekumuutus haldusserveris\n\npartition Paigaldamine {\n  :UUS;\n\n    while (Kas teenuse konto alt õnnestub teenuse olekut küsida?) is (ei)\n      if (Kas teenusmasina juurkonto alt \\n õnnestub teenuspaki olekut küsida?) then (jah)\n        :Paigalda teenuse pakk;\n        :Loo ligipääs teenuse kontole;\n      else (ei)\n        :Määra uus olek VIGANE;\n        stop\n      endif\n    endwhile (jah)\n\n    while (Kas teenusmasina juurkontole õnnestub ligi pääseda?) is (jah)\n      :eemalda ligipääs teenusmasina juurkontole;\n    endwhile (ei)\n}\n\npartition Seadistamine {\n  :PAIGALDATUD;\n    while (Kas teenusele on rakendatud värskeimad seadistused?) is (ei)\n      :Laadi seadistused teenusmasinasse;\n      :Rakenda seadistused teenusele;\n    endwhile (jah)\n}\n\npartition Käitamine {\n  :SEADISTATUD;\n    while (Kas teenuse oleku küsimine õnnestub) is (jah)\n      :Laadi haldusteenusesse teenuse viimased andmed;\n      :Oota;\n      if (Kui haldur keelab teenuse?) then (jah)\n        :Määra uus olek KEELATUD;\n        stop\n      else (ei)\n      endif\n    endwhile (ei)\n}\n\n:VIGANE;\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-service-status.en.pu",
    "content": "@startuml\n\nTitle Subservice State Diagram of the Collection Service\n\nskinparam state {\n  BackgroundColor<<OK>> LightGreen\n  BackgroundColor<<Invalid>> LightCoral\n}\n\nstate NotInstalled\nstate Installed\nstate Configured <<OK>>\nstate Error <<Invalid>>\nstate Removed\n\nNotInstalled: Service described in technical settings,\nNotInstalled: but the service software is not installed or\nNotInstalled: no technical settings have been applied to the service.\n\nInstalled : Service software is installed and the status of applied configurations is:\nInstalled : - root certificates are applied;\nInstalled : - technical settings are applied;\nInstalled : - election settings are not applied.\n\nConfigured : Service is operational\n\nError : Service is not operational\n\nRemoved : Service has been removed from the collection service\nRemoved : composition\n\n[*] --> NotInstalled\n\nNotInstalled --> Installed : Management service installs \\n service software and \\n applies root and \\n technical settings\n\nInstalled --> Configured : Management service applies \\n election settings to the service\nInstalled --> Removed : Service is removed \\n from the collection service\n\nConfigured --> Error : Management service \\n loses connection \\n to the service\nConfigured --> Removed : Service is removed \\n from the collection service\n\nError --> Configured : Error in the \\n configured service \\n is resolved\nError --> Removed : Service is removed \\n from the collection service\nError --> [*] : Voting period \\n ends\n\nConfigured --> [*] : Voting period \\n ends\n\nRemoved --> [*]\n\n@enduml\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-service-status.et.pu",
    "content": "@startuml\n\nTitle Kogumisteenuse alamteenuse olekudiagramm\n\nskinparam state {\n  BackgroundColor<<OK>> LightGreen\n  BackgroundColor<<Invalid>> LightCoral\n}\n\nstate Paigaldamata\nstate Paigaldatud\nstate Seadistatud <<OK>>\nstate Tõrge <<Invalid>>\nstate Eemaldatud\n\nPaigaldamata: Teenus kirjeldatud tehnilistes seadistustes,\nPaigaldamata: kuid teenuse tarkvara on paigaldamata või\nPaigaldamata: pole teenusele tehnilisi seadistusi rakendatud.\n\nPaigaldatud : Teenuse tarkvara on paigaldatud ja teenusele\nPaigaldatud : rakendatud seadistuste seisund on:\nPaigaldatud : - usaldusjuure seadistused on rakendatud;\nPaigaldatud : - tehnilised seadistused on rakendatud;\nPaigaldatud : - valimiste seadistused on rakendamata.\n\nSeadistatud : Teenus on töökorras\n\nTõrge : Teenus pole töökorras\n\nEemaldatud : Teenus on kogumisteenuse\nEemaldatud : koosseisust eemaldatud\n\n[*] --> Paigaldamata\n\nPaigaldamata --> Paigaldatud : Haldusteenus paigaldab \\n teenuse tarkvara ning \\n rakendab usaldusjuure ja \\n tehnilised seadistused\n\nPaigaldatud --> Seadistatud : Haldusteenus rakendab \\n teenusele \\n valimiste seadistused\nPaigaldatud --> Eemaldatud : Teenus eemaldatakse \\n kogumisteenusest\n\nSeadistatud --> Tõrge : Haldusteenusel \\n kaob ühendus \\n teenusega\nSeadistatud --> Eemaldatud : Teenus eemaldatakse \\n kogumisteenusest\n\nTõrge --> Seadistatud : Seadistatud \\n teenuse \\n tõrge \\n kõrvaldatakse\nTõrge --> Eemaldatud : Teenus eemaldatakse \\n kogumisteenusest\nTõrge --> [*] : Hääletusperiood \\n lõppeb\n\nSeadistatud --> [*] : Hääletusperiood \\n lõppeb\n\nEemaldatud --> [*]\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-upload-command.en.pu",
    "content": "@startuml\n\nTitle Execution of Commands in the Management Service\n\nActor \"User (Browser)\" as Browser\nparticipant \"Web Server \\n (Apache)\" as WSGI\nparticipant \"Daemon \\n (HTTP)\" as Daemon_HTTP\nparticipant \"Management \\n Library\" as Util\nDatabase \"File System\" as FS\nparticipant Service\n\nBrowser -> WSGI : Uploading \\n command\nActivate Browser\nActivate WSGI\n\nWSGI -> FS : Saving command\n\nalt Command is valid\n\n  WSGI -> Daemon_HTTP : Request to \\n apply command\n  Activate Daemon_HTTP\n  Daemon_HTTP -> Util : Request to apply \\n command\n\n  Activate Util\n  Util <- FS : Reading command\n  Util -> Util : Validating command\n  alt Immediate application\n    Util -> Service : Applying command\n  else Issuing digital ballot-box\n    Service -> Util : Loading digital ballot-box\n  end\n  Util -> Daemon_HTTP : Response to apply \\n command\n  Deactivate Util\n\n  Daemon_HTTP -> WSGI : Sending response\n  Deactivate Daemon_HTTP\nend\n\nWSGI -> Browser : Sending response\n\nDeactivate WSGI\nDeactivate Browser\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-upload-command.et.pu",
    "content": "@startuml\n\nTitle Korralduste täitmine haldusteenuses\n\nActor \"Kasutaja (Brauser)\" as Brauser\nparticipant \"Veebiserver \\n (Apache)\" as WSGI\nparticipant \"Deemon \\n (HTTP)\" as Deemon_HTTP\nparticipant \"Halduse \\n teek\" as Util\nDatabase \"Failisüsteem\" as FS\nparticipant Teenus\n\nBrauser -> WSGI : Korralduse \\n üleslaadimine\nActivate Brauser\nActivate WSGI\n\nWSGI -> FS : Korralduse salvestamine\n\nalt Korraldus on valiidne\n\n  WSGI -> Deemon_HTTP : Korralduse \\n rakendamise \\n päring\n\n  Activate Deemon_HTTP\n  Deemon_HTTP -> Util : Korralduse \\n rakendamise päring\n\n  Activate Util\n  Util <- FS : Korralduse lugemine\n  Util -> Util : Korralduse valideerimine\n  alt Vahetu rakendamine\n    Util -> Teenus : Korralduse rakendamine\n  else E-valimiskasti väljastamine\n    Teenus -> Util : E-valimiskasti laadimine\n  end\n  Util -> Deemon_HTTP : Korralduse \\n rakendamise vastus\n  Deactivate Util\n\n  Deemon_HTTP -> WSGI : Vastuse \\n edastamine\n  Deactivate Deemon_HTTP\nend\n\nWSGI -> Brauser : Vastuse \\n edastamine\n\nDeactivate WSGI\nDeactivate Brauser\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-voter-list-status.en.pu",
    "content": "@startuml\ntitle Voter List State Diagram\n\nskinparam state {\n  BackgroundColor<<NEW>> LightGray\n  BackgroundColor<<OK>> LightGreen\n  BackgroundColor<<Invalid>> LightCoral\n}\n\nstate \"Available\" as AVAILABLE <<NEW>>\nstate \"Pending Application\" as PENDING\nstate \"Applied\" as APPLIED <<OK>>\nstate \"Invalid\" as INVALID <<Invalid>>\nstate \"Skipped\" as SKIPPED <<Invalid>>\n\nnote right of AVAILABLE\n  External system state not\n  registered in the management\n  service database\nend note\n\n[*] --> AVAILABLE : Detection of list availability\nAVAILABLE : List is available in the Voting Information System\n\nAVAILABLE --> PENDING : Loading of correct list \\n into the management service\nPENDING : List is Loaded into the management service\n\nAVAILABLE --> INVALID : Loading of \\n invalid list \\n into the management service\nPENDING --> INVALID : Failed application of \\n the change list \\n to the list service\nINVALID : List is marked as invalid\n\nPENDING --> APPLIED : Application of the list \\n to the list service\nAPPLIED : List is applied to the collection service\n\nINVALID --> SKIPPED : Marking the list as skipped\nSKIPPED : List is marked as skipped\n\nSKIPPED --> [*]\nAPPLIED --> [*]\n\n@enduml\n\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/ms-voter-list-status.et.pu",
    "content": "@startuml\ntitle Valijate nimekirja olekudiagramm\n\nskinparam state {\n  BackgroundColor<<NEW>> LightGray\n  BackgroundColor<<OK>> LightGreen\n  BackgroundColor<<Invalid>> LightCoral\n}\n\nstate \"Saadaval\" as AVAILABLE <<NEW>>\nstate \"Rakendamise ootel\" as PENDING\nstate \"Rakendatud\" as APPLIED <<OK>>\nstate \"Vigane\" as INVALID <<Invalid>>\nstate \"Vahele jäetud\" as SKIPPED <<Invalid>>\n\nnote right of AVAILABLE\n  Süsteemiväline seisund, mida\n  haldusteenuse andmebaasis\n  ei registreerita\nend note\n\n[*] --> AVAILABLE : Nimekirja saadavaloleku tuvastamine\nAVAILABLE : Nimekiri on saadaval Valimiste Infosüsteemis\n\nAVAILABLE --> PENDING : Korrektse nimekirja \\n laadimine \\n haldusteenusesse\nPENDING : Nimekiri on Laaditud haldusteenusesse\n\nAVAILABLE --> INVALID : Vigase \\n muudatusnimekirja \\n laadimine \\n haldusteenusesse\nPENDING --> INVALID : Muudatusnimekirja \\n nurjunud rakendamine \\n nimekirjateenusele\nINVALID : Nimekiri on märgitud vigaseks\n\nPENDING --> APPLIED : Nimekirja \\n rakendamine \\n nimekirjateenusele\nAPPLIED : Nimekiri on rakendatud kogumisteenusele\n\nINVALID --> SKIPPED : Nimekirja \\n märkimine \\n vahelejätmiseks\nSKIPPED : Nimekirja on märgitud vahelejätmiseks\n\nSKIPPED --> [*]\nAPPLIED --> [*]\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/skin.inc",
    "content": "skinparam interface {\n    backgroundColor<<TR_IVXV()>> Gold\n    backgroundColor<<TR_EXTERNAL()>> LightSalmon\n    backgroundColor<<TR_NEC()>> DarkGoldenRod\n    backgroundColor<<TR_UNSPECIFIED()>> Black\n}\n\nskinparam actor {\n    backgroundColor<<TR_IVXV()>> Gold\n    backgroundColor<<TR_EXTERNAL()>> LightSalmon\n    backgroundColor<<TR_NEC()>> DarkGoldenRod\n    backgroundColor<<TR_UNSPECIFIED()>> Black\n}\n\nskinparam component {\n    backgroundColor<<TR_IVXV()>> Gold\n    backgroundColor<<TR_NEC()>> DarkGoldenRod\n    backgroundColor<<TR_EXTERNAL()>> LightSalmon\n    backgroundColor<<TR_UNSPECIFIED()>> Black\n}\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-audit.pu",
    "content": "\n@startuml\n\ntitle \"Auditirakendus - offline\"\n\nusecase (Kogumisteenuse auditimaterjali import)\nusecase (Lugemisteenuse auditimaterjali import)\nusecase (Miksimisteenuse auditimaterjali import)\nusecase (Ajatembeldusteenuse auditimaterjali import)\nusecase (Auditi läbiviimine)\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-offline.pu",
    "content": "\n@startuml\n\ntitle \"Lugemisfaas - offline\"\n\npackage \"Töötleja\" {\n    usecase (Allkirjastatud häälte import)\n    usecase (Ajamärgendite import)\n    usecase (Kontrollimine - ver, ATO)\n    usecase (Korduvate häälte tühistamine)\n    usecase (E-hääletanute nimekiri jaoskondade kaupa)\n\n    usecase (Tühistusavalduste import)\n    usecase (Tühistamise ennistusavalduste import)\n    usecase (Anonümiseerimine)\n    usecase (Anonüümsete häälte eksport)\n    usecase (Lugemisele läinud E-hääletanute nimekiri jaoskondade kaupa)\n}\n\npackage \"Miksimine\" {\n    usecase (Auditeeritud random)\n    usecase (Anonüümsete häälte import)\n    usecase (Häälte miksimine)\n    usecase (Miksitud häälte eksport)\n    usecase (Miksimistõestuse eksport)\n}\n\npackage \"Lugemine\" {\n    usecase (Krüpteeritud häälte import)\n    usecase (E-häälte kokkulugemine)\n    usecase (E-hääletamise tulemuste eksport)\n    usecase (Dekrüptimistõestuse eksport)\n}\n\npackage \"Võtmehaldus\" {\n    usecase (Privaatvõtme aktiveerimine)\n}\n\npackage  \"Audit\" {\n    usecase (Kogumisteenuse auditimaterjali import)\n    usecase (Lugemisteenuse auditimaterjali import)\n    usecase (Ajatembeldusteenuse auditimaterjali import)\n    usecase (Auditi läbiviimine)\n}\n\npackage \"Valimise lõpetamine\" {\n    usecase (Arhiveerimine)\n    usecase (Privaatvõtme ja koopiate hävitamine)\n}\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-online.pu",
    "content": "\n@startuml\n\ntitle \"Hääletamisfaas hostingus\"\n\nusecase (*) as UC0\n\nnote left of UC0\n* Ainult talletamiskomponent ja ajatembeldus säilitavad olekut\n* Muud komponendid on stateless - loadbalancing on võimalik\n* Talletamiskomponendi veakindlus - HadoopFSi või byzantine?\n* On-line komponentide väliste liideste definitsioon ja valideerimine\n* Nõue TLSi kasutamiseks ja 443 pordiga piirdumiskes\n* HTTP ei ole hädavajalik kuid võib aidata\nendnote\n\npackage \"Support\" {\n    usecase (Varundamine hääletamisperioodi peatamata)\n    usecase (Taastamine puhtale süsteemile)\n    usecase (Logimine monitooringusüsteemi)\n    usecase (Süsteemi seisundi kaugvaatlus)\n    usecase (Süsteemi seisundi vaatlus käsurealt) as UC1.5\n    note left of UC1.5\n    * pseudograafika asemel git laadne käsustik\n    * võimalusel süsteemsete lahenduste kasutamine iseleiutamise asemel\n    endnote\n}\n\npackage \"Hääletamisperioodi eelne\" {\n    usecase (Proovihääletamine ühenduste testimiseks)\n    usecase (Hääletamisperioodi kaugkäivitamine)\n}\n\npackage \"Hääletamisperiood\" {\n    usecase (Valijate nimekirja perioodiline uuendamine)\n    usecase (Häälte talletamine) as UC2.2\n    usecase (Kontrollpäringutele vastamine)\n    usecase (Vaheaudit)\n    usecase (Serverite ühtsuse kontroll)\n    usecase (Hääletamisperioodi järkjärguline kaugpeatamine)\n\n    note left of UC2.2\n    * isikute autentimine\n    * valimisõigus(t)e tuvastamine\n    * valikute väljastamine\n    * häälte vastu võtmine ja talletamine\n    * kontrollidentifikaatorite väljastamine\n    endnote\n\n}\n\n' osa vähemalt häältest eesti pinnal vähemalt 1 kord päevas, võib ka logiandmed\n' eesti pinnal olev backup peab olema loetav\n\npackage \"Hääletamisperioodi järgne\" {\n    usecase (E-hääletanute nimekirja koostamine valimisjaoskondade kaupa)\n    usecase (Häälte eksport kogumisteenusest)\n    usecase (Auditimaterjali eksport kogumisteenusest)\n'    usecase (Auditimaterjali eksport ajatempliteenusest)\n    usecase (Logi eksport kogumisteenusest)\n}\n\n\npackage \"Valija tegevused\" {\n    usecase (Valijarakenduse paigaldamine autentsest allikast)\n    usecase (Kontrollrakenduse paigaldamine autentsest allikast)\n    usecase (E-hääletamine)\n    usecase (E-hääle kontrollimine)\n}\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-prep.pu",
    "content": "@startuml\n\ntitle \"Ettevalmistusfaas kontoris\"\n\nusecase (*) as UC0\n\nnote left  of UC0\nMujal menetlemata nüansid\n* Võimekus mitme samaaegse eritüübilise valimise korraldamiseks\n* Võimekus null-autentimismeetodite ja nimekirjade kasutamiseks\n* Andmevormingute spetsifikatsioon\n* Väliste liideste ja sõltuvuste spetsifikatsioon\nend note\n\nactor Omanik\nactor Hääletaja\nactor Koguja\nactor Töötleja\nactor Audiitor\nactor IDP\nactor ATO\nactor STO\nactor Klienditugi\n\n\n\n\npackage \"Konfiguratsiooni haldamine\" {\n    usecase (Tekstide haldus) as UC1.5\n    usecase (Valimisinfo laadimine) as UC1.1\n    usecase (Tehniliste parameetrite haldus) as UC1.4\n    usecase (Autentimisteenuse seadistamine) as UC1.6\n    usecase (Kehtivuskinnitusteenuse seadistamine) as UC1.7\n    usecase (Ajatempliteenuse seadistamine) as UC1.8\n    usecase (Konfiguratsiooni valideerimine) as UC1.9\n    usecase (Konfiguratsiooni allkirjastamine) as UC1.10\n    usecase (Konfiguratsiooni eksport) as UC1.11\n\n    usecase (Allkirja moodustamine, ajatempli saamine, kehtivuses veendumine meetodite määramine)\n\n    usecase (Algoritmi ja votme määramine)\n\nnote bottom of UC1.11\nKomponentide põhiste allkirjastatud konfipakkide loomine\nendnote\n\nnote bottom of UC1.1\n* Valimine\n* Reeglid\n* Valijad\n* Valikud\n* Jaoskonnad\nendnote\n\n}\n\nOmanik -> UC1.10\nKoguja -> UC1.5\n\nnote left of UC1.9\n* Olemasolu kontroll\n* Vormingu kontroll\n* Kooskõlalisuse kontroll\nendnote\n\nnote top of UC1.5\n* Kontrollrakendus\n* Valijarakendus\nendnote\n\n\npackage \"Võtmehaldus\" {\n    usecase (Võtmepaari ja PIN-ide genereerimine) as UC2.1\n    usecase (Võtmepaari varukoopia loomine) as UC2.2\n    usecase (Avaliku komponendi transport valijarakendusele) as UC2.3\n    usecase (Valijarakenduse ettevalmistamine) as UC2.4\n\n    ' osad pinniga osad mitte\n\n    note left of UC2.1\n    * MofN\n    * kiipkaart vs. HSM\n    endnote\n\n}\n\npackage \"Keskkonna ettevalmistamine\" {\n    usecase (Paigalduspakkide haldamine) as UC3.1\n    usecase (Süsteemi paigaldamine) as UC3.2\n    usecase (Usaldusnimekiri) as UC3.3\n    usecase (Volitatud isikute sertifikaatide haldus)\n    usecase (Konfiguratsiooni laadmine) as UC3.4\n}\n\npackage \"Laua peal testimine\" {\n    usecase (Võtmepaari kontroll, testhääletamine) as UC4.1\n}\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-tootleja.pu",
    "content": "\n@startuml\n\ntitle \"Töötleja\"\n\nusecase (Töötleja usaldusjuurte laadimine)\nusecase (Töötleja seadistuse import)\nusecase (Allkirjastatud häälte import)\nusecase (Ajamärgendite import)\nusecase (Kontrollimine - ver, ATO)\nusecase (Korduvate häälte tühistamine)\nusecase (E-hääletanute nimekiri jaoskondade kaupa)\nusecase (Tühistusavalduste import)\nusecase (Tühistamise ennistusavalduste import)\nusecase (Anonümiseerimine)\nusecase (Anonüümsete häälte eksport)\nusecase (Lugemisele läinud E-hääletanute nimekiri jaoskondade kaupa)\nusecase (Tegevuslogi)\nusecase (Tulemuslogi)\nusecase (Hääle olekulogi)\n\nusecase (Korrektse korduvhääle tühistamise tõestamine)\nusecase (Korrektse hääle tühistamise tõestamine p-hääle tõttu)\nusecase (Miksimissisendi autentsuse tõestamine)\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/arhitektuur/model/uc-votmerakendus.pu",
    "content": "\n@startuml\n\ntitle \"Võtmerakendus\"\n\n    usecase (Verifitseerimiskonfiguratsiooni import)\n    usecase (Kandidaatide ja ringkondade nimekirja import)\n    usecase (Krüpteeritud häälte import)\n    usecase (E-häälte dekrüpteerimine)\n    usecase (E-häälte kokkulugemine)\n    usecase (Sisendinfo verifitseerimine)\n    usecase (Väljundinfo signeerimine)\n    usecase (E-hääletamise tulemuste eksport)\n    usecase (Kehtetute sedelite loendi eksport)\n    usecase (Dekrüptimistõestuse eksport)\n    usecase (Tegevuslogi eksport)\n    usecase (Hääle olekulogi)\n\n    usecase (Võtmepaari genereerimine)\n    usecase (Juhuslikkuse lisamine)\n    usecase (Juhuslikkuse mõõtmine)\n    usecase (Sertifikaadi allkirjastamine)\n    usecase (Privaatvõtme aktiveerimine)\n    usecase (Privaatvõtme ja koopiate hävitamine)\n\n@enduml\n\n\n' Rakenduse konfigureerimine\n' Võtme genereerimine\n' Sertifikaadi väljastamine\n' Hääletamistulemuse väljastamine\n"
  },
  {
    "path": "Documentation/public/arhitektuur/spelling_wordlist.txt",
    "content": "\nAdvances\nAnnual\nCryptosystems\nImproving\nOctober\npp\nth\nThreshold\n\n\nPDF'ide\n\nBouncy\nCastle\n\nIP\nXML'il\nTLS'i\nSSH\nSNI\nTCP\n\napache\nBDoc\nclause\ncrontab\ndeb\ndebian\ndigidoc\ndocker'i\netcd\ngo\njammy\njellyfish\njinja\nlibdigidocpp\nlogback\npython\nrsyslog\nubuntu\nurlencode\n\nGuardtime\n\nGradle\nJackson\n\nadmincode\ncc\nchoices\ncount\ncounties\ndistrict\ndistricts\nid\nEHAK\nNimeste\nocsp\nprevious\nseq\nstats\ntime\ntspreg\ntype\nversion\nvote\nvoted\nvoter\nvotername\nvoters\nvotes\n\n\nSven\nHeiberg\nTarvi\nMartens\nPriit\nVinkel\nJan\nWillemson\n\nOliver\nSpycher\nVanessa\nTeague\nGregor\nWenda\n\nMelanie\nVolkamer\nJordi\nBarrat\nJosh\nBenaloh\nNicole\nGoodman\nPeter\nRyan\n\nInternational\nConference\nElectronic\nVoting\n\nCryptology\nSanta\nBarbara\nCalifornia\nUSA\n\nDesmedt\nFrankel\nBrassard\n\ned\neds\nthe\nof\nin\nor\npdf\n\nverifiability\nEstonian\nInternet\nVoting\nscheme\n\nRobert\nKrimmer\n\n\nAustria\n\nBregenz\n\nLochau\n\nProceedings\n\nSpringer\n\nSchnorri\nShamir\nShamiri\n\nassert\ncollections\ncommons\ndateutil\nglassfish\nhamcrest\nJUnit\nlater\nmockimise\nMockito\npyopenssl\npythoni\npythonile\nrequests\ntools\nx\n\nparametriseerimise\nhaldusutiliitide\nvallasrežiimis\n"
  },
  {
    "path": "Documentation/public/arhitektuur/tehnoloogiad.rst",
    "content": "..  IVXV arhitektuur\n\n.. _tehnoloogiad:\n\nKasutatavad tehnoloogiad\n========================\n\nKogumisteenuse programmeerimiskeel\n----------------------------------\n\nKogumisteenuse tuumikfunktsionaalsus on programmeeritud keeles Go, mis vastab\njärgmistele hanke nõuetele:\n\n* Staatiline tüüpimine;\n\n* Automaatne mäluhaldus;\n\n* Kompilaator avatud lähtekoodiga;\n\n* Ribastamine (rööprapse).\n\nKogumisteenuse haldusteenus on programmeeritud keeles Python.\n\n\nRakenduste programmeerimiskeel\n------------------------------\n\nRakendused on programmeeritud keeles Java, mis vastab hanke nõuetele keele laia\nleviku ja jätkusuutlikkuse kohta.\n\n\nProjekti sõltuvused\n-------------------\n\nProjektis kasutatavad kolmandate osapoolte komponendid koos nende motiveeritud\nkasutamisvajadusega on üles loetletud järgnevates tabelites. Eraldi tabelid on\nraamistiku pakendamiseks ja töötamiseks ning raamistiku arenduseks ja\ntestimiseks.\n\nKõik IVXV projektis kasutatavad välised teegid asuvad ``ivxv-external.git``\nhoidlas või on saadaval platvormil, kus rakendus tööle hakkab.\n\nKõik kogumisteenuses kasutatavad komponendid on avatud lähtekoodiga.\n\n.. tabularcolumns:: |p{0.2\\linewidth}|p{0.1\\linewidth}|p{0.15\\linewidth}|p{0.55\\linewidth}|\n.. list-table::\n   IVXV raamistiku tööks kasutatavad kolmandate osapoolte komponendid\n   :header-rows: 1\n\n   *  - Nimi\n      - Versioon\n      - Litsents (SPDX)\n      - Kasutusvajadus\n\n   *  - `Bootstrap <http://getbootstrap.com>`_\n      - 3.4.1, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliidese kujundus\n\n   *  - Bouncy Castle\n      - 1.78.1, Java\n      - MIT\n      - ASN1 käsitlemine, andmetüübi BigInteger abifunktsioonid\n\n   *  - `Bottle <https://bottlepy.org/>`_\n      - 0.13.2, Python\n      - MIT\n      - Raamistik kogumisteenuse haldusteenuse veebiliidese teostamiseks\n\n   *  - CAL10N\n      - 0.8.1, Java\n      - MIT\n      - Mitmekeelsuse tugi, tõlkefailide valideerimine\n\n   *  - Digidoc 4j\n      - 5.3.1, Java\n      - LGPL-2.1-only\n      - BDoc konteinerite käsitlemine\n\n   *  - Apache Commons (collections4 4.4)\n      - Java\n      - Apache-2.0\n      - Digidoc 4j ja PDFBox sõltuvused\n\n   *  - `Docopt <http://docopt.org/>`_\n      - 0.6.2, Python\n      - MIT\n      - Kogumisteenuse haldusutiliitide käsurealiidese teostus\n\n   *  - `Fasteners <https://github.com/harlowja/fasteners>`_\n      - 0.19, Python\n      - Apache-2.0\n      - Kogumisteenuse haldusteenuse protsesside lukustus\n\n   *  - `gin-gonic <https://github.com/gin-gonic>`_\n      - 1.9.1, Go\n      - MIT\n      - Veebiraamistik x-tee liidese jaoks\n\n   *  - `etcd <https://coreos.com/etcd>`_\n      - 3.5.9, Go\n      - Apache-2.0\n      - Talletusteenusena kasutatav hajus võti-väärtus andmebaas\n\n   *  - Glassfish JAXB\n      - 4.0.5, Java\n      - BSD-3-Clause\n      - Java XML teek\n\n   *  - Gradle\n      - 8.11, Java\n      - Apache-2.0\n      - Java rakenduste ehitamise raamistik\n\n   *  - `HAProxy <http://www.haproxy.org/>`_\n      - 2.4.24\n      - GPL-2.0-or-later\n      - Vahendusteenusena kasutatav TCP-proksi\n\n   *  - Jackson\n      - 2.18.1, Java\n      - Apache-2.0\n      - JSON vormingus failide lugemine ja kirjutamine\n\n   *  - Jinja2\n      - 3.1.4, Python\n      - BSD\n      - Jinja mallide kasutamine haldusteenuses\n\n   *  - `jQuery <https://jquery.org/>`_\n      - 3.7.1, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliides\n\n   *  - jsonschema\n      - 4.23.0, Python\n      - MIT\n      - JSON valideerimine haldusteenuses\n\n   *  - Logback\n      - 1.5.12, Java\n      - EPL-1.0 or LGPL-v2.1-only\n      - Logimise API teostus\n\n   *  - Logback JSON\n      - 0.1.5, Java\n      - EPL-1.0 or LGPL-v2.1-only\n      - Logback logija laiendus JSON vormingus logikirjete koostamiseks\n        Jackson teegi abil\n\n   *  - `Logrus <https://github.com/sirupsen/logrus>`_\n      - 1.9.3, Go\n      - MIT\n      - Logimisraamistik x-tee liidese jaoks\n\n   *  - `metisMenu <https://github.com/onokumus/metisMenu>`_\n      - 1.1.3, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliides\n\n   *  - `FontAwesome <https://github.com/FortAwesome/Font-Awesome>`_\n      - 6.7.2, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliides\n\n   *  - `DataTables <https://github.com/DataTables/DataTablesSrc>`_\n      - 2.3.2, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliides\n\n   *  - PDFBox\n      - 3.0.3, Java\n      - Apache-2.0\n      - PDF vormingus raportite genereerimise tugi Java rakendustele\n\n   *  - `PyYAML <http://pyyaml.org/>`_\n      - 6.0.2, Python\n      - MIT\n      - Kogumisteenuse seadistusfailide töötlemise tugi haldusteenusele\n\n   *  - python-crontab\n      - 3.3.0, Python\n      - LGPLv3\n      - Crontab haldusteenuses\n\n   *  - python-dateutil\n      - 2.9.0, Python\n      - BSD\n      - Kuupäevad ja kellaajad haldusteenuses\n\n   *  - python-debian\n      - 0.1.49, Python\n      - GPLv2\n      - Debian pakkide lugemine haldusteenuses\n\n   *  - pyopenssl\n      - 24.2.1, Python\n      - Apache\n      - OpenSSL kasutus haldusteenuses\n\n   *  - `Schematics <https://github.com/schematics/schematics>`_\n      - 2.1.1, Python\n      - BSD-3-Clause\n      - Kogumisteenuse seadistusfailide valideerimise tugi haldusteenusele\n\n   *  - SnakeYAML\n      - 2.3, Java\n      - Apache-2.0\n      - YAML vormingus andmete lugemine\n\n   *  - `SB Admin 2 <https://github.com/BlackrockDigital/startbootstrap-sb-admin-2>`_\n      - 3.3.7+1, JavaScript\n      - MIT\n      - Kogumisteenuse haldusteenuse kasutajaliidese kujundus\n\n.. tabularcolumns:: |p{0.2\\linewidth}|p{0.1\\linewidth}|p{0.15\\linewidth}|p{0.55\\linewidth}|\n.. list-table::\n   IVXV raamistiku testide\n   kasutatavad kolmandate osapoolte komponendid\n   :header-rows: 1\n\n   *  - Nimi\n      - Versioon\n      - Litsents (SPDX)\n      - Kasutusvajadus\n\n   *  - Hamcrest\n      - 3.0, Java\n      - BSD-3-Clause\n      - Loetavam assert-meetodite kasutamine Java üksuste testides\n\n   *  - JUnit\n      - 5.10.0, Java\n      - EPL-1.0\n      - Java testimisraamistik\n\n   *  - JUnitParams\n      - 1.1.1, Java\n      - Apache-2.0\n      - Testide parametriseerimise tugi\n\n   *  - Mockito\n      - 5.14.2, Java\n      - MIT\n      - Testitava koodi sõltuvuste mockimise tugi\n\n   *  - libdigidocpp-tools\n      - 3.14.5 .1404\n      - LGPL-2.1-or-later\n      - Testandmete genereerimine\n\n   *  - PyTest\n      - 7.4.2, Python\n      - MIT\n      - Üksuste testimise tugi Pythonile\n\n   *  - Requests\n      - 2.32.3, Python\n      - Apache 2.0\n      - HTTP päringute moodul Pythoni testidele\n\n.. tabularcolumns:: |p{0.2\\linewidth}|p{0.1\\linewidth}|p{0.15\\linewidth}|p{0.55\\linewidth}|\n.. list-table::\n   IVXV raamistiku arendamiseks ja/või testimiseks\n   kasutatavad kolmandate osapoolte tööriistad\n   :header-rows: 1\n\n   *  - Nimi\n      - Versioon\n      - Litsents (SPDX)\n      - Kasutusvajadus\n\n   *  - `Behave <https://github.com/behave/behave>`_\n      - 1.2.6, Python\n      - BSD-2-Clause\n      - Regressioonitestide käivitaja (*Behavior-driven development*)\n\n   *  - `Docker <http://www.docker.com/>`_\n      - 18.06 (või uuem)\n      - Apache-2.0\n      - Regressioonitestide läbiviimise keskkond - tarkvarakonteinerid\n\n   *  - `Sphinx <http://www.sphinx-doc.org/>`_\n      - 7.2.5, Python\n      - BSD\n      - Dokumentatsiooni genereerimine\n"
  },
  {
    "path": "Documentation/public/arhitektuur/vallasrezhiim.rst",
    "content": "..  IVXV arhitektuur\n\nRakendused\n==========\n\n\nÜldpõhimõtted\n-------------\n\nKõik rakendused on käsurealiidesega rakendused, mis on pakendatud töötama\noperatsioonisüsteemi Windows 10 (või uuem) keskkonnas. Komponentide\nkasutajaliidesed on ühekeelsed. Komponendid tarnitakse eestikeelsetena, nende\ntõlkimine on võimalik tõlkefaili abil.\n\nRakendused on programmeeritud Java keeles.\n\nVäliste infosüsteemidega suhtlevad rakendused kasutavad maksimaalselt\nolemasolevaid liideseid/andmestruktuure.\n\nRakendused saavad oma sisendi rakenduste seadistustest ja seadistustes näidatud\nfailidest failisüsteemis ning salvestavad oma väljundi kasutaja näidatud kausta\nfailisüsteemis. Failid võivad paikneda ka operatiiv-mälukettal.\n\nRelevantsed rakendused toetavad ElGamal krüptosüsteemi täisarvujäägikorpustel\nning P-384 elliptkõveral. Lugemistõend on realiseeritud Schnorri\nnullteadmustõestusel põhineval protokollil.\n\n.. figure:: model/img/app_modules.png\n\n   Rakenduste abimoodulid\n\nValimise liides on rakenduste jaoks unifitseeritud, see võimaldab erinevate\nvalimistüüpide realiseerimist moodulitena. Digiallkirja verifitseerimise\nfunktsionaalsus on loodud `digidoc4j <https://github.com/open-eid/digidoc4j>`_\nteegi abil. Abimoodulite kasutamist alljärgnevatel skeemidel eraldi välja ei\ntooda.\n\nRakenduste seadistamine\n```````````````````````\n\nRakendused seadistatakse kas digitaalallkirjastatud konfiguratsioonipakiga või\nkäsureavõtmetega. Käsureavõtmed ei toeta hierarhilise struktuuriga seadistuste\nsisestamist. Seadistused konfiguratsioonipakis kirjeldatakse YAML-keeles:\n\n.. code-block:: yaml\n\n   check:\n     ballotbox: votes.zip\n     ballotbox_checksum: votes.zip.sha256sum.bdoc\n     districts: TESTKOV2017.districts.json\n     registrationlist: register.zip\n     registrationlist_checksum: register.zip.sha256sum.bdoc\n     tskey: ts.pub.key\n     vlkey: test.gen.pub.key\n     voterlists:\n       -\n         path: 00.TESTKOV2017.gen.voters\n         signature: 00.TESTKOV2017.gen.voters.signature\n       -\n         path: 03.TESTKOV2017.gen.voters\n         signature: 03.TESTKOV2017.gen.voters.signature\n       -\n         path: 06.TESTKOV2017.gen.voters\n         signature: 06.TESTKOV2017.gen.voters.signature\n       -\n         path: 09.TESTKOV2017.gen.voters\n         signature: 09.TESTKOV2017.gen.voters.signature\n     election_start: 2017-05-01T12:00:00+03:00\n     out: out-1\n   squash:\n     ballotbox: out-1/bb-1.json\n     ballotbox_checksum: out-1/bb-1.json.sha256sum.bdoc\n     districts: TESTKOV2017.districts.json\n     out: out-2\n   revoke:\n     ballotbox: out-2/bb-2.json\n     ballotbox_checksum: out-2/bb-2.json.sha256sum.bdoc\n     districts: TESTKOV2017.districts.json\n     revocationlists:\n       - 12.TESTKOV2017.gen.revoke.json\n       - 13.TESTKOV2017.gen.revoke.json\n       - 14.TESTKOV2017.gen.revoke.json\n       - 15.TESTKOV2017.gen.revoke.json\n     out: out-3\n\n\nSisendite kooskõlalisuse kontroll\n`````````````````````````````````\n\nKõik rakendused teostavad konfiguratsioonile sisendite kooskõlalisuse kontrolli\nvastavalt nende poolt kasutatavale konfiguratsioonile:\n\n#. sertifikaatide konfiguratsiooni laadimine;\n\n#. konfiguratsiooni digiallkirja verifitseerimine;\n\n#. ringkondade nimekirja verifitseerimine;\n\n#. ringkondade nimekirja kooskõlalisuse kontroll;\n\n#. ringkondade nimekirja laadimine;\n\n#. valikute nimekirja verifitseerimine;\n\n#. valikute nimekirja kooskõlalisuse kontroll;\n\n#. valikute nimekirja laadimine;\n\n#. valijate nimekirjade verifitseerimine;\n\n#. valijate nimekirjade kooskõlalisus kontroll;\n\n#. valijate nimekirjade laadimine.\n\n\nVõtmerakendus\n-------------\n\n.. figure:: model/img/key.png\n\n   Võtmerakenduse liidesed\n\nVõtmerakendusega genereeritakse iga hääletamise jaoks häälte salastamise ja\nhäälte avamise võti, samuti toimub selle abil häälte lugemine ja tulemuse\nväljastamine.\n\nVõtmerakendus kasutab [DesmedtF89]_ läviskeemi, mis põhineb usaldataval\nosakujagajal ning rakendab Shamiri osakujagamist, mis on\ninformatsiooniteoreetiliselt turvaline :math:`t < M` osapoole korral, kus M on\nlävipiir.\n\nVõtmeosakud genereeritakse operatiivmälus ning talletatakse PKCS15-liidese\nvahendusel kiipkaardile.\n\nVõtmerakenduse sisend võtme genereerimisel on:\n\n- Võtmepaari identifikaator;\n\n- Krüptosüsteemi ElGamal spetsifikatsioon – täisarvujäägikorpus või P-384\n  elliptkõver ning võtmepikkus;\n\n- M-N läviskeemi spetsifikatsioon, mis peab vastama reeglile\n  :math:`N >= 2 * M - 1`;\n\n- N PKCS15-ühilduvat kiipkaarti;\n\nVõtmerakenduse väljund võtme genereerimisel on:\n\n- Isesigneeritud sertifikaat;\n\n- N võtmeosakut talletatuna kiipkaartidel;\n\n- Rakenduse detailne tegevuslogi;\n\n- Rakenduse detailne vealogi.\n\nVõtmerakenduse sisend häälte lugemisel on:\n\n- Miksitud hääled;\n\n- Võtmepaari identifikaator;\n\n- M võtmeosakut vastavalt läviskeemi spetsifikatsioonile.\n\nVõtmerakenduse väljund häälte lugemisel on:\n\n- Signeeritud hääletamistulemus;\n\n- Kehtetute häälte loend;\n\n- Lugemistõend (Schnorri nullteadmustõestusel põhinev protokoll vastavalt\n  hankedokumentides viidatule);\n\n- Rakenduse detailne tegevuslogi;\n\n- Rakenduse detailne vealogi.\n\nTöötlemisrakendus\n-----------------\n\nTöötlemisrakendusega verifitseeritakse, tühistatakse ning anonüümitakse\nhääletamisperioodil kogutud hääli vastavalt Üldkirjelduse jaotisele 7.6.\n\nTöötlemisrakenduse sisendid on:\n\n- kogumisteenuse poolt talletatud elektroonilised hääled;\n\n- registreerimisteenuse poolt väljastatud ajamärgendid;\n\n- valijate nimekirjad;\n\n- ringkondade nimekiri;\n\n- tühistusnimekirjad;\n\n- ennistusnimekirjad.\n\nTöötlemisrakenduse väljundid on:\n\n- rakenduse detailne tegevuslogi;\n\n- rakenduse detailne vealogi;\n\n- e-hääletanute nimekiri PDF-vormingus, vastavalt töötlemise etapile;\n\n- e-hääletanute nimekiri masintöödeldaval kujul, vastavalt töötlemise etapile;\n\n- anonüümitud hääled.\n\nLisaks varem defineeritud liidestele ja sõltuvustele kasutab töötlemisrakendus\nkolmanda osapoole teeki PDF'ide väljastamise funktsionaalsuse teostamiseks.\n\n.. figure:: model/img/processing.png\n\n   Töötlemisrakenduse liidesed\n\nElektrooniliste häälte täielik töötlemine\n`````````````````````````````````````````\n\nElektrooniliste häälte täielik töötlemine on tegevus, mille käigus\ntöötlemisrakendus võrdleb Kogumisteenuse poolt talletatud häälte hulka\nregistreerimisteenuse poolt talletatud häälte hulgaga, kontrollib talletatud\nhäälte vastavust valimiste konfiguratsioonile, tuvastab loendamisele minevad\nhääled ning anonüümib need Võtmerakendusele üle andmiseks.\n\n#. rakenduse seadistuste laadimine;\n\n#. elektrooniliste häälte digitaalallkirjade verifitseerimine;\n\n#. registreerimisteenuse kinnituste verifitseerimine;\n\n#. ajatemplite verifitseerimine;\n\n#. iga valija kohta viimase kehtiva hääle tuvastamine;\n\n#. algse elektrooniliselt hääletanute nimekirja väljastamine PDF-vormingus;\n\n#. tühistus- ja ennistusnimekirjade verifitseerimine;\n\n#. tühistus- ja ennistusnimekirjade kooskõlalisuse kontroll;\n\n#. tühistus- ja ennistusnimekirjade rakendamine;\n\n#. miksimisele minevate häälte nimekirja koostamine, krüptogrammide eraldamine\n   digitaalallkirjadest;\n\n#. lõpliku elektrooniliselt hääletanute nimekirja väljastamine masinloetavas\n   vormingus.\n\n\nElektrooniliselt hääletanute nimekirja genereerimine\n````````````````````````````````````````````````````\n\n#. rakenduse seadistuste laadimine;\n\n#. elektrooniliste häälte digitaalallkirjade verifitseerimine;\n\n#. algse elektrooniliselt hääletanute nimekirja väljastamine PDF-vormingus.\n\n\nAuditirakendus\n--------------\n\n.. figure:: model/img/audit.png\n\n   Auditirakenduse liidesed\n\nAuditirakendusega (joonis 9) verifitseeritakse matemaatiliselt häälte\nkokkulugemise korrektsust ning miksimise kasutamisel ka miksimise korrektsust.\n\nAuditirakenduse sisendid on;\n\n- anonüümitud hääled;\n\n- miksitud hääled;\n\n- Verificatumi miksimistõend;\n\n- hääletamistulemus.\n\nAuditirakenduse väljund on rakenduse detailne tegevuslogi, mis sisaldab ka\nhinnangut auditi tervikliku õnnestumise kohta. Vajadusel väljastatakse ka\nrakenduse detailne vealogi.\n"
  },
  {
    "path": "Documentation/public/arhitektuur/viited.rst",
    "content": "..  IVXV arhitektuur\n\n\n.. [DesmedtF89] Desmedt, Y. & Frankel, Y. Brassard, G. (Ed.) Threshold\n   Cryptosystems Advances in Cryptology - CRYPTO '89, 9th Annual International\n   Cryptology Conference, Santa Barbara, California, USA, August 20-24, 1989,\n   Proceedings, Springer, 1989, 435, 307-315.\n\n.. [HMVW16] Sven Heiberg, Tarvi Martens, Priit Vinkel, Jan Willemson, Improving\n   the verifiability of the Estonian Internet Voting scheme. In Robert Krimmer,\n   Melanie Volkamer, Jordi Barrat, Josh Benaloh, Nicole Goodman, Peter Y.A. Ryan,\n   Oliver Spycher, Vanessa Teague, Gregor Wenda (Eds.), The International\n   Conference on Electronic Voting E-Vote-ID 2016, 18-21 October 2016,\n   Lochau/Bregenz, Austria, TUT Press, pp. 213-229, ISBN 978-9949-83-022-0\n\n.. [TK2016] Tehniline kirjeldus. Elektroonilise hääletamise infosüsteemi\n   arenduse hange, Vabariigi Valimiskomisjon, 2016\n\n.. [ÜK2016] Elektroonilise hääletamise üldraamistik ja selle kasutamine Eesti\n   riiklikel valimistel. Elektroonilise Hääletamise Komisjon, Tallinn 2016\n"
  },
  {
    "path": "Documentation/public/arhitektuur/yldpohimotted.rst",
    "content": "..  IVXV arhitektuur\n\nSissejuhatus\n============\n\nElektroonilise hääletamise infosüsteem IVXV on loodud lähtuvalt e-hääletamise\nraamistikust [ÜK2016]_ ja riigihanke 171780 tehnilisest kirjeldusest [TK2016]_.\nKäesolevas dokumendis kirjeldatakse IVXV arhitektuurset lahendust. Elektroonilise\nhääletamise infosüsteem koosneb vallasrežiimirakendustest ning\nsidusrežiimikomponentidest. Täiendavalt sõltub infosüsteem välistest\ninfosüsteemidest ning mõjutab vahetult elektrooniliseks hääletamiseks ja hääle\nkontrollimiseks kasutatavaid komponente.\n\nArhitektuuridokument kirjeldab IVXV komponente, nende omavahelisi liideseid ja\nliideseid väliste süsteemidega ning komponentide poolt realiseeritavaid protokolle.\n\nIVXV kontseptsioon\n------------------\n\nÜldine, kuid terviklik ülevaade elektroonilise hääletamise raamistiku (\"IVXV\")\ntehnilisest ja organisatsioonilisest poolest ning selle rakendamisest Eesti\nriiklikel valimistel on antud e-hääletamise raamistiku üldkirjelduses\n[ÜK2016]_.\n\nIVXV infosüsteemina teostab \"ümbrikuskeemil\" põhinevat e-hääletamise\nprotokolli. IVXV toimib hääletamiseelsel etapil, hääletamisetapil,\ntöötlusetapil ning lugemisetapil ja pakub vahendeid elektroonilise hääletamise\nprotsessis osalemiseks Korraldajale, Lugejale, Hääletajale, Kogujale,\nTöötlejale, Miksijale, Audiitorile, Klienditoele, Valijate nimekirja koostajale\nja täiendajale.\n\nInfosüsteemi komponendid on Kogumisteenus, Töötlemisrakendus, Võtmerakendus\nning Auditirakendus. Infosüsteemiga on tihedalt seotud Valijarakendus,\nKontrollrakendus ning Miksimisrakendus.\n\nInfosüsteem kasutab oma töös väliseid teenuseid - Tuvastusteenus,\nAllkirjastamisteenus, Registreerimisteenus, Valimiste Infosüsteem ning X-tee.\n\nIVXV krüptograafiline protokoll\n-------------------------------\n\nElektroonilise hääletamise turvalisuse, verifitseeritavuse ning hääletamise\nsalajasuse, hääletamise korrektsuse ja hääletaja sõltumatuse saavutamiseks on\nrangelt kirjeldatud elektroonilise hääletamise krüptograafiline protokoll\n[HMVW16]_. Protokoll annab vajaliku ja piisava ülevaate IVXV ülesehitusest ning\nselle turvaaspektidest. IVXV komponendid realiseerivad krüptograafilise\nprotokolli alamosi.\n\nNotatsioon\n----------\n\nArhitektuurse lahenduse visandi illustreerimiseks kasutatakse dokumendis\nUML-skeeme, kus eristame värvide ja märgenditega ``<<>>`` kodeeritult olemite –\ntegijad, liidesed, komponendid – järgmisi aspekte:\n\n* Märgend ``<<IVXV>>`` (Kollane) – infosüsteemi liides või komponent\n  defineeritakse/realiseeritakse konkreetse pakkumuse raames tehtavate tööde\n  käigus\n\n* Märgend ``<<Väline>>`` (Punane) – infosüsteem sõltub mingi funktsionaalsuse\n  realiseerimisel kolmanda osapoole komponendist või olemasolevast liidesest,\n  mille ümberdefineerimine eeldab ka kolmandate osapoolte tööd.\n\n* Märgend ``<<VVK>>`` (Pruun) – sarnane eelmisele, kuid liidese/komponendi\n  omanikuks on VVK.\n\n* Märgend ``<<Määratlemata>>`` (Must) – infosüsteemi jaoks oluline liides on\n  määratlemata.\n\n.. figure:: model/img/example.png\n\n   Näiteskeem\n"
  },
  {
    "path": "Documentation/public/liidesed/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/public/liidesed/examples.rst",
    "content": "\nSkeemid ja näited\n=================\n\n---------------------------\nValimisringkondade nimekiri\n---------------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/1_Valimisringkondade_nimekiri/districts.schema\n   :language: json\n   :linenos:\n\nNäide: KOV\n----------\n\n.. literalinclude:: VIS3-EHS/1_Valimisringkondade_nimekiri/district_KOV.json\n   :language: json\n   :linenos:\n\nNäide: EP\n---------\n\n.. literalinclude:: VIS3-EHS/1_Valimisringkondade_nimekiri/districts_EP.json\n   :language: json\n   :linenos:\n\nNäide: RH\n---------\n\n.. literalinclude:: VIS3-EHS/1_Valimisringkondade_nimekiri/districts_RH.json\n   :language: json\n   :linenos:\n\nNäide: RK\n---------\n.. literalinclude:: VIS3-EHS/1_Valimisringkondade_nimekiri/districts_RK.json\n   :language: json\n   :linenos:\n\n-----------------\nValikute nimekiri\n-----------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/2_Valikute_nimekiri/choices.schema\n   :language: json\n   :linenos:\n\nNäide: EP\n---------\n.. literalinclude:: VIS3-EHS/2_Valikute_nimekiri/choices_EP.json\n   :language: json\n   :linenos:\n\nNäide: KOV\n----------\n\n.. literalinclude:: VIS3-EHS/2_Valikute_nimekiri/choices_KOV.json\n   :language: json\n   :linenos:\n\nNäide: RH\n---------\n.. literalinclude:: VIS3-EHS/2_Valikute_nimekiri/choices_RH.json\n   :language: json\n   :linenos:\n\nNäide: RK\n---------\n.. literalinclude:: VIS3-EHS/2_Valikute_nimekiri/choices_RK.json\n   :language: json\n   :linenos:\n\n----------------------\nE-hääletanute nimekiri\n----------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/4_e_haaletanute_nimekiri/onlinevoters.schema\n   :language: json\n   :linenos:\n\nNäide: EP\n---------\n.. literalinclude:: VIS3-EHS/4_e_haaletanute_nimekiri/onlinevoters_EP.json\n   :language: json\n   :linenos:\n\nNäide: KOV\n----------\n.. literalinclude:: VIS3-EHS/4_e_haaletanute_nimekiri/onlinevoters_KOV.json\n   :language: json\n   :linenos:\n\nNäide: RH\n---------\n.. literalinclude:: VIS3-EHS/4_e_haaletanute_nimekiri/onlinevoters_RH.json\n   :language: json\n   :linenos:\n\nNäide: RK\n---------\n.. literalinclude:: VIS3-EHS/4_e_haaletanute_nimekiri/onlinevoters_RK.json\n   :language: json\n   :linenos:\n\n----------------\nTühistusnimekiri\n----------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/5_Tyhistusnimekiri/revoke.schema\n   :language: json\n   :linenos:\n\nNäide: EP\n---------\n.. literalinclude:: VIS3-EHS/5_Tyhistusnimekiri/revoke_EP.json\n   :language: json\n   :linenos:\n\nNäide: KOV\n----------\n.. literalinclude:: VIS3-EHS/5_Tyhistusnimekiri/revoke_KOV.json\n   :language: json\n   :linenos:\n\nNäide: RH\n---------\n.. literalinclude:: VIS3-EHS/5_Tyhistusnimekiri/revoke_RH.json\n   :language: json\n   :linenos:\n\nNäide: RK\n---------\n.. literalinclude:: VIS3-EHS/5_Tyhistusnimekiri/revoke_RK.json\n   :language: json\n   :linenos:\n\n---------------------\nE-hääletamise tulemus\n---------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/6_e_haaletamise_tulemus/results.schema\n   :language: json\n   :linenos:\n\nNäide: EP\n---------\n.. literalinclude:: VIS3-EHS/6_e_haaletamise_tulemus/results_EP.json\n   :language: json\n   :linenos:\n\nNäide: KOV\n----------\n.. literalinclude:: VIS3-EHS/6_e_haaletamise_tulemus/results_KOV.json\n   :language: json\n   :linenos:\n\nNäide: RH\n---------\n.. literalinclude:: VIS3-EHS/6_e_haaletamise_tulemus/results_RH.json\n   :language: json\n   :linenos:\n\nNäide: RK\n---------\n.. literalinclude:: VIS3-EHS/6_e_haaletamise_tulemus/results_RK.json\n   :language: json\n   :linenos:\n\n---------------------------\nE-hääletamise üldstatistika\n---------------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/7_e_haaletamise_yldstatistika/online-voters-total.schema\n   :language: json\n   :linenos:\n\nNäide\n-----\n.. literalinclude:: VIS3-EHS/7_e_haaletamise_yldstatistika/online-voters-total.json\n   :language: json\n   :linenos:\n\n------------------------------\nE-hääletamise detailstatistika\n------------------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/8_e_haaletamise_detailstatistika/online-voters-counties.schema\n   :language: json\n   :linenos:\n\n-----------------------\nE-hääletamiste nimekiri\n-----------------------\n\nSkeem\n-----\n\n.. literalinclude:: VIS3-EHS/9_e_haaletamiste_nimekiri/ehs-xroad-api.yaml\n   :language: yaml\n   :linenos:\n\n"
  },
  {
    "path": "Documentation/public/liidesed/index.rst",
    "content": "..  IVXV registreerimisteenus\n\nIVXV registreerimisteenus\n=================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n\n.. toctree::\n   :maxdepth: 3\n   :numbered:\n\n   VIS3-EHS/README.md\n   VIS3-EHS/valimissündmuse_identifikaator\n   VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md\n   VIS3-EHS/2_Valikute_nimekiri/SPEC.md\n   VIS3-EHS/3_Valijate_nimekiri/SPEC.md\n   VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md\n   VIS3-EHS/5_Tyhistusnimekiri/SPEC.md\n   VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md\n   VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md\n   VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md\n   VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md\n   examples\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-03-02 12:43+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:31+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:1\nmsgid \"1 Valimisringkondade nimekiri\"\nmsgstr \"List of constituencies\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:5\nmsgid \"VIS3 edastab EHS-le teabe valimisringkondade kohta.\"\nmsgstr \"VIS3 transmits constituency information to EHS.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:7\nmsgid \"Andmed edastatakse inim-masin-protseduuriga:\"\nmsgstr \"The data are transmitted by human-machine procedure:\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:9\nmsgid \"VIS peakasutaja laeb JSON faili VIS3-st alla;\"\nmsgstr \"The VIS head user downloads the JSON file from VIS3;\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:11\nmsgid \"\"\n\"VIS peakasutaja allkirjastab faili digitaalselt väljaspool VIS-i ja annab \"\n\"selle EHS operaatorile;\"\nmsgstr \"\"\n\"The VIS head user digitally signs the file outside the VIS and gives it to \"\n\"the EHS operator;\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:13\nmsgid \"EHS operaator laeb digiallkirjastatud faili EHS-i.\"\nmsgstr \"The EHS operator uploads the digitally signed file to the EHS.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:15\nmsgid \"Fail on JSON-formaadis. Faili ei allkirjastata.\"\nmsgstr \"The file is in JSON format. The file will not be signed.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:17\nmsgid \"\"\n\"Faili struktuur on kirjeldatud JSON schema-ga. Kirjeldus on kooskõlastatud \"\n\"EHS omanikuga.\"\nmsgstr \"\"\n\"The file structure is described by a JSON schema. The description has been \"\n\"agreed with the EHS owner.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:19\nmsgid \"\"\n\"EHS senine liides VIS2-ga on spetsifitseeritud dokumendis \\\"IVXV \"\n\"protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), [https://www.\"\n\"valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.pdf](https://\"\n\"www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.pdf), \"\n\"jaotises 3.2 \\\"Valimisjaoskondade ja -ringkondade nimekiri\\\".\"\nmsgstr \"\"\n\"The current interface of the EHS with VIS2 is specified in the document \"\n\"\\\"Description of the IVXV protocols\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.\"\n\"pdf](https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf), section 3.2 \\\"List of polling stations and \"\n\"constituencies\\\".\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Valimistel, kus saavad osaleda alaliselt välisriigis elavad valijad \"\n\"kantakse nad valijate nimekirja tunnusega `FOREIGN`. Sellisel juhul peab \"\n\"valimisringkondade nimekiri sisaldama regiooni nende valijate jaoks ning \"\n\"iga ringkond peab sisaldama eraldi jaoskonda, kus nende häälte üle \"\n\"arvestust peetakse. Nii selle regiooni kui jaoskondade tunnuseks on \"\n\"fiktiivne EHAK: `0000`. Näitefailis [districts_RK.json](districts_RK.json) \"\n\"on näha nii regiooni korrektne lisamine kui ka välishääletajate jaoskonna \"\n\"korrektne lisamine igasse ringkonda.\"\nmsgstr \"\"\n\"In elections where voters permanently resident abroad are eligible, they \"\n\"are entered on the electoral roll with the symbol `FOREIGN`. In this case, \"\n\"the list of constituencies must include the region for these voters and \"\n\"each constituency must include a separate polling station where their votes \"\n\"are counted. Both this region and the precincts shall be identified by the \"\n\"fictitious EHAK: `0000'. The example file [districts_RK.json](districts_RK.\"\n\"json) shows both the correct inclusion of a region and the correct \"\n\"inclusion of an out-of-town voter precinct in each district.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:25\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:27\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:29\nmsgid \"Faili struktuur (JSON-skeem): [districts.schema](districts.schema)\"\nmsgstr \"File structure (JSON schema): [districts.schema](districts.schema)\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:31\nmsgid \"Näited (JSON):\"\nmsgstr \"Examples (JSON):\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:33\nmsgid \"[district_KOV.json](district_KOV.json)\"\nmsgstr \"[district_KOV.json](district_KOV.json)\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:34\nmsgid \"[districts_EP.json](districts_EP.json)\"\nmsgstr \"[districts_EP.json](districts_EP.json)\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:35\nmsgid \"[districts_RH.json](districts_RH.json)\"\nmsgstr \"[districts_RH.json](districts_RH.json)\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:36\nmsgid \"[districts_RK.json](districts_RK.json)\"\nmsgstr \"[districts_RK.json](districts_RK.json)\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:38\nmsgid \"Taustainfo\"\nmsgstr \"Background information\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:40\nmsgid \"\"\n\"Kandidaate on võimalik valimisele üles seada ainult konkreetses \"\n\"valimisringkonnas. Ringkondade järgi antakse valijatele hääletamise valikud:\"\nmsgstr \"\"\n\"For any voter it is only possible to vote for candidates from a single \"\n\"constituency. Voters are given voting options by constituency:\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:42\nmsgid \"Iga valija kuulub talle määratud valimisringkonda;\"\nmsgstr \"Each voter belongs to the constituency assigned to him or her;\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:43\nmsgid \"\"\n\"Kõigis ühe ringkonna jaoskondades saavad valijad teha valiku vaid selle \"\n\"ringkonna valikute vahel;\"\nmsgstr \"\"\n\"In each of the polling station in a district, voters can only choose \"\n\"between the options for that district;\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:45\nmsgid \"\"\n\"Eesti riiklikel valimistel eristatakse kohalike omavalitsuste volikogude \"\n\"(KOV) valimisi, Riigikogu valimisi, Euroopa Parlamendi valimisi ning \"\n\"rahvahääletusi.\"\nmsgstr \"\"\n\"In Estonian national elections, a distinction is made between local \"\n\"government elections, Riigikogu elections, European Parliament elections \"\n\"and referendums.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:47\nmsgid \"\"\n\"KOV valimised korraldatakse vastavalt seadusele „Kohaliku omavalitsuse \"\n\"volikogu valimise seadus“. Valimine toimub kohaliku omavalitsuse tasandil, \"\n\"igal omavalitsusel on oma hääletamistulemus. Valimisringkonnad \"\n\"moodustatakse omavalitsuse tasemel vastavalt seaduses kirjeldatud \"\n\"reeglitele.\"\nmsgstr \"\"\n\"Local elections are organised in accordance with the law \\\"Law on the \"\n\"election of local government councils\\\". Elections are held at the level of \"\n\"the local municipality, each municipality has its own voting results. \"\n\"Constituencies are formed at the municipal level according to the rules \"\n\"described in the law.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:49\nmsgid \"\"\n\"Riigikogu valimised korraldatakse vastavalt seadusele „Riigikogu valimise \"\n\"seadus“. Valimine toimub riigi tasandil. Riik jaguneb 12 \"\n\"valimisringkonnaks. Hääletamistulemus tehakse kindlaks iga valimisringkonna \"\n\"kohta.\"\nmsgstr \"\"\n\"Elections to the Riigikogu are organised in accordance with the Act on the \"\n\"Election of the Riigikogu. Elections are held at the national level. The \"\n\"country is divided into 12 constituencies. The result of the vote is \"\n\"determined for each constituency.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:51\nmsgid \"\"\n\"Europarlamendi valimised korraldatakse vastavalt seadusele „Euroopa \"\n\"Parlamendi valimise seadus“. Valimine toimub riigi tasandil, \"\n\"hääletamistulemus on kõigile kohalikele omavalitsustele ühine. Terve riik \"\n\"on üks valimisringkond.\"\nmsgstr \"\"\n\"The elections to the European Parliament are organised in accordance with \"\n\"the law \\\"Law on the election of the European Parliament\\\". Elections are \"\n\"held at national level, with the result of the vote being common to all \"\n\"local authorities. The whole country is one constituency.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:53\nmsgid \"\"\n\"Rahvahääletused korraldatakse vastavalt seadusele „Rahvahääletuse seadus“. \"\n\"Valimine toimub riigi tasandil, hääletamistulemus on kõigile kohalikele \"\n\"omavalitsustele ühine. Terve riik on üks valimisringkond.\"\nmsgstr \"\"\n\"Referendums are organised in accordance with the law \\\"Referendums Act\\\". \"\n\"Elections are held at the national level, and the result of the vote is \"\n\"common to all local authorities. The whole country is one constituency.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:55\nmsgid \"\"\n\"Erinevad valimised ei erine elektroonilise hääletamise andmevormingute ja \"\n\"protseduuride poolest. Erinevad ringkondade jaotused hallatakse VIS3 poolt.\"\nmsgstr \"\"\n\"The different elections do not differ in terms of electronic voting data \"\n\"formats and procedures. The different constituency breakdowns are managed \"\n\"by VIS3.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:57\nmsgid \"\"\n\"Kandidaate on võimalik valimisele üles seada ainult konkreetses \"\n\"valimisringkonnas. Valijad on jaotatud valimisringkondade vahel. Valija \"\n\"saab teha valiku ainult tema ringkonnas kandideerivate kandidaatide vahel.\"\nmsgstr \"\"\n\"It is only possible to stand as a candidate in a particular constituency. \"\n\"Voters are allocated between constituencies. Voters can only choose between \"\n\"candidates standing in their constituency.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:59\nmsgid \"\"\n\"Kuna kohaliku omavalitsuse volikogude valimisel toimub valimine Eesti \"\n\"omavalitsuste (vallad, linnad) tasemel, siis kasutatakse elektroonilise \"\n\"hääletamise protokollistikus valimisringkondade kirjeldamisel ning valijate \"\n\"ja valikute ringkonnakuuluvuse näitamisel Eesti haldus- ja asustusjaotuse \"\n\"klassifikaatorit EHAK\"\nmsgstr \"\"\n\"Since the election of local government councils is held at the level of \"\n\"Estonian local governments (municipalities, cities), the Estonian \"\n\"Administrative and Settlement Classification of Estonia (EHAK) is used in \"\n\"the electronic voting protocol to describe the electoral districts and to \"\n\"indicate the district coverage of voters and choices\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:61\nmsgid \"\"\n\"Näiteks: • Tallinna linna Pirita linnaosa EHAK kood on 0596; • Anija valla \"\n\"EHAK kood on 0141.\"\nmsgstr \"\"\n\"For example: - the EHAK code of Pirita district of Tallinn is 0596; - the \"\n\"EHAK code of Anija municipality is 0141.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:65\nmsgid \"\"\n\"Riigi tasemel toimuvatel valimistel pannakse ringkonna EHAK koodiks \"\n\"kokkuleppeliselt 0.\"\nmsgstr \"\"\n\"For national elections, the district EHAK code is set to 0 by convention.\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:67\nmsgid \"\"\n\"Riigikogu ja europarlamendi valimistel ning rahvahääletusel moodustatakse \"\n\"nimekirjas igasse ringkonda fiktiivne üksus alaliselt välisriigis elavate \"\n\"valijate tarbeks. Selle üksuse number on 0 ning vastav EHAK kood on 0000.\"\nmsgstr \"\"\n\"In Riigikogu and European Parliament elections and in referendums, a \"\n\"fictitious unit is created in each district for voters permanently resident \"\n\"abroad. The number of this unit is 0 and the corresponding EHAK code is \"\n\"0000.\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/2_Valikute_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-03-02 12:43+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:34+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:1\nmsgid \"2 Valikute nimekiri (kandidaatide nimekiri)\"\nmsgstr \"List of Candidates\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:5\nmsgid \"\"\n\"VIS3 edastab EHS-le andmed registreeritud kandidaatide (valimistel) või \"\n\"vastusevariantide (rahvahääletusel) kohta.\"\nmsgstr \"\"\n\"VIS3 transmits to EHS data on registered candidates (in elections) or \"\n\"options (in referendums).\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:7\nmsgid \"Andmed edastatakse inim-masin-protseduuriga:\"\nmsgstr \"The data are transmitted by human-machine procedure:\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:9\nmsgid \"\"\n\"VIS peakasutaja laeb JSON faili VIS3-st alla; Kasutaja peab valima \"\n\"aktiivsete valimissündmuste seast soovitud sündmuse, mille kohta infot alla \"\n\"laadida. Valimissündmus on aktiivne, kui tema staatus pole `closed` ega \"\n\"`deleted`.\"\nmsgstr \"\"\n\"The VIS head user downloads a JSON file from VIS3; the user has to select \"\n\"the desired event to download information about from the list of active \"\n\"election events. An election event is active if its status is neither \"\n\"`closed` nor `deleted`.\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:10\nmsgid \"\"\n\"VIS peakasutaja allkirjastab faili digitaalselt väljaspool VIS-i ja annab \"\n\"selle EHS operaatorile;\"\nmsgstr \"\"\n\"The VIS head user digitally signs the file outside the VIS and gives it to \"\n\"the EHS operator;\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:11\nmsgid \"EHS operaator laeb digiallkirjastatud faili EHS-i.\"\nmsgstr \"The EHS operator uploads the digitally signed file to the EHS.\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:13\nmsgid \"\"\n\"Fail on JSON-formaadis (näidis lisatud tööülesandele). Faili ei \"\n\"allkirjastata.\"\nmsgstr \"\"\n\"The file is in JSON format (sample attached to the assignment). The file is \"\n\"not signed.\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:15\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:17\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis \"\n\"\\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.\"\n\"pdf](https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf), jaotises \\\"Valikute nimekiri\\\" (jaotis 3.4).\"\nmsgstr \"\"\n\"It is based on the current interface of the EHS with VIS2, as specified in \"\n\"the document \\\"Description of the IVXV protocols\\\" (v 1.5.0, IVXV-PR-1.5.0, \"\n\"20.04.2019), [https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh/\"\n\"IVXV-protokollid.pdf), section \\\"List of options\\\" (section 3.4).\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:19\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:21\nmsgid \"Faili struktuur (JSON-skeem): [choices.schema](choices.schema)\"\nmsgstr \"File structure (JSON schema): [choices.schema](choices.schema)\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:23\nmsgid \"Faili näited (JSON):\"\nmsgstr \"File examples (JSON):\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:25\nmsgid \"[choices_KOV.json](choices_KOV.json)\"\nmsgstr \"[choices_KOV.json](choices_KOV.json)\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:26\nmsgid \"[choices_EP.json](choices_EP.json)\"\nmsgstr \"[choices_EP.json](choices_EP.json)\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:27\nmsgid \"[choices_RK.json](choices_RK.json)\"\nmsgstr \"[choices_RK.json](choices_RK.json)\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:29\nmsgid \"Taustainfo\"\nmsgstr \"Background information\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:31\nmsgid \"\"\n\"Valikute nimekiri sisaldab andmeid kandidaatide (valimistel) või \"\n\"vastusevariantide (rahvahääletusel) kohta. Valimiste korral on lisaks \"\n\"kandidaadi andmetele nimekirjas ka tema erakonna või valimisliidu nimi, \"\n\"mille nimekirjas ta kandideerib.\"\nmsgstr \"\"\n\"The list of choices contains information on the candidates (in the \"\n\"election) or the options (in the referendum). In the case of an election, \"\n\"the list includes, in addition to the candidate's details, the name of the \"\n\"political party or electoral alliance on whose list he or she is standing.\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:34\nmsgid \"\"\n\"Valijale elektroonilise hääletamise käigus nähtavaid valimiste vahelisi \"\n\"süsteemseid erinevusi on kaks:\"\nmsgstr \"\"\n\"There are two systemic differences between elections that are visible to \"\n\"the voter when voting electronically:\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:36\nmsgid \"\"\n\"Rahvahääletusel ei valita erakondadesse kuuluvate kandidaatide vahel vaid \"\n\"vastatakse „JAH“/“EI“ rahvahääletuse küsimusele;\"\nmsgstr \"\"\n\"The referendum is not a choice between candidates belonging to political \"\n\"parties, but a \\\"YES\\\"/\\\"NO\\\" answer to a referendum question;\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:38\nmsgid \"\"\n\"Riigikogu, KOV ja Euroopa Parlamendi valimistel antakse hääl ühele \"\n\"kandidaadile, kes võib, aga ei pruugi kuuluda poliitilise ühenduse \"\n\"nimekirja.\"\nmsgstr \"\"\n\"In the elections to the Riigikogu, Municipalities and the European \"\n\"Parliament, votes are cast for one candidate, who may or may not be on the \"\n\"list of a political association.\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:40\nmsgid \"\"\n\"Protokollistik kodeerib valija võimalikud valikud ringkonnas kuni 11-\"\n\"kohalise arvväärtusena, mis valikute nimekirjas kodeeritakse koos ringkonna \"\n\"EHAK-koodiga. Valijale tohivad kättesaadavad olla ainult tema \"\n\"ringkonnakohased valikud. Valijarakendus peab seda omadust tagama ning \"\n\"hääletamistulemust arvutav rakendus kontrollima.\"\nmsgstr \"\"\n\"The protocol encodes the voter's possible choices in the district as a \"\n\"numeric value of up to 11 digits, which in the list of choices is encoded \"\n\"together with the district's EHAK code. A voter may only have options \"\n\"available for his/her district. The voter application must guarantee this \"\n\"property and the application calculating the vote result must check it.\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/3_Valijate_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:04+0000\\n\"\n\"PO-Revision-Date: 2024-03-02 13:43+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:1\nmsgid \"3  Valijate nimekiri EHS-le\"\nmsgstr \"Voters list for the EHS\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:3\nmsgid \"1. Annotatsioon\"\nmsgstr \"Annotation\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:5\nmsgid \"\"\n\"Spetsifikatsioon määratleb Valimiste infosüsteemi (VIS3) ja e-hääletamise\"\n\" süsteemi (EHS) vahelise liidese, mille kaudu VIS3 edastab EHS-le \"\n\"valijate algnimekirja ja valijate nimekirja muudatusi ehk \"\n\"muudatusnimekirju.\"\nmsgstr \"\"\n\"The specification defines the interface between the Election Information \"\n\"System (VIS3) and the e-voting system (EHS), through which VIS3 transmits\"\n\" to EHS the initial voters' list and changes to the voters' list, or \"\n\"change lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:9\nmsgid \"\"\n\"Spetsifikatsioon on avalik. Spetsifikatsioon ei käsitle VIS3 ega EHS \"\n\"konfidentsiaalset siseehitust ega liidese konfidentsiaalseid elemente.\"\nmsgstr \"\"\n\"The specification is public. The specification does not address the \"\n\"confidential internal architecture of VIS3 or EHS, nor the confidential \"\n\"elements of the interface.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:12\nmsgid \"\"\n\"Käesolevat spetsifikatsiooni tuleb kasutada koos VIS3 EHS API OpenAPI \"\n\"spetsifikatsiooniga (asub käesolevas repos, failis `vis-ehs-api.yaml`).\"\nmsgstr \"\"\n\"This specification should be used in conjunction with the VIS3 EHS API \"\n\"OpenAPI specification (located in this repo, in the file `vis-ehs-\"\n\"api.yaml`).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:14\nmsgid \"2. Sõnumivahetus nimekirjade edastamiseks\"\nmsgstr \"Exchange of messages for the transmission of lists\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:16\nmsgid \"\"\n\"Valijate nimekirja põhjal tuvastab EHS isiku hääleõiguse ja \"\n\"ringkonnakuuluvuse. Valijate nimekiri võib hääletamisperioodi jooksul \"\n\"muutuda, sellest tulenevalt võib valija saada hääleõiguse, jääda oma \"\n\"hääleõigusest ilma või saada hääleõiguse senisest erinevas ringkonnas. \"\n\"EHS peab nende muutustega arvestama. Muudatused saab EHS VIS3 vahendusel \"\n\"muudatusnimekirjadena.\"\nmsgstr \"\"\n\"Based on the voters' list, the EHS determines a person's eligibility to \"\n\"vote and district eligibility. The electoral roll may change during the \"\n\"voting period, and as a result, a voter may become eligible to vote, be \"\n\"disenfranchised, or become eligible to vote in a different district. The \"\n\"EHS must take these changes into account. The changes can be obtained by \"\n\"EHS via VIS3 as change lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:22\nmsgid \"\"\n\"Spetsifikatsioonis defineeritud liidese vahendusel suhtlevad vahetult EHS\"\n\" ja VIS3:\"\nmsgstr \"\"\n\"EHS and VIS3 communicate directly via the interface defined in the \"\n\"specification:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:25\nmsgid \"\"\n\"VIS3 liidestub Rahvastikuregistri andmekoguga ning hangib seal nii \"\n\"valijate algnimekirja kui ka muudatusnimekirjad.\"\nmsgstr \"\"\n\"VIS3 interfaces with the Population Register database and retrieves both \"\n\"the initial voters' list and the amended voters' lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:27\nmsgid \"EHS esitab VIS3-le päringuid muudatusnimekirjade saamiseks.\"\nmsgstr \"The EHS will query VIS3 for change lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:29\nmsgid \"Kaudselt on liidesega seotud:\"\nmsgstr \"Indirectly linked to the interface:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:31\nmsgid \"Rahvastikuregister, kus toimub nimekirjade haldamine;\"\nmsgstr \"Population register, where the lists are managed;\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:32\nmsgid \"\"\n\"VIS3 ja EHS operaatorid, kes vahetavad taustakanalis algnimekirja ning \"\n\"suhtlevad võimalike tõrgete lahendamisel;\"\nmsgstr \"\"\n\"VIS3 and EHS operators exchanging the initial list in the background \"\n\"channel and communicating on possible glitches;\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:34\nmsgid \"Audiitor, kes veendub et EHS on VIS3 poolt tarnitud nimekirjad rakendanud;\"\nmsgstr \"\"\n\"An auditor who makes sure that EHS has implemented the lists delivered by\"\n\" VIS3;\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:35\nmsgid \"\"\n\"EHS vallasrežiimis töötlemisrakendus, mis verifitseerib EHS sidusrežiimi \"\n\"komponentide poolt üle antud urni.\"\nmsgstr \"\"\n\"A processing application in EHS standalone mode that verifies the ballot \"\n\"box passed by the EHS online mode components.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:38\nmsgid \"2.1 Ettevalmistused nimekirjade edastamiseks\"\nmsgstr \"Preparations for the transmission of lists\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:40\nmsgid \"![Joonis 1: Sõnumivahetuse ettevalmistamine](model/list_prepare.png)\"\nmsgstr \"![Figure 1: Preparing a message exchange](model/list_prepare.png)\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:40\nmsgid \"Joonis 1: Sõnumivahetuse ettevalmistamine\"\nmsgstr \"Figure 1: Preparing the exchange of messages\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:42\nmsgid \"\"\n\"Valijate alg- ja muudatusnimekirjade vahetamisele eelnevalt tuleb \"\n\"vahetada nimekirjadele juurdepääsuks ja nimekirjade autentsuse \"\n\"kontrolliks vajalikud võtmed ja sertifikaadid (sammud 1-5)\"\nmsgstr \"\"\n\"Prior to the exchange of the voters' initial and amended lists, the keys \"\n\"and certificates needed to access the lists and to verify their \"\n\"authenticity must be exchanged (steps 1-5).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:46\nmsgid \"2.2 Valijate algnimekirja edastamine\"\nmsgstr \"Transmission of the initial list of voters\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:48\nmsgid \"![Joonis 2: Algnimekirja edastamine](model/list_initial.png)\"\nmsgstr \"![Figure 2: Transmitting the initial list](model/list_initial.png)\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:48\nmsgid \"Joonis 2: Algnimekirja edastamine\"\nmsgstr \"Figure 2: Transmission of the initial list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:50\nmsgid \"\"\n\"Valijate algnimekiri järjekorranumbriga 0 laetakse EHSi haldusliidese \"\n\"veebiliidesest. Valijate algnimekirja laadimine on eelduseks \"\n\"muudatusnimekirjade edasiseks automaatseks laadimiseks.\"\nmsgstr \"\"\n\"The initial list of voters with order number 0 is downloaded from the web\"\n\" interface of the EHS management interface. The loading of the initial \"\n\"list of voters is a prerequisite for further automatic loading of the \"\n\"modification lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:54\nmsgid \"Valijate algnimekirja laadimine toimub järgmistes etappides:\"\nmsgstr \"\"\n\"The loading of the initial list of voters will take place in the \"\n\"following steps:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:56\nmsgid \"\"\n\"VIS3 kasutab Rahvastikuregistri X-tee teenust valijate algnimekirja \"\n\"laadimiseks (sammud 1-3).\"\nmsgstr \"\"\n\"VIS3 uses the Population Register X-Road service to load the initial list\"\n\" of voters (steps 1-3).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:58\nmsgid \"\"\n\"VIS3 peakasutaja pärib allkirjastatud algnimekirja VIS3 teenusest, ning \"\n\"allkirjastab algnimekirja täiendavalt ID-kaardiga (sammud 4-6).\"\nmsgstr \"\"\n\"The VIS3 end-user inherits the signed initial list from the VIS3 service,\"\n\" and additionally signs the initial list with an ID card (steps 4-6).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:60\nmsgid \"\"\n\"VIS3 peakasutaja edastab allkirjastatud algnimekirja EHS operaatorile \"\n\"(samm 7)\"\nmsgstr \"\"\n\"The VIS3 head user forwards the signed initial list to the EHS operator \"\n\"(step 7).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:62\nmsgid \"\"\n\"EHS operaator laeb digitaalselt allkirjastatud algnimekirja EHSi, kus see\"\n\" rakendatakse (sammud 8-9).\"\nmsgstr \"\"\n\"The EHS operator uploads the digitally signed initial list to the EHS \"\n\"where it is implemented (steps 8-9).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:65\nmsgid \"2.3 Valijate muudatusnimekirja edastamine\"\nmsgstr \"Transmission of the voters' change list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:67\nmsgid \"![Joonis 3: Muudatusnimekirja edastamine](model/list_changeset.png)\"\nmsgstr \"![Figure 3: Forwarding a change list](model/list_changeset.png)\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:67\nmsgid \"Joonis 3: Muudatusnimekirja edastamine\"\nmsgstr \"Figure 3: Forwarding the revision list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:69\nmsgid \"\"\n\"Valijate nimekirja muudatusnimekirja laadimise algatab EHS. \"\n\"Muudatusnimekirjad on järjestatud, nii EHS kui ka VIS3 peavad arvet, \"\n\"milline on viimane loodud muudatusnimekiri.\"\nmsgstr \"\"\n\"The EHS initiates the loading of the voters' list. The changelists are \"\n\"ordered, both EHS and VIS3 keep a record of the last changelist created.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:73\nmsgid \"\"\n\"EHS algatab uue muudatusnimekirja laadimise pöördudes uue \"\n\"järjekorranumbriga VIS3 vastava otspunkti poole. EHS võib varasemaid \"\n\"muudatusnimekirju uuesti pärida, kasutades varasemat järjekorranumbrit.\"\nmsgstr \"\"\n\"EHS initiates the loading of the new changelist by addressing the \"\n\"corresponding VIS3 endpoint with the new queue number. EHS may re-request\"\n\" previous changelists using the previous sequence number.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:77\nmsgid \"\"\n\"VIS3 saab muudatused Rahvastikuregistri X-tee teenusest. VIS3 poolne \"\n\"muudatuste hankimine on EHSile muudatuste esitamisest sõltumatu \"\n\"paralleelprotsess. See tähendab, et üks EHSile minev muudatusnimekiri \"\n\"võib sisaldada mitut Rahvastikuregistrist tulnud muudatust.\"\nmsgstr \"\"\n\"The VIS3 will receive changes from the X-Road service of the Population \"\n\"Register. The retrieval of amendments by VIS3 is a parallel process \"\n\"independent of the submission of amendments to the EHS. This means that a\"\n\" single list of changes going to the EHS may contain several changes \"\n\"coming from the Population Register.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:82\nmsgid \"Muudatusnimekirjade edastamine EHSi käib järgmiselt:\"\nmsgstr \"The transmission of the changelists to the ETS takes place as follows:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:84\nmsgid \"EHS esitab päringu muudatusnimekirja saamiseks (samm 1)\"\nmsgstr \"EHS will submit a request for a change list (step 1).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:85\nmsgid \"Kui muudatusi ei ole, siis ei ole ka muudatusnimekirja (samm 2)\"\nmsgstr \"If there are no changes, there is no changelist (step 2).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:86\nmsgid \"\"\n\"EHS kordab mingi aja möödudes päringut muudatusnimekirja saamiseks (samm \"\n\"3)\"\nmsgstr \"EHS repeats the query after some time to get the changelist (step 3).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:87\nmsgid \"\"\n\"VIS3 edastab vahepeal saabunud muudatused EHSile ning EHS rakendab need \"\n\"edukalt (sammud 4-5)\"\nmsgstr \"\"\n\"VIS3 forwards the changes that have arrived in the meantime to EHS and \"\n\"EHS successfully implements them (steps 4-5).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:89\nmsgid \"\"\n\"Nii VIS3 kui EHS suurendavad muudatusnimekirjade järjekorranumbrit ning \"\n\"mõne aja möödudes edastatakse uus muudatusnimekiri (sammud 6-8)\"\nmsgstr \"\"\n\"Both VIS3 and EHS will increment the queue number of the changelists and \"\n\"after some time a new changelist will be transmitted (steps 6-8).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:92\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:96\nmsgid \"**Märkused**\"\nmsgstr \"**Notes**\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:93\nmsgid \"\"\n\"Kui järjekorras järgmise muudatusnimekirja pärimisel muudatuskirjed \"\n\"puuduvad vastatakse HTTP staatusega `404` (not found).\"\nmsgstr \"\"\n\"If the next in the queue of changelists is queried and no changelists are\"\n\" found, the HTTP status `404` (not found) is returned.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:97\nmsgid \"\"\n\"Kui päritakse muudatusnimekirja suurema järjenumbriga, kui tegelik \"\n\"järgmise muudatusnimekirja järjenumber, vastatakse HTTP staatusega `409` \"\n\"(conflict).\"\nmsgstr \"\"\n\"If a changelist with a higher sequence number than the actual sequence \"\n\"number of the next changelist is received, HTTP status `409` (conflict) \"\n\"is returned.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:100\nmsgid \"2.4 Veajuhtumite lahendamine\"\nmsgstr \"Troubleshooting situations\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:102\nmsgid \"![Joonis 4: Veajuhtumite lahendamine](model/list_errors.png)\"\nmsgstr \"![Figure 4: Error handling](model/list_errors.png)\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:102\nmsgid \"Joonis 4: Veajuhtumite lahendamine\"\nmsgstr \"Figure 4: Error handling\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:104\nmsgid \"\"\n\"Muudatusnimekirjade edastamisel ei saa välistada vigu. Olenevalt vea \"\n\"iseloomust on võimalik taaste, keerulisematel juhtudel tuleb mõni \"\n\"vigaseks osutunud nimekiri vahele jätta. Veajuhtumi menetlemise ajal \"\n\"muudatusnimekirju ei edastata.\"\nmsgstr \"\"\n\"Errors cannot be excluded in the transmission of changelists. Depending \"\n\"on the nature of the error, it may be possible to revert, in more complex\"\n\" cases some lists that are found to contain errors should be skipped. No \"\n\"changelists will be forwarded during the processing of an error.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:109\nmsgid \"Veajuhtumite lahendamine toimub järgmiselt:\"\nmsgstr \"Error handling is as follows:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:111\nmsgid \"\"\n\"EHS ja VIS3 käivitavad protokolli muudatusnimekirja edastamiseks (sammud \"\n\"1-3).\"\nmsgstr \"\"\n\"EHS and VIS3 will trigger a protocol for the transmission of the \"\n\"changelist (steps 1-3).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:113\nmsgid \"\"\n\"EHS tunnistab nimekirja vigaseks ning teavitab sellest operaatorit (samm \"\n\"4).\"\nmsgstr \"EHS will declare the list as corrupt and inform the operator (step 4).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:114\nmsgid \"\"\n\"Probleemi analüüsides tuvastatakse EHS-poolne tõrge, mida on võimalik \"\n\"lahendada. Tõrge lahendatakse ning EHS ja VIS3 kordavad edukalt \"\n\"protokolli (sammud 5-7).\"\nmsgstr \"\"\n\"Analysis of the problem identifies an EHS failure that can be resolved. \"\n\"The fault is resolved and EHS and VIS3 successfully replicate the \"\n\"protocol (steps 5-7).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:117\nmsgid \"\"\n\"EHS ja VIS3 käivitavad protokolli muudatusnimekirja edastamiseks (sammud \"\n\"8-10).\"\nmsgstr \"\"\n\"EHS and VIS3 will trigger a protocol for the transmission of the \"\n\"changelist (steps 8-10).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:119\nmsgid \"\"\n\"EHS tunnistab nimekirja vigaseks ning teavitab sellest operaatorit (samm \"\n\"11).\"\nmsgstr \"EHS will declare the list as corrupt and inform the operator (step 11).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:121\nmsgid \"\"\n\"Probleemi analüüsides tuvastatakse sisuline probleem nimekirjas. Vigast \"\n\"nimekirja ei muudeta, luuakse uus korrektne nimekiri. EHS operaator laeb \"\n\"digitaalselt allkirjastatud korralduse vigase nimekirja vahele jätmiseks \"\n\"(samm 12)\"\nmsgstr \"\"\n\"Analysing the problem identifies a substantive problem with the list. The\"\n\" incorrect list is not modified, a new correct list is created. The EHS \"\n\"operator uploads a digitally signed order to skip the incorrect list \"\n\"(step 12).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:125\nmsgid \"\"\n\"EHS ja VIS3 suurendavad järjekorranumbrit ning käivitavad protokolli \"\n\"muudatusnimekirja edastamiseks (sammud 13-15).\"\nmsgstr \"\"\n\"EHS and VIS3 increment the queue number and start a protocol to transmit \"\n\"the changelist (steps 13-15).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:127\nmsgid \"\"\n\"EHS ja VIS3 suurendavad järjekorranumbrit ning käivitavad protokolli \"\n\"muudatusnimekirja edastamiseks (sammud 16-17). VIS3 vastab HTTP \"\n\"staatusega 409 (conflict).\"\nmsgstr \"\"\n\"EHS and VIS3 increment the queue number and start a protocol to transmit \"\n\"the changelist (steps 16-17). VIS3 responds with HTTP status 409 \"\n\"(conflict).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:130\nmsgid \"\"\n\"EHS tuvastab vale järjenumbriga nimekirja laadimise ning teavitab sellest\"\n\" operaatorit (samm 18).\"\nmsgstr \"\"\n\"The EHS detects the loading of a list with the wrong sequence number and \"\n\"informs the operator (step 18).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:132\nmsgid \"\"\n\"Tõrge lahendatakse ning EHS ja VIS3 kordavad edukalt protokolli (sammud \"\n\"19-21).\"\nmsgstr \"\"\n\"The failure is resolved and EHS and VIS3 successfully repeat the protocol\"\n\" (steps 19-21).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:134\nmsgid \"2.5 Nimekirjade loendi edastamine\"\nmsgstr \"Transmission of the list of lists\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:136\nmsgid \"3. Nimekirja andmevorming\"\nmsgstr \"Data format of the list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:138\nmsgid \"\"\n\"Nimekirja andmevorming on sama nii valijate algnimekirja kui \"\n\"muudatusnimekirjade jaoks.\"\nmsgstr \"\"\n\"The list data format is the same for both the initial list of voters and \"\n\"the amended lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:141\nmsgid \"\"\n\"Nimekiri esitatakse UTF-8-NOBOM vormingus tekstifailina. \"\n\"Andmestruktuuride eraldajaks kasutatakse reavahetusmärki `LF` (ASCII-kood\"\n\" `0x0A`). Andmestruktuuride väljade eraldajaks kasutatakse tabeldusmärki \"\n\"`TAB` (ASCII-kood `0x09`).\"\nmsgstr \"\"\n\"The list is presented as a UTF-8-NOBOM text file. The separator of the \"\n\"data structures shall be the space character `LF` (ASCII code `0x0A`). \"\n\"Fields of data structures shall be delimited by the tab delimiter `TAB` \"\n\"(ASCII code `0x09`).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:146\nmsgid \"Nimekiri koosneb päiseridadest ja kirjetest.\"\nmsgstr \"The list consists of headers and entries.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:148\nmsgid \"Päiseridade sisu on järgmine:\"\nmsgstr \"The content of the headlines is as follows:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:150\nmsgid \"\"\n\"`version` - andmestruktuuri versiooninumber, mille pikkus on piiratud 2 \"\n\"tähemärgiga. Spetsifikatsioonile vastava nimekirja korral on selle välja \"\n\"väärtus 2.\"\nmsgstr \"\"\n\"`version` - version number of the data structure, limited to 2 \"\n\"characters. For a specification conforming list, its field value is 2.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:153\nmsgid \"\"\n\"`election_identifier` - valimissündmuse identifikaator, mille pikkus on \"\n\"piiratud 28 tähemärgiga ASCII kooditabelist. Nimekirja rakendamine toimub\"\n\" ainult vastava identifikaatoriga valimissündmuse kontekstis. \"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"`election_identifier` - election event identifier, limited to 28 \"\n\"characters from the ASCII code table. The implementation of the \"\n\"enumeration shall only take place in the context of an election event \"\n\"with the corresponding identifier. The election event identifier must \"\n\"conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:156\nmsgid \"\"\n\"`changeset` - nimekirja järjekorranumber. Rangelt kasvav number (0, 1, 2,\"\n\" ...), mis defineerib nimekirjade rakendamise järjekorra. Algnimekirja \"\n\"järjekorranumbriks on 0.\"\nmsgstr \"\"\n\"`changeset` - the sequence number of the list. An ordinal number (0, 1, \"\n\"2, ...) that defines the order in which lists are implemented. The queue \"\n\"number of the initial list is 0.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:159\nmsgid \"\"\n\"`period` - nimekirjas kajastuvate muudatuste ajavahemik. Esimene väli \"\n\"sisaldab perioodi algust, teine lõppu. Algnimekirja puhul on perioodi \"\n\"algus ja lõpu väärtused võrdsed. Väli on informatiivne.\"\nmsgstr \"\"\n\"`period` - the period of time of the changes in the list. The first field\"\n\" contains the start of the period, the second the end. In the case of a \"\n\"sub-list, the start and end values of the period are equal. The field is \"\n\"informative.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:163\nmsgid \"Kirje koosneb väljadest, mille sisu on järgmine:\"\nmsgstr \"The record consists of fields, the content of which is as follows:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:165\nmsgid \"\"\n\"`person_code` - isikukood on valija unikaalne identifikaator, mille \"\n\"alusel EHS tuvastab isiku hääleõiguse ja ringkonnakuuluvuse.\"\nmsgstr \"\"\n\"`person_code` - the person code is the unique identifier of the voter, \"\n\"which EHS uses to determine the person's eligibility to vote and to be \"\n\"elected.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:167\nmsgid \"\"\n\"`voter_name` - valija nimi, informaalne väli, otsuste tegemisel ei \"\n\"kasutata.\"\nmsgstr \"\"\n\"`voter_name` - name of voter, informal field, not used for decision \"\n\"making.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:168\nmsgid \"\"\n\"`action` - kirjega seotud tegevus. `lisamine` tähendab uue valija \"\n\"lisamist ja `kustutamine` olemasoleva valija eemaldamist.\"\nmsgstr \"\"\n\"`action` - action related to an item. An `add' action is an action to add\"\n\" a new selector and a `delete' action is an action to remove an existing \"\n\"voter.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:170\nmsgid \"\"\n\"`kov_code` - haldusüksus, kuhu valija kuulub. Haldusüksuse \"\n\"identifitseerimiseks kasutatakse kohaliku omavalitsuse EHAK-koodi, \"\n\"Tallinna korral linnaosa EHAK-koodi, alaliselt välisriigis elava valija \"\n\"korral kasutatakse väärtust `FOREIGN`.\"\nmsgstr \"\"\n\"`kov_code` - the administrative unit to which the voter belongs. To \"\n\"identify the administrative unit, the EHAK code of the local municipality\"\n\" is used, in the case of Tallinn the EHAK code of the district, in the \"\n\"case of a voter permanently resident abroad the value `FOREIGN` is used.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:174\nmsgid \"\"\n\"`electoral_district_id` - valimisringkonna number identifitseerib \"\n\"valimisringkonna, kus valija hääletab. KOV valimiste korral kehtib \"\n\"identifikaator haldusüksuse sees. RK, EP ja RH valimiste korral on \"\n\"identifikaator haldusüksuste ülene.\"\nmsgstr \"\"\n\"`electoral_district_id` - the electoral district number identifies the \"\n\"electoral district where the voter votes. In the case of local elections,\"\n\" the identifier is valid within the municipality. In the case of CoR, EP \"\n\"and EPP elections, the identifier is supra-administrative.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:179\nmsgid \"\"\n\"Välju `voter_name`, `kov_code` ja `electoral_district_id` kasutatakse \"\n\"ainult lisamiskirjes (`action` väärtus `lisamine`).\"\nmsgstr \"\"\n\"The fields `voter_name`, `kov_code` and `electoral_district_id` are only \"\n\"used in the addition record (`action` value `add`).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:182\nmsgid \"Andmevormingu formaalne kirjeldus Backus-Naur notatsioonis:\"\nmsgstr \"Formal description of the data format in Backus-Naur notation:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:212\nmsgid \"3.1 Nimekirja tõlgendamine\"\nmsgstr \"Interpreting the list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:214\nmsgid \"Nimekirju töötlev rakendus lähtub järgmistest reeglitest:\"\nmsgstr \"The application that processes the lists follows these rules:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:216\nmsgid \"Nimekiri rakendatakse tervikuna.\"\nmsgstr \"The list will be implemented in its entirety.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:217\nmsgid \"\"\n\"Nimekirja rakendamisele eelnevad vormingu ja kooskõlalisuse kontrollid, \"\n\"vigaseid nimekirju ei rakendata.\"\nmsgstr \"\"\n\"The implementation of the list is preceded by formatting and consistency \"\n\"checks, and incorrect lists are not implemented.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:219\nmsgid \"Rakendus kontrollib nimekirja versiooni.\"\nmsgstr \"The application checks the version of the list.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:220\nmsgid \"Rakendus kontrollib valimissündmuse identifikaatorit.\"\nmsgstr \"The application checks the election event identifier.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:221\nmsgid \"Rakendus kontrollib nimekirja järjekorranumbrit.\"\nmsgstr \"The application checks the order number of the list.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:222\nmsgid \"\"\n\"Rakendus kontrollib nimekirja kõigi kirjete kooskõlalisust oma \"\n\"andmebaasiga.\"\nmsgstr \"\"\n\"The application checks the consistency of all the entries in the list \"\n\"with its database.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:223\nmsgid \"Kooskõlalisust kontrollitakse kirjete esinemisjärjekorras.\"\nmsgstr \"Coordination is checked in the order of occurrence of the records.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:224\nmsgid \"\"\n\"Kui tegevus on `lisamine`, siis ei tohi vastava isikukoodiga kirjet \"\n\"rakenduse andmebaasis olla.\"\nmsgstr \"\"\n\"If the activity is an `addition', then there must be no record with the \"\n\"corresponding ID in the application database.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:226\nmsgid \"\"\n\"Kui tegevus on `kustutamine`, siis rakendus kontrollib, kas vastava \"\n\"isikukoodiga kirje on rakenduse andmebaasis olemas. Kui ei ole, siis \"\n\"rakendus logib veateate ja jätkab järgmiste kirjete töötlust.\"\nmsgstr \"\"\n\"If the action is 'delete', the application will check if the \"\n\"corresponding record with the personal identification code exists in the \"\n\"application database. If not, the application logs an error message and \"\n\"continues processing the following records.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:229\nmsgid \"\"\n\"Kui valijaga soetud andmeid on vaja muuta, näiteks valija liigub ühest \"\n\"haldusüksusest või valimisringkonnast teise, siis kantakse valijate \"\n\"nimekirja muudatuste hulka üks kustutamise kirje, millega valija oma \"\n\"eelmisest üksusest kustutatakse ja üks lisamise kirje, millega valija \"\n\"uues üksuses valijate nimekirja lisatakse. Ka kõik teised muudatused \"\n\"valija andmetes - näiteks nimemuutus - toimuvad läbi vana kirje \"\n\"kustutamise ja uue lisamise.\"\nmsgstr \"\"\n\"If it is necessary to modify the voter's record, for example, if the \"\n\"voter moves from one municipality or constituency to another, a deletion \"\n\"entry to delete the voter from his/her previous unit and an addition \"\n\"entry to add the voter to the voter list in the new unit are included in \"\n\"the changes to the voter list. All other changes to the voter's details -\"\n\" such as a name change - are also made by deleting the old record and \"\n\"adding the new one.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:236\nmsgid \"\"\n\"Valijate algnimekirjas on ainult lisamiskirjed, iga valija kohta \"\n\"maksimaalselt üks kirje. Muudatusnimekirjas peavad ühe valija kohta \"\n\"käivad kirjed olema nimekirjas loogilises järjestuses ning liiasuseta. \"\n\"Ehk kustutamine enne lisamist ning maksimaalselt üks kustutamise-lisamise\"\n\" paar.\"\nmsgstr \"\"\n\"The initial list of voters contains only additions, with a maximum of one\"\n\" entry per voter. In the amendment list, the entries per voter must be in\"\n\" a logical order in the list and must not be redundant. Perhaps a \"\n\"deletion before an addition, and a maximum of one deletion-addition pair.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:241\nmsgid \"\"\n\"On võimalik, et valimise ajal muutub valija isikukood. Sellisel juhul \"\n\"lisatakse nimekirja vana isikukoodiga kustutamise kirje ning uue \"\n\"isikukoodiga lisamise kirje. Täiendav korduvhääletamise kontroll ei ole \"\n\"selle liidese skoobis ja teostakse VIS3 poolt eraldi.\"\nmsgstr \"\"\n\"It is possible that the voter's personal identification number will \"\n\"change during the election. In such a case, a deletion entry with the old\"\n\" ID will be added to the list, and an addition entry with the new ID will\"\n\" be added. The additional re-vote check is not in the scope of this \"\n\"interface and is performed separately by VIS3.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:247\nmsgid \"3.2 Näited nimekirjadest\"\nmsgstr \"Examples of lists\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:249\nmsgid \"Valijate algnimekiri, 0.\"\nmsgstr \"Initial list of voters, 0.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:267\nmsgid \"Muudatusnimekiri, 1:\"\nmsgstr \"List of changes, 1:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:269\nmsgid \"Muutub valija 20000000002 haldusüksus.\"\nmsgstr \"Change the municipality of the voter 20000000002.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:270\nmsgid \"Valija 30000000003 kaotab hääleõiguse.\"\nmsgstr \"The voter 30000000003 loses the right to vote.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:271\nmsgid \"Valijale 11000000011 antakse hääleõigus.\"\nmsgstr \"The voter 11000000011 is given the right to vote.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:285\nmsgid \"Muudatusnimekiri, 2:\"\nmsgstr \"List of changes, 2:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:287\nmsgid \"Muutub valija 20000000002 nimi.\"\nmsgstr \"The name of the website 20000000002 will change.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:288\nmsgid \"Valija 60000000006 kaotab hääleõiguse.\"\nmsgstr \"The voter 60000000006 loses the right to vote.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:300\nmsgid \"Muudatusnimekiri, 3:\"\nmsgstr \"List of changes, 3:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:302\nmsgid \"Muutub valija 20000000003->10000000003 isikukood koos nimega.\"\nmsgstr \"\"\n\"The voter's 20000000003->10000000003 personal identification number will \"\n\"be changed together with the name.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:303\nmsgid \"Valija 60000000006 saab hääleõiguse.\"\nmsgstr \"The voter 60000000006 gets the right to vote.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:315\nmsgid \"4. Nimekirja signeerimine\"\nmsgstr \"Signing the list\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:317\nmsgid \"\"\n\"Nii algnimekirjaga kui muudatusnimekirjadega kaasatakse allkirjafail \"\n\"(FIPS 186-4), mille moodustab andmeallikas, arvutades algsest valijate \"\n\"nimekirjast SHA256-räsi ning allkirjastades selle räsi ECDSA võtmega \"\n\"(kasutame P-256 kõverat). Andmeallika poolt genereeritud avalik võti \"\n\"tehakse taustakanalis kättesaadavaks EHSile ning selle võtme alusel \"\n\"kontrollitakse EHS komponentides valijate nimekirjade terviklust.\"\nmsgstr \"\"\n\"Both the initial list and the changelists are accompanied by a signature \"\n\"file (FIPS 186-4), which is formed by the data source by computing the \"\n\"SHA256 hash from the initial voter list and signing this hash with the \"\n\"ECDSA key (we use the P-256 curve). The public key generated by the data \"\n\"source is made available in the background channel to the EHS and is used\"\n\" by the EHS components to verify the integrity of the voter lists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:324\nmsgid \"\"\n\"Võtmete genereerimiseks, signeerimiseks ning verifitseerimiseks võib \"\n\"kasutada tööriista OpenSSL:\"\nmsgstr \"The OpenSSL tool can be used to generate, sign and verify keys:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:327\nmsgid \"\"\n\"Võtme genereerimine: `openssl ecparam -name prime256v1 -genkey -noout \"\n\"-out private.key.pem`\"\nmsgstr \"\"\n\"Key generation: `openssl ecparam -name prime256v1 -genkey -noout -out \"\n\"private.key.pem`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:329\nmsgid \"\"\n\"Avaliku võtme eraldamine: `openssl ec -in private.key.pem -pubout -out \"\n\"public.key.pem`\"\nmsgstr \"\"\n\"Public key extraction: `openssl ec -in private.key.pem -pubout -out \"\n\"public.key.pem`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:331\nmsgid \"\"\n\"Andmefaili signeerimine: `openssl dgst -sha256 -sign private.key.pem -out\"\n\" data.sig data.txt`\"\nmsgstr \"\"\n\"Signing a data file: `openssl dgst -sha256 -sign private.key.pem -out \"\n\"data.sig data.txt`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:333\nmsgid \"\"\n\"Signatuuri verifitseerimine: `openssl dgst -sha256 -verify public.key.pem\"\n\" -signature data.sig data.txt`\"\nmsgstr \"\"\n\"Signature verification: `openssl dgst -sha256 -verify public.key.pem \"\n\"-signature data.sig data.txt`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:336\nmsgid \"\"\n\"Näide antud meetodi kasutamiseks golang keeles on leitav repositooriumis \"\n\"[DigiSign](https://github.com/e-gov/DigiSign)\"\nmsgstr \"\"\n\"An example of using this method in golang can be found in the repository \"\n\"[DigiSign](https://github.com/e-gov/DigiSign).\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:339\nmsgid \"5. Transpordiprotokoll\"\nmsgstr \"Transport protocol\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:341\nmsgid \"VIS3-EHS käesolev masinliides koosneb kahest API otspunktist.\"\nmsgstr \"This VIS3-EHS machine interface consists of two API endpoints.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:343\nmsgid \"\"\n\"API otspunkt `ehs-voters-changeset` konkreetse muudatusnimekirja \"\n\"laadimiseks VIS3-st\"\nmsgstr \"\"\n\"API endpoint `ehs-voters-changeset` to load a specific changelist from \"\n\"VIS3.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:345\nmsgid \"\"\n\"API otspunkt `ehs-list-election-changesets` ülevaate saamiseks \"\n\"avalikustatud nimekirjadest\"\nmsgstr \"\"\n\"API endpoint `ehs-list-election-changesets` for an overview of published \"\n\"lists\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:348\nmsgid \"\"\n\"Transpordiprotokoll on HTTPS, kuna volitamata ligipääs nimekirjadele \"\n\"tuleb tõkestada kasutatakse mõlemapoolselt autenditud TLS ühendusi.\"\nmsgstr \"\"\n\"The transport protocol is HTTPS, since unauthorised access to the lists \"\n\"must be blocked using mutually authenticated TLS connections.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:351\nmsgid \"5.1 `ehs-voters-changeset`\"\nmsgstr \"`ehs-voters-changeset`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:353\nmsgid \"\"\n\"HTTP meetod on GET. Päringu tegemisel tuleb kasutada kohustuslikke \"\n\"parameetreid `changeset` ja `election_identifier`, kus\"\nmsgstr \"\"\n\"The HTTP method is GET. When making a query, you must use the mandatory \"\n\"parameters `changeset` and `election_identifier`, where\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:356\nmsgid \"`changeset` on muudatusnimekirja `integer` tüüpi järjekorranumber.\"\nmsgstr \"`changeset` is the `integer` type sequence number of the changelist.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:357\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:379\nmsgid \"`election_identifier` on `string` tüüpi valimissündmuse identifikaator.\"\nmsgstr \"\"\n\"`election_identifier` is an identifier of type `string` for an election \"\n\"event.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:359\nmsgid \"\"\n\"Kui EHS teeb API otspunkti GET päringu, siis juhul kui vastava \"\n\"identifikaatori ja järjekorranumbriga nimekiri eksisteerib, vastab VIS3 \"\n\"`application/octet-stream` tüüpi baidijadaga, mis esitab kahest failist \"\n\"koosnevat ZIP konteinerit:\"\nmsgstr \"\"\n\"When the EHS makes an API endpoint GET request, if a list with the \"\n\"corresponding identifier and sequence number exists, VIS3 will respond \"\n\"with a `application/octet-stream` type byte string representing a ZIP \"\n\"container of two files:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:364\nmsgid \"nimekirjafail nimega `<election_identifier>-voters-<changeset>.utf`\"\nmsgstr \"listfile named `<election_identifier>-voters-<changeset>.utf`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:366\nmsgid \"signatuurifail nimega `election_identifier<>-voters-<changeset>.sig`\"\nmsgstr \"signature file named `election_identifier<>-voters-<changeset>.sig`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:369\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:414\nmsgid \"Sellise vastuse korral on HTTP status 200.\"\nmsgstr \"The HTTP status for this response is 200.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:371\nmsgid \"\"\n\"Juhul kui vastava järjekorranumbriga muudatusnimekirja veel ei eksisteeri\"\n\" antakse vastuses HTTP status 404.\"\nmsgstr \"\"\n\"If the corresponding changelist with the corresponding queue number does \"\n\"not yet exist, the HTTP status 404 is returned.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:374\nmsgid \"5.1 `ehs-list-election-changesets`\"\nmsgstr \"`ehs-list-election-changesets`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:376\nmsgid \"\"\n\"HTTP meetod on GET. Päringu tegemisel tuleb kasutada kohustuslikku \"\n\"parameetrit `election_identifier`:\"\nmsgstr \"\"\n\"The HTTP method is GET. When making a query, the mandatory \"\n\"`election_identifier` parameter must be used:\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:381\nmsgid \"\"\n\"Kui EHS teeb API otspunkti, siis juhul kui vastava identifikaatoriga \"\n\"valimine eksisteerib, vastab VIS3 `application/json` tüüpi baidijadaga, \"\n\"mis sisaldab endas JSON vormingus viiteid kõigile väljastatud \"\n\"muudatusnimekirjadele.\"\nmsgstr \"\"\n\"When EHS makes an API endpoint, if a election with the corresponding \"\n\"identifier exists, VIS3 will respond with a `application/json` type byte \"\n\"string containing JSON format references to all issued changelists.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:385\nmsgid \"\"\n\"MÄRKUS. Viidetes tarnitavad URL-id ei ole õiged ja seetõttu \"\n\"mittekasutatavad. EHS peab konkreetse muudatuste nimekirja poole \"\n\"pöördumise URL-i koostama vastavalt VIS3 EHS API OpenAPI vormingus \"\n\"spetsifikatsioonile (asub käesolevas repos, failis `vis3-ehs-api.yaml`). \"\n\"VIS3 edasisestes versioonides eemaldame viidetest URL-id. 20.07.2021.\"\nmsgstr \"\"\n\"NOTE. The URLs supplied in the references are not correct and therefore \"\n\"cannot be used. The URL to a specific changelist must be generated by EHS\"\n\" according to the VIS3 EHS API OpenAPI format specification (located in \"\n\"this repo, in the file `vis3-ehs-api.yaml`). In future versions of VIS3, \"\n\"we will remove URLs from the references. 20.07.2021.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:387\nmsgid \"\"\n\"MÄRKUS. Viidetes tarnitavate URL-de käsitlust muudetud: elemendis `url` \"\n\"tarnitakse mitte täis-URL, vaid ainult _path_ ja -query_ osa.\"\nmsgstr \"\"\n\"NOTE. Changed the handling of URLs supplied in references: the `url` \"\n\"element does not supply the full URL, but only the _path_ and -query_ \"\n\"parts.\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:416\nmsgid \"6. Näited\"\nmsgstr \"Examples\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:418\nmsgid \"7. Viited\"\nmsgstr \"References\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:420\nmsgid \"\"\n\"[\\\"DigiSign\\\" - lihtne protokoll edastatavate failide allkirjastamiseks, \"\n\"koos teostusnäitega](https://github.com/e-gov/DigiSign)\"\nmsgstr \"\"\n\"[\\\"DigiSign\\\" - a simple protocol for signing files in transit, with \"\n\"examples](https://github.com/e-gov/DigiSign)\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:44+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:1\nmsgid \"E-hääletanute nimekiri\"\nmsgstr \"List of e-voters\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:5\nmsgid \"\"\n\"E-hääletanute nimekiri on pärast e-hääletamise lõppu EHS poolt genereeritav \"\n\"ja väljastatav nimekiri e-hääletanud isikutest. E-hääletanute nimekiri \"\n\"loetakse sisse VIS3-e (moodul NIM).\"\nmsgstr \"\"\n\"The e-vote list is the list of e-voters generated and issued by EHS after \"\n\"the end of e-voting. The list of e-voters shall be read into VIS3 (module \"\n\"NIM).\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:7\nmsgid \"E-hääletanute nimekiri edastakse inim-masin-protseduuriga:\"\nmsgstr \"The list of e-voters is transmitted by human-machine procedure:\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:9\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"The EHS operator downloads the JSON file from the EHS;\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:11\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\"The EHS operator digitally signs the file outside the EHS and gives it to \"\n\"the VIS head user;\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:13\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\"The VIS head user uploads the file taken from the digital container to VIS3.\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:15\nmsgid \"Fail on JSON-formaadis. Faili näidised on tööülesandes.\"\nmsgstr \"The file is in JSON format. File examples are in the appendix.\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:17\nmsgid \"Faili struktuur on kirjeldatud JSON-skeemiga.\"\nmsgstr \"The file structure is described by a JSON schema.\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:19\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis \"\n\"\\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.\"\n\"pdf](https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf), jaotises \\\"E-hääletanute nimekiri\\\" (jaotis 9.2).\"\nmsgstr \"\"\n\"It is based on the current interface of the EHS with VIS2, as specified in \"\n\"the document \\\"Description of the IVXV protocols\\\" (v 1.5.0, IVXV-PR-1.5.0, \"\n\"20.04.2019), [https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh/\"\n\"IVXV-protokollid.pdf), section \\\"E-voting list\\\" (section 9.2).\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:23\nmsgid \"\"\n\"Faili struktuur (JSON-skeem): [onlinevoters.schema](onlinevoters.schema)\"\nmsgstr \"\"\n\"File structure (JSON schema): [onlinevoters.schema](onlinevoters.schema)\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:25\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:27\nmsgid \"Faili näited (JSON):\"\nmsgstr \"File examples (JSON):\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:29\nmsgid \"[onlinevoters_EP.json](onlinevoters_EP.json)\"\nmsgstr \"[onlinevoters_EP.json](onlinevoters_EP.json)\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:30\nmsgid \"[onlinevoters_KOV.json](onlinevoters_KOV.json)\"\nmsgstr \"[onlinevoters_KOV.json](onlinevoters_KOV.json)\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:31\nmsgid \"[onlinevoters_RH.json](onlinevoters_RH.json)\"\nmsgstr \"[onlinevoters_RH.json](onlinevoters_RH.json)\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:32\nmsgid \"[onlinevoters_RK.json](onlinevoters_RK.json)\"\nmsgstr \"[onlinevoters_RK.json](onlinevoters_RK.json)\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/5_Tyhistusnimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:46+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:1\nmsgid \"5 Tühistus- ja ennistusnimekiri\"\nmsgstr \"5 Revokation and Restoration list\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:3\nmsgid \"\"\n\"VIS3 edastab EHS-le tühistus- ja ennistusnimekirja (lühidalt - \"\n\"tühistusnimekiri).\"\nmsgstr \"\"\n\"VIS3 transmits to the EHS the revocation and restoration list (in short - \"\n\"the revokation list).\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:5\nmsgid \"\"\n\"Tühistusnimekiri sisaldab andmeid isikute kohta, kelle e-hääl tuleb \"\n\"tühistada (on paberhääletanud ja sellest tulenevalt ei lähe e-hääl arvesse \"\n\"valimistulemuste kokkulugemisel) või ennistada (s.t. tühistatakse eelnev \"\n\"tühistamine ning häälte uuesti üle lugemisel võetakse ennistatud e-hääl \"\n\"arvesse).\"\nmsgstr \"\"\n\"The revokation list contains the details of persons whose e-vote is to be \"\n\"cancelled (they have voted by paper ballot and therefore their e-vote will \"\n\"not be counted in the recount) or restored (i.e. the previous revokation is \"\n\"cancelled and the restored e-vote is counted in the tally).\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:7\nmsgid \"Fail on JSON-formaadis.\"\nmsgstr \"The file is in JSON format.\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:9\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:11\nmsgid \"Tühistusnimekiri edastakse inim-masin-protseduuriga:\"\nmsgstr \"The revokation list is transmitted by human-machine procedure:\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:13\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"The EHS operator downloads the JSON file from the EHS;\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:15\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\"The EHS operator digitally signs the file outside the EHS and gives it to \"\n\"the VIS head user;\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:17\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\"The VIS head user uploads the file taken from the digital container to VIS3.\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:19\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:21\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis \"\n\"\\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.\"\n\"pdf](https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf), jaotises \\\"Tühistus- ja ennistusnimekiri\\\" (jaotis 9.1).\"\nmsgstr \"\"\n\"It is based on the EHS current interface with VIS2, as specified in the \"\n\"document \\\"Description of the IVXV Protocols\\\" (v 1.5.0, IVXV-PR-1.5.0, \"\n\"20.04.2019), [https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh/\"\n\"IVXV-protokollid.pdf), section \\\"Cancellation and Restoration \"\n\"List\\\" (section 9.1).\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:23\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:25\nmsgid \"Faili struktuur (JSON-skeem): [revoke.schema](revoke.schema)\"\nmsgstr \"File structure (JSON schema): [revoke.schema](revoke.schema)\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:27\nmsgid \"Faili näited (JSON):\"\nmsgstr \"File examples (JSON):\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:29\nmsgid \"[revoke_EP.json](revoke_EP.json)\"\nmsgstr \"[revoke_EP.json](revoke_EP.json)\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:30\nmsgid \"[revoke_KOV.json](revoke_KOV.json)\"\nmsgstr \"[revoke_KOV.json](revoke_KOV.json)\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:31\nmsgid \"[revoke_RH.json](revoke_RH.json)\"\nmsgstr \"[revoke_RH.json](revoke_RH.json)\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:32\nmsgid \"[revoke_RK.json](revoke_RK.json)\"\nmsgstr \"[revoke_RK.json](revoke_RK.json)\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/6_e_haaletamise_tulemus/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:47+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:1\nmsgid \"E-hääletamise tulemus\"\nmsgstr \"Result of e-voting\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:3\nmsgid \"\"\n\"E-hääletamise lõppedes genereerib EHS e-hääletamise tulemuste faili \"\n\"(hääletamistulemuse fail). Fail laetakse VIS3-e (valimistulemuse moodulisse \"\n\"TUL).\"\nmsgstr \"\"\n\"At the end of the e-voting, the EHS generates an e-voting results file \"\n\"(voting result file). The file is uploaded to VIS3 (in the election result \"\n\"module TUL).\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:5\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:7\nmsgid \"Fail edastatakse inim-masin-protseduuriga:\"\nmsgstr \"The file is transmitted by human-machine procedure:\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:9\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"The EHS operator downloads the JSON file from the EHS;\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:11\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\"The EHS operator digitally signs the file outside the EHS and gives it to \"\n\"the VIS head user;\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:13\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\"The VIS head user uploads the file taken from the digital container to VIS3.\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:15\nmsgid \"Fail on JSON-formaadis.\"\nmsgstr \"The file is in JSON format.\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:17\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:19\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis \"\n\"\\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-protokollid.\"\n\"pdf](https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf), jaotises \\\"E-hääletamise tulemus\\\" (jaotis 9.3).\"\nmsgstr \"\"\n\"It is based on the current interface of the EHS with VIS2, as specified in \"\n\"the document \\\"Description of the IVXV protocols\\\" (v 1.5.0, IVXV-PR-1.5.0, \"\n\"20.04.2019), [https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh/\"\n\"IVXV-protokollid.pdf), section \\\"E-voting result\\\" (section 9.3).\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:21\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:23\nmsgid \"Faili struktuur (JSON-skeem): [results.schema](results.schema)\"\nmsgstr \"File structure (JSON schema): [results.schema](results.schema)\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:25\nmsgid \"Faili näited (JSON):\"\nmsgstr \"File examples (JSON):\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:27\nmsgid \"[results_EP.json](results_EP.json)\"\nmsgstr \"[results_EP.json](results_EP.json)\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:28\nmsgid \"[results_KOV.json](results_KOV.json)\"\nmsgstr \"[results_KOV.json](results_KOV.json)\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:29\nmsgid \"[results_RH.json](results_RH.json)\"\nmsgstr \"[results_RH.json](results_RH.json)\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:30\nmsgid \"[results_RK.json](results_RK.json)\"\nmsgstr \"[results_RK.json](results_RK.json)\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:48+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:1\nmsgid \"7 E-hääletamisest osavõtu üldstatistika\"\nmsgstr \"General statistics on participation in e-voting\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:3\nmsgid \"\"\n\"EHS annab lühikese perioodiga (nt 15 min) VIS3-le andmeid kui palju \"\n\"valijaid on e-hääletanud.\"\nmsgstr \"\"\n\"The EHS will provide VIS3 with information on how many voters have e-voted \"\n\"in a short period of time (e.g. 15 min).\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:5\nmsgid \"EHS teeb seda HTTPS päringutega VIS3 poolt pakutava otspunkti vastu.\"\nmsgstr \"\"\n\"EHS does this by HTTPS requests against the endpoint provided by VIS3.\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:7\nmsgid \"Päringu vastuses tagastatav fail:\"\nmsgstr \"The file to be returned in response to the query:\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:9\nmsgid \"JSON-skeem: [online-voters-total.schema](online-voters-total.schema)\"\nmsgstr \"JSON schema: [online-voters-total.schema](online-voters-total.schema)\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:11\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier must conform to the format [Election event \"\n\"identifier](../valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:13\nmsgid \"Näide (JSON): [online-voters-total.json](online-voters-total.json)\"\nmsgstr \"Example (JSON): [online-voters-total.json](online-voters-total.json)\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:49+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:1\nmsgid \"8 E-hääletamisest osavõtu detailne statistika\"\nmsgstr \"Detailed statistics on participation in e-voting\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:3\nmsgid \"\"\n\"E-hääletamise lõppedes annab EHS VIS3-le e-hääletamisest osavõtu detailse \"\n\"statistika.\"\nmsgstr \"\"\n\"At the end of e-voting, EHS will provide VIS3 with detailed statistics on \"\n\"e-voting participation.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:5\nmsgid \"\"\n\"E-hääletamisest osavõtu statistikat kuvatakse valimiste veebilehel, samuti \"\n\"edastatakse masintöödeldaval kujul meediaorganisatsioonidele ja lõpuks \"\n\"publitseeritakse avaandmetena.\"\nmsgstr \"\"\n\"The e-voting statistics will be displayed on the election website, as well \"\n\"as being provided in a machine-readable format to media organisations and \"\n\"eventually published as open data.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:7\nmsgid \"Protseduur\"\nmsgstr \"Procedure\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:9\nmsgid \"EHS operaator laeb faili alla.\"\nmsgstr \"The EHS operator downloads the file.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:11\nmsgid \"EHS operaator edastab faili VIS3 peakasutajale.\"\nmsgstr \"The EHS operator forwards the file to the VIS3 head user.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:13\nmsgid \"VIS3 peakasutaja laeb faili VIS3-e üles.\"\nmsgstr \"The VIS3 head user uploads the file to VIS3.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:15\nmsgid \"Faili edastatakse eelhääletamise ajal, kord päevas.\"\nmsgstr \"The file will be transmitted during the advance voting, once a day.\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:17\nmsgid \"Edastatav fail\"\nmsgstr \"Transmitted file\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:19\nmsgid \"\"\n\"JSON-skeem: [online-voters-counties.schema](online-voters-counties.schema)\"\nmsgstr \"\"\n\"JSON-skeem: [online-voters-counties.schema](online-voters-counties.schema)\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:21\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The polling event identifier must conform to the format [Polling event \"\n\"identifier](../polling_event_identifier.md).\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:23\nmsgid \"\"\n\"Näide (JSON): [online-voters-counties.json](online-voters-counties.json)\"\nmsgstr \"\"\n\"Example (JSON): [online-voters-counties.json](online-voters-counties.json)\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:55+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:1\nmsgid \"E-hääletamiste nimekiri\"\nmsgstr \"List of e-votes\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:3\nmsgid \"kavand v0.4\"\nmsgstr \"draft v0.4\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:5\nmsgid \"Muutelugu\"\nmsgstr \"Migration story\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:7\nmsgid \"Eemaldatud sõna \\\"jooksev\\\" / Priit Parmakson, 23.11.2022\"\nmsgstr \"Removed the word \\\"current\\\" / Priit Parmakson, 23.11.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:8\nmsgid \"\"\n\"Lisatud OpenAPI kirjelduse publitseerimise teave. / Priit Parmakson, \"\n\"13.12.2022\"\nmsgstr \"\"\n\"Added information on the publication of the OpenAPI specification / Priit \"\n\"Parmakson, 13.12.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:9\nmsgid \"\"\n\"Täpsustatud e-hääletamiste väljanäitamist VIS3-s. / Priit Parmakson, \"\n\"15.11.2022\"\nmsgstr \"\"\n\"The e-voting field in VIS3 has been specified. / Priit Parmakson, 15.11.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:10\nmsgid \"Lisatud jõudluskaalutlused. / Priit Parmakson, 01.11.2022\"\nmsgstr \"Performance scales added / Priit Parmakson, 01.11.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:11\nmsgid \"\"\n\"Asendatud \\\"e-hääletanu\\\" -> \\\"e-hääletamine\\\". Põhjus: inimene võib e-\"\n\"hääletada mitu korda (ümberhääletamine); vastavalt korrigeeritud JSON \"\n\"väljanimesid \\\"evotersbatch\\\" -> \\\"evotingsbatch\\\". /  Priit Parmakson, \"\n\"01.09.2022\"\nmsgstr \"\"\n\"Replaced \\\"e-vote\\\" -> \\\"e-voting\\\". Reason: a person can e-vote multiple \"\n\"times (re-vote); corrected JSON field names \\\"evotersbatch\\\" -> \"\n\"\\\"evotingsbatch\\\" accordingly. / Priit Parmakson, 01.09.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:12\nmsgid \"\"\n\"Eemaldatud: hääletamise kuupäeva/aja edastamine. Põhjus: turvakaalutlus \"\n\"(häälemüügi takistamine). / Priit Parmakson, 01.09.2022 (Sven Heibergiga \"\n\"arutelu alusel)\"\nmsgstr \"\"\n\"Removed: transmission of voting date/time. Reason: security considerations \"\n\"(to prevent the sale of votes) / Priit Parmakson, 01.09.2022 (in discussion \"\n\"with Sven Heiberg).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:13\nmsgid \"\"\n\"Lisatud: 1) ärivajaduse täpsustus; 2) e-hääletamise fakti aja edastamine; 3) \"\n\"edastuse kontroll ja vajadusel täiendamine või parandamine lõpliku faili \"\n\"abil. / Priit Parmakson, 04.05.2022\"\nmsgstr \"\"\n\"Attached: 1) specification of the business need; 2) transmission of the e-\"\n\"voting fact time; 3) checking of the transmission and, if necessary, \"\n\"completion or correction by means of the final file / Priit Parmakson, \"\n\"04.05.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:14\nmsgid \"Arutelu - RVT ja RIA inimesed, 04.05.2022\"\nmsgstr \"Debate - RVT and RIA people, 04.05.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:15\nmsgid \"\"\n\"\\\"E-hääletanute jooksev nimekiri\\\", kavand v0.2. / Priit Parmakson, 02.05.2022\"\nmsgstr \"\\\"Rolling list of e-voters\\\", draft v0.2. / Priit Parmakson, 02.05.2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:16\nmsgid \"Arutelu: X-tee kasutamine, JSON - Tarmo Hanga, Priit Parmakson, apr 2022\"\nmsgstr \"Discussion: using X-tee, JSON - Tarmo Hanga, Priit Parmakson, Apr 2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:17\nmsgid \"\\\"EHS- VIS3 liidestus\\\" (kavand) - Indrek Leesi, apr 2022\"\nmsgstr \"\\\"EHS- VIS3 interface\\\" (draft) - Indrek Leesi, Apr 2022\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:19\nmsgid \"Ülevaade\"\nmsgstr \"Overview\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Käesolev spetsifikatsioon määratleb protokolli e-hääletamiste nimekirja \"\n\"edastamiseks e-hääletamise süsteemist (edaspidi - EHS) Valimiste infosüsteemi \"\n\"(edaspidi - VIS3).\"\nmsgstr \"\"\n\"This specification defines the protocol for the transfer of the list of e-\"\n\"votes from the e-voting system (hereinafter referred to as EHS) to the \"\n\"Election Information System (hereinafter referred to as VIS3).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:23\nmsgid \"\"\n\"Varasematel valimistel (k.a KOV 2021) on peale hääletamise lõppu, \"\n\"valimispäeval edastatud EHS-st VIS3-le e-hääletanute nimekiri (edaspidi - e-\"\n\"hääletanute lõplik nimekiri). See edastus on spetsifitseeritud: [E-\"\n\"hääletanute nimekiri](https://github.com/e-gov/VIS3-EHS/blob/\"\n\"main/4_e_haaletanute_nimekiri/SPEC.md).\"\nmsgstr \"\"\n\"In previous elections (including Municipal elections 2021), the list of e-\"\n\"voters (hereafter referred to as the final list of e-voters) has been \"\n\"transmitted from the EHS to VIS3 after the end of voting on the day of the \"\n\"election. This transmission is specified in [e-vote list](https://github.com/\"\n\"e-gov/VIS3-EHS/blob/main/4_e_haaletanute_nimekiri/SPEC.md).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:25\nmsgid \"\"\n\"Siiski on vajadus VIS3-s saada teavet, kas valija on e-hääletanud, juba enne \"\n\"ülalnimetatud lõplikku edastust. Valija, kes on e-hääletanud, võib tulla \"\n\"eelhääletamise perioodil valimisjaoskonda ja soovida paberil hääletada. \"\n\"Valimisjaoskonna töötajal oleks hea omada võimalust VIS3-st vaadata, kas \"\n\"valija on e-hääletanud. Seda teavet saab muuhulgas kasutada valija \"\n\"hoiatamiseks, et paberhääletamisega tema e-hääletamine tühistub.\"\nmsgstr \"\"\n\"However, there is a need to obtain information in VIS3 on whether the voter \"\n\"has e-voted, even before the final transmission mentioned above. A voter who \"\n\"has e-voted may come to a polling station during the early voting period and \"\n\"wish to vote by paper ballot. It would be useful for the polling station \"\n\"staff to have the possibility to check in VIS3 whether the voter has e-voted. \"\n\"This information can be used, among other things, to warn the voter that his/\"\n\"her e-vote will be cancelled if he/she votes by paper ballot.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:27\nmsgid \"E-hääletamiste nimekiri edastatakse EHS-st VIS3-e X-tee teenusega.\"\nmsgstr \"\"\n\"The list of e-votes will be transmitted from the EHS to VIS3 via the X-road \"\n\"service.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:29\nmsgid \"\"\n\"Teenust pakub EHS. VIS3 pöördub regulaarselt teenuse poole. EHS edastab e-\"\n\"hääletamiste andmete paki. VIS3 saab paki ja salvestab andmed VIS3 andmebaasi.\"\nmsgstr \"\"\n\"The service is provided by EHS. VIS3 regularly calls on the service. EHS will \"\n\"transmit the e-voting data packet. VIS3 receives the packet and stores the \"\n\"data in the VIS3 database.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:31\nmsgid \"Sünkroonimiseks kasutatakse e-hääletamiste nummerdamist.\"\nmsgstr \"Numbering of e-votes is used for synchronisation.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:33\nmsgid \"Kaalutlused\"\nmsgstr \"Reflections\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:35\nmsgid \"\"\n\"Teenus on arendatud X-tee REST võimalusi kasutades, vastavalt X-tee REST \"\n\"sõnumiprotokollile ([X-Road: Message Protocol for REST](https://www.x-tee.ee/\"\n\"docs/live/xroad/pr-rest_x-road_message_protocol_for_rest.html)). Andmed \"\n\"väljastatakse JSON-vormingus.\"\nmsgstr \"\"\n\"The service has been developed using the REST capabilities of the X-Road, \"\n\"according to the X-Road REST message protocol ([X-Road: Message Protocol for \"\n\"REST](https://www.x-tee.ee/docs/live/xroad/pr-rest_x-\"\n\"road_message_protocol_for_rest.html)). The data is output in JSON format.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:37\nmsgid \"\"\n\"X-teed on otstarbekas kasutada, sest kuigi VIS3 ja EHS võivad olla käitatud \"\n\"samas taristus, on ikkagi vaja tagada usaldus, logimine ja paindlikkus - \"\n\"omadused, mille tagamine X-tee kasutamisega kokkuvõttes tõenäoliselt ei saaks \"\n\"olema ei lihtsam ega odavam.\"\nmsgstr \"\"\n\"The X-road makes sense because, although VIS3 and EHS can be run on the same \"\n\"infrastructure, there is still a need to ensure trust, logging and \"\n\"flexibility - features that, in the end, using the X-road is unlikely to be \"\n\"easier or cheaper to provide.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:39\nmsgid \"\"\n\"Protokollis on arvesse võetud Rahvastikuregistri ja VIS3 vaheliste X-tee \"\n\"teenuste kasutamise kogemust (REST sõnumiprotokoll, JSON, OpenAPI).\"\nmsgstr \"\"\n\"The protocol takes into account the experience of using X-Road services \"\n\"between the Population Register and VIS3 (REST message protocol, JSON, \"\n\"OpenAPI).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:41\nmsgid \"\"\n\"Erilist tähelepanu on pööratud andmete re-sünkroonimise võimalusele tõrgete \"\n\"korral. Selleks on pakkide pärimine kavandatud idenmpotentsena.\"\nmsgstr \"\"\n\"Particular attention has been paid to the possibility of re-synchronising \"\n\"data in case of failures. For this purpose, packet inheritance is designed as \"\n\"an idempotent.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:43\nmsgid \"\"\n\"E-hääletanute nimekirja \\\"peegeldamiseks\\\" EHS-st VIS3-e võiks \"\n\"põhimõtteliselt kasutada ka mõnda standardset sünkroonimisprotokolli (nt Git, \"\n\"Rsync vms, vt \\\\[1], \\\\[2], \\\\[3]). Kuna vajadus on suhteliselt lihtne, siis \"\n\"seda ei ole tehtud.\"\nmsgstr \"\"\n\"To \\\"mirror\\\" the list of e-voters from the EHS to VIS3, in principle a \"\n\"standard synchronisation protocol could also be used (e.g. Git, Rsync etc., \"\n\"see \\\\[1], \\\\[2], \\\\[3]). Since the need is relatively simple, this has not \"\n\"been done.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:45\nmsgid \"\"\n\"Jõudluskaalutlused: E-hääletamise jooksvat nimekirja edastatakse e-\"\n\"hääletamise perioodil (6 päeva). E-hääletamise fakte edastatakse kokku u 300 \"\n\"000. E-hääletamise perioodi esimesel tunnil võib oodata u 5000 e-hääletamist; \"\n\"perioodi viimasel tunnil u 10 000 e-hääletamist.\"\nmsgstr \"\"\n\"Performance considerations: the e-voting rolling list will be made available \"\n\"during the e-voting period (6 days). The total number of e-voting files to be \"\n\"transmitted will be around 300 000. Approximately 5,000 e-votes can be \"\n\"expected in the first hour of the e-voting period; approximately 10,000 e-\"\n\"votes in the last hour of the period.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:47\nmsgid \"E-hääletamiste järjenumbrid\"\nmsgstr \"E-vote sequential numbers\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:49\nmsgid \"\"\n\"Tagamaks, et e-hääletamiste nimekiri kantakse EHS-st VIS3-e õigeaegselt ja \"\n\"täielikult, kasutatakse järjenumbreid. EHS omistab igale e-hääletamisele \"\n\"järjenumbri (ingl Sequence Number).\"\nmsgstr \"\"\n\"Sequential numbers will be used to ensure that the list of e-votes is \"\n\"transferred from the EHS to VIS3 in a timely and complete manner. The EHS \"\n\"assigns a Sequence Number to each e-vote.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:51\nmsgid \"\"\n\"Järjenumber on naturaalarv, alates ühest. Väärtus `0` tähistab olukorda, kus \"\n\"e-hääletamisi veel ei ole toimunud.\"\nmsgstr \"\"\n\"The sequence number is a natural number, starting from one. The value `0` \"\n\"indicates a situation where no e-voting has yet taken place.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:53\nmsgid \"Igas valimissündmuses on oma numeratsioon.\"\nmsgstr \"Each election event has its own numbering.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:55\nmsgid \"Teenus\"\nmsgstr \"Service\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:57\nmsgid \"\"\n\"Teenuse vastutav töötleja on Riigi Valimisteenistus (RVT). Teenuse volitatud \"\n\"töötleja on Riigi Infosüsteemi Amet (RIA). Teenust osutav süsteem on EHS.\"\nmsgstr \"\"\n\"The controller of the service is the State Election Service (RVT). The \"\n\"authorised processor of the service is the State Information System Authority \"\n\"(RIA). The system providing the service is EHS.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:59\nmsgid \"\"\n\"Teenust kasutav süsteem on VIS3. Teenust kasutava süsteemi vastutav töötleja \"\n\"on RIA.\"\nmsgstr \"\"\n\"The system using the service is VIS3. The system using the service is the RIA.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:61\nmsgid \"\"\n\"Teenuse ärinimi on \\\"E-hääletamiste nimekiri\\\". Teenuse tehniline nimi (X-tee \"\n\"REST teenusekood, Service Code) on `e-votings-running-list`.\"\nmsgstr \"\"\n\"The business name of the service is \\\"E-voting list\\\". The technical name of \"\n\"the service (X-way REST Service Code) is `e-votings-running-list`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:63\nmsgid \"Teenus pakub järgmisi otspunkte:\"\nmsgstr \"The service offers the following endpoints:\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:65\nmsgid \"\"\n\"1  `GET /elections`.  \\\"Valimissündmuste loetelu\\\" väljastab aktiivsete \"\n\"valimissündmuste loetelu. Aktiivne valimissündmus teenuse kontekstis on \"\n\"selline, mille kohta EHS on valmis väljastama e-hääletamiste nimekirja.\"\nmsgstr \"\"\n\"1 `GET /elections`.  \\\"List of election events\\\" outputs a list of active \"\n\"election events. An active election event in the context of the service is \"\n\"one for which EHS is ready to issue a list of e-votes.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:67\nmsgid \"\"\n\"2  `GET /elections/{electionId}/lastseqno`. \\\"Viimane järjenumber\\\" väljastab \"\n\"konkreetse valimissündmuse viimase EHS-s registreeritud e-hääletamise \"\n\"järjenumbri.\"\nmsgstr \"\"\n\"2 `GET /elections/{electionId}/lastseqno`. \\\"Last Sequence Number\\\" gives the \"\n\"last e-voting sequence number registered in the EHS for a particular election \"\n\"event.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:69\nmsgid \"\"\n\"3  `GET /elections/{electionId}/evotingsbatchfrom/{fromseqno}`. \\\"e-\"\n\"hääletamiste pakk\\\". Selle päringuga pärib VIS3 EHS-lt valimissündmuse \"\n\"`{electionId}` e-hääletamiste paki, alatest e-hääletamisest järjenumbriga \"\n\"`{fromseqno}`.\"\nmsgstr \"\"\n\"3 `GET /elections/{electionId}/evotingsbatchfrom/{fromseqno}`. \\\"e-voting \"\n\"pack\\\". With this query, VIS3 shall inherit from EHS the e-voting batch for \"\n\"the election event `{electionId}`, starting from e-voting with the suffix \"\n\"`{fromseqno}`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:71\nmsgid \"\"\n\"Päringute ja vastuste andmestruktuuride ja samuti vastuskoodide \"\n\"spetsifikatsiooni vt OpenAPI spetsifikatsioonis: [ehs-xroad-api.yaml](ehs-\"\n\"xroad-api.yaml).\"\nmsgstr \"\"\n\"For the specification of the query and response data structures and also the \"\n\"response codes, see the OpenAPI specification: [ehs-xroad-api.yaml](ehs-xroad-\"\n\"api.yaml).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:73\nmsgid \"\"\n\"Teenuse OpenAPI spetsifikatsioon publitseeritakse turvaserveris, vastavalt \"\n\"spetsifikatsioonile [https://www.x-tee.ee/docs/live/xroad/pr-mrest_x-\"\n\"road_service_metadata_protocol_for_rest.html#4-retrieving-list-of-services](X-\"\n\"Road: Service Metadata Protocol for REST), jaotis 5 \\\"Retrieving the OpenAPI \"\n\"description of a Service\\\".\"\nmsgstr \"\"\n\"The OpenAPI specification of a Service is published in the Security Server, \"\n\"according to the specification [https://www.x-tee.ee/docs/live/xroad/pr-\"\n\"mrest_x-road_service_metadata_protocol_for_rest.html#4-retrieving-list-of-\"\n\"services](X-Road: Service Metadata Protocol for REST), section 5 \\\"Retrieving \"\n\"the OpenAPI description of a Service\\\".\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:75\nmsgid \"Teenuse OpenAPI spetsifikatsioon näitepäring:\"\nmsgstr \"OpenAPI service specification example request:\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:77\nmsgid \"\"\n\"`curl -H \\\"accept: application/json\\\" -H \\\"X-Road-Client:ee-dev/COM/<MEMBER>/\"\n\"dev\\\" \\\"https://.../r1/ee-dev/COM/<MEMBER>/ehs/getOpenAPI?serviceCode=e-\"\n\"votings-running-list\\\"`\"\nmsgstr \"\"\n\"`curl -H \\\"accept: application/json\\\" -H \\\"X-Road-Client:ee-dev/COM/<MEMBER>/\"\n\"dev\\\" \\\"https://.../r1/ee-dev/COM/<MEMBER>/ehs/getOpenAPI?serviceCode=e-\"\n\"votings-running-list\\\"`\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:79\nmsgid \"Otspunkt \\\"Valimissündmuste loetelu\\\"\"\nmsgstr \"Endpoint \\\"List of electoral events\\\"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:81\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:99\nmsgid \"Näide.\"\nmsgstr \"Example.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:83\nmsgid \"Päring: `GET /elections`\"\nmsgstr \"Query: `GET /elections`\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:85\nmsgid \"VIS3 pärib EHS-lt aktiivsete valimissündmuste loetelu.\"\nmsgstr \"VIS3 will query the list of active electoral events from EHS.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:87\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:105\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:128\nmsgid \"Vastus:\"\nmsgstr \"Answer:\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:93\nmsgid \"EHS vastab, et aktiivseid valimissündmusi on üks - `RK_2023`.\"\nmsgstr \"EHS replies that there is one active election event - `RK_2023`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:95\nmsgid \"\"\n\"Kui aktiivseid valimissündmusi ei ole, siis EHS peab vastuses saatma tühja \"\n\"massiivi (JSON Array).\"\nmsgstr \"\"\n\"If there are no active election events, EHS must send an empty array (JSON \"\n\"Array) in the response.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:97\nmsgid \"Otspunkt \\\"Viimane järjenumber\\\"\"\nmsgstr \"Endpoint \\\"Last serial\\\"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:101\nmsgid \"Päring: `GET /elections/RK_2023/lastseqno`\"\nmsgstr \"Query: `GET /elections/RK_2023/lastseqno`\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:103\nmsgid \"VIS3 pärib valimissündmuse `RK_2023` viimase e-hääletamise järjenumbrit.\"\nmsgstr \"\"\n\"VIS3 will query the last e-voting sequence number of the election event \"\n\"`RK_2023`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:114\nmsgid \"\"\n\"EHS vastab, et valimissündmuse `RK_2023` viimase e-hääletamise järjenumber on \"\n\"`54002`.\"\nmsgstr \"\"\n\"The EHS replies that the last e-voting sequence number for the election event \"\n\"`RK_2023` is `54002`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:116\nmsgid \"\"\n\"Kui valimissündmus on EHS-le tundmatu, siis EHS vastab HTTP vastuskoodiga \"\n\"`404 Not Found`.\"\nmsgstr \"\"\n\"If the election event is unknown to EHS, EHS will respond with the HTTP \"\n\"response code `404 Not Found`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:118\nmsgid \"\"\n\"Järjenumbrid algavad ühest (`1`). Kui valimisündmuses ei ole veel ükski \"\n\"valija e-hääletanud, siis vastab EHS `lastseqno` väärtusega `0`.\"\nmsgstr \"\"\n\"Sequential numbers start with one (`1`). If no voter has yet e-voted in the \"\n\"election event, the EHS will match `lastseqno` with a value of `0`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:120\nmsgid \"Otspunkt \\\"e-hääletanute pakk\\\"\"\nmsgstr \"Endpoint \\\"e-vote pack\\\"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:122\nmsgid \"Näide 3.\"\nmsgstr \"Example 3.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:124\nmsgid \"Päring: `G /elections/RK_2023/evotingsbatchfrom/54001`\"\nmsgstr \"Query: `G /elections/RK_2023/evotingsbatchfrom/54001`\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:126\nmsgid \"\"\n\"VIS3 pärib valimissündmuse `RK_2023` e-hääletamiste andmeid, alates \"\n\"järjenumbrist `54001`.\"\nmsgstr \"\"\n\"VIS3 will query the e-voting data for the election event 'RK_2023', starting \"\n\"with the sequence number '54001'.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:155\nmsgid \"\"\n\"EHS vastab, et saadab valimissündmuse `RK_2023` e-hääletamiste andmeid, \"\n\"alates järjenumbrist `54001`, pakina, milles on kuni `100` kirjet. \"\n\"Konkreetses pakis on kaks kirjet, kuna paki moodustamise hetkel on viimase \"\n\"EHS-s registreeritud e-hääletamise järjenumber `54002`. Esimene kirje \"\n\"tähendab, et valija `LEO KASS`, isikukoodiga `38101010020` on valimistel \"\n\"`RK_2023` e-hääletanud. Kirjes on ka valija KOV EHAK-kood ja valimisringkonna \"\n\"number.\"\nmsgstr \"\"\n\"The EHS replies that it will send the e-voting data for the election event \"\n\"`RK_2023`, starting with the sequence number `54001`, as a packet with up to \"\n\"`100` records. There will be two records in a particular packet, as the last \"\n\"e-voting sequence number registered in the EHS at the time the packet is \"\n\"formed is '54002'. The first record means that the voter `LEO KASS`, with the \"\n\"personal identification number `38101010020` has e-voted in the election \"\n\"`RK_2023`. The record also contains the voter's Municipal Electoral \"\n\"Registration Number and the constituency number.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:157\nmsgid \"\"\n\"E-hääletamise fakti kohta EHS e-hääletamise aega ei saada - turvakaalutlustel.\"\nmsgstr \"\"\n\"The fact of e-voting will not be reported to EHS at the time of e-voting - \"\n\"for security reasons.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:159\nmsgid \"\"\n\"Paki maksimaalsuuruse `batchmaxsize` määrab EHS, arvestusega, et andmed \"\n\"saadetakse X-tee vastussõnumi kehas (mitte manuses). Vastussõnumi töötlemisel \"\n\"turvaserveris loetakse keha üheaegselt põhimällu. Seetõttu ei tohi \"\n\"vastusõnumi keha suurus ületada 10 MB (turvaserveri vaikeseadistus).\"\nmsgstr \"\"\n\"The maximum batch size `batchmaxsize` is determined by the EHS, taking into \"\n\"account that the data is sent in the body of the X-road response message (not \"\n\"in the attachment). When processing the response message in the security \"\n\"server, the body is read simultaneously into the main memory. Therefore, the \"\n\"body size of the response message shall not exceed 10 MB (default setting of \"\n\"the security server).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:161\nmsgid \"\"\n\"Vastuses võib olla kuni `batchmaxsize` kirjet. Kui vastuses on kirjeid vähem \"\n\"kui `batchmaxsize`, siis see tähendab, et EHS-l ei ole vastuse koostamise \"\n\"hetkel rohkem andmeid e-hääletamiste kohta.\"\nmsgstr \"\"\n\"The response can contain up to `batchmaxsize` records. If there are fewer \"\n\"than `batchmaxsize` items in the response, this means that the EHS does not \"\n\"have any more data on e-voting at the time the response is generated.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:163\nmsgid \"VIS3 peab suutma töödelda erineva `batchmaxsize` väärtusega vastuseid.\"\nmsgstr \"\"\n\"VIS3 must be able to handle responses with different `batchmaxsize` values.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:165\nmsgid \"Töötluse ülevaade\"\nmsgstr \"Overview of treatment\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:167\nmsgid \"\"\n\"EHS registreerib e-hääletamise fakte. Igale e-hääletamisele omistab EHS \"\n\"järjenumbri. E-hääletamise fakte võib hoida nt järjenumbri järgi \"\n\"indekseeritud tabelis - siis on VIS3-i päringutele vastamine kiire ja \"\n\"efektiivne - kuid see on EHS siseasi. Paki maksimaalsuurus peaks olema EHS \"\n\"seadistuses määratav.\"\nmsgstr \"\"\n\"EHS records the facts of e-voting. EHS assigns a serial number to each e-\"\n\"vote. E-voting files could be kept in a table indexed by e.g. sequence number \"\n\"- then the response to VIS3 queries is fast and efficient - but this is an \"\n\"internal matter for EHS. The maximum packet size should be configurable in \"\n\"the EHS configuration.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:169\nmsgid \"\"\n\"VIS3 saadab valimiste aktiivsel perioodil regulaarselt päringuid EHS-i X-tee \"\n\"teenuse otspunkti \\\"Viimane järjenumber\\\". EHS saadab vastuses viimase e-\"\n\"hääletamise järjenumbri.\"\nmsgstr \"\"\n\"During the active period of the elections, VIS3 will regularly send queries \"\n\"to the EHS X-Road service endpoint \\\"Last Sequence Number\\\". EHS will send \"\n\"the last e-voting sequential number in the response.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:171\nmsgid \"\"\n\"Viimase järjenumbri alusel leiab VIS3, kas VIS3-e kantud e-hääletamiste \"\n\"andmed on EHS-iga sünkroonis. Kui viimane järjenumber osutab, et EHS-s on \"\n\"lisandunud kirjeid, mis vajavad VIS3-e kandmist, siis saadab VIS3 järjest \"\n\"päringud EHS X-tee teenuse otspunkti \\\"e-hääletamiste pakk\\\", alates \"\n\"esimesest järjenumbrist, mis on VIS3-s puudu; EHS saadab küsitud paki; VIS3 \"\n\"salvestab saadud andmed ja saadab järgmise paki päringu.\"\nmsgstr \"\"\n\"Based on the last sequential number, VIS3 will find out whether the e-voting \"\n\"data entered in VIS3 are in sync with EHS. If the last sequential number \"\n\"indicates that there are additional records in EHS that need to be entered in \"\n\"VIS3, VIS3 will send successive queries to the EHS X-Road service endpoint 'e-\"\n\"voting batch', starting from the first sequential number that is missing in \"\n\"VIS3; EHS will send the requested batch; VIS3 will store the received data \"\n\"and send the next batch query.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:173\nmsgid \"\"\n\"Päringute \\\"e-hääletamiste pakk\\\" töötlusloogika EHS-i poolel peab võimaldama \"\n\"VIS3-l päringuid esitada mistahes järjekorras ja kuitahes palju kordi.\"\nmsgstr \"\"\n\"The processing logic of the 'e-voting packet' of queries on the EHS side must \"\n\"allow VIS3 to query in any order and as many times as necessary.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:175\nmsgid \"\"\n\"Töötlus peab olema idempotentne (samajõuline) - selles mõttes, et VIS3 võib \"\n\"päritud andmeid igal ajal uuesti küsida. Uuesti pärimisega ei tohi tekkida \"\n\"duubelandmeid, tähendusnihkeid ega kinnijooksmisi.\"\nmsgstr \"\"\n\"The processing must be idempotent (concurrent) - in the sense that VIS3 can \"\n\"retrieve the requested data at any time. The re-query must not result in \"\n\"duplicates, semantic ambiguities, or stalls.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:177\nmsgid \"\"\n\"Tehnilise taustateabena märgime, et EHS hoiab e-hääletamise fakte mitte \"\n\"relatsioonilises andmebaasis, vaid etcd mäluteenuses.\"\nmsgstr \"\"\n\"As a technical background, EHS stores e-voting facts not in a relational \"\n\"database but in the etcd storage service.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:179\nmsgid \"Kohaletoimetamise garantii\"\nmsgstr \"Delivery guarantee\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:180\nmsgid \"\"\n\"Sõltuvalt e-hääletamise sagedusest ja EHS jõudlusest ning seadistusest võib \"\n\"EHS-s juhtuda, et e-hääletamise väga suure sageduse perioodil e-hääletamise \"\n\"faktile järjenumbri omistamine ajalõpu (ingl timeout) tõttu ebaõnnestub. \"\n\"Selline e-hääletamise fakt jääb e-hääletamiste nimekirjas VIS3-e edastamata.\"\nmsgstr \"\"\n\"Depending on the frequency of e-voting and the performance and configuration \"\n\"of the EHS, it is possible that during a period of very high frequency of e-\"\n\"voting, the assignment of a sequential number to the e-voting fact may fail \"\n\"due to a timeout. Such an e-voting fact will not be transmitted to VIS3 in \"\n\"the list of e-votes.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:182\nmsgid \"\"\n\"Seega e-hääletamiste nimekiri ei anna kõigi e-hääletamiste VIS3-e jooksvalt \"\n\"kohaletoimetamise garantiid. VIS3-e kohaletoimetatud fakte tuleb käsitada \"\n\"informatiivsetena. E-hääletamise faktid toimetatakse VIS3-e kindlalt \"\n\"täielikus koosseisus e-hääletanute (lõplikus) nimekirjas, pärast e-\"\n\"hääletamise perioodi lõppu (eraldi liides EHS ja VIS3 vahel).\"\nmsgstr \"\"\n\"Therefore, the list of e-votes does not guarantee the delivery of all e-votes \"\n\"VIS3s on a rolling basis. Facts delivered to the VIS3 must be considered as \"\n\"informative. The facts of an e-vote will be delivered to the VIS3 in the \"\n\"(final) list of e-voters in its definitive complete composition, after the \"\n\"end of the e-voting period (separate interface between EHS and VIS3).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:184\nmsgid \"Teenuse pakkumise ajaline ulatus\"\nmsgstr \"Time scale for the provision of the service\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:186\nmsgid \"\"\n\"Konkreetse valimissündmuse kohta pakub EHS e-hääletamiste nimekirja ainult \"\n\"piiratud perioodil. See periood hõlmab e-hääletamise perioodi (kehtiva õiguse \"\n\"kohaselt 6 päeva) koos lühikeste siirdeperioodidega enne ja pärast.\"\nmsgstr \"\"\n\"For a specific election event, the EHS only offers a list of e-votes for a \"\n\"limited period. This period includes the e-voting period (6 days under \"\n\"current law) with short transition periods before and after.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:188\nmsgid \"\"\n\"Kui EHS valimissündmuse kohta e-hääletamiste nimekirja enam ei paku, siis \"\n\"päringud otspunktidesse \\\"Viimane järjenumber\\\" ja \\\"e-hääletanute pakk\\\" \"\n\"saavad HTTP vastuskoodi `410 Gone`.\"\nmsgstr \"\"\n\"If the EHS no longer provides a list of e-votes for an election event, \"\n\"queries to the endpoints \\\"Last Sequence Number\\\" and \\\"e-vote pack\\\" will \"\n\"receive the HTTP response code `410 Gone`.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:190\nmsgid \"\"\n\"Valimissündmuste loetelu pakub EHS pidevalt (otspunkt \\\"Valimissündmuste \"\n\"loetelu\\\"). Loetelus on valimissündmused, mille kohta EHS on valmis e-\"\n\"hääletamiste nimekirja pakkuma.\"\nmsgstr \"\"\n\"A list of electoral events is provided by EHS on an ongoing basis (under \"\n\"\\\"List of electoral events\\\"). The list includes election events for which \"\n\"EHS is ready to provide a list of e-voting events.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:192\nmsgid \"Kontroll ja veaolukordade käsitlemine\"\nmsgstr \"Control and error handling\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:194\nmsgid \"\"\n\"Eeldatakse, et EHS-i poolt VIS3-le väljastatav on korrektne ja muutumatu (e-\"\n\"hääletamiste nimekirja piires). Parandus- ja muutmiskirjeid käesolev \"\n\"protokoll ei sisalda.\"\nmsgstr \"\"\n\"It is assumed that the EHS output to VIS3 is correct and unchanged (within \"\n\"the limits of the list of e-votes). Letters of correction and amendment are \"\n\"not included in these minutes.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:196\nmsgid \"\"\n\"Pärast e-hääletamise lõppu, valimispäeval, edastatakse EHS-st VIS3-le e-\"\n\"hääletanute lõplik nimekiri). See edastus on spetsifitseeritud: [E-\"\n\"hääletanute nimekiri](https://github.com/e-gov/VIS3-EHS/blob/\"\n\"main/4_e_haaletanute_nimekiri/SPEC.md).\"\nmsgstr \"\"\n\"After the end of e-voting, on election day, the final list of e-voters is \"\n\"transmitted from the EHS to VIS3). This transmission is specific: [e-vote \"\n\"list](https://github.com/e-gov/VIS3-EHS/blob/main/4_e_haaletanute_nimekiri/\"\n\"SPEC.md).\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:198\nmsgid \"VIS3 operaator laeb e-hääletanute lõpliku nimekirja VIS3-e.\"\nmsgstr \"The VIS3 operator uploads the final list of e-voters to VIS3.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:200\nmsgid \"\"\n\"Jooksva nimekirja ja lõpliku nimekirja erinevuse korral loetakse \\\"tõe \"\n\"allikaks\\\" lõplik nimekiri. Seega: 1) kui lõplikus nimekirjas on isik, kes \"\n\"hääletamisperioodil edastatud nimekirjas puudub, siis loetakse isik e-\"\n\"hääletanuks; 2) kui hääletamisaegses nimekirjas on isik, kes lõplikus \"\n\"nimekirjas puudub, siis märge sellise isiku kohta VIS3-s küll säilitatakse, \"\n\"kuid töötluses ja toimingutes lähtutakse lõplikust nimekirjast, s.t loetakse, \"\n\"et isik ei ole e-hääletanud. VIS3 peab omama võimekust hääletamisaegse ja \"\n\"lõpliku nimekirja erinevust avastada ja operaatorile teada anda.\"\nmsgstr \"\"\n\"In the event of a discrepancy between the current list and the definitive \"\n\"list, the definitive list is considered to be the \\\"source of truth\\\". Thus: \"\n\"1) if the final list contains a person who is not on the list transmitted \"\n\"during the voting period, the person is considered to have e-voted; 2) if the \"\n\"voting period list contains a person who is not on the final list, the record \"\n\"of such person is kept in VIS3, but processing and operations are based on \"\n\"the final list, i.e. the person is considered not to have e-voted. The VIS3 \"\n\"shall have the capability to detect a discrepancy between the voting list and \"\n\"the final list and to inform the operator.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:203\nmsgid \"\"\n\"EHS-st VIS3-e edastatud e-hääletamise faktid on VIS3 kasutajaliideses \"\n\"nähtavad valimiste korraldajale, vaates \\\"Valijaga seotud toimingute \"\n\"ajalugu\\\". Kui valija e-hääletas mitu korda, siis on valijaga seotud \"\n\"toimingute ajaloos esitatud kõik valija e-hääletamised. E-hääletamise juures \"\n\"on näha kuupäev ja kellaaeg, millal e-hääletamise fakt EHS-st VIS3-e edastati.\"\nmsgstr \"\"\n\"The e-voting facts transmitted from the EHS to VIS3 will be visible to the \"\n\"election organiser in the VIS3 interface, in the 'Voter History' view. If a \"\n\"voter has e-voted more than once, all e-votes of the voter are shown in the \"\n\"voter activity history. The date and time at which the fact of the e-vote was \"\n\"transmitted from the EHS to VIS3 is shown next to the e-vote.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:205\nmsgid \"Kirjandus\"\nmsgstr \"Literature\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:207\nmsgid \"\\\\[1] Git. https://git-scm.com/.\"\nmsgstr \"\\\\[1] Git. https://git-scm.com/.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:209\nmsgid \"\"\n\"\\\\[2] CouchDB Replication Protocol, https://guide.couchdb.org/draft/\"\n\"replication.html.\"\nmsgstr \"\"\n\"\\\\[2] CouchDB Replication Protocol, https://guide.couchdb.org/draft/\"\n\"replication.html.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:211\nmsgid \"\"\n\"\\\\[3] Principle and application of Rsync algorithm. https://developpaper.com/\"\n\"principle-and-application-of-rsync-algorithm/.\"\nmsgstr \"\"\n\"\\\\[3] Principle and application of Rsync algorithm. https://developpaper.com/\"\n\"principle-and-application-of-rsync-algorithm/.\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/README.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-05-13 21:04+0000\\n\"\n\"PO-Revision-Date: 2024-03-02 13:27+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/README.md:1\nmsgid \"VIS3-EHS liideste spetsifikatsioonid\"\nmsgstr \"VIS3-EHS interface specifications\"\n\n#: ../../VIS3-EHS/README.md:3\nmsgid \"\"\n\"Eesti riiklikel valimistel toimub e-hääletamine Valimiste infosüsteemi \"\n\"(VIS3) ja e-hääletamise süsteemi (EHS) koostöös. EHS roll on \"\n\"elektroonilise hääletamise läbiviimine lähtudes valimise definitsioonist,\"\n\" mis koostatakse valimise korraldaja poolt VIS3 abil. E-hääletamise ajal \"\n\"vahetavad EHS ja VIS3 informatsiooni - võivad muutuda valijate nimekirjad\"\n\" ning liigub info e-häälte laekumise kohta.\"\nmsgstr \"\"\n\"In Estonian national elections, e-voting takes place in cooperation \"\n\"between the Election Information System (VIS3) and the e-voting system \"\n\"(EHS). The role of the EHS is to carry out electronic voting based on the\"\n\" definition of an election, which is drawn up by the election organiser \"\n\"using VIS3. During e-voting, the EHS and VIS3 exchange information - \"\n\"voters' lists may change and information on the collection of e-votes is \"\n\"passed on.\"\n\n#: ../../VIS3-EHS/README.md:5\nmsgid \"\"\n\"Dokument esitab VIS3 ja e-hääletamise süsteemi EHS vaheliste liideste \"\n\"spetsifikatsioonid.\"\nmsgstr \"\"\n\"The document sets out the specifications for the interfaces between VIS3 \"\n\"and the e-voting system EHS.\"\n\n#: ../../VIS3-EHS/README.md:7\nmsgid \"\"\n\"Spetsifikatsioonid on avalikud. Spetsifikatsioonid ei käsitle VIS3 ega \"\n\"EHS konfidentsiaalset siseehitust ega liideste konfidentsiaalseid \"\n\"elemente.\"\nmsgstr \"\"\n\"The specifications are public. The specifications do not deal with the \"\n\"confidential internal architecture of VIS3 and EHS, nor with the \"\n\"confidential elements of the interfaces.\"\n\n#: ../../VIS3-EHS/README.md:10\nmsgid \"Ülevaade\"\nmsgstr \"Overview\"\n\n#: ../../VIS3-EHS/README.md:12\nmsgid \"![VIS3 ja EHS liidesed (ülevaade)](img/vis-ehs.png)\"\nmsgstr \"![VIS3 and EHS interfaces (overview)](img/vis-ehs.png)\"\n\n#: ../../VIS3-EHS/README.md:12\nmsgid \"VIS3 ja EHS liidesed (ülevaade)\"\nmsgstr \"VIS3 and EHS interfaces (overview)\"\n\n#: ../../VIS3-EHS/README.md:14\nmsgid \"Joonis 1. VIS3 ja EHS liidesed (ülevaade).\"\nmsgstr \"Figure 1. VIS3 and EHS interfaces (overview).\"\n\n#: ../../VIS3-EHS/README.md:16\nmsgid \"Märkus: Joonise originaal vt VIS3 dok-s RIAs, fail \\\"EHS\\\".\"\nmsgstr \"Note: For the original drawing, see VIS3 doc in RIA, file \\\"EHS\\\".\"\n\n#: ../../VIS3-EHS/README.md:18\nmsgid \"Vasakul on VIS3, mooduli täpsusega:\"\nmsgstr \"On the left is VIS3, with module precision:\"\n\n#: ../../VIS3-EHS/README.md:20\nmsgid \"KAN - Kandidaadimoodul\"\nmsgstr \"KAN - Candidate module\"\n\n#: ../../VIS3-EHS/README.md:21\nmsgid \"NIM - Nimekirjamoodul\"\nmsgstr \"NIM - List module\"\n\n#: ../../VIS3-EHS/README.md:22\nmsgid \"TUL - Valimistulemuse moodul\"\nmsgstr \"TUL - Election results module\"\n\n#: ../../VIS3-EHS/README.md:23\nmsgid \"VAL - Valimissündmuse moodul\"\nmsgstr \"VAL - Election event module\"\n\n#: ../../VIS3-EHS/README.md:24\nmsgid \"VTA - Valimistulemuse avalikustamise moodul.\"\nmsgstr \"VTA - Election result publishing module.\"\n\n#: ../../VIS3-EHS/README.md:26\nmsgid \"\"\n\"Andmed liiguvad noole suunas. HTTPS masinliidestes on teenust pakkuvad \"\n\"otspunktid VIS3 poolel. X-tee liideses on teenusepakkujaks EHS. Inim-\"\n\"masinliides tähendab seda, et ühe süsteemi operaator laeb faili \"\n\"süsteemist alla ja edastab teise süsteemi operaatorile, kes laeb faili \"\n\"teise süsteemi üles.\"\nmsgstr \"\"\n\"The data moves in the direction of the arrow. In HTTPS machine \"\n\"interfaces, the service-providing endpoints are on the VIS3 side. In an \"\n\"X-road interface, the service provider is EHS. A human machine interface \"\n\"means that an operator on one system downloads a file from one system and\"\n\" forwards it to an operator on another system, who uploads the file to \"\n\"the other system.\"\n\n#: ../../VIS3-EHS/README.md:29\nmsgid \"Liidesed on täpsemalt kirjeldatud allpool.\"\nmsgstr \"The interfaces are described in more detail below.\"\n\n#: ../../VIS3-EHS/README.md:31\nmsgid \"Spetsifikatsioonid\"\nmsgstr \"Specifications\"\n\n#: ../../VIS3-EHS/README.md:33\nmsgid \"[1 Valimisringkondade nimekiri](1_Valimisringkondade_nimekiri/SPEC.md)\"\nmsgstr \"[1 List of constituencies](1_Valimisringkondade_nimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:35\nmsgid \"[2 Valikute nimekiri (kandidaatide nimekiri)](2_Valikute_nimekiri/SPEC.md)\"\nmsgstr \"[2 List of candidates (list of candidates)](2_Valikute_nimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:37\nmsgid \"[3 Valijate nimekiri EHS-le](3_Valijate_nimekiri/SPEC.md)\"\nmsgstr \"[3 Voters list for EHS](3_Valijate_nimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:39\nmsgid \"[4 E-hääletanute nimekiri](4_e_haaletanute_nimekiri/SPEC.md)\"\nmsgstr \"[4 List of e-voters](4_e_haaletanute_nimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:41\nmsgid \"[5 Tühistus- ja ennistusnimekiri](5_Tyhistusnimekiri/SPEC.md)\"\nmsgstr \"[5 Revokation and Restoration List](5_Tyhistusnimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:43\nmsgid \"[6 E-hääletamise tulemus](6_e_haaletamise_tulemus/SPEC.md)\"\nmsgstr \"[6 Result of the e-voting](6_e_haaletamise_tulemus/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:45\nmsgid \"\"\n\"[7 E-hääletamisest osavõtu \"\n\"üldstatistika](7_e_haaletamise_yldstatistika/SPEC.md)\"\nmsgstr \"\"\n\"[7 General statistics on e-voting \"\n\"participation](7_e_haaletamise_uldstatistika/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:47\nmsgid \"\"\n\"[8 E-hääletamisest osavõtu \"\n\"detailstatistika](8_e_haaletamise_detailstatistika/SPEC.md)\"\nmsgstr \"\"\n\"[8 Detailed statistics on e-voting \"\n\"participation](8_e_haaletamise_detailstatistika/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:49\nmsgid \"[9 E-hääletamiste nimekiri](9_e_haaletamiste_nimekiri/SPEC.md)\"\nmsgstr \"[9 e-vote list](9_e_haaletamiste_nimekiri/SPEC.md)\"\n\n#: ../../VIS3-EHS/README.md:51\nmsgid \"\"\n\"Spetsifikatsioonides kasutatav valimissündmuse identifikaator peab \"\n\"vastama formaadile [Valimissündmuse \"\n\"identifikaator](valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\"The election event identifier used in the specifications must conform to \"\n\"the format [Election event \"\n\"identifier](valimissündmuse_identifikaator.md).\"\n\n#: ../../VIS3-EHS/README.md:53\nmsgid \"Usalduse loomine masinliidestes\"\nmsgstr \"Building trust in machine interfaces\"\n\n#: ../../VIS3-EHS/README.md:55\nmsgid \"Masinliidesed kaitstakse HTTPS-ga ja TLS mõlemapoolse autentimisega.\"\nmsgstr \"Machine interfaces are protected by HTTPS and TLS two-way authentication.\"\n\n#: ../../VIS3-EHS/README.md:57\nmsgid \"\"\n\"Lisaks piiratakse VIS3 seadistusega EHS pääs ainult EHS-le määratud \"\n\"otspunktidele (3b ja 7).\"\nmsgstr \"\"\n\"In addition, the VIS3 setting restricts EHS access to the endpoints \"\n\"assigned to EHS only (3b and 7).\"\n\n#: ../../VIS3-EHS/README.md:59\nmsgid \"Veakäsitlus masinliidestes\"\nmsgstr \"Error handling in machine interfaces\"\n\n#: ../../VIS3-EHS/README.md:61\nmsgid \"\"\n\"VIS3 peab andma HTTP standardi kohase vastuskoodi ja veateate, kui EHS \"\n\"saadetud päring on ebakorrektne või VIS3 ei suuda päringut teenendada.\"\nmsgstr \"\"\n\"The VIS3 must provide an HTTP standard response code and an error message\"\n\" if the request sent by the EHS is incorrect or if the VIS3 fails to \"\n\"serve the request.\"\n\n#: ../../VIS3-EHS/README.md:63\nmsgid \"\"\n\"EHS peab arvestama VIS3 tõrke võimalusega. Standardne reaktsioon tõrkele \"\n\"on päringu uuestisaatmine.\"\nmsgstr \"\"\n\"EHS must take into account the possibility of a VIS3 failure. The \"\n\"standard response to a failure is to resend the request.\"\n\n#: ../../VIS3-EHS/README.md:65\nmsgid \"Veakäsitlus faili inimese poolt edastamise liidestes\"\nmsgstr \"Error handling in human file transfer interfaces\"\n\n#: ../../VIS3-EHS/README.md:67\nmsgid \"\"\n\"VIS3 peab kontrollima üleslaetud faili süntaksit ja kus võimalik, kas \"\n\"semantikat. Ebakorrektse faili kohta tuleb anda operaatorile teada. \"\n\"Ebakorrektse faili andmeid ei kanta VIS3 andmebaasi.\"\nmsgstr \"\"\n\"VIS3 must check the syntax and, where possible, the semantics of the \"\n\"uploaded file. An incorrect file must be reported to the operator. The \"\n\"data of an incorrect file shall not be entered into the VIS3 database.\"\n\n#: ../../VIS3-EHS/README.md:69\nmsgid \"\"\n\"Korrektse, kuid eksitusena üleslaetud faili andmeid saab VIS3 \"\n\"andmebaasist kustutada ja õige fail uuesti üles laadida.\"\nmsgstr \"\"\n\"The data of a correct file uploaded in error can be deleted from the VIS3\"\n\" database and the correct file uploaded again.\"\n\n#: ../../VIS3-EHS/README.md:71\nmsgid \"Muudatused võrreldes VIS2-EHS-ga\"\nmsgstr \"Changes compared to VIS2-EHS\"\n\n#: ../../VIS3-EHS/README.md:73\nmsgid \"VIS3-EHS liideses on võrreldes VIS2-ga muudetud ja ajakohastatud:\"\nmsgstr \"The VIS3-EHS interface has been modified and updated compared to VIS2:\"\n\n#: ../../VIS3-EHS/README.md:75\nmsgid \"\"\n\"osapoolte vahetumisest (valijate nimekirja hakkab EHS-le saame mitte RR\"\n\"/SMIT-ist, vaid RIA VIS3-st)\"\nmsgstr \"\"\n\"change of parties (we will get the list of voters for EHS not from \"\n\"RR/SMIT but from RIA VIS3).\"\n\n#: ../../VIS3-EHS/README.md:76\nmsgid \"valimisõiguse muutumisest (jaoskondade tähenduse muutumine)\"\nmsgstr \"changes to electoral law (changing the meaning of polling stations)\"\n\n#: ../../VIS3-EHS/README.md:77\nmsgid \"valijate nimekirja ja selle uuenduste edastamisest üle liidese.\"\nmsgstr \"the transmission of the electoral roll and its updates over the interface.\"\n\n#: ../../VIS3-EHS/README.md:79\nmsgid \"\"\n\"VIS2 ja EHS omavahelisi liideseid spetsifitseerib \\\"IVXV protokollide \"\n\"kirjeldus\\\" (v 1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf). \\\"Elektroonilise hääletamise protokollistik \"\n\"defineerib elektroonilise hääletamise süsteemi komponentide vahelise \"\n\"sõnumivahetuse, kasutatavad andmestruktuurid, algoritmid ning liidesed \"\n\"väliste süsteemidega.\\\" Spetsifitseeritud on:\"\nmsgstr \"\"\n\"The interfaces between VIS2 and EHS are specified in the \\\"Specification \"\n\"of the IVXV Protocols\\\" (v 1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf). \\\"The Electronic Voting Protocol Specification \"\n\"defines the messaging between the components of the electronic voting \"\n\"system, the data structures to be used, the algorithms, and the \"\n\"interfaces to external systems.\\\" Specified are:\"\n\n#: ../../VIS3-EHS/README.md:81\nmsgid \"liides \\\"Valimisringkondade nimekiri\\\" (jaotis 3.2)\"\nmsgstr \"the interface \\\"List of constituencies\\\" (section 3.2).\"\n\n#: ../../VIS3-EHS/README.md:82\nmsgid \"liides \\\"Valikute nimekiri\\\" (jaotis 3.4)\"\nmsgstr \"the \\\"List of choices\\\" interface (section 3.4)\"\n\n#: ../../VIS3-EHS/README.md:83\nmsgid \"liides \\\"Valijate nimekiri\\\" (jaotis 3.3)\"\nmsgstr \"the \\\"Voters' list\\\" interface (section 3.3)\"\n\n#: ../../VIS3-EHS/README.md:84\nmsgid \"liides \\\"Tühistus- ja ennistusnimekiri\\\" (jaotis 9.1)\"\nmsgstr \"the interface \\\"Revokation and Restoration List\\\" (section 9.1).\"\n\n#: ../../VIS3-EHS/README.md:85\nmsgid \"liides \\\"E-hääletanute nimekiri\\\" (jaotis 9.2)\"\nmsgstr \"the interface \\\"E-vote list\\\" (section 9.2).\"\n\n#: ../../VIS3-EHS/README.md:86\nmsgid \"liides \\\"E-hääletamise tulemus\\\" (jaotis 9.3)\"\nmsgstr \"the interface \\\"Result of e-voting\\\" (section 9.3).\"\n\n#: ../../VIS3-EHS/README.md:88\nmsgid \"\"\n\"Käesolevad spetsifikatsioonid tuginevad ülalnimetatud \"\n\"spetsifikatsioonidele. Täpsemalt vt konkreetsete spetsifikatsioonide \"\n\"juures.\"\nmsgstr \"\"\n\"These specifications are based on the above specifications. For more \"\n\"details, see the specific specifications.\"\n\n#: ../../VIS3-EHS/README.md:90\nmsgid \"\"\n\"EHS lähtekood on kättesaadav: [https://github.com/vvk-\"\n\"ehk/ivxv](https://github.com/vvk-ehk/ivxv).\"\nmsgstr \"\"\n\"The EHS source code is available at: \"\n\"[https://github.com/valimised/ivxv](https://github.com/valimised/ivxv).\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/Taustateave/README.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 22:58+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:1\nmsgid \"\"\n\"Kaustas on spetsifikatsioonid X-tee teenustele, millega VIS3 pärib \"\n\"Rahvastikuregistrist valijate nimekirja ja nimekirja muudatused:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:4\nmsgid \"\"\n\"`RRValimisteAlgNimekiri_kirjeldus.pdf` - Rahvastikuregistri poolt \"\n\"osutatava  Valimiste algnimekirja X-tee teenuse spetsifikatsioon.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:7\nmsgid \"\"\n\"`RRValimisteNimekirjaMuudatused.pdf` - Rahvastikuregistri poolt osutatava\"\n\"  Valimiste algnimekirja muudatuste X-tee teenuse spetsifikatsioon.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:10\nmsgid \"\"\n\"Spetsifikatsioonid võivad aidata mõista VIS3 poolt EHS-le pakutavate \"\n\"andmete koosseisu ja tähendust.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/VIS3-EHS/valimissündmuse_identifikaator.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-03-02 12:43+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:19+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:1\nmsgid \"Valimissündmuse identifikaator\"\nmsgstr \"Election event identifier\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:3\nmsgid \"\"\n\"Üht valimist puudutav andmestik on seotud unikaalse valimissündmuse \"\n\"identifikaatori abil.\"\nmsgstr \"\"\n\"A set of data relating to a single election is linked by a unique election \"\n\"event identifier.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:5\nmsgid \"\"\n\"Valimissündmuse identifikaator on sõne, mis koosneb kahest kohustuslikust \"\n\"ja kahest valikulisest osast:\"\nmsgstr \"\"\n\"The election event identifier is a string consisting of two mandatory and \"\n\"two optional parts:\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:6\nmsgid \"valimissündmuse tüüp, vastavalt allolevale tabelile, nt `RK`.\"\nmsgstr \"the type of event, according to the table below, e.g. `RK'.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:7\nmsgid \"aastanumbrist (nt `2023`)\"\nmsgstr \"from the year (e.g. `2023`)\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:8\nmsgid \"valikulisest erakorraliste valimiste tunnusest `_E` ja\"\nmsgstr \"the optional special election flag `_E` and\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:9\nmsgid \"valikulisest järjenumbrist <nr>, nt `2`.\"\nmsgstr \"from an optional sequence number <nr>, e.g. `2`.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:11\nmsgid \"Ülalnimetatud osad eraldatakse üksteisest allkriipsudega `_`.\"\nmsgstr \"The above parts are separated by underscores `_'.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:13\nmsgid \"\"\n\"Järjenummerdatakse tüüpide kaupa. Seejuures aasta esimese samatüübilise \"\n\"valimissündmuse korral järjenumbrit ei näidata.\"\nmsgstr \"\"\n\"Sequence numbering by type. The sequence number is not shown for the first \"\n\"election event of the same type in the year.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:15\nmsgid \"Näited:\"\nmsgstr \"Examples:\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:16\nmsgid \"`KOV_2021`\"\nmsgstr \"'KOV_2021\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:17\nmsgid \"`RH_2021`\"\nmsgstr \"'RH_2021\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:18\nmsgid \"`RH_2021_2` (2021. a teine rahvahääletus)\"\nmsgstr \"`RH_2021_2` (second referendum in 2021)\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:19\nmsgid \"`RK_2023`\"\nmsgstr \"'RK_2023\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:20\nmsgid \"`RK_2023_E` (2023. a Riigikogu erakorralised valimised)\"\nmsgstr \"`RK_2023_E` (2023 special elections to the Riigikogu)\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:21\nmsgid \"`RK_2023_E_2` (2023. a Riigikogu teised erakorralised valimised).\"\nmsgstr \"`RK_2023_E_2` (other special elections to the Riigikogu in 2023).\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:23\nmsgid \"\"\n\"EHSi jaoks on valimise identifikaator kuni 28 ASCII-tähemärgi pikkune sõne \"\n\"ning eelnevalt kirjeldatud struktuuri põhjal EHS otsuseid ei tee.\"\nmsgstr \"\"\n\"For the EHS, the election identifier is a string up to 28 ASCII characters \"\n\"long, and the EHS does not make decisions based on the structure described \"\n\"above.\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:25\nmsgid \"Valimissündmuse tüüp\"\nmsgstr \"Type of election event\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:27\nmsgid \"Valimissündmuse tüüp esitatakse koodiga:\"\nmsgstr \"The type of the election event is indicated by a code:\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:29\nmsgid \"`KOV` - Kohaliku omavalitsuse volikogu valimised\"\nmsgstr \"`KOV` - Local government council elections\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:30\nmsgid \"`EP` - Euroopa Parlamendi valimised\"\nmsgstr \"`EP` - European Parliament elections\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:31\nmsgid \"`RK` - Riigikogu valimised\"\nmsgstr \"`RK` - Riigikogu elections\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:32\nmsgid \"`RH` - Rahvahääletus.\"\nmsgstr \"`RH` - Referendum.\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/examples.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-03-02 12:43+0200\\n\"\n\"PO-Revision-Date: 2024-03-02 13:16+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../examples.rst:3\nmsgid \"Skeemid ja näited\"\nmsgstr \"Schemes and examples\"\n\n#: ../../examples.rst:7\nmsgid \"Valimisringkondade nimekiri\"\nmsgstr \"List of constituencies\"\n\n#: ../../examples.rst:10 ../../examples.rst:48 ../../examples.rst:84\n#: ../../examples.rst:119 ../../examples.rst:154 ../../examples.rst:189\n#: ../../examples.rst:206 ../../examples.rst:217\nmsgid \"Skeem\"\nmsgstr \"Scheme\"\n\n#: ../../examples.rst:17 ../../examples.rst:61 ../../examples.rst:97\n#: ../../examples.rst:132 ../../examples.rst:167\nmsgid \"Näide: KOV\"\nmsgstr \"Example: KOV\"\n\n#: ../../examples.rst:24 ../../examples.rst:55 ../../examples.rst:91\n#: ../../examples.rst:126 ../../examples.rst:161\nmsgid \"Näide: EP\"\nmsgstr \"Example: EP\"\n\n#: ../../examples.rst:31 ../../examples.rst:68 ../../examples.rst:103\n#: ../../examples.rst:138 ../../examples.rst:173\nmsgid \"Näide: RH\"\nmsgstr \"Example: RH\"\n\n#: ../../examples.rst:38 ../../examples.rst:74 ../../examples.rst:109\n#: ../../examples.rst:144 ../../examples.rst:179\nmsgid \"Näide: RK\"\nmsgstr \"Example: RK\"\n\n#: ../../examples.rst:45\nmsgid \"Valikute nimekiri\"\nmsgstr \"List of choices\"\n\n#: ../../examples.rst:81\nmsgid \"E-hääletanute nimekiri\"\nmsgstr \"List of e-voters\"\n\n#: ../../examples.rst:116\nmsgid \"Tühistusnimekiri\"\nmsgstr \"Revokation list\"\n\n#: ../../examples.rst:151\nmsgid \"E-hääletamise tulemus\"\nmsgstr \"Result of e-voting\"\n\n#: ../../examples.rst:186\nmsgid \"E-hääletamise üldstatistika\"\nmsgstr \"General statistics on e-voting\"\n\n#: ../../examples.rst:196\nmsgid \"Näide\"\nmsgstr \"Example\"\n\n#: ../../examples.rst:203\nmsgid \"E-hääletamise detailstatistika\"\nmsgstr \"Detailed statistics on e-voting\"\n\n#: ../../examples.rst:214\nmsgid \"E-hääletamiste nimekiri\"\nmsgstr \"List of e-votes\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/en/LC_MESSAGES/index.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.9.1\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\n#: ../../index.rst:4\nmsgid \"IVXV registreerimisteenus\"\nmsgstr \"IVXV registration service\"\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:1\nmsgid \"1 Valimisringkondade nimekiri\"\nmsgstr \"Valimisringkondade nimekiri\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:5\nmsgid \"VIS3 edastab EHS-le teabe valimisringkondade kohta.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:7\nmsgid \"Andmed edastatakse inim-masin-protseduuriga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:9\nmsgid \"VIS peakasutaja laeb JSON faili VIS3-st alla;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:11\nmsgid \"\"\n\"VIS peakasutaja allkirjastab faili digitaalselt väljaspool VIS-i ja annab\"\n\" selle EHS operaatorile;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:13\nmsgid \"EHS operaator laeb digiallkirjastatud faili EHS-i.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:15\nmsgid \"Fail on JSON-formaadis. Faili ei allkirjastata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:17\nmsgid \"\"\n\"Faili struktuur on kirjeldatud JSON schema-ga. Kirjeldus on \"\n\"kooskõlastatud EHS omanikuga.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:19\nmsgid \"\"\n\"EHS senine liides VIS2-ga on spetsifitseeritud dokumendis \\\"IVXV \"\n\"protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf), jaotises 3.2 \\\"Valimisjaoskondade ja -ringkondade\"\n\" nimekiri\\\".\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Valimistel, kus saavad osaleda alaliselt välisriigis elavad valijad \"\n\"kantakse nad valijate nimekirja tunnusega `FOREIGN`. Sellisel juhul peab \"\n\"valimisringkondade nimekiri sisaldama regiooni nende valijate jaoks ning \"\n\"iga ringkond peab sisaldama eraldi jaoskonda, kus nende häälte üle \"\n\"arvestust peetakse. Nii selle regiooni kui jaoskondade tunnuseks on \"\n\"fiktiivne EHAK: `0000`. Näitefailis \"\n\"[districts_RK.json](districts_RK.json) on näha nii regiooni korrektne \"\n\"lisamine kui ka välishääletajate jaoskonna korrektne lisamine igasse \"\n\"ringkonda.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:25\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:27\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:29\nmsgid \"Faili struktuur (JSON-skeem): [districts.schema](districts.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:31\nmsgid \"Näited (JSON):\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:33\nmsgid \"[district_KOV.json](district_KOV.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:34\nmsgid \"[districts_EP.json](districts_EP.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:35\nmsgid \"[districts_RH.json](districts_RH.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:36\nmsgid \"[districts_RK.json](districts_RK.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:38\nmsgid \"Taustainfo\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:40\nmsgid \"\"\n\"Kandidaate on võimalik valimisele üles seada ainult konkreetses \"\n\"valimisringkonnas. Ringkondade järgi antakse valijatele hääletamise \"\n\"valikud:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:42\nmsgid \"Iga valija kuulub talle määratud valimisringkonda;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:43\nmsgid \"\"\n\"Kõigis ühe ringkonna jaoskondades saavad valijad teha valiku vaid selle \"\n\"ringkonna valikute vahel;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:45\nmsgid \"\"\n\"Eesti riiklikel valimistel eristatakse kohalike omavalitsuste volikogude \"\n\"(KOV) valimisi, Riigikogu valimisi, Euroopa Parlamendi valimisi ning \"\n\"rahvahääletusi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:47\nmsgid \"\"\n\"KOV valimised korraldatakse vastavalt seadusele „Kohaliku omavalitsuse \"\n\"volikogu valimise seadus“. Valimine toimub kohaliku omavalitsuse \"\n\"tasandil, igal omavalitsusel on oma hääletamistulemus. Valimisringkonnad \"\n\"moodustatakse omavalitsuse tasemel vastavalt seaduses kirjeldatud \"\n\"reeglitele.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:49\nmsgid \"\"\n\"Riigikogu valimised korraldatakse vastavalt seadusele „Riigikogu valimise\"\n\" seadus“. Valimine toimub riigi tasandil. Riik jaguneb 12 \"\n\"valimisringkonnaks. Hääletamistulemus tehakse kindlaks iga \"\n\"valimisringkonna kohta.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:51\nmsgid \"\"\n\"Europarlamendi valimised korraldatakse vastavalt seadusele „Euroopa \"\n\"Parlamendi valimise seadus“. Valimine toimub riigi tasandil, \"\n\"hääletamistulemus on kõigile kohalikele omavalitsustele ühine. Terve riik\"\n\" on üks valimisringkond.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:53\nmsgid \"\"\n\"Rahvahääletused korraldatakse vastavalt seadusele „Rahvahääletuse \"\n\"seadus“. Valimine toimub riigi tasandil, hääletamistulemus on kõigile \"\n\"kohalikele omavalitsustele ühine. Terve riik on üks valimisringkond.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:55\nmsgid \"\"\n\"Erinevad valimised ei erine elektroonilise hääletamise andmevormingute ja\"\n\" protseduuride poolest. Erinevad ringkondade jaotused hallatakse VIS3 \"\n\"poolt.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:57\nmsgid \"\"\n\"Kandidaate on võimalik valimisele üles seada ainult konkreetses \"\n\"valimisringkonnas. Valijad on jaotatud valimisringkondade vahel. Valija \"\n\"saab teha valiku ainult tema ringkonnas kandideerivate kandidaatide \"\n\"vahel.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:59\nmsgid \"\"\n\"Kuna kohaliku omavalitsuse volikogude valimisel toimub valimine Eesti \"\n\"omavalitsuste (vallad, linnad) tasemel, siis kasutatakse elektroonilise \"\n\"hääletamise protokollistikus valimisringkondade kirjeldamisel ning \"\n\"valijate ja valikute ringkonnakuuluvuse näitamisel Eesti haldus- ja \"\n\"asustusjaotuse klassifikaatorit EHAK\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:61\nmsgid \"\"\n\"Näiteks: • Tallinna linna Pirita linnaosa EHAK kood on 0596; • Anija \"\n\"valla EHAK kood on 0141.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:65\nmsgid \"\"\n\"Riigi tasemel toimuvatel valimistel pannakse ringkonna EHAK koodiks \"\n\"kokkuleppeliselt 0.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/1_Valimisringkondade_nimekiri/SPEC.md:67\nmsgid \"\"\n\"Riigikogu ja europarlamendi valimistel ning rahvahääletusel moodustatakse\"\n\" nimekirjas igasse ringkonda fiktiivne üksus alaliselt välisriigis \"\n\"elavate valijate tarbeks. Selle üksuse number on 0 ning vastav EHAK kood \"\n\"on 0000.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/2_Valikute_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:1\nmsgid \"2 Valikute nimekiri (kandidaatide nimekiri)\"\nmsgstr \"Valikute nimekiri (kandidaatide nimekiri)\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:5\nmsgid \"\"\n\"VIS3 edastab EHS-le andmed registreeritud kandidaatide (valimistel) või \"\n\"vastusevariantide (rahvahääletusel) kohta.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:7\nmsgid \"Andmed edastatakse inim-masin-protseduuriga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:9\nmsgid \"\"\n\"VIS peakasutaja laeb JSON faili VIS3-st alla; Kasutaja peab valima \"\n\"aktiivsete valimissündmuste seast soovitud sündmuse, mille kohta infot \"\n\"alla laadida. Valimissündmus on aktiivne, kui tema staatus pole `closed` \"\n\"ega `deleted`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:10\nmsgid \"\"\n\"VIS peakasutaja allkirjastab faili digitaalselt väljaspool VIS-i ja annab\"\n\" selle EHS operaatorile;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:11\nmsgid \"EHS operaator laeb digiallkirjastatud faili EHS-i.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:13\nmsgid \"\"\n\"Fail on JSON-formaadis (näidis lisatud tööülesandele). Faili ei \"\n\"allkirjastata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:15\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:17\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis\"\n\" \\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf), jaotises \\\"Valikute nimekiri\\\" (jaotis 3.4).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:19\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:21\nmsgid \"Faili struktuur (JSON-skeem): [choices.schema](choices.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:23\nmsgid \"Faili näited (JSON):\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:25\nmsgid \"[choices_KOV.json](choices_KOV.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:26\nmsgid \"[choices_EP.json](choices_EP.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:27\nmsgid \"[choices_RK.json](choices_RK.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:29\nmsgid \"Taustainfo\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:31\nmsgid \"\"\n\"Valikute nimekiri sisaldab andmeid kandidaatide (valimistel) või \"\n\"vastusevariantide (rahvahääletusel) kohta. Valimiste korral on lisaks \"\n\"kandidaadi andmetele nimekirjas ka tema erakonna või valimisliidu nimi, \"\n\"mille nimekirjas ta kandideerib.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:34\nmsgid \"\"\n\"Valijale elektroonilise hääletamise käigus nähtavaid valimiste vahelisi \"\n\"süsteemseid erinevusi on kaks:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:36\nmsgid \"\"\n\"Rahvahääletusel ei valita erakondadesse kuuluvate kandidaatide vahel vaid\"\n\" vastatakse „JAH“/“EI“ rahvahääletuse küsimusele;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:38\nmsgid \"\"\n\"Riigikogu, KOV ja Euroopa Parlamendi valimistel antakse hääl ühele \"\n\"kandidaadile, kes võib, aga ei pruugi kuuluda poliitilise ühenduse \"\n\"nimekirja.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/2_Valikute_nimekiri/SPEC.md:40\nmsgid \"\"\n\"Protokollistik kodeerib valija võimalikud valikud ringkonnas kuni \"\n\"11-kohalise arvväärtusena, mis valikute nimekirjas kodeeritakse koos \"\n\"ringkonna EHAK-koodiga. Valijale tohivad kättesaadavad olla ainult tema \"\n\"ringkonnakohased valikud. Valijarakendus peab seda omadust tagama ning \"\n\"hääletamistulemust arvutav rakendus kontrollima.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/3_Valijate_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:1\nmsgid \"3  Valijate nimekiri EHS-le\"\nmsgstr \"Valijate nimekiri EHS-le\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:3\nmsgid \"1. Annotatsioon\"\nmsgstr \"Annotatsioon\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:5\nmsgid \"\"\n\"Spetsifikatsioon määratleb Valimiste infosüsteemi (VIS3) ja e-hääletamise\"\n\" süsteemi (EHS) vahelise liidese, mille kaudu VIS3 edastab EHS-le \"\n\"valijate algnimekirja ja valijate nimekirja muudatusi ehk \"\n\"muudatusnimekirju.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:9\nmsgid \"\"\n\"Spetsifikatsioon on avalik. Spetsifikatsioon ei käsitle VIS3 ega EHS \"\n\"konfidentsiaalset siseehitust ega liidese konfidentsiaalseid elemente.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:12\nmsgid \"\"\n\"Käesolevat spetsifikatsiooni tuleb kasutada koos VIS3 EHS API OpenAPI \"\n\"spetsifikatsiooniga (asub käesolevas repos, failis `vis-ehs-api.yaml`).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:14\nmsgid \"2. Sõnumivahetus nimekirjade edastamiseks\"\nmsgstr \"Sõnumivahetus nimekirjade edastamiseks\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:16\nmsgid \"\"\n\"Valijate nimekirja põhjal tuvastab EHS isiku hääleõiguse ja \"\n\"ringkonnakuuluvuse. Valijate nimekiri võib hääletamisperioodi jooksul \"\n\"muutuda, sellest tulenevalt võib valija saada hääleõiguse, jääda oma \"\n\"hääleõigusest ilma või saada hääleõiguse senisest erinevas ringkonnas. \"\n\"EHS peab nende muutustega arvestama. Muudatused saab EHS VIS3 vahendusel \"\n\"muudatusnimekirjadena.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:22\nmsgid \"\"\n\"Spetsifikatsioonis defineeritud liidese vahendusel suhtlevad vahetult EHS\"\n\" ja VIS3:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:25\nmsgid \"\"\n\"VIS3 liidestub Rahvastikuregistri andmekoguga ning hangib seal nii \"\n\"valijate algnimekirja kui ka muudatusnimekirjad.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:27\nmsgid \"EHS esitab VIS3-le päringuid muudatusnimekirjade saamiseks.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:29\nmsgid \"Kaudselt on liidesega seotud:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:31\nmsgid \"Rahvastikuregister, kus toimub nimekirjade haldamine;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:32\nmsgid \"\"\n\"VIS3 ja EHS operaatorid, kes vahetavad taustakanalis algnimekirja ning \"\n\"suhtlevad võimalike tõrgete lahendamisel;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:34\nmsgid \"Audiitor, kes veendub et EHS on VIS3 poolt tarnitud nimekirjad rakendanud;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:35\nmsgid \"\"\n\"EHS vallasrežiimis töötlemisrakendus, mis verifitseerib EHS sidusrežiimi \"\n\"komponentide poolt üle antud urni.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:38\nmsgid \"2.1 Ettevalmistused nimekirjade edastamiseks\"\nmsgstr \"Ettevalmistused nimekirjade edastamiseks\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:40\nmsgid \"![Joonis 1: Sõnumivahetuse ettevalmistamine](model/list_prepare.png)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:40\nmsgid \"Joonis 1: Sõnumivahetuse ettevalmistamine\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:42\nmsgid \"\"\n\"Valijate alg- ja muudatusnimekirjade vahetamisele eelnevalt tuleb \"\n\"vahetada nimekirjadele juurdepääsuks ja nimekirjade autentsuse \"\n\"kontrolliks vajalikud võtmed ja sertifikaadid (sammud 1-5)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:46\nmsgid \"2.2 Valijate algnimekirja edastamine\"\nmsgstr \"Valijate algnimekirja edastamine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:48\nmsgid \"![Joonis 2: Algnimekirja edastamine](model/list_initial.png)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:48\nmsgid \"Joonis 2: Algnimekirja edastamine\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:50\nmsgid \"\"\n\"Valijate algnimekiri järjekorranumbriga 0 laetakse EHSi haldusliidese \"\n\"veebiliidesest. Valijate algnimekirja laadimine on eelduseks \"\n\"muudatusnimekirjade edasiseks automaatseks laadimiseks.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:54\nmsgid \"Valijate algnimekirja laadimine toimub järgmistes etappides:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:56\nmsgid \"\"\n\"VIS3 kasutab Rahvastikuregistri X-tee teenust valijate algnimekirja \"\n\"laadimiseks (sammud 1-3).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:58\nmsgid \"\"\n\"VIS3 peakasutaja pärib allkirjastatud algnimekirja VIS3 teenusest, ning \"\n\"allkirjastab algnimekirja täiendavalt ID-kaardiga (sammud 4-6).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:60\nmsgid \"\"\n\"VIS3 peakasutaja edastab allkirjastatud algnimekirja EHS operaatorile \"\n\"(samm 7)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:62\nmsgid \"\"\n\"EHS operaator laeb digitaalselt allkirjastatud algnimekirja EHSi, kus see\"\n\" rakendatakse (sammud 8-9).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:65\nmsgid \"2.3 Valijate muudatusnimekirja edastamine\"\nmsgstr \"Valijate muudatusnimekirja edastamine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:67\nmsgid \"![Joonis 3: Muudatusnimekirja edastamine](model/list_changeset.png)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:67\nmsgid \"Joonis 3: Muudatusnimekirja edastamine\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:69\nmsgid \"\"\n\"Valijate nimekirja muudatusnimekirja laadimise algatab EHS. \"\n\"Muudatusnimekirjad on järjestatud, nii EHS kui ka VIS3 peavad arvet, \"\n\"milline on viimane loodud muudatusnimekiri.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:73\nmsgid \"\"\n\"EHS algatab uue muudatusnimekirja laadimise pöördudes uue \"\n\"järjekorranumbriga VIS3 vastava otspunkti poole. EHS võib varasemaid \"\n\"muudatusnimekirju uuesti pärida, kasutades varasemat järjekorranumbrit.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:77\nmsgid \"\"\n\"VIS3 saab muudatused Rahvastikuregistri X-tee teenusest. VIS3 poolne \"\n\"muudatuste hankimine on EHSile muudatuste esitamisest sõltumatu \"\n\"paralleelprotsess. See tähendab, et üks EHSile minev muudatusnimekiri \"\n\"võib sisaldada mitut Rahvastikuregistrist tulnud muudatust.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:82\nmsgid \"Muudatusnimekirjade edastamine EHSi käib järgmiselt:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:84\nmsgid \"EHS esitab päringu muudatusnimekirja saamiseks (samm 1)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:85\nmsgid \"Kui muudatusi ei ole, siis ei ole ka muudatusnimekirja (samm 2)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:86\nmsgid \"\"\n\"EHS kordab mingi aja möödudes päringut muudatusnimekirja saamiseks (samm \"\n\"3)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:87\nmsgid \"\"\n\"VIS3 edastab vahepeal saabunud muudatused EHSile ning EHS rakendab need \"\n\"edukalt (sammud 4-5)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:89\nmsgid \"\"\n\"Nii VIS3 kui EHS suurendavad muudatusnimekirjade järjekorranumbrit ning \"\n\"mõne aja möödudes edastatakse uus muudatusnimekiri (sammud 6-8)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:92\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:96\nmsgid \"**Märkused**\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:93\nmsgid \"\"\n\"Kui järjekorras järgmise muudatusnimekirja pärimisel muudatuskirjed \"\n\"puuduvad vastatakse HTTP staatusega `404` (not found).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:97\nmsgid \"\"\n\"Kui päritakse muudatusnimekirja suurema järjenumbriga, kui tegelik \"\n\"järgmise muudatusnimekirja järjenumber, vastatakse HTTP staatusega `409` \"\n\"(conflict).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:100\nmsgid \"2.4 Veajuhtumite lahendamine\"\nmsgstr \"Veajuhtumite lahendamine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:102\nmsgid \"![Joonis 4: Veajuhtumite lahendamine](model/list_errors.png)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:102\nmsgid \"Joonis 4: Veajuhtumite lahendamine\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:104\nmsgid \"\"\n\"Muudatusnimekirjade edastamisel ei saa välistada vigu. Olenevalt vea \"\n\"iseloomust on võimalik taaste, keerulisematel juhtudel tuleb mõni \"\n\"vigaseks osutunud nimekiri vahele jätta. Veajuhtumi menetlemise ajal \"\n\"muudatusnimekirju ei edastata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:109\nmsgid \"Veajuhtumite lahendamine toimub järgmiselt:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:111\nmsgid \"\"\n\"EHS ja VIS3 käivitavad protokolli muudatusnimekirja edastamiseks (sammud \"\n\"1-3).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:113\nmsgid \"\"\n\"EHS tunnistab nimekirja vigaseks ning teavitab sellest operaatorit (samm \"\n\"4).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:114\nmsgid \"\"\n\"Probleemi analüüsides tuvastatakse EHS-poolne tõrge, mida on võimalik \"\n\"lahendada. Tõrge lahendatakse ning EHS ja VIS3 kordavad edukalt \"\n\"protokolli (sammud 5-7).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:117\nmsgid \"\"\n\"EHS ja VIS3 käivitavad protokolli muudatusnimekirja edastamiseks (sammud \"\n\"8-10).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:119\nmsgid \"\"\n\"EHS tunnistab nimekirja vigaseks ning teavitab sellest operaatorit (samm \"\n\"11).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:121\nmsgid \"\"\n\"Probleemi analüüsides tuvastatakse sisuline probleem nimekirjas. Vigast \"\n\"nimekirja ei muudeta, luuakse uus korrektne nimekiri. EHS operaator laeb \"\n\"digitaalselt allkirjastatud korralduse vigase nimekirja vahele jätmiseks \"\n\"(samm 12)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:125\nmsgid \"\"\n\"EHS ja VIS3 suurendavad järjekorranumbrit ning käivitavad protokolli \"\n\"muudatusnimekirja edastamiseks (sammud 13-15).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:127\nmsgid \"\"\n\"EHS ja VIS3 suurendavad järjekorranumbrit ning käivitavad protokolli \"\n\"muudatusnimekirja edastamiseks (sammud 16-17). VIS3 vastab HTTP \"\n\"staatusega 409 (conflict).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:130\nmsgid \"\"\n\"EHS tuvastab vale järjenumbriga nimekirja laadimise ning teavitab sellest\"\n\" operaatorit (samm 18).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:132\nmsgid \"\"\n\"Tõrge lahendatakse ning EHS ja VIS3 kordavad edukalt protokolli (sammud \"\n\"19-21).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:134\nmsgid \"2.5 Nimekirjade loendi edastamine\"\nmsgstr \"Nimekirjade loendi edastamine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:136\nmsgid \"3. Nimekirja andmevorming\"\nmsgstr \"Nimekirja andmevorming\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:138\nmsgid \"\"\n\"Nimekirja andmevorming on sama nii valijate algnimekirja kui \"\n\"muudatusnimekirjade jaoks.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:141\nmsgid \"\"\n\"Nimekiri esitatakse UTF-8-NOBOM vormingus tekstifailina. \"\n\"Andmestruktuuride eraldajaks kasutatakse reavahetusmärki `LF` (ASCII-kood\"\n\" `0x0A`). Andmestruktuuride väljade eraldajaks kasutatakse tabeldusmärki \"\n\"`TAB` (ASCII-kood `0x09`).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:146\nmsgid \"Nimekiri koosneb päiseridadest ja kirjetest.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:148\nmsgid \"Päiseridade sisu on järgmine:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:150\nmsgid \"\"\n\"`version` - andmestruktuuri versiooninumber, mille pikkus on piiratud 2 \"\n\"tähemärgiga. Spetsifikatsioonile vastava nimekirja korral on selle välja \"\n\"väärtus 2.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:153\nmsgid \"\"\n\"`election_identifier` - valimissündmuse identifikaator, mille pikkus on \"\n\"piiratud 28 tähemärgiga ASCII kooditabelist. Nimekirja rakendamine toimub\"\n\" ainult vastava identifikaatoriga valimissündmuse kontekstis. \"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:156\nmsgid \"\"\n\"`changeset` - nimekirja järjekorranumber. Rangelt kasvav number (0, 1, 2,\"\n\" ...), mis defineerib nimekirjade rakendamise järjekorra. Algnimekirja \"\n\"järjekorranumbriks on 0.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:159\nmsgid \"\"\n\"`period` - nimekirjas kajastuvate muudatuste ajavahemik. Esimene väli \"\n\"sisaldab perioodi algust, teine lõppu. Algnimekirja puhul on perioodi \"\n\"algus ja lõpu väärtused võrdsed. Väli on informatiivne.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:163\nmsgid \"Kirje koosneb väljadest, mille sisu on järgmine:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:165\nmsgid \"\"\n\"`person_code` - isikukood on valija unikaalne identifikaator, mille \"\n\"alusel EHS tuvastab isiku hääleõiguse ja ringkonnakuuluvuse.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:167\nmsgid \"\"\n\"`voter_name` - valija nimi, informaalne väli, otsuste tegemisel ei \"\n\"kasutata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:168\nmsgid \"\"\n\"`action` - kirjega seotud tegevus. `lisamine` tähendab uue valija \"\n\"lisamist ja `kustutamine` olemasoleva valija eemaldamist.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:170\nmsgid \"\"\n\"`kov_code` - haldusüksus, kuhu valija kuulub. Haldusüksuse \"\n\"identifitseerimiseks kasutatakse kohaliku omavalitsuse EHAK-koodi, \"\n\"Tallinna korral linnaosa EHAK-koodi, alaliselt välisriigis elava valija \"\n\"korral kasutatakse väärtust `FOREIGN`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:174\nmsgid \"\"\n\"`electoral_district_id` - valimisringkonna number identifitseerib \"\n\"valimisringkonna, kus valija hääletab. KOV valimiste korral kehtib \"\n\"identifikaator haldusüksuse sees. RK, EP ja RH valimiste korral on \"\n\"identifikaator haldusüksuste ülene.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:179\nmsgid \"\"\n\"Välju `voter_name`, `kov_code` ja `electoral_district_id` kasutatakse \"\n\"ainult lisamiskirjes (`action` väärtus `lisamine`).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:182\nmsgid \"Andmevormingu formaalne kirjeldus Backus-Naur notatsioonis:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:212\nmsgid \"3.1 Nimekirja tõlgendamine\"\nmsgstr \"Nimekirja tõlgendamine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:214\nmsgid \"Nimekirju töötlev rakendus lähtub järgmistest reeglitest:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:216\nmsgid \"Nimekiri rakendatakse tervikuna.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:217\nmsgid \"\"\n\"Nimekirja rakendamisele eelnevad vormingu ja kooskõlalisuse kontrollid, \"\n\"vigaseid nimekirju ei rakendata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:219\nmsgid \"Rakendus kontrollib nimekirja versiooni.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:220\nmsgid \"Rakendus kontrollib valimissündmuse identifikaatorit.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:221\nmsgid \"Rakendus kontrollib nimekirja järjekorranumbrit.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:222\nmsgid \"\"\n\"Rakendus kontrollib nimekirja kõigi kirjete kooskõlalisust oma \"\n\"andmebaasiga.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:223\nmsgid \"Kooskõlalisust kontrollitakse kirjete esinemisjärjekorras.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:224\nmsgid \"\"\n\"Kui tegevus on `lisamine`, siis ei tohi vastava isikukoodiga kirjet \"\n\"rakenduse andmebaasis olla.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:226\nmsgid \"\"\n\"Kui tegevus on `kustutamine`, siis rakendus kontrollib, kas vastava \"\n\"isikukoodiga kirje on rakenduse andmebaasis olemas. Kui ei ole, siis \"\n\"rakendus logib veateate ja jätkab järgmiste kirjete töötlust.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:229\nmsgid \"\"\n\"Kui valijaga soetud andmeid on vaja muuta, näiteks valija liigub ühest \"\n\"haldusüksusest või valimisringkonnast teise, siis kantakse valijate \"\n\"nimekirja muudatuste hulka üks kustutamise kirje, millega valija oma \"\n\"eelmisest üksusest kustutatakse ja üks lisamise kirje, millega valija \"\n\"uues üksuses valijate nimekirja lisatakse. Ka kõik teised muudatused \"\n\"valija andmetes - näiteks nimemuutus - toimuvad läbi vana kirje \"\n\"kustutamise ja uue lisamise.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:236\nmsgid \"\"\n\"Valijate algnimekirjas on ainult lisamiskirjed, iga valija kohta \"\n\"maksimaalselt üks kirje. Muudatusnimekirjas peavad ühe valija kohta \"\n\"käivad kirjed olema nimekirjas loogilises järjestuses ning liiasuseta. \"\n\"Ehk kustutamine enne lisamist ning maksimaalselt üks kustutamise-lisamise\"\n\" paar.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:241\nmsgid \"\"\n\"On võimalik, et valimise ajal muutub valija isikukood. Sellisel juhul \"\n\"lisatakse nimekirja vana isikukoodiga kustutamise kirje ning uue \"\n\"isikukoodiga lisamise kirje. Täiendav korduvhääletamise kontroll ei ole \"\n\"selle liidese skoobis ja teostakse VIS3 poolt eraldi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:247\nmsgid \"3.2 Näited nimekirjadest\"\nmsgstr \"Näited nimekirjadest\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:249\nmsgid \"Valijate algnimekiri, 0.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:267\nmsgid \"Muudatusnimekiri, 1:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:269\nmsgid \"Muutub valija 20000000002 haldusüksus.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:270\nmsgid \"Valija 30000000003 kaotab hääleõiguse.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:271\nmsgid \"Valijale 11000000011 antakse hääleõigus.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:285\nmsgid \"Muudatusnimekiri, 2:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:287\nmsgid \"Muutub valija 20000000002 nimi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:288\nmsgid \"Valija 60000000006 kaotab hääleõiguse.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:300\nmsgid \"Muudatusnimekiri, 3:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:302\nmsgid \"Muutub valija 20000000003->10000000003 isikukood koos nimega.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:303\nmsgid \"Valija 60000000006 saab hääleõiguse.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:315\nmsgid \"4. Nimekirja signeerimine\"\nmsgstr \"Nimekirja signeerimine\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:317\nmsgid \"\"\n\"Nii algnimekirjaga kui muudatusnimekirjadega kaasatakse allkirjafail \"\n\"(FIPS 186-4), mille moodustab andmeallikas, arvutades algsest valijate \"\n\"nimekirjast SHA256-räsi ning allkirjastades selle räsi ECDSA võtmega \"\n\"(kasutame P-256 kõverat). Andmeallika poolt genereeritud avalik võti \"\n\"tehakse taustakanalis kättesaadavaks EHSile ning selle võtme alusel \"\n\"kontrollitakse EHS komponentides valijate nimekirjade terviklust.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:324\nmsgid \"\"\n\"Võtmete genereerimiseks, signeerimiseks ning verifitseerimiseks võib \"\n\"kasutada tööriista OpenSSL:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:327\nmsgid \"\"\n\"Võtme genereerimine: `openssl ecparam -name prime256v1 -genkey -noout \"\n\"-out private.key.pem`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:329\nmsgid \"\"\n\"Avaliku võtme eraldamine: `openssl ec -in private.key.pem -pubout -out \"\n\"public.key.pem`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:331\nmsgid \"\"\n\"Andmefaili signeerimine: `openssl dgst -sha256 -sign private.key.pem -out\"\n\" data.sig data.txt`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:333\nmsgid \"\"\n\"Signatuuri verifitseerimine: `openssl dgst -sha256 -verify public.key.pem\"\n\" -signature data.sig data.txt`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:336\nmsgid \"\"\n\"Näide antud meetodi kasutamiseks golang keeles on leitav repositooriumis \"\n\"[DigiSign](https://github.com/e-gov/DigiSign)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:339\nmsgid \"5. Transpordiprotokoll\"\nmsgstr \"Transpordiprotokoll\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:341\nmsgid \"VIS3-EHS käesolev masinliides koosneb kahest API otspunktist.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:343\nmsgid \"\"\n\"API otspunkt `ehs-voters-changeset` konkreetse muudatusnimekirja \"\n\"laadimiseks VIS3-st\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:345\nmsgid \"\"\n\"API otspunkt `ehs-list-election-changesets` ülevaate saamiseks \"\n\"avalikustatud nimekirjadest\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:348\nmsgid \"\"\n\"Transpordiprotokoll on HTTPS, kuna volitamata ligipääs nimekirjadele \"\n\"tuleb tõkestada kasutatakse mõlemapoolselt autenditud TLS ühendusi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:351\nmsgid \"5.1 `ehs-voters-changeset`\"\nmsgstr \"`ehs-voters-changeset`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:353\nmsgid \"\"\n\"HTTP meetod on GET. Päringu tegemisel tuleb kasutada kohustuslikke \"\n\"parameetreid `changeset` ja `election_identifier`, kus\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:356\nmsgid \"`changeset` on muudatusnimekirja `integer` tüüpi järjekorranumber.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:357\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:379\nmsgid \"`election_identifier` on `string` tüüpi valimissündmuse identifikaator.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:359\nmsgid \"\"\n\"Kui EHS teeb API otspunkti GET päringu, siis juhul kui vastava \"\n\"identifikaatori ja järjekorranumbriga nimekiri eksisteerib, vastab VIS3 \"\n\"`application/octet-stream` tüüpi baidijadaga, mis esitab kahest failist \"\n\"koosnevat ZIP konteinerit:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:364\nmsgid \"nimekirjafail nimega `<election_identifier>-voters-<changeset>.utf`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:366\nmsgid \"signatuurifail nimega `election_identifier<>-voters-<changeset>.sig`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:369\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:414\nmsgid \"Sellise vastuse korral on HTTP status 200.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:371\nmsgid \"\"\n\"Juhul kui vastava järjekorranumbriga muudatusnimekirja veel ei eksisteeri\"\n\" antakse vastuses HTTP status 404.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:374\nmsgid \"5.1 `ehs-list-election-changesets`\"\nmsgstr \"`ehs-list-election-changesets`\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:376\nmsgid \"\"\n\"HTTP meetod on GET. Päringu tegemisel tuleb kasutada kohustuslikku \"\n\"parameetrit `election_identifier`:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:381\nmsgid \"\"\n\"Kui EHS teeb API otspunkti, siis juhul kui vastava identifikaatoriga \"\n\"valimine eksisteerib, vastab VIS3 `application/json` tüüpi baidijadaga, \"\n\"mis sisaldab endas JSON vormingus viiteid kõigile väljastatud \"\n\"muudatusnimekirjadele.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:385\nmsgid \"\"\n\"MÄRKUS. Viidetes tarnitavad URL-id ei ole õiged ja seetõttu \"\n\"mittekasutatavad. EHS peab konkreetse muudatuste nimekirja poole \"\n\"pöördumise URL-i koostama vastavalt VIS3 EHS API OpenAPI vormingus \"\n\"spetsifikatsioonile (asub käesolevas repos, failis `vis3-ehs-api.yaml`). \"\n\"VIS3 edasisestes versioonides eemaldame viidetest URL-id. 20.07.2021.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:387\nmsgid \"\"\n\"MÄRKUS. Viidetes tarnitavate URL-de käsitlust muudetud: elemendis `url` \"\n\"tarnitakse mitte täis-URL, vaid ainult _path_ ja -query_ osa.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:416\nmsgid \"6. Näited\"\nmsgstr \"Näited\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:418\nmsgid \"7. Viited\"\nmsgstr \"Viited\"\n\n#: ../../VIS3-EHS/3_Valijate_nimekiri/SPEC.md:420\nmsgid \"\"\n\"[\\\"DigiSign\\\" - lihtne protokoll edastatavate failide allkirjastamiseks, \"\n\"koos teostusnäitega](https://github.com/e-gov/DigiSign)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:1\nmsgid \"E-hääletanute nimekiri\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:3\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:5\nmsgid \"\"\n\"E-hääletanute nimekiri on pärast e-hääletamise lõppu EHS poolt \"\n\"genereeritav ja väljastatav nimekiri e-hääletanud isikutest. \"\n\"E-hääletanute nimekiri loetakse sisse VIS3-e (moodul NIM).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:7\nmsgid \"E-hääletanute nimekiri edastakse inim-masin-protseduuriga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:9\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:11\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:13\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:15\nmsgid \"Fail on JSON-formaadis. Faili näidised on tööülesandes.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:17\nmsgid \"Faili struktuur on kirjeldatud JSON-skeemiga.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:19\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis\"\n\" \\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf), jaotises \\\"E-hääletanute nimekiri\\\" (jaotis 9.2).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:23\nmsgid \"Faili struktuur (JSON-skeem): [onlinevoters.schema](onlinevoters.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:25\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:27\nmsgid \"Faili näited (JSON):\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:29\nmsgid \"[onlinevoters_EP.json](onlinevoters_EP.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:30\nmsgid \"[onlinevoters_KOV.json](onlinevoters_KOV.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:31\nmsgid \"[onlinevoters_RH.json](onlinevoters_RH.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/4_e_haaletanute_nimekiri/SPEC.md:32\nmsgid \"[onlinevoters_RK.json](onlinevoters_RK.json)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/5_Tyhistusnimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:1\nmsgid \"5 Tühistus- ja ennistusnimekiri\"\nmsgstr \"Tühistus- ja ennistusnimekiri\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:3\nmsgid \"\"\n\"VIS3 edastab EHS-le tühistus- ja ennistusnimekirja (lühidalt - \"\n\"tühistusnimekiri).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:5\nmsgid \"\"\n\"Tühistusnimekiri sisaldab andmeid isikute kohta, kelle e-hääl tuleb \"\n\"tühistada (on paberhääletanud ja sellest tulenevalt ei lähe e-hääl \"\n\"arvesse valimistulemuste kokkulugemisel) või ennistada (s.t. tühistatakse\"\n\" eelnev tühistamine ning häälte uuesti üle lugemisel võetakse ennistatud \"\n\"e-hääl arvesse).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:7\nmsgid \"Fail on JSON-formaadis.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:9\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:11\nmsgid \"Tühistusnimekiri edastakse inim-masin-protseduuriga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:13\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:15\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:17\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:19\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:21\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis\"\n\" \\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf), jaotises \\\"Tühistus- ja ennistusnimekiri\\\" \"\n\"(jaotis 9.1).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:23\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:25\nmsgid \"Faili struktuur (JSON-skeem): [revoke.schema](revoke.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:27\nmsgid \"Faili näited (JSON):\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:29\nmsgid \"[revoke_EP.json](revoke_EP.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:30\nmsgid \"[revoke_KOV.json](revoke_KOV.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:31\nmsgid \"[revoke_RH.json](revoke_RH.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/5_Tyhistusnimekiri/SPEC.md:32\nmsgid \"[revoke_RK.json](revoke_RK.json)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/6_e_haaletamise_tulemus/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:1\nmsgid \"E-hääletamise tulemus\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:3\nmsgid \"\"\n\"E-hääletamise lõppedes genereerib EHS e-hääletamise tulemuste faili \"\n\"(hääletamistulemuse fail). Fail laetakse VIS3-e (valimistulemuse \"\n\"moodulisse TUL).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:5\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:7\nmsgid \"Fail edastatakse inim-masin-protseduuriga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:9\nmsgid \"EHS operaator laeb JSON-faili EHS-st alla;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:11\nmsgid \"\"\n\"EHS operaator allkirjastab faili digitaalselt väljaspool EHS-i ja annab \"\n\"selle VIS peakasutajale;\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:13\nmsgid \"VIS peakasutaja laeb digikonteinerist välja võetud faili VIS3-i.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:15\nmsgid \"Fail on JSON-formaadis.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:17\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:19\nmsgid \"\"\n\"Aluseks on EHS senine liides VIS2-ga, mis on spetsifitseeritud dokumendis\"\n\" \\\"IVXV protokollide kirjeldus\\\" (v 1.5.0, IVXV-PR-1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf), jaotises \\\"E-hääletamise tulemus\\\" (jaotis 9.3).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:21\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:23\nmsgid \"Faili struktuur (JSON-skeem): [results.schema](results.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:25\nmsgid \"Faili näited (JSON):\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:27\nmsgid \"[results_EP.json](results_EP.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:28\nmsgid \"[results_KOV.json](results_KOV.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:29\nmsgid \"[results_RH.json](results_RH.json)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/6_e_haaletamise_tulemus/SPEC.md:30\nmsgid \"[results_RK.json](results_RK.json)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:1\nmsgid \"7 E-hääletamisest osavõtu üldstatistika\"\nmsgstr \"E-hääletamisest osavõtu üldstatistika\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:3\nmsgid \"\"\n\"EHS annab lühikese perioodiga (nt 15 min) VIS3-le andmeid kui palju \"\n\"valijaid on e-hääletanud.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:5\nmsgid \"EHS teeb seda HTTPS päringutega VIS3 poolt pakutava otspunkti vastu.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:7\nmsgid \"Päringu vastuses tagastatav fail:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:9\nmsgid \"JSON-skeem: [online-voters-total.schema](online-voters-total.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:11\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/7_e_haaletamise_yldstatistika/SPEC.md:13\nmsgid \"Näide (JSON): [online-voters-total.json](online-voters-total.json)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:1\nmsgid \"8 E-hääletamisest osavõtu detailne statistika\"\nmsgstr \"E-hääletamisest osavõtu detailne statistika\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:3\nmsgid \"\"\n\"E-hääletamise lõppedes annab EHS VIS3-le e-hääletamisest osavõtu detailse\"\n\" statistika.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:5\nmsgid \"\"\n\"E-hääletamisest osavõtu statistikat kuvatakse valimiste veebilehel, \"\n\"samuti edastatakse masintöödeldaval kujul meediaorganisatsioonidele ja \"\n\"lõpuks publitseeritakse avaandmetena.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:7\nmsgid \"Protseduur\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:9\nmsgid \"EHS operaator laeb faili alla.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:11\nmsgid \"EHS operaator edastab faili VIS3 peakasutajale.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:13\nmsgid \"VIS3 peakasutaja laeb faili VIS3-e üles.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:15\nmsgid \"Faili edastatakse eelhääletamise ajal, kord päevas.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:17\nmsgid \"Edastatav fail\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:19\nmsgid \"JSON-skeem: [online-voters-counties.schema](online-voters-counties.schema)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:21\nmsgid \"\"\n\"Valimissündmuse identifikaator peab vastama formaadile [Valimissündmuse \"\n\"identifikaator](../valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/8_e_haaletamise_detailstatistika/SPEC.md:23\nmsgid \"Näide (JSON): [online-voters-counties.json](online-voters-counties.json)\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:1\nmsgid \"E-hääletamiste nimekiri\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:3\nmsgid \"kavand v0.4\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:5\nmsgid \"Muutelugu\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:7\nmsgid \"Eemaldatud sõna \\\"jooksev\\\" / Priit Parmakson, 23.11.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:8\nmsgid \"\"\n\"Lisatud OpenAPI kirjelduse publitseerimise teave. / Priit Parmakson, \"\n\"13.12.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:9\nmsgid \"\"\n\"Täpsustatud e-hääletamiste väljanäitamist VIS3-s. / Priit Parmakson, \"\n\"15.11.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:10\nmsgid \"Lisatud jõudluskaalutlused. / Priit Parmakson, 01.11.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:11\nmsgid \"\"\n\"Asendatud \\\"e-hääletanu\\\" -> \\\"e-hääletamine\\\". Põhjus: inimene võib \"\n\"e-hääletada mitu korda (ümberhääletamine); vastavalt korrigeeritud JSON \"\n\"väljanimesid \\\"evotersbatch\\\" -> \\\"evotingsbatch\\\". /  Priit Parmakson, \"\n\"01.09.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:12\nmsgid \"\"\n\"Eemaldatud: hääletamise kuupäeva/aja edastamine. Põhjus: turvakaalutlus \"\n\"(häälemüügi takistamine). / Priit Parmakson, 01.09.2022 (Sven Heibergiga \"\n\"arutelu alusel)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:13\nmsgid \"\"\n\"Lisatud: 1) ärivajaduse täpsustus; 2) e-hääletamise fakti aja edastamine;\"\n\" 3) edastuse kontroll ja vajadusel täiendamine või parandamine lõpliku \"\n\"faili abil. / Priit Parmakson, 04.05.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:14\nmsgid \"Arutelu - RVT ja RIA inimesed, 04.05.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:15\nmsgid \"\"\n\"\\\"E-hääletanute jooksev nimekiri\\\", kavand v0.2. / Priit Parmakson, \"\n\"02.05.2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:16\nmsgid \"Arutelu: X-tee kasutamine, JSON - Tarmo Hanga, Priit Parmakson, apr 2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:17\nmsgid \"\\\"EHS- VIS3 liidestus\\\" (kavand) - Indrek Leesi, apr 2022\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:19\nmsgid \"Ülevaade\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:21\nmsgid \"\"\n\"Käesolev spetsifikatsioon määratleb protokolli e-hääletamiste nimekirja \"\n\"edastamiseks e-hääletamise süsteemist (edaspidi - EHS) Valimiste \"\n\"infosüsteemi (edaspidi - VIS3).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:23\nmsgid \"\"\n\"Varasematel valimistel (k.a KOV 2021) on peale hääletamise lõppu, \"\n\"valimispäeval edastatud EHS-st VIS3-le e-hääletanute nimekiri (edaspidi -\"\n\" e-hääletanute lõplik nimekiri). See edastus on spetsifitseeritud: \"\n\"[E-hääletanute \"\n\"nimekiri](https://github.com/e-gov/VIS3-EHS/blob/main/4_e_haaletanute_nimekiri/SPEC.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:25\nmsgid \"\"\n\"Siiski on vajadus VIS3-s saada teavet, kas valija on e-hääletanud, juba \"\n\"enne ülalnimetatud lõplikku edastust. Valija, kes on e-hääletanud, võib \"\n\"tulla eelhääletamise perioodil valimisjaoskonda ja soovida paberil \"\n\"hääletada. Valimisjaoskonna töötajal oleks hea omada võimalust VIS3-st \"\n\"vaadata, kas valija on e-hääletanud. Seda teavet saab muuhulgas kasutada \"\n\"valija hoiatamiseks, et paberhääletamisega tema e-hääletamine tühistub.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:27\nmsgid \"E-hääletamiste nimekiri edastatakse EHS-st VIS3-e X-tee teenusega.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:29\nmsgid \"\"\n\"Teenust pakub EHS. VIS3 pöördub regulaarselt teenuse poole. EHS edastab \"\n\"e-hääletamiste andmete paki. VIS3 saab paki ja salvestab andmed VIS3 \"\n\"andmebaasi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:31\nmsgid \"Sünkroonimiseks kasutatakse e-hääletamiste nummerdamist.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:33\nmsgid \"Kaalutlused\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:35\nmsgid \"\"\n\"Teenus on arendatud X-tee REST võimalusi kasutades, vastavalt X-tee REST \"\n\"sõnumiprotokollile ([X-Road: Message Protocol for \"\n\"REST](https://www.x-tee.ee/docs/live/xroad/pr-rest_x-\"\n\"road_message_protocol_for_rest.html)). Andmed väljastatakse JSON-\"\n\"vormingus.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:37\nmsgid \"\"\n\"X-teed on otstarbekas kasutada, sest kuigi VIS3 ja EHS võivad olla \"\n\"käitatud samas taristus, on ikkagi vaja tagada usaldus, logimine ja \"\n\"paindlikkus - omadused, mille tagamine X-tee kasutamisega kokkuvõttes \"\n\"tõenäoliselt ei saaks olema ei lihtsam ega odavam.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:39\nmsgid \"\"\n\"Protokollis on arvesse võetud Rahvastikuregistri ja VIS3 vaheliste X-tee \"\n\"teenuste kasutamise kogemust (REST sõnumiprotokoll, JSON, OpenAPI).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:41\nmsgid \"\"\n\"Erilist tähelepanu on pööratud andmete re-sünkroonimise võimalusele \"\n\"tõrgete korral. Selleks on pakkide pärimine kavandatud idenmpotentsena.\"\nmsgstr \"\"\n\"Erilist tähelepanu on pööratud andmete re-sünkroonimise võimalusele \"\n\"tõrgete korral. Selleks on pakkide pärimine kavandatud idempotentsena.\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:43\nmsgid \"\"\n\"E-hääletanute nimekirja \\\"peegeldamiseks\\\" EHS-st VIS3-e võiks \"\n\"põhimõtteliselt kasutada ka mõnda standardset sünkroonimisprotokolli (nt \"\n\"Git, Rsync vms, vt \\\\[1], \\\\[2], \\\\[3]). Kuna vajadus on suhteliselt \"\n\"lihtne, siis seda ei ole tehtud.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:45\nmsgid \"\"\n\"Jõudluskaalutlused: E-hääletamise jooksvat nimekirja edastatakse \"\n\"e-hääletamise perioodil (6 päeva). E-hääletamise fakte edastatakse kokku \"\n\"u 300 000. E-hääletamise perioodi esimesel tunnil võib oodata u 5000 \"\n\"e-hääletamist; perioodi viimasel tunnil u 10 000 e-hääletamist.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:47\nmsgid \"E-hääletamiste järjenumbrid\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:49\nmsgid \"\"\n\"Tagamaks, et e-hääletamiste nimekiri kantakse EHS-st VIS3-e õigeaegselt \"\n\"ja täielikult, kasutatakse järjenumbreid. EHS omistab igale \"\n\"e-hääletamisele järjenumbri (ingl Sequence Number).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:51\nmsgid \"\"\n\"Järjenumber on naturaalarv, alates ühest. Väärtus `0` tähistab olukorda, \"\n\"kus e-hääletamisi veel ei ole toimunud.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:53\nmsgid \"Igas valimissündmuses on oma numeratsioon.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:55\nmsgid \"Teenus\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:57\nmsgid \"\"\n\"Teenuse vastutav töötleja on Riigi Valimisteenistus (RVT). Teenuse \"\n\"volitatud töötleja on Riigi Infosüsteemi Amet (RIA). Teenust osutav \"\n\"süsteem on EHS.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:59\nmsgid \"\"\n\"Teenust kasutav süsteem on VIS3. Teenust kasutava süsteemi vastutav \"\n\"töötleja on RIA.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:61\nmsgid \"\"\n\"Teenuse ärinimi on \\\"E-hääletamiste nimekiri\\\". Teenuse tehniline nimi \"\n\"(X-tee REST teenusekood, Service Code) on `e-votings-running-list`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:63\nmsgid \"Teenus pakub järgmisi otspunkte:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:65\nmsgid \"\"\n\"1  `GET /elections`.  \\\"Valimissündmuste loetelu\\\" väljastab aktiivsete \"\n\"valimissündmuste loetelu. Aktiivne valimissündmus teenuse kontekstis on \"\n\"selline, mille kohta EHS on valmis väljastama e-hääletamiste nimekirja.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:67\nmsgid \"\"\n\"2  `GET /elections/{electionId}/lastseqno`. \\\"Viimane järjenumber\\\" \"\n\"väljastab konkreetse valimissündmuse viimase EHS-s registreeritud \"\n\"e-hääletamise järjenumbri.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:69\nmsgid \"\"\n\"3  `GET /elections/{electionId}/evotingsbatchfrom/{fromseqno}`. \"\n\"\\\"e-hääletamiste pakk\\\". Selle päringuga pärib VIS3 EHS-lt \"\n\"valimissündmuse `{electionId}` e-hääletamiste paki, alatest \"\n\"e-hääletamisest järjenumbriga `{fromseqno}`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:71\nmsgid \"\"\n\"Päringute ja vastuste andmestruktuuride ja samuti vastuskoodide \"\n\"spetsifikatsiooni vt OpenAPI spetsifikatsioonis: [ehs-xroad-api.yaml\"\n\"](ehs-xroad-api.yaml).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:73\nmsgid \"\"\n\"Teenuse OpenAPI spetsifikatsioon publitseeritakse turvaserveris, \"\n\"vastavalt spetsifikatsioonile [https://www.x-tee.ee/docs/live/xroad/pr-\"\n\"mrest_x-road_service_metadata_protocol_for_rest.html#4-retrieving-list-\"\n\"of-services](X-Road: Service Metadata Protocol for REST), jaotis 5 \"\n\"\\\"Retrieving the OpenAPI description of a Service\\\".\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:75\nmsgid \"Teenuse OpenAPI spetsifikatsioon näitepäring:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:77\nmsgid \"\"\n\"`curl -H \\\"accept: application/json\\\" -H \\\"X-Road-Client:ee-\"\n\"dev/COM/<MEMBER>/dev\\\" \\\"https://.../r1/ee-\"\n\"dev/COM/<MEMBER>/ehs/getOpenAPI?serviceCode=e-votings-running-list\\\"`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:79\nmsgid \"Otspunkt \\\"Valimissündmuste loetelu\\\"\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:81\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:99\nmsgid \"Näide.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:83\nmsgid \"Päring: `GET /elections`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:85\nmsgid \"VIS3 pärib EHS-lt aktiivsete valimissündmuste loetelu.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:87\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:105\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:128\nmsgid \"Vastus:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:93\nmsgid \"EHS vastab, et aktiivseid valimissündmusi on üks - `RK_2023`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:95\nmsgid \"\"\n\"Kui aktiivseid valimissündmusi ei ole, siis EHS peab vastuses saatma \"\n\"tühja massiivi (JSON Array).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:97\nmsgid \"Otspunkt \\\"Viimane järjenumber\\\"\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:101\nmsgid \"Päring: `GET /elections/RK_2023/lastseqno`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:103\nmsgid \"VIS3 pärib valimissündmuse `RK_2023` viimase e-hääletamise järjenumbrit.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:114\nmsgid \"\"\n\"EHS vastab, et valimissündmuse `RK_2023` viimase e-hääletamise \"\n\"järjenumber on `54002`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:116\nmsgid \"\"\n\"Kui valimissündmus on EHS-le tundmatu, siis EHS vastab HTTP vastuskoodiga\"\n\" `404 Not Found`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:118\nmsgid \"\"\n\"Järjenumbrid algavad ühest (`1`). Kui valimisündmuses ei ole veel ükski \"\n\"valija e-hääletanud, siis vastab EHS `lastseqno` väärtusega `0`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:120\nmsgid \"Otspunkt \\\"e-hääletanute pakk\\\"\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:122\nmsgid \"Näide 3.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:124\nmsgid \"Päring: `G /elections/RK_2023/evotingsbatchfrom/54001`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:126\nmsgid \"\"\n\"VIS3 pärib valimissündmuse `RK_2023` e-hääletamiste andmeid, alates \"\n\"järjenumbrist `54001`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:155\nmsgid \"\"\n\"EHS vastab, et saadab valimissündmuse `RK_2023` e-hääletamiste andmeid, \"\n\"alates järjenumbrist `54001`, pakina, milles on kuni `100` kirjet. \"\n\"Konkreetses pakis on kaks kirjet, kuna paki moodustamise hetkel on \"\n\"viimase EHS-s registreeritud e-hääletamise järjenumber `54002`. Esimene \"\n\"kirje tähendab, et valija `LEO KASS`, isikukoodiga `38101010020` on \"\n\"valimistel `RK_2023` e-hääletanud. Kirjes on ka valija KOV EHAK-kood ja \"\n\"valimisringkonna number.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:157\nmsgid \"\"\n\"E-hääletamise fakti kohta EHS e-hääletamise aega ei saada - \"\n\"turvakaalutlustel.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:159\nmsgid \"\"\n\"Paki maksimaalsuuruse `batchmaxsize` määrab EHS, arvestusega, et andmed \"\n\"saadetakse X-tee vastussõnumi kehas (mitte manuses). Vastussõnumi \"\n\"töötlemisel turvaserveris loetakse keha üheaegselt põhimällu. Seetõttu ei\"\n\" tohi vastusõnumi keha suurus ületada 10 MB (turvaserveri \"\n\"vaikeseadistus).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:161\nmsgid \"\"\n\"Vastuses võib olla kuni `batchmaxsize` kirjet. Kui vastuses on kirjeid \"\n\"vähem kui `batchmaxsize`, siis see tähendab, et EHS-l ei ole vastuse \"\n\"koostamise hetkel rohkem andmeid e-hääletamiste kohta.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:163\nmsgid \"VIS3 peab suutma töödelda erineva `batchmaxsize` väärtusega vastuseid.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:165\nmsgid \"Töötluse ülevaade\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:167\nmsgid \"\"\n\"EHS registreerib e-hääletamise fakte. Igale e-hääletamisele omistab EHS \"\n\"järjenumbri. E-hääletamise fakte võib hoida nt järjenumbri järgi \"\n\"indekseeritud tabelis - siis on VIS3-i päringutele vastamine kiire ja \"\n\"efektiivne - kuid see on EHS siseasi. Paki maksimaalsuurus peaks olema \"\n\"EHS seadistuses määratav.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:169\nmsgid \"\"\n\"VIS3 saadab valimiste aktiivsel perioodil regulaarselt päringuid EHS-i \"\n\"X-tee teenuse otspunkti \\\"Viimane järjenumber\\\". EHS saadab vastuses \"\n\"viimase e-hääletamise järjenumbri.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:171\nmsgid \"\"\n\"Viimase järjenumbri alusel leiab VIS3, kas VIS3-e kantud e-hääletamiste \"\n\"andmed on EHS-iga sünkroonis. Kui viimane järjenumber osutab, et EHS-s on\"\n\" lisandunud kirjeid, mis vajavad VIS3-e kandmist, siis saadab VIS3 \"\n\"järjest päringud EHS X-tee teenuse otspunkti \\\"e-hääletamiste pakk\\\", \"\n\"alates esimesest järjenumbrist, mis on VIS3-s puudu; EHS saadab küsitud \"\n\"paki; VIS3 salvestab saadud andmed ja saadab järgmise paki päringu.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:173\nmsgid \"\"\n\"Päringute \\\"e-hääletamiste pakk\\\" töötlusloogika EHS-i poolel peab \"\n\"võimaldama VIS3-l päringuid esitada mistahes järjekorras ja kuitahes \"\n\"palju kordi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:175\nmsgid \"\"\n\"Töötlus peab olema idempotentne (samajõuline) - selles mõttes, et VIS3 \"\n\"võib päritud andmeid igal ajal uuesti küsida. Uuesti pärimisega ei tohi \"\n\"tekkida duubelandmeid, tähendusnihkeid ega kinnijooksmisi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:177\nmsgid \"\"\n\"Tehnilise taustateabena märgime, et EHS hoiab e-hääletamise fakte mitte \"\n\"relatsioonilises andmebaasis, vaid etcd mäluteenuses.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:179\nmsgid \"Kohaletoimetamise garantii\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:180\nmsgid \"\"\n\"Sõltuvalt e-hääletamise sagedusest ja EHS jõudlusest ning seadistusest \"\n\"võib EHS-s juhtuda, et e-hääletamise väga suure sageduse perioodil \"\n\"e-hääletamise faktile järjenumbri omistamine ajalõpu (ingl timeout) tõttu\"\n\" ebaõnnestub. Selline e-hääletamise fakt jääb e-hääletamiste nimekirjas \"\n\"VIS3-e edastamata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:182\nmsgid \"\"\n\"Seega e-hääletamiste nimekiri ei anna kõigi e-hääletamiste VIS3-e \"\n\"jooksvalt kohaletoimetamise garantiid. VIS3-e kohaletoimetatud fakte \"\n\"tuleb käsitada informatiivsetena. E-hääletamise faktid toimetatakse \"\n\"VIS3-e kindlalt täielikus koosseisus e-hääletanute (lõplikus) nimekirjas,\"\n\" pärast e-hääletamise perioodi lõppu (eraldi liides EHS ja VIS3 vahel).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:184\nmsgid \"Teenuse pakkumise ajaline ulatus\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:186\nmsgid \"\"\n\"Konkreetse valimissündmuse kohta pakub EHS e-hääletamiste nimekirja \"\n\"ainult piiratud perioodil. See periood hõlmab e-hääletamise perioodi \"\n\"(kehtiva õiguse kohaselt 6 päeva) koos lühikeste siirdeperioodidega enne \"\n\"ja pärast.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:188\nmsgid \"\"\n\"Kui EHS valimissündmuse kohta e-hääletamiste nimekirja enam ei paku, siis\"\n\" päringud otspunktidesse \\\"Viimane järjenumber\\\" ja \\\"e-hääletanute \"\n\"pakk\\\" saavad HTTP vastuskoodi `410 Gone`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:190\nmsgid \"\"\n\"Valimissündmuste loetelu pakub EHS pidevalt (otspunkt \\\"Valimissündmuste \"\n\"loetelu\\\"). Loetelus on valimissündmused, mille kohta EHS on valmis \"\n\"e-hääletamiste nimekirja pakkuma.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:192\nmsgid \"Kontroll ja veaolukordade käsitlemine\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:194\nmsgid \"\"\n\"Eeldatakse, et EHS-i poolt VIS3-le väljastatav on korrektne ja muutumatu \"\n\"(e-hääletamiste nimekirja piires). Parandus- ja muutmiskirjeid käesolev \"\n\"protokoll ei sisalda.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:196\nmsgid \"\"\n\"Pärast e-hääletamise lõppu, valimispäeval, edastatakse EHS-st VIS3-le \"\n\"e-hääletanute lõplik nimekiri). See edastus on spetsifitseeritud: \"\n\"[E-hääletanute \"\n\"nimekiri](https://github.com/e-gov/VIS3-EHS/blob/main/4_e_haaletanute_nimekiri/SPEC.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:198\nmsgid \"VIS3 operaator laeb e-hääletanute lõpliku nimekirja VIS3-e.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:200\nmsgid \"\"\n\"Jooksva nimekirja ja lõpliku nimekirja erinevuse korral loetakse \\\"tõe \"\n\"allikaks\\\" lõplik nimekiri. Seega: 1) kui lõplikus nimekirjas on isik, \"\n\"kes hääletamisperioodil edastatud nimekirjas puudub, siis loetakse isik \"\n\"e-hääletanuks; 2) kui hääletamisaegses nimekirjas on isik, kes lõplikus \"\n\"nimekirjas puudub, siis märge sellise isiku kohta VIS3-s küll \"\n\"säilitatakse, kuid töötluses ja toimingutes lähtutakse lõplikust \"\n\"nimekirjast, s.t loetakse, et isik ei ole e-hääletanud. VIS3 peab omama \"\n\"võimekust hääletamisaegse ja lõpliku nimekirja erinevust avastada ja \"\n\"operaatorile teada anda.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:203\nmsgid \"\"\n\"EHS-st VIS3-e edastatud e-hääletamise faktid on VIS3 kasutajaliideses \"\n\"nähtavad valimiste korraldajale, vaates \\\"Valijaga seotud toimingute \"\n\"ajalugu\\\". Kui valija e-hääletas mitu korda, siis on valijaga seotud \"\n\"toimingute ajaloos esitatud kõik valija e-hääletamised. E-hääletamise \"\n\"juures on näha kuupäev ja kellaaeg, millal e-hääletamise fakt EHS-st \"\n\"VIS3-e edastati.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:205\nmsgid \"Kirjandus\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:207\nmsgid \"\\\\[1] Git. https://git-scm.com/.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:209\nmsgid \"\"\n\"\\\\[2] CouchDB Replication Protocol, \"\n\"https://guide.couchdb.org/draft/replication.html.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/9_e_haaletamiste_nimekiri/SPEC.md:211\nmsgid \"\"\n\"\\\\[3] Principle and application of Rsync algorithm. \"\n\"https://developpaper.com/principle-and-application-of-rsync-algorithm/.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/README.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/README.md:1\nmsgid \"VIS3-EHS liideste spetsifikatsioonid\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:3\nmsgid \"\"\n\"Eesti riiklikel valimistel toimub e-hääletamine Valimiste infosüsteemi \"\n\"(VIS3) ja e-hääletamise süsteemi (EHS) koostöös. EHS roll on \"\n\"elektroonilise hääletamise läbiviimine lähtudes valimise definitsioonist,\"\n\" mis koostatakse valimise korraldaja poolt VIS3 abil. E-hääletamise ajal \"\n\"vahetavad EHS ja VIS3 informatsiooni - võivad muutuda valijate nimekirjad\"\n\" ning liigub info e-häälte laekumise kohta.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:5\nmsgid \"\"\n\"Dokument esitab VIS3 ja e-hääletamise süsteemi EHS vaheliste liideste \"\n\"spetsifikatsioonid.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:7\nmsgid \"\"\n\"Spetsifikatsioonid on avalikud. Spetsifikatsioonid ei käsitle VIS3 ega \"\n\"EHS konfidentsiaalset siseehitust ega liideste konfidentsiaalseid \"\n\"elemente.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:10\nmsgid \"Ülevaade\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:12\nmsgid \"![VIS3 ja EHS liidesed (ülevaade)](img/vis-ehs.png)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:12\nmsgid \"VIS3 ja EHS liidesed (ülevaade)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:14\nmsgid \"Joonis 1. VIS3 ja EHS liidesed (ülevaade).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:16\nmsgid \"Märkus: Joonise originaal vt VIS3 dok-s RIAs, fail \\\"EHS\\\".\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:18\nmsgid \"Vasakul on VIS3, mooduli täpsusega:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:20\nmsgid \"KAN - Kandidaadimoodul\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:21\nmsgid \"NIM - Nimekirjamoodul\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:22\nmsgid \"TUL - Valimistulemuse moodul\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:23\nmsgid \"VAL - Valimissündmuse moodul\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:24\nmsgid \"VTA - Valimistulemuse avalikustamise moodul.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:26\nmsgid \"\"\n\"Andmed liiguvad noole suunas. HTTPS masinliidestes on teenust pakkuvad \"\n\"otspunktid VIS3 poolel. X-tee liideses on teenusepakkujaks EHS. Inim-\"\n\"masinliides tähendab seda, et ühe süsteemi operaator laeb faili \"\n\"süsteemist alla ja edastab teise süsteemi operaatorile, kes laeb faili \"\n\"teise süsteemi üles.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:29\nmsgid \"Liidesed on täpsemalt kirjeldatud allpool.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:31\nmsgid \"Spetsifikatsioonid\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:33\nmsgid \"[1 Valimisringkondade nimekiri](1_Valimisringkondade_nimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:35\nmsgid \"[2 Valikute nimekiri (kandidaatide nimekiri)](2_Valikute_nimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:37\nmsgid \"[3 Valijate nimekiri EHS-le](3_Valijate_nimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:39\nmsgid \"[4 E-hääletanute nimekiri](4_e_haaletanute_nimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:41\nmsgid \"[5 Tühistus- ja ennistusnimekiri](5_Tyhistusnimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:43\nmsgid \"[6 E-hääletamise tulemus](6_e_haaletamise_tulemus/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:45\nmsgid \"\"\n\"[7 E-hääletamisest osavõtu \"\n\"üldstatistika](7_e_haaletamise_yldstatistika/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:47\nmsgid \"\"\n\"[8 E-hääletamisest osavõtu \"\n\"detailstatistika](8_e_haaletamise_detailstatistika/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:49\nmsgid \"[9 E-hääletamiste nimekiri](9_e_haaletamiste_nimekiri/SPEC.md)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:51\nmsgid \"\"\n\"Spetsifikatsioonides kasutatav valimissündmuse identifikaator peab \"\n\"vastama formaadile [Valimissündmuse \"\n\"identifikaator](valimissündmuse_identifikaator.md).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:53\nmsgid \"Usalduse loomine masinliidestes\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:55\nmsgid \"Masinliidesed kaitstakse HTTPS-ga ja TLS mõlemapoolse autentimisega.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:57\nmsgid \"\"\n\"Lisaks piiratakse VIS3 seadistusega EHS pääs ainult EHS-le määratud \"\n\"otspunktidele (3b ja 7).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:59\nmsgid \"Veakäsitlus masinliidestes\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:61\nmsgid \"\"\n\"VIS3 peab andma HTTP standardi kohase vastuskoodi ja veateate, kui EHS \"\n\"saadetud päring on ebakorrektne või VIS3 ei suuda päringut teenendada.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:63\nmsgid \"\"\n\"EHS peab arvestama VIS3 tõrke võimalusega. Standardne reaktsioon tõrkele \"\n\"on päringu uuestisaatmine.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:65\nmsgid \"Veakäsitlus faili inimese poolt edastamise liidestes\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:67\nmsgid \"\"\n\"VIS3 peab kontrollima üleslaetud faili süntaksit ja kus võimalik, kas \"\n\"semantikat. Ebakorrektse faili kohta tuleb anda operaatorile teada. \"\n\"Ebakorrektse faili andmeid ei kanta VIS3 andmebaasi.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:69\nmsgid \"\"\n\"Korrektse, kuid eksitusena üleslaetud faili andmeid saab VIS3 \"\n\"andmebaasist kustutada ja õige fail uuesti üles laadida.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:71\nmsgid \"Muudatused võrreldes VIS2-EHS-ga\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:73\nmsgid \"VIS3-EHS liideses on võrreldes VIS2-ga muudetud ja ajakohastatud:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:75\nmsgid \"\"\n\"osapoolte vahetumisest (valijate nimekirja hakkab EHS-le saame mitte RR\"\n\"/SMIT-ist, vaid RIA VIS3-st)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:76\nmsgid \"valimisõiguse muutumisest (jaoskondade tähenduse muutumine)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:77\nmsgid \"valijate nimekirja ja selle uuenduste edastamisest üle liidese.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:79\nmsgid \"\"\n\"VIS2 ja EHS omavahelisi liideseid spetsifitseerib \\\"IVXV protokollide \"\n\"kirjeldus\\\" (v 1.5.0, 20.04.2019), \"\n\"[https://www.valimised.ee/sites/default/files/uploads/eh/IVXV-\"\n\"protokollid.pdf](https://www.valimised.ee/sites/default/files/uploads/eh\"\n\"/IVXV-protokollid.pdf). \\\"Elektroonilise hääletamise protokollistik \"\n\"defineerib elektroonilise hääletamise süsteemi komponentide vahelise \"\n\"sõnumivahetuse, kasutatavad andmestruktuurid, algoritmid ning liidesed \"\n\"väliste süsteemidega.\\\" Spetsifitseeritud on:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:81\nmsgid \"liides \\\"Valimisringkondade nimekiri\\\" (jaotis 3.2)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:82\nmsgid \"liides \\\"Valikute nimekiri\\\" (jaotis 3.4)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:83\nmsgid \"liides \\\"Valijate nimekiri\\\" (jaotis 3.3)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:84\nmsgid \"liides \\\"Tühistus- ja ennistusnimekiri\\\" (jaotis 9.1)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:85\nmsgid \"liides \\\"E-hääletanute nimekiri\\\" (jaotis 9.2)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:86\nmsgid \"liides \\\"E-hääletamise tulemus\\\" (jaotis 9.3)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:88\nmsgid \"\"\n\"Käesolevad spetsifikatsioonid tuginevad ülalnimetatud \"\n\"spetsifikatsioonidele. Täpsemalt vt konkreetsete spetsifikatsioonide \"\n\"juures.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/README.md:90\nmsgid \"\"\n\"EHS lähtekood on kättesaadav: [https://github.com/vvk-\"\n\"ehk/ivxv](https://github.com/vvk-ehk/ivxv).\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/Taustateave/README.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:1\nmsgid \"\"\n\"Kaustas on spetsifikatsioonid X-tee teenustele, millega VIS3 pärib \"\n\"Rahvastikuregistrist valijate nimekirja ja nimekirja muudatused:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:4\nmsgid \"\"\n\"`RRValimisteAlgNimekiri_kirjeldus.pdf` - Rahvastikuregistri poolt \"\n\"osutatava  Valimiste algnimekirja X-tee teenuse spetsifikatsioon.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:7\nmsgid \"\"\n\"`RRValimisteNimekirjaMuudatused.pdf` - Rahvastikuregistri poolt osutatava\"\n\"  Valimiste algnimekirja muudatuste X-tee teenuse spetsifikatsioon.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/Taustateave/README.md:10\nmsgid \"\"\n\"Spetsifikatsioonid võivad aidata mõista VIS3 poolt EHS-le pakutavate \"\n\"andmete koosseisu ja tähendust.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/VIS3-EHS/valimissündmuse_identifikaator.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:1\nmsgid \"Valimissündmuse identifikaator\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:3\nmsgid \"\"\n\"Üht valimist puudutav andmestik on seotud unikaalse valimissündmuse \"\n\"identifikaatori abil.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:5\nmsgid \"\"\n\"Valimissündmuse identifikaator on sõne, mis koosneb kahest kohustuslikust\"\n\" ja kahest valikulisest osast:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:6\nmsgid \"valimissündmuse tüüp, vastavalt allolevale tabelile, nt `RK`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:7\nmsgid \"aastanumbrist (nt `2023`)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:8\nmsgid \"valikulisest erakorraliste valimiste tunnusest `_E` ja\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:9\nmsgid \"valikulisest järjenumbrist <nr>, nt `2`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:11\nmsgid \"Ülalnimetatud osad eraldatakse üksteisest allkriipsudega `_`.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:13\nmsgid \"\"\n\"Järjenummerdatakse tüüpide kaupa. Seejuures aasta esimese samatüübilise \"\n\"valimissündmuse korral järjenumbrit ei näidata.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:15\nmsgid \"Näited:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:16\nmsgid \"`KOV_2021`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:17\nmsgid \"`RH_2021`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:18\nmsgid \"`RH_2021_2` (2021. a teine rahvahääletus)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:19\nmsgid \"`RK_2023`\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:20\nmsgid \"`RK_2023_E` (2023. a Riigikogu erakorralised valimised)\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:21\nmsgid \"`RK_2023_E_2` (2023. a Riigikogu teised erakorralised valimised).\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:23\nmsgid \"\"\n\"EHSi jaoks on valimise identifikaator kuni 28 ASCII-tähemärgi pikkune \"\n\"sõne ning eelnevalt kirjeldatud struktuuri põhjal EHS otsuseid ei tee.\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:25\nmsgid \"Valimissündmuse tüüp\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:27\nmsgid \"Valimissündmuse tüüp esitatakse koodiga:\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:29\nmsgid \"`KOV` - Kohaliku omavalitsuse volikogu valimised\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:30\nmsgid \"`EP` - Euroopa Parlamendi valimised\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:31\nmsgid \"`RK` - Riigikogu valimised\"\nmsgstr \"\"\n\n#: ../../VIS3-EHS/valimissündmuse_identifikaator.md:32\nmsgid \"`RH` - Rahvahääletus.\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/examples.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../examples.rst:3\nmsgid \"Skeemid ja näited\"\nmsgstr \"\"\n\n#: ../../examples.rst:7\nmsgid \"Valimisringkondade nimekiri\"\nmsgstr \"\"\n\n#: ../../examples.rst:10 ../../examples.rst:48 ../../examples.rst:84\n#: ../../examples.rst:119 ../../examples.rst:154 ../../examples.rst:189\n#: ../../examples.rst:206 ../../examples.rst:217\nmsgid \"Skeem\"\nmsgstr \"\"\n\n#: ../../examples.rst:17 ../../examples.rst:61 ../../examples.rst:97\n#: ../../examples.rst:132 ../../examples.rst:167\nmsgid \"Näide: KOV\"\nmsgstr \"\"\n\n#: ../../examples.rst:24 ../../examples.rst:55 ../../examples.rst:91\n#: ../../examples.rst:126 ../../examples.rst:161\nmsgid \"Näide: EP\"\nmsgstr \"\"\n\n#: ../../examples.rst:31 ../../examples.rst:68 ../../examples.rst:103\n#: ../../examples.rst:138 ../../examples.rst:173\nmsgid \"Näide: RH\"\nmsgstr \"\"\n\n#: ../../examples.rst:38 ../../examples.rst:74 ../../examples.rst:109\n#: ../../examples.rst:144 ../../examples.rst:179\nmsgid \"Näide: RK\"\nmsgstr \"\"\n\n#: ../../examples.rst:45\nmsgid \"Valikute nimekiri\"\nmsgstr \"\"\n\n#: ../../examples.rst:81\nmsgid \"E-hääletanute nimekiri\"\nmsgstr \"\"\n\n#: ../../examples.rst:116\nmsgid \"Tühistusnimekiri\"\nmsgstr \"\"\n\n#: ../../examples.rst:151\nmsgid \"E-hääletamise tulemus\"\nmsgstr \"\"\n\n#: ../../examples.rst:186\nmsgid \"E-hääletamise üldstatistika\"\nmsgstr \"\"\n\n#: ../../examples.rst:196\nmsgid \"Näide\"\nmsgstr \"\"\n\n#: ../../examples.rst:203\nmsgid \"E-hääletamise detailstatistika\"\nmsgstr \"\"\n\n#: ../../examples.rst:214\nmsgid \"E-hääletamiste nimekiri\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/locales/et/LC_MESSAGES/index.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 23:20+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: et\\n\"\n\"Language-Team: et <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../index.rst:4\nmsgid \"IVXV registreerimisteenus\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "Documentation/public/liidesed/spelling_wordlist.txt",
    "content": "com\netcd\ngit\nhtml\nhttps\nrsync\nscm\nwww\ndocs\n\ni\nb\ndok\nle\ningl\nof\nga\nlt\n\nedasisestes\nevotersbatch\nevotingsbatch\n\nedastakse\n\nliidestus\n\nid\nquery\ntabeldusmärki\ndigikonteinerist\nschema\nrepos\nretrieving\nlist\narray\ngolang\nliidestub\nnot\nfound\nconflict\nduubelandmeid\nidempotentne\ninformaalne\nsünkroonis\nsünkroonimisprotokolli\nteenendada\ntähelepanu\n\nEHS\nRR\nSMIT\nURL\nVIS\n\nlive\nmrest\ntimeout\nprinciple\napplication\ndraft\nreplication\ndeveloppaper\nalgorithm\nx\nRoad\nService\nmetadata\nprotocol\nfor\nREST\nRetrieving\nthe\nOpenAPI\ndescription\nservice\nservices\nCouchDB\nReplication\nguide\ncouchdb\nsequence\ncode\n\nAnija\nIndrek\nLeesi\nParmakson\nTarmo\nHanga\nBackus\nNaur\nPirita\nHeibergiga\n"
  },
  {
    "path": "Documentation/public/protokollid/01-annotatsioon.rst",
    "content": "..  IVXV protokollid\n\n\n================================================================================\nAnnotatsioon\n================================================================================\n\nKäesolev dokument kirjeldab elektroonilise hääletamise infosüsteemi IVXV\nprotokollistikku.\n\nDokument annab üldise ülevaate elektroonilise hääletamise süsteemi tehnilisest\nülesehitusest ja kasutatavatest protokollidest. Dokumendis defineeritakse\nprotokollides kasutatavad ühised mõisted ja andmestruktuurid.\n"
  },
  {
    "path": "Documentation/public/protokollid/02-ylevaade.rst",
    "content": "..  IVXV protokollid\n\n================================================================================\nÜlevaade\n================================================================================\n\nElektroonilise hääletamise protokollistik (edaspidi protokollistik) defineerib\nelektroonilise hääletamise süsteemi komponentide vahelise sõnumivahetuse,\nkasutatavad andmestruktuurid, algoritmid ning liidesed väliste süsteemidega.\nSõnumivahetus esitatakse UML suhtlusskeemidena, mis üheselt defineerivad\nsõnumite järgnevuse. Andmestruktuuride kirjeldused on varustatud BNF, ASN.1\nvõi JSON-schema notatsioonis spetsifikatsioonidega. Algoritmid esitatakse\npseudokoodina.\n\nNB! Kõigis protokollistiku andmestruktuuride väljades tuleb rangelt kinni pidada\nlubatud märkidest ning väljade minimaalsetest ja maksimaalsetest pikkustest.\nTäiendavate tühikute, tabulaatorite jms. kasutamine on keelatud ning\nspetsifikatsiooni realiseerivad rakendused peavad vorminguga mitte-vastavate\nandmete töötlemisest keelduma.\n\nProtokollistik defineerib elektroonilise hääletamise protokolli ning selle\nprotokolli realiseerimiseks vajalikud tugistruktuurid.\n\nElektroonilise hääletamise protokoll\n====================================\n\nElektroonilise hääletamise protokoll spetsifitseerib:\n\n#. elektroonilise hääle vormingu, mis võimaldab üheselt määratleda valija tahte\n   konkreetsel valimisel;\n\n#. elektroonilise hääle krüpteerimise hääle salajasuse tagamiseks;\n\n#. elektroonilise hääle digitaalse allkirjastamise tervikluse ja valija\n   identifitseerimise tagamiseks;\n\n#. elektroonilise hääle kvalifitseerimise kogumisteenuse poolt, hääle\n   vastuvõtmise tähistamiseks;\n\nProtokoll eeldab, et valimise korraldaja defineerib valimise ning genereerib\nhäälte salastamise võtmepaari, mille avalik komponent tehakse valijarakendusele\nkättesaadavaks.\n\nProtokolli vahendusel liigub valija tahe kogumisteenuses talletatavasse e-valimiskasti\nning võetakse tulemuse kujunemisel arvesse järgmist sündmusterida pidi:\n\n#. Valija kasutab valijarakendust oma tahteavalduse elektrooniliseks\n   vormistamiseks:\n\n   #. tahteavaldus vormistatakse elektroonilise häälena;\n\n   #. vormistatud hääl krüpteeritakse;\n\n   #. krüpteeritud hääl signeeritakse valija arvutis.\n\n#. Kogumisteenus talletab elektroonilise hääle, moodustades selle käigus\n   häälele kvalifitseeritud digitaalallkirja:\n\n   #. elektrooniline hääl registreeritakse välises registreerimisteenuses;\n\n   #. elektroonilisele häälele võetakse digitaalne ajatempel;\n\n   #. elektroonilisele häälele võetakse valija sertifikaadi\n      kehtivuskinnitus;\n\n   #. elektroonilist häält kvalifitseerivad elemendid tagastatakse mh. ka\n      valijarakendusele kontrollimiseks ning valija informeerimiseks\n      kvalifitseerimise tulemustest;\n\n   #. valijale võimaldatakse kvalifitseeritud elektroonilise hääle\n      kontrollimine kontrollrakenduse abil.\n\n.. note::\n\n   Elektroonilise hääle digitaalne allkirjastamine erineb tavapärasest\n   dokumentide digitaalallkirjastamisest, kus kõik allkirja kvalifitseerimiseks\n   vajalikud toimingud algatatakse vahetult allkirjastaja seadmes.\n   Elektroonilise hääle kvalifitseerimise kohustus on kogumisteenusel, kelle\n   ülesanne on veenduda vastuvõetavate häälte korrektses allkirjastatuses. Kuna\n   e-hääletamise perioodil on koormus seotud teenustele kõrge, võimaldab\n   kogumisteenuse poolt juhitud kvalifitseerimine tagada paremat teenuse\n   kvaliteeti.\n\n#. Valija võib kasutada kontrollrakendust veendumaks oma hääle korrektses\n   käitlemises kogumisteenuse poolt;\n\n#. Hääletamisperioodi lõppedes väljastab kogumisteenus valimise korraldajale\n   e-valimiskasti ning registreerimisteenus väljavõtte kogumisteenuse poolt\n   registreeritud häältest;\n\n   #. e-valimiskasti koosseisus antakse valimise korraldajale üle:\n\n      #. valija krüpteeritud tahteavaldus koos signatuuriga;\n\n      #. registreerimisteenuse kinnitus hääle registreerimisest;\n\n      #. ajatempliteenuse poolt väljastatud digitaalne ajatempel\n         elektroonilisele häälele;\n\n      #. kehtivuskinnitusteenuse poolt väljastatud kinnitus valija sertifikaadi\n         kehtivuse kohta;\n\n      #. registreerimisteenuse väljavõtte koosseisus antakse valimise korraldajale üle:\n\n         #. kõik e-hääletamise perioodil kogumisteenuse poolt\n            registreerimisteenusele saadetud päringud elektrooniliste häälte\n            registreerimiseks.\n\n#. Valimise korraldaja arvutab hääletamistulemuse:\n\n   #. kontrollitaks üle antud elektrooniliste häälte allkirjade kehtivust\n\n   #. kontrollitakse, et kõik registreerimisteenuses registreeritud hääled on\n      e-valimiskasti koosseisus üle antud;\n\n   #. eraldatakse krüpteeritud hääled ja digitaalallkirjad;\n\n   #. anonüümitakse krüpteeritud hääled krüptograafiliselt;\n\n   #. dekrüpteeritakse krüpteeritud hääled;\n\n   #. dekrüpteeritud häälte põhjal arvutatakse hääletamistulemus.\n\nProtokoll on analoogne paberil posti teel hääletamise protokolliga, kus valija\ntahe liigub valimiskomisjonini kahes ümbrikus – välimise ümbriku sees on\nsisemine ümbrik, mis omakorda sisaldab valija tahteavaldusega hääletussedelit.\nVälimine ümbrik kannab valijat identifitseerivat infot ning võimaldab\nmh. kontrollida valija õigust hääletada. Sisemine ümbrik on anonüümne ning\nkaitseb hääle salajasust. Enne häälte kokkulugemist eraldatakse sisemised\nümbrikud välimistest.\n\nElektroonilise hääletamise kontekstis on sisemine ümbrik vormistatud\nkrüpteeritud häälena ning välimine ümbrik digitaalselt allkirjastatud\ndokumendina.\n"
  },
  {
    "path": "Documentation/public/protokollid/04-seadistus.rst",
    "content": "..  IVXV protokollid\n\n================================================================================\nValimise definitsioon\n================================================================================\n\nValimise defineerib valimise korraldaja. Eesti riiklikel valimistel jagunevad\nkõik hääleõiguslikud isikud ühte või mitmesse valimisringkonda. Valijal on\nvõimalik hääletamisel valida ainult selle ringkonna kandidaatide vahel, kuhu ta\nkuulub.\n\nValimise defineerimiseks tuleb määratleda vähemalt\n\n#. valimise unikaalne identifikaator ning küsimuste unikaalsed identifikaatorid;\n\n#. täielik loend valimisringkondadest ja -jaoskondadest;\n\n#. hääleõiguslike isikute nimekiri ja jagunemine valimisringkondadesse;\n\n#. kandidaatide nimekiri ja jagunemine valimisringkondadesse.\n\nValimise sisendandmed koostatakse Valimise Infosüsteemis (VIS), vormingukirjeldused on\nspetsifitseeritud `VIS ja EHS ühisspetsifikatsioonis <https://github.com/e-gov/VIS3-EHS/>`_.\n"
  },
  {
    "path": "Documentation/public/protokollid/05-ehaal.rst",
    "content": "..  IVXV protokollid\n\n===================\nElektrooniline hääl\n===================\n\nIVXV hääletamisprotokoll põhineb topeltümbrikuskeemil, mis tähendab, et valija\navakujul tahteavaldus krüpteeritakse valimise korraldaja poolt levitatud\navaliku võtmega. Krüpteeritud tahteavaldus allkirjastatakse digitaalselt valija\nkäsutuses oleva allkirjastamisvahendiga ning edastatakse kogumisteenusesse\nmingis kokkulepitud konteinervormingus. Kogumisteenus võib valija poolt\nallkirjastatud häält täiendavalt kvalifitseerida, veendudes näiteks\nallkirjastamissertifikaadi kehtivuses. IVXV protokollistik näeb mh. ette\nkogumisteenuse poolt vastuvõetud häälte registreerimise välises\nregistreerimisteenuses.\n\nKogumisteenuse poolt talletamisele võetud hääl koos kvalifitseerivate\nelementidega tehakse kättesaadavaks nii valijarakendusele kui\nkontrollrakendusele, mis teostavad üksiku hääle peal samad kontrollid, mida\nhilisem valimise korraldaja töötlemisrakendus teostab kõigi häälte peal.\nKvalifitseerivate elementide kontrollimise võimalus annab valijale kindluse, et\ntema häält on hilisemates protsessides korrektselt menetletud.\n\n\nValija tahteavaldus avakujul\n============================\n\nValija tahteavaldus avakujul eksisteerib valijarakenduses ning hiljem ka\nkontrollrakenduses ja häälte kokkulugemisel võtmerakenduses. Tahteavaldus\nsisaldab valiku koodi ringkonnas ja ringkonna EHAK-koodi. Eraldajana\nkasutatakse ASCII kooditabeli sümbolit `0x2E` ehk \".\".\nVormingus spetsifitseerimata sümbolite kasutamine ei ole lubatud, vormingule\nmittevastavad sedelid loetakse kehtetuks.\n\n.. code-block:: bnf\n\n   <digit> ::= \"0\" | \"1\" | \"2\" | \"3\" | \"4\" | \"5\" | \"6\" | \"7\" | \"8\" | \"9\"\n\n   <ehak-kood> ::= <digit> <digit> <digit> <digit>\n\n   <kandidaadi-number> ::= <digit> <digit> <digit>\n                         | <digit> <digit> <digit> <digit>\n\n   <dot> ::= 0x2E\n\n   <ballot> ::= <ehak-kood> <dot> <kandidaadi-number>\n\n\nJärgmised tahteavaldused on vormistatud korrektselt:\n\n.. code-block:: bnf\n\n   0000.123\n   0321.1234\n   0000.1234\n\nJärgmised tahteavaldused ei ole vormistatud korrektselt:\n\n.. code-block:: bnf\n\n   0000 . 123      (*  vorming ei näe ette tühikute kasutamist     *)\n   0000-123        (*  vorming lubab eraldajana vaid sümbolit \".\"  *)\n   123             (*  mõlemad vormingukomponendid tuleb esitada   *)\n   321.1234        (*  EHAK kood on 4-kohaline identifikaator      *)\n\n\nKrüpteeritud sedel\n==================\n\nValija tahteavaldus avakujul :token:`ballot` krüpteeritakse valijarakenduse\npoolt valimise korraldaja genereeritud avaliku võtmega. IVXV vajab\nkrüpteerimiseks mitte-deterministlikku, homomorfset avaliku võtme\nkrüptosüsteemi. Selliseks süsteemiks sobib ElGamal krüptosüsteem, mida täna\nrakendatakse IVXV kontekstis nii jäägiklassiringi `Zp` multiplikatiivsel rühmal\n`Zp*` (MODP tüüpi rühmad) kui ka elliptkõveratel (ECC tüüpi rühmad).\n\nElGamal avalik võti kodeeritakse koos ElGamal krüptosüsteemi parameetritega ning\nkonkreetset valimist iseloomustava identifikaatoriga. Krüptosüsteemi parameetrid\non osaks algoritmi identifikaatori struktuurist, avalik võti on kodeeritud X509\nstandardis kirjeldatud :token:`SubjectPublicKeyInfo` struktuuri\n:token:`subjectPublicKey` välja.\n\n::\n\n    SubjectPublicKeyInfo ::= SEQUENCE {\n        algorithm   AlgorithmIdentifier,\n        subjectPublicKey    BIT STRING\n    }\n\nMODP tüüpi rühmade korral identifitseerib algoritmi identifikaator\n:token:`id-ivxv-modp-elgamal`. Parameetrid vastavad struktuurile\n:token:`IVXVModPElGamalParameters` ning avalik võti struktuurile\n:token:`IVXVModPElGamalPublicKey` (:ref:`asn-modp`).\n\nECC tüüpi rühmade korral identifitseerib algoritmi identifikaator\n:token:`id-ivxv-ecc-elgamal`. Parameetrid vastavad struktuurile\n:token:`IVXVECCElGamalParameters` ning avalik võti struktuurile\n:token:`IVXVECCElGamalPublicKey` (:ref:`asn-ecc`).\n\nValija tahteavalduse krüpteerimiseks võetakse UTF-8 kodeeringus struktuur\n:token:`ballot` ning teisendatakse see ElGamal parameetrite poolt kirjeldatud\nrühma elemendiks vastavalt MODP või ECC algoritmidele. Rühma elemendiks\nteisendatud tahteavaldus krüpteeritakse ElGamal algoritmiga ning kodeeritakse\nMODP korral struktuuri :token:`IVXVModPElGamalCiphertext` (:ref:`asn-modp`) ja\nECC korral struktuuri :token:`IVXVECCElGamalCiphertext` (:ref:`asn-ecc`).\nLõplik krüpteeritud tahteavaldus esitatakse struktuuris\n:token:`IVXVElGamalCiphertext` (:ref:`asn-general`), kus väli\n:token:`algorithm` viitab rühma tüübile ning väli :token:`ciphertext`\nrühmaspetsiifilisele krüptogrammile. Andmestruktuuri\n:token:`IVXVElGamalCiphertext` DER-kodeering on krüpteeritud sedel ehk sisemine\nümbrik topeltümbriku skeemis.\n\n..\n  NB! Nende algoritmide spetsifikatsioonidele tuleb viidata.\n\nTahteavalduse krüpteerimise käigus genereeritakse valijarakenduses juhuarv, mida\nElGamal krüpteerimisel kasutab. Sama juhuarv avalikustatakse hiljem\nkontrollrakendusele. Tulenevalt ElGamal krüptosüsteemi eripärast funktsioneerib\nsee juhuarv nö. teise võtmena ning võimaldab krüptogrammi dekodeerimist\nkontrollrakenduses.\n\n.. _signed-vote:\n\nValija poolt allkirjastatud hääl\n================================\n\nKrüpteeritud sedel tuleb enne kogumisteenusesse talletamisele saatmist\ndigitaalselt allkirjastada, milleks on võimalik kasutada kõiki Eesti Vabariigis\nkehtivaid digitaalallkirjavahendeid – ID-kaart, Digi-ID, Mobiil-ID, Smart-ID.\nID-kaarti saab kasutada nii TLS-CCA kui Web-eID protokolliga.\n\n.. attention::\n\n   Loend digitaalallkirjavahenditest ja nendega seotud protokollidest on\n   tehniliselt korrektne, kuid valimiste korraldajal on võimalus kasutada vaid\n   mõnda alamhulka nimetatud vahenditest.\n\nKäesolev spetsifikatsioon näeb ette Eesti Vabariigi Standardikavandis [BDOC2.1]\ndefineeritud BDOC allkirjavormingu kasutamise. BDOC allkirjavorming koosneb ETSI\nstandardi TS 101 903 (XadES) profiilist ning OpenDocument konteineri vormingust.\n\nOlenevalt käimasoleval valimisel esitatud küsimuste arvust võib digitaalselt\nallkirjastatud hääl sisaldada ühte või mitut andmefaili MIME-tüübiga\n``application/octet-stream``. Iga andmefaili sisuks on krüpteeritud sedel.\nAndmefaili ja teiste signeeritavate andmeobjektide räsimiseks enne\nallkirjastamist kasutatakse räsifunktsiooni SHA-256. Andmefaili nimi\nmoodustatakse laiendist '``ballot``' ning valimise identifikaatorist ja\nküsimuse identifikaatorist. Kõik viidatud andmefailid peavad sisalduma\nallkirjakonteineris. Digitaalselt allkirjastatud hääl ei tohi sisaldada muid\nandmefaile kui neid, mis sisaldavad hääli mõne käimasoleva valimise kontekstis.\nSeadistusele mittevastavate häälte vastuvõtmisest, talletamisest ja\ntöötlemisest peab kogumisteenus keelduma.\n\n.. attention::\n\n   Valimise identifikaator ja küsimuse identifikaator on defineeritud valimise\n   korraldaja poolt loodud seadistustes ning töötlevad rakendused peavad\n   lähtuma neist seadistustest otsustamaks, millised vormingule vastavad\n   identifikaatorid on konkreetse sündmuse kontekstis lubatud ja millised\n   mitte.\n\n\n.. code-block:: bnf\n\n   <ascii-char> ::= [ASCII]\n\n   <election-identifier> ::= 1..28 <ascii-char>\n\n   <question-identifier> ::= 1..28 <ascii-char>\n\n   <extension> ::= \"ballot\"\n\n   <dot> ::= 0x2E\n\n   <encrypted-ballot-name> ::= <election-identifier> <dot> <question-identifier> <dot> <extension>\n\n\nValija poolt valijarakenduses allkirjastatud hääl moodustatakse nii, et on\nvõimalik selle edasine kvalifitseerimine kogumisteenuses. Käesolev\nspetsifikatsioon näeb ette hääle kvalifitseerimiseks nii PKIX ajatempli kui\nOCSP kehtivuskinnituse võtmise. Sellisena on lõplik kvalifitseeritud hääl\nvastav BDOC-TS profiilile.\n\nKui hääl allkirjastatakse ID-kaardi või Digi-ID'ga, siis toimub algse\nallkirjastatud konteineri moodustamine valijarakenduses. Kui hääl\nallkirjastatakse Mobiil-ID või Smart-ID'ga, siis toimub konteineri moodustamine\nvalijarakenduse ning kogumisteenuse poolt vahendatava Mobiil-ID/Smart-ID\nteenuse koostöös. Mobiil-ID/Smart-ID juhtumil kasutab kogumisteenus\nMobiil-ID/Smart-ID teenust ainult signatuuri saamiseks krüpteeritud sedelile.\nKõik hääle kvalifitseerimiseks vajalikud elemendid hangitakse vastavatelt\nteenustelt alles siis kui valijarakendus on saatnud signeeritud hääle\ntalletamiseks. Kvalifitseeritud hääl esitatakse kogumisteenuse poolt\nvalijarakendusele verifitseerimiseks, ainult kvalifitseeritud hääl peab vastama\nBDOC 2.1 standardi tingimustele -- valijarakenduse poolt moodustatud hääl on\nvaheetapp kvalifitseeritud hääleni jõudmiseks.\n\nValijarakenduses signeeritud häälel peab olema üks ja ainult üks allkiri, mida\nhoitakse signatuurifailis :file:`META-INF/signature0.xml`. Häält ja allkirja\nsisaldav konteiner (edaspidi viidatud kui ``SignedVote``) moodustatakse BDOC 2.1\nstandardis kirjeldatud meetodit kasutades.\n\nSpetsifitseerime valijarakenduses allkirjastatud hääle vormingu ühe\nküsimuse korral.\n\nRäsialgoritmina ``DIGEST_ALG`` on kasutusel SHA-256\n(http://www.w3.org/2001/04/xmlenc#sha256). XML kanoniseerimiseks\n(``CANON_ALG``) kasutatakse meetodit ``c14n11``\n(http://www.w3.org/2006/12/xml-c14n11).\n\nECC võtmete korral on allkirjastamismeetodiks\nhttp://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256. RSA võtmeid enam ei\nkasutata.\n\nIdentifikaatorite `VOTE_REF`, `SP_REF`, `SP_URI` ning `SV_URI` täpne väärtus ei\nole fikseeritud.\n\nElement `SignedProperties`\n---------------------------\n\nElement ``SignedProperties`` moodustatakse kooskõlas BDOC 2.1 standardiga. Kui\nkvalifitseerimisel kasutatakse ajatemplit, siis elementi\n``SignaturePolicyIdentifier`` ei kasutata. Ühtegi mitte-kohustuslikku elementi\nei kasutata. Allkirjastamise kellaaja fikseerib andmestruktuuri täitev arvuti\nning valija X509-sertifikaat saadakse kas ID-kaardilt, Mobiil-ID, Smart-ID\nvõi Web eID teenuse vahendusel.\n\n\n.. literalinclude:: ../../common/xmltemplates/sp.template\n   :language: xml\n   :linenos:\n\n\nElement `SignedInfo`\n---------------------\n\nElement ``SignedInfo`` moodustatakse kooskõlas BDOC 2.1 standardiga, viidates\nnii krüpteeritud sedelile (``VOTE_DIGEST``) kui elemendile ``SignedProperties``\n(``SP_DIGEST``).\n\n.. literalinclude:: ../../common/xmltemplates/si.template\n   :language: xml\n   :linenos:\n\n\nElement `SignatureValue`\n------------------------\n\nElement ``SignatureValue`` moodustatakse kooskõlas BDOC 2.1 standardiga.\nKanoniseeritud elemendist ``SignedInfo`` arvutatakse räsi, mis allkirjastatakse\nPKCS1-meetodiga.\n\n.. literalinclude:: ../../common/xmltemplates/sv.template\n   :language: xml\n   :linenos:\n\n\nElement `XAdESSignatures`\n-------------------------\n\nElement ``XAdESSignatures`` sisaldab ühte ``Signature`` elementi, mis on\nkoostatud lähtudes kõigist eelmistest elementidest ning valija X509\nsertifikaadist. Elementi ``UnsignedProperties`` ei kasutata.\n\n.. literalinclude:: ../../common/xmltemplates/sig.template\n   :language: xml\n   :linenos:\n"
  },
  {
    "path": "Documentation/public/protokollid/06-talletamine.rst",
    "content": "..  IVXV protokollid\n\n====================================================\nElektroonilise hääle kvalifitseerimine talletamiseks\n====================================================\n\nKvalifitseeritud hääl\n=====================\n\nValijarakenduse töö tulemusena saadetakse kogumisteenusesse talletamiseks\ntopeltümbrik, mis sisaldab endas valija tahteavaldust krüpteeritud kujul, valija\nallkirja krüpteeritud tahteavaldusel kooskõlastatud allkirja- ja\nkonteinervormingus ning valija allkirjastamissertifikaati X509-vormingus.\n\nHääle edukaks talletamiseks näeb IVXV protokoll ette hääle registreerimise\nvälise registreerimisteenuse osutaja juures ning registreerimistõendi\nvalijarakendusele kättesaadavaks tegemise. Valimise korraldaja võib hääle\nkvalifitseerimiseks näha ette täiendavaid samme lisaks registreerimisele --\nnäiteks kehtivuskinnituse hankimist hääle allkirjastanud sertifikaadi kohta.\n\nKõik kogumisteenuse poolt hangitavad kvalifitseerivad elemendid, mis määravad\nhääle staatuse hilisemates töötlusetappides, tuleb esitada valijarakendusele ning\nnõudmise korral ka kontrollrakendusele tagamaks, et valija saab oma hääle\nkorrektse menetlemise võimalikkusest õigeaegselt teada.\n\nOCSP kehtivuskinnitus\n---------------------\n\nOCSP (*Online Certificate Status Protocol*) on standartne protokoll\nX509-sertifikaatide kehtivusinfo pärimiseks. Kogumisteenus võib seda protokolli\nkasutada hääle allkirjastanud sertifikaadi kehtivuse teadasaamiseks. OCSP\nvastus ütleb, et sertifikaat kehtis päringu tegemise ajahetkel, kuid ei seosta\nOCSP vastust konkreetse allkirjaga.\n\nRFC3161 ajatempel\n-----------------\n\nRFC3161 ajatempli protokolliga saadakse usaldusteenuse pakkujalt kinnitus, et\nmingi andmekogum eksisteeris enne teatud ajahetke. BDOC-TS kontekstis\najatembeldatakse allkirja element ``SignatureValue`` kanoniseeritud kujul.\nKlassikaline OCSP vastus koos RFC 3161 vormingus ajatempliga kvalifitseerivad\nBDOC-TS allkirja.\n\nTalletamine\n====================================================\n\nElektroonilise hääle talletamine kogumisteenuses tähendab:\n\n#. hääle vastuvõtmist valijarakenduselt ning hääletaja allkirja\n   verifitseerimist;\n\n#. hääle võimalikku kvalifitseerimist -- näiteks sertifikaadi kehtivuse\n   tõendamist hääle allkirjastamisele lähedasel ajahetkel;\n\n#. hääle registreerimist sõltumatus registreerimisteenuses;\n\n#. häält kvalifitseerivate elementide vahendamist valijarakendusele.\n\nErinevad kombinatsioonid allkirjavormingust ning hääli kvalifitseerivatest\nteenustest võivad tekitada erinevaid IVXV-profiile. Konkreetse dokumendi raames\non IVXV profiil:\n\n#. Allkirjastatud hääle vorming on BDOC-TS;\n\n#. Kehtivuskinnitusprotokolliks on standartne OCSP;\n\n#. BDOC-TS kvalifitseerimiseks kasutatav RFC3161 ajatempel on kasutusel ka\n   registreerimistõendina.\n"
  },
  {
    "path": "Documentation/public/protokollid/06a-regteenus.rst",
    "content": "..  IVXV registreerimisteenus\n\n====================================\nElektroonilise hääle registreerimine\n====================================\n\nHääle edukaks talletamiseks näeb IVXV protokoll ette hääle registreerimise\nvälise registreerimisteenuse osutaja juures ning registreerimistõendi\nvalijarakendusele kättesaadavaks tegemise. Registreerimisteenus toimub RFC3161\najatempli protokolli baasil. Protokolli on laiendatud selliselt, et\nkogumisteenus saab ajatempli päringule anda oma signatuuri, mis teeb võimalikuks\nhilisema võrdleva väljavõtte registreerimisteenusest. Sõltumatu\nregistreerimisteenuse olemasolu vähendab häälte kogumisteenuse poolt \"kaotamise\"\nriski.\n\nRegistreerimisteenus\n====================\n\nRegistreerimisteenus on teenus, mille abil Kogumisteenus registreerib kõik\nHääletajatelt saadud hääled. Pärast hääletamisperioodi lõppu edastab\nKogumisteenus talletatud hääled Töötlejale ning Registreerimisteenus edastab\nregistreeritud hääled Töötlejale.\n\nRegistreerimisteenus aitab tagada e-valimiskasti terviklust. Eeldame et Kogumisteenusel\npuudub võimalus võltsida digitaalallkirja ning sellisel moel tekitada juurde\nhääli või muuta juba talletatud hääli. Riskina säilib võimalus, et Kogumisteenus\nei anna kõiki talletatud hääli Töötlejale üle. Häälte valikulise üleandmise\nriski maandamiseks kasutab IVXV protokoll täiendavat Registreerimisteenust, kuhu\nKogumisteenus iga talletatud hääle registreerib.  Hääletajal on\nprotokollikohaselt võimalus korrektses registreerimises veenduda --\nRegistreerimisteenuse digitaalselt allkirjastatud kinnitus konkreetse hääle\nregistreerimise kohta esitatakse ka konkreetsele hääletajale.\n\nRegistreerimisteenus protokollis\n--------------------------------\n\nKogumisteenus saadab Registreerimisteenusele enda poolt allkirjastatud\nregistreerimiskorralduse (edaspidi KORRALDUS), mis sisaldab hääle\nidentifikaatorit ja allkirjastatud hääle räsi::\n\n  KORRALDUS = Sign_K(V_id, Hash(VOTE))\n\nRegistreerimisteenus talletab KORRALDUSE hilisemaks väljastamiseks ning vastab\nKogumisteenusele oma poolt allkirjastatud registreerimiskinnitusega (edaspidi\nKINNITUS), mis allkirjastab algset KORRALDUST ning KINNITUSE väljastamise aega::\n\n  KINNITUS = Sign_R(Hash(KORRALDUS), t)\n\nKogumisteenus tagastab nii KINNITUSE kui KORRALDUSE valijarakendusele, Hääletaja\nsaab teate hääle positiivsest talletamisest ainult KINNITUSE ja selle aluseks\nolnud KORRALDUSE edukal verifitseerimisel.\n\nHääletamisperioodi lõppedes edastab Registreerimisteenus Kogumisteenuse poolt\ntehtud KORRALDUSED Töötlejale.\n\nRegistreerimisteenus peab Töötlejale algselt andma üle vähemalt loendi::\n\n  (V_id, Hash(VOTE))\n\nKui Registreerimisteenus annab üle ainult ülalkirjeldatud loendi, siis peab ta\nhiljem oleme võimeline andma loendi nõutud elemendile vastava korralduse::\n\n  (V_id, Hash(VOTE)), Sign_K(V_id, Hash(VOTE))\n\n\n.. figure:: model/img/phase1.png\n\n   Registreerimisteenuse roll hääletamisel\n\n\n.. figure:: model/img/phase2.png\n\n   Registreerimisteenuse roll hääle kontrollimisel\n\n\n.. figure:: model/img/phase3.png\n\n   Registreerimisteenuse roll häälte üleandmisel\n\n\nRegistreerimisteenuse liidesed\n------------------------------\n\nRegistreerimisteenusel on kaks liidest\n\n#. KORRALDUSTE vastuvõtmise ja KINNITUSTE väljastamise liides;\n\n#. KORRALDUSE alusel väljastatud KINNITUSTE loetelu ja nende aluseks olnud\n   KORRALDUSTE väljastamise liides.\n\nRegistreerimisteenuse funktsionaalsuseks on Kogumisteenusele KINNITUSTE\nväljastamine, väljastatud KINNITUSTE ja neile aluseks olevate KORRALDUSTE\nsäilitamine ja hilisem Töötlejale üleandmine.\n\nKui Registreerimisteenus osutab teenust mitmele erinevale osapoolele, siis peab\nolema garanteeritud, et konkreetse valimisega seotud Kogumisteenuse KORRALDUSED\nja neile vastavad KINNITUSED on Valijarakenduse ja Kontrollrakenduse poolt\nkontrollitavalt eristatavad teiste osapoolte KORRALDUSTEST ning neile\nvastavatest KINNITUSTEST. Vastasel juhul võib tekkida olukord, kus Kogumisteenus\nküll registreerib hääle, kuid info sellest ei jõua Töötlejani.\n\nRegistreerimisteenus peab olema võimeline kõiki Kogumisteenuse poolt tulnud\nKORRALDUSI üle andma.\n\nOsapoolte nõuded registreerimisteenusele\n----------------------------------------\n\nTöötleja\n````````\n\nTöötleja ülesanne on muuhulgas tuvastada,\n\n#. millised Kogumisteenuse poolt üle antud hääled lähevad lugemisele ja\n\n#. kas Kogumisteenus on jätnud hääli üle andmata.\n\nTöötleja töö tulemusena selguvaid erisusi tuleb lahendada ning siin on järgmised\n3 juhtumit:\n\n#. Kogumisteenus annab Töötlejale üle allkirjastatud hääle koos KINNITUSega,\n   Registreerimisteenus annab Töötlejale üle Kogumisteenuse KORRALDUSE, mille\n   alusel KINNITUS anti - vaidlust ei ole, kui konkreetne hääl oli antud\n   Hääletaja jaoks viimane, siis suunatakse ta lugemisele.\n\n#. Kogumisteenus annab Töötlejale üle allkirjastatud hääle koos KINNITUSega,\n   Registreerimisteenus ei anna Töötlejale selle hääle kohta midagi üle. Kuna\n   KINNITUS on Registreerimisteenuse poolt allkirjastatud, siis on viga\n   Registreerimisteenuse poolel. Kui konkreetne hääl oli antud Hääletaja jaoks\n   viimane, siis suunatakse ta lugemisele.\n\n#. Registreerimisteenus annab Töötlejale üle Kogumisteenuse KORRALDUSE,\n   Kogumisteenus ei anna Töötlejale vastava räsiga häält üle. Kuna KORRALDUS on\n   Kogumisteenuse poolt allkirjastatud, siis on viga Kogumisteenuse poolel ning\n   antud hääl tuleb üles otsida.\n\nHääletaja\n`````````\n\nHääletaja jaoks on oht, et Kogumisteenus võib tema hääle 'unustada'.\nNõuetekohase KINNITUSE nägemine annab Hääletajale kindluse, et väline osapool\ngaranteerib tema hääle Töötlejani jõudmist. Kindluse jaoks on oluline:\n\n#. KINNITUS on Registreerimisteenuse poolt allkirjastatud;\n\n#. KINNITUSES sisalduv algne KORRALDUS on Kogumisteenuse poolt allkirjastatud;\n\n#. usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise fakti meeles\n   pidama;\n\n#. usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise kohasust\n   Töötlejale tõestama;\n\n#. usaldus, et Kogumisteenusel ei ole võimalik hankida alternatiivset\n   valekinnitust, mis Valijarakenduses verifitseerub, kuid Töötlejani ei jõua.\n\nKogumisteenus\n`````````````\n\nKogumisteenuse jaoks on oht, et Registreerimisteenuse poolt üleantud KINNITUSTE\nja talletatud häälte vaated erinevad. Kogumisteenuse poolt KORRALDUSELE antud\nallkiri on Kogumisteenuse garantii, et Registreerimisteenuse poolt ei saa\ntekkida fiktiivseid KINNITUSI, mida Kogumisteenus tegelikult nõudnud pole.\n\nKogumisteenus talletab kõiki Registreerimisteenuse vastuseid. Kuna need on\nallkirjastatud, siis on täiendav info oluline vaid siis kui Kogumisteenus\nväidab, et mingit KORRALDUST ei ole antud, kuigi Registreerimisteenus on ``(v_id,\nHash(VOTE))`` esitanud. Sellisel juhul saab Registreerimisteenus esitada terve\nKogumisteenuse KORRALDUSE (või vähemalt selle allkirjastatud komponendi)\n\nRegistreerimisteenus\n````````````````````\n\nRegistreerimisteenus on huvitatud, et vaidlusolukordades, kus Kogumisteenus\njätab midagi üle andmata, oleks tal võimalik oma tegevuse korrektsust tõestada.\nOluline on tagada:\n\n#. Kogumisteenuse poolt konkreetse valimise raames antavad KORRALDUSED on\n   teiste klientide poolt esitatud KORRALDUSTEST kontrollitavalt eristatavad.\n\n#. Kogumisteenus ei saa juba esitatud KORRALDUSTE kohta väita, et ta neid ei\n   esitanud.\n\nRegistreerimisteenuse realiseerimine RFC 3161 protokolli raamistikus\n--------------------------------------------------------------------\n\nPKIX on ajatembeldusprotokoll, kus usaldatav kolmas osapool (ajatempliteenuse\nosutaja ehk ATO) kinnitab oma allkirjaga andmete eksisteerimist konkreetsel\najahetkel. Protokoll koosneb ühest päringust ja vastusest.\n\nAjatemplipäring::\n\n  TimeStampReq ::= SEQUENCE  {\n    version               INTEGER  { v1(1) },\n    messageImprint        MessageImprint,\n      --a hash algorithm OID and the hash value of the data to be\n      --time-stamped\n    reqPolicy             TSAPolicyId              OPTIONAL,\n    nonce                 INTEGER                  OPTIONAL,\n    certReq               BOOLEAN                  DEFAULT FALSE,\n    extensions            [0] IMPLICIT Extensions  OPTIONAL  }\n\nAjatembeldatavad andmed esitatakse teenusele ``messageImprint`` koosseisus räsina.\n``TimeStampReq`` ei sisalda endas päringu esitaja allkirja.\n\nATO vastus ajatemplipäringule::\n\n  TimeStampResp ::= SEQUENCE  {\n    status                PKIStatusInfo,\n    timeStampToken        TimeStampToken           OPTIONAL  }\n\n  TimeStampToken ::= ContentInfo\n    -- contentType is id-signedData ([CMS])\n    -- content is SignedData ([CMS])\n\n  TSTInfo ::= SEQUENCE  {\n    version               INTEGER  { v1(1) },\n    policy                TSAPolicyId,\n    messageImprint        MessageImprint,\n      -- MUST have the same value as the similar field in\n      -- TimeStampReq\n    serialNumber          INTEGER,\n      -- Time-Stamping users MUST be ready to accommodate integers\n      -- up to 160 bits.\n    genTime               GeneralizedTime,\n    accuracy              Accuracy                 OPTIONAL,\n    ordering              BOOLEAN                  DEFAULT FALSE,\n    nonce                 INTEGER                  OPTIONAL,\n      -- MUST be present if the similar field was present\n      -- in TimeStampReq.  In that case it MUST have the same value.\n    tsa                   [0] GeneralName          OPTIONAL,\n    extensions            [1] IMPLICIT Extensions  OPTIONAL }\n\n\n``TimeStampResp`` on ATO poolt digitaalselt allkirjastatud konteiner, mis sisaldab\nendas päringu koosseisus saadud välja ``messageImprint`` ning nonssi.\n\nRegistreerimisteenuse huvides on, et Kogumisteenuse päring oleks signeeritud.\nKuna RFC 3161 ei toeta allkirjastatud päringuid on alternatiiviks kasutada mõnda\nlaiendust, mis võimaldab Kogumisteenuse signatuuri edastamist. See laiendus\ntuleks teenuse poolt ajatempli koosseisus ka tagasi saata. Kuna RFC 3161 ei\nsõnasta laienduste tagasipeegeldamise nõuet ühemõtteliselt on reaalne võimalus\nkasutada protokolli laiendamiseks ajatemplipäringu nonssi. Nonss on ASN.1\nINTEGER andmetüüp kuhu saab kodeerida suvalise struktuuriga andmeid, mis teeb\nvõimalikuks järgmise skeemi:\n\nEnne hääletamist:\n\n#. Kogumisteenus genereerib allkirjastamisvõtme ja sertifikaadi.\n\n#. Kogumisteenus annab sertifikaadi Korraldajale üle.\n\n#. Kogumisteenus seadistab ennast ATO'd kasutama.\n\nHääletamise ajal:\n\n#. Valija saadab hääle talletamiseks.\n\n#. Kogumisteenus räsib hääle, allkirjastab räsi ning võtab räsile ajatempli,\n   kasutades ajatemplipäringu TimeStampReq nonsina oma allkirja sellel räsil.\n\n#. ATO töötleb ajatemplipäringut kooskõlas RFC 3161 nõuetega ning väljastab\n   allkirjastatud ajatempli.\n\n#. Kogumisteenus vahendab ajatempli Valijarakendusele, mis teostab järgmised\n   kontrollid:\n\n   a) ajatempel on ATO poolt allkirjastatud,\n   b) ajatempel sisaldab nonssi,\n   c) ajatempel sisaldab tema hääle räsi,\n   d) nonss on Kogumisteenuse poolt allkirjastatud valija hääle räsi.\n\nPeale hääletamist:\n\n#. Korraldaja annab ATO'le ajavahemiku ja Kogumisteenuse sertifikaadi\n\n#. ATO otsib kõigi selle ajavahemiku ajatemplipäringute ja vastuste hulgast\n   neid, millel\n\n   a) on nonss,\n   b) nonss dekodeerub kokkuleppeliseks andmestruktuuriks,\n   c) andmestruktuur verifitseerub Kogumisteenuse sertifikaadiga.\n\n#. ATO annab üle kõik leitud ajatemplipäringud ja ajatemplid.\n\n#. Kogumisteenus annab üle kõik ajatemplipäringud, ajatemplid ja hääled.\n\n#. Töötleja analüüsib andmeid vastavalt protokollile\n\n#. KINNITUS on Registreerimisteenuse poolt allkirjastatud;\n\n#. KINNITUSES sisalduv algne KORRALDUS on Kogumisteenuse poolt allkirjastatud;\n\n#. Usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise fakti meeles\n   pidama;\n\n#. Usaldus, et Kogumisteenusel ei ole võimalik hankida alternatiivset\n   valekinnitust, mis Valijarakenduses verifitseerub, kuid Töötlejani ei jõua\n\n#. Usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise kohasust\n   Töötlejale tõestama;\n\n#. Registreerimisteenuse poolt ei saa tekkida fiktiivseid KINNITUSI, mida\n   Kogumisteenus tegelikult nõudnud pole\n\n#. Kogumisteenus ei saa juba esitatud KORRALDUSTE kohta väita, et ta neid ei\n   esitanud\n\nNonsi vorming::\n\n  Signature ::= SEQUENCE {\n    signingAlgorithm AlgorithmIdentifier,\n    signature        ANY DEFINED BY signingAlgorithm\n  }\n\n  AlgorithmIdentifier ::= SEQUENCE {\n    algorithm  OBJECT IDENTIFIER,\n    parameters ANY DEFINED BY algorithm OPTIONAL\n  }\n\nSõnumiks on TimeStampReq.messageImprint DER-kodeering::\n\n  MessageImprint ::= SEQUENCE {\n    hashAlgorithm AlgorithmIdentifier,\n    hashedMessage OCTET STRING\n  }\n\nRSA kasutamisel allkirjastamiseks. välja\n``Signature.signingAlgorithm.algorithm`` väärtus sõltub sõnumi\n``hashAlgorithm`` välja väärtusest::\n\n  pkcs-1 OBJECT IDENTIFIER ::= { iso(1) member-body(2) US(840) rsadsi(113549) pkcs(1) 1 }\n\n  sha-1WithRSAEncryption   OBJECT IDENTIFIER  ::=  { pkcs-1  5 }\n  sha224WithRSAEncryption  OBJECT IDENTIFIER  ::=  { pkcs-1 14 }\n  sha256WithRSAEncryption  OBJECT IDENTIFIER  ::=  { pkcs-1 11 }\n  sha384WithRSAEncryption  OBJECT IDENTIFIER  ::=  { pkcs-1 12 }\n  sha512WithRSAEncryption  OBJECT IDENTIFIER  ::=  { pkcs-1 13 }\n\nVäli ``Signature.signingAlgorithm.parameters`` puudub või on NULL.\n\nVäli ``Signature.signature`` on OCTET STRING, mis sisaldab RSA signatuuri sõnumil.\n\nATO väljavõte\n-------------\n\nATO väljavõte Kogumisteenuse poolt esitatud TimeStampReq vormingus päringutest\nesitatakse ZIP failina, kus iga päring on salvestatud ühte faili, mis vastab\njärgmistele tingimustele:\n\n* Kaustastruktuur puudub, ükski fail ei paikne kaustas.\n* Failinimed on unikaalsed (failinimedele samas tähendust ei omistata).\n\nÜleantav andmekomplekt on:\n\n  * Andmefail: `<andmefailinimi>.zip`\n  * Kontrollsummafail: `<andmefailinimi>.zip.sha256sum.asice`\n\nÜherealise kontrollsummafaili sisu on HEX kodeeringus SHA256 räsi andmefailist.\n\n"
  },
  {
    "path": "Documentation/public/protokollid/07-kontrollimine.rst",
    "content": "..  IVXV protokollid\n\n================================================================================\nElektroonilise hääle kontrollimine eraldi ja e-valimiskasti koosseisus\n================================================================================\n\nElektroonilist häält kontrollitakse töötlemisrakenduses, kogumisteenuses,\nvalijarakenduses ja kontrollrakenduses.\n\nKõige põhjalikuma kontrolli läbib elektrooniline hääl e-valimiskasti koosseisus\ntöötlemisrakenduses, kus otsustatakse konkreetse hääle lugemisele saatmine või\nmittesaatmine. Nende kontrollide käigus vaadeldakse e-häält nii eraldi kui ka\nsuhtes kõigi teiste sama valija poolt antud häältega. Täiendavalt kõrvutatakse\ne-valimiskasti registreerimisteenuse väljavõttega.\n\nIga üksiku hääle kohta läbitakse töötlemisrakendusega analoogsel tasemel\nkontroll valijarakenduses, kus veendutakse, et kogumisteenus on hääle\nkvalifitseerinud selliselt, et töötlemisrakenduses tehtavad kontrollid\nõnnestuvad. Valijarakenduse kontrollidega samaväärsed kontrollid viib läbi\nkontrollrakendus.\n\nKogumisteenus on lisaks vastutav mitme hääle lõplikuks kvalifitseerumiseks\nvajaliku elemendi hankimise eest ning teostab ka nende hankimise järgselt kontrollid.\n\n\nLõpliku elektroonilise hääle komponendid\n----------------------------------------\n\nElemendid, mis on kättesaadavad otsustamise hetkel, kas hääl kvalifitseerub\nlugemisele saatmiseks või mitte:\n\n   #. elektroonilist häält sisaldav konteiner - :ref:`entity-haale-konteiner`;\n\n      #. krüpteeritud sedel - :ref:`entity-krypteeritud-sedel`;\n\n      #. valija signatuur krüpteeritud sedelil - :ref:`entity-haale-signatuur`;\n\n      #. valija allkiri krüpteeritud sedelil - :ref:`entity-haale-allkiri`;\n\n      #. valija allkirjastamissertifikaat - :ref:`entity-valija-sertifikaat`;\n\n      #. valija isikukood - :ref:`entity-valija-identiteet`;\n\n   #. kvalifitseeriv element - valija sertifikaadi kehtivuskinnitus -\n      :ref:`entity-kehtivuskinnitus`;\n\n   #. kvalifitseeriv element - ajatempel allkirjastatud krüpteeritud sedelile -\n      :ref:`entity-ajatempel`;\n\n   #. kvalifitseeriv element - registreerimispäringu konteiner\n      :ref:`entity-registreerimisparing-konteiner`;\n\n      #. kvalifitseeriv element - registreerimispäring allkirjastatud\n         krüpteeritud sedelile - :ref:`entity-registreerimisparing`;\n\n   #. kvalifitseeriv element - registreerimistõend allkirjastatud krüpteeritud\n      sedeli kohta - :ref:`entity-registreerimistoend`;\n\n   #. valija ringkonnakuuluvuse tõend hääle andmise hetkel -\n      :ref:`entity-nimekirjatunnus`.\n\n\nElemendid, mis on kättesaadavad otsustamise hetkel, kuidas häält kokkulugemisel\narvesse võtta:\n\n   #. miksitud krüpteeritud sedel - :ref:`entity-miksitud-krypteeritud-sedel`;\n\n   #. valija tahteavaldus avakujul - :ref:`entity-tahteavaldus`;\n\n   #. ringkonnakuuluvuse tunnus - :ref:`entity-ringkonnatunnus`;\n\n   #. ringkonnapõhine valikute nimekiri - :ref:`entity-ringkonna-valikutenimekiri`.\n\n\nTäiendavad elemendid:\n\n   #. krüpteerimisel kasutatud juhuslikkus - :ref:`entity-juhuslikkus`; -\n      luuakse valijarakenduses hääle krüpteerimisel ning vahendatakse ainult\n      kontrollrakendusele.\n\n\n.. _entity-tahteavaldus:\n\n````````````\nTAHTEAVALDUS\n````````````\n\nOlem moodustatakse valijarakenduses ning tuvastatakse võtmerakenduses. Tegemist\non EHS spetsiifilises vormingus UTF-8 kodeeringus baidijadaga.\n\n.. _check-tahteavaldus-correctness:\n\nTAHTEAVALDUS, vormingu korrektsus\n`````````````````````````````````\n\nTahteavalduse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n\n.. _check-tahteavaldus-ringkonnatunnus-valikutenimekiri-consistency:\n\nTAHTEAVALDUS, RINGKONNATUNNUS, RINGKONNA VALIKUTENIMEKIRI kooskõlalisus\n```````````````````````````````````````````````````````````````````````\n\nTahteavalduses sisalduva valiku kontroll ringkonnatunnuse ja valikute nimekirja\nsuhtes. Kontroll õnnestub, kui tuvastatud valik on kättesaadav antud\nringkonnas.\n\n.. _check-tahteavaldus-ringkonna-valikutenimekiri-consistency:\n\nTAHTEAVALDUS, RINGKONNA VALIKUTENIMEKIRI, kooskõlalisus\n```````````````````````````````````````````````````````\n\nTahteavalduses sisalduva valiku kontroll ringkonnapõhise valikute nimekirja\nsuhtes. Kontroll õnnestub, kui tuvastatud valik on kättesaadav antud ringkonnas.\n\n\n.. _entity-juhuslikkus:\n\n```````````\nJUHUSLIKKUS\n```````````\n\nOlem moodustatakse valijarakenduses ning kasutatakse ka kontrollrakenduses.\nOlem esitatakse EHS spetsiifilises vormingus baidijadana ning peab olema\nühilduv avaliku võtme parameetrite poolt määratud matemaatilise rühmaga.\n\n.. _check-juhuslikkus-correctness:\n\nJUHUSLIKKUS, vormingu korrektsus\n````````````````````````````````\n\nJuhuslikkuse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n.. _check-juhuslikkus-public-key-consistency:\n\nJUHUSLIKKUS, kooskõlalisus avaliku võtmega\n``````````````````````````````````````````\n\nJuhuslikkuse kooskõlalisuse kontroll avaliku võtmega. Kontroll õnnestub kui\njuhuslikkus on kasutatav skalaarina arvutusteks avaliku võtme parameetrite\npoolt määratud matemaatilises rühmas.\n\n.. _entity-krypteeritud-sedel:\n\n``````````````````\nKRÜPTEERITUD SEDEL\n``````````````````\n\nOlem moodustatakse valijarakenduses. Tegemist on EHS spetsiifilises vormingus\nDER-kodeeritud andmestruktuuriga, mille kontrollimise aluseks on EHS avalik\nvõti, mis muuhulgas määratleb krüpteerimisel kasutatud matemaatilise rühma.\n\n.. _check-krypteeritud-sedel-correctness:\n\nKRÜPTEERITUD SEDEL, vormingu korrektsus\n```````````````````````````````````````\n\nKrüpteeritud sedeli vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n\n.. _check-krypteeritud-sedel-public-key-consistency:\n\nKRÜPTEERITUD SEDEL, kooskõlalisus avaliku võtmega\n`````````````````````````````````````````````````\n\nKrüpteeritud sedeli kooskõlalisuse kontroll avaliku võtmega. Kontroll õnnestub kui\nkrüpteeritud sedeli komponendid on kasutatavad rühma liikmetena arvutusteks\navaliku võtme parameetrite poolt määratud matemaatilises rühmas.\n\n.. _check-krypteeritud-sedel-juhuslikkus-consistency:\n\nKRÜPTEERITUD SEDEL, JUHUSLIKKUS, kooskõlalisus\n``````````````````````````````````````````````\n\nKrüpteeritud sedeli kooskõlalisuse kontroll juhuslikkusega lähtudes rühma\nparameetritest. Kontroll õnnestub kui õnnestub näidata, et antud juhuslikkust\non kasutatud krüpteeritud sedeli komponendi `uBlind` arvutamiseks antud rühma\nparameetrite järgi.\n\n\n.. _entity-valija-sertifikaat:\n\n``````````````````\nVALIJA SERTIFIKAAT\n``````````````````\n\nOlem on valijale omistatud süsteemiväliselt. Tegemist on X.509 vormingus\nsertifikaadiga, mille kehtivuse kontrolli aluseks on samasse avaliku võtme\ninfrastruktuuri kuuluvad juursertifikaadid ja kehtivuskinnitusteenuse osutaja.\n\n.. _check-valija-sertifikaat-correctness:\n\nVALIJA SERTIFIKAAT, vormingu korrektsus\n```````````````````````````````````````\n\nValija sertifikaadi vormingu korrektsuse kontroll lähtudes X.509 spetsifikatsioonist.\n\n.. _check-valija-sertifikaat-consistency-protocol-settings:\n\nVALIJA SERTIFIKAAT, protokollikohane kooskõlalisus valimise seadistustega\n`````````````````````````````````````````````````````````````````````````\n\nValija sertifikaadi protokollikohase kooskõlalisuse kontroll valimise\nseadistustega. Kontroll õnnestub kui:\n\n   #. Valija sertifikaat on kehtiv lähtudes sertifikaadis sisalduvast kehtivusajast;\n\n   #. Valija sertifikaat kuulub mõnda valimise seadistustes kirjeldatud\n      sertifitseerimishierarhiatest.\n\n\n.. _check-valija-sertifikaat-kehtivuskinnitus-consistency:\n\nVALIJA SERTIFIKAAT, KEHTIVUSKINNITUS, protokollikohane kooskõlalisus valimise seadistustega\n```````````````````````````````````````````````````````````````````````````````````````````\n\nValija sertifikaadi ja kehtivuskinnituse protokollikohase kooskõlalisuse\nkontroll valimise seadistustega. Kontroll õnnestub kui:\n\n   #. Kehtivuskinnitus on korrektselt allkirjastatud kehtivuskinnituse teenuse\n      osutaja poolt, kellel on seadistuste kohaselt voli valija sertifikaadi\n      kehtivust tõendada\n\n   #. Kehtivuskinnitus on väljastatud valija sertifikaadi kohta;\n\n   #. Kehtivuskinnitus näitab valija sertifikaadi OCSP olekuks 'Kehtiv'.\n\n\n.. _entity-valija-identiteet:\n\n`````````````````\nVALIJA IDENTITEET\n`````````````````\n\nOlem on valijale omistatud süsteemiväliselt. Tegemist on isikukoodiga, mille\nkontrollimise aluseks on VALIJA SERTIFIKAAT.\n\n.. _check-valija-identiteet-correctness:\n\nVALIJA IDENTITEET, vormingu korrektsus\n``````````````````````````````````````\n\nValija identiteedi vormingu korrektsuse kontroll lähtudes Eesti Vabariigi\nisikukoodi vormingust.\n\n.. _check-valija-identiteet-nimekirjatunnus-eligibility:\n\nVALIJA IDENTITEET, NIMEKIRJATUNNUS, hääleõigus\n``````````````````````````````````````````````\n\nHääleõiguse kontroll valija identiteedi ja nimekirjatunnuse alusel. Kontroll\nõnnestub, kui valija identiteet on kantud nimekirjatunnusele vastavasse\nvalijate nimekirja. Sellisel juhul on identiteedile omistatud ka\nringkonnatunnus, mis määrab ringkonnaspetsiifilise valikute nimekirja.\n\n.. _entity-haale-signatuur:\n\n```````````````\nHÄÄLE SIGNATUUR\n```````````````\n\nOlem arvutatakse allkirjastamise vahendi poolt lähtudes valijarakenduse poolt\nsisendiks antud KRÜPTEERITUD SEDELI räsist.\n\n.. _check-haale-signatuur-correctness:\n\nHÄÄLE SIGNATUUR, vormingu korrektsus\n````````````````````````````````````\n\nHääle signatuuri vormingu korrektsuse kontroll lähtudes konkreetsest\nsigneerimismeetodist.\n\n.. _check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-consistency:\n\nHÄÄLE SIGNATUUR, KRÜPTEERITUD SEDEL, VALIJA SERTIFIKAAT, kooskõlalisus\n``````````````````````````````````````````````````````````````````````\n\nKontroll, mis õnnestub siis ja ainult siis kui õnnestub verifitseerida, et\nhääle signatuur on arvutatud krüpteeritud sedeli räsist kasutades valija\nsertifikaadis leiduvale avalikule võtmele vastavat privaatvõtit.\n\n.. _entity-haale-allkiri:\n\n`````````````\nHÄÄLE ALLKIRI\n`````````````\n\nOlem moodustatakse valijarakenduses, talletades hääle signatuuri allkirja vormingusse.\n\n.. _check-haale-allkiri-correctness:\n\nHÄÄLE ALLKIRI, vormingu korrektsus\n``````````````````````````````````\n\nHääle allkirja vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n.. _check-haale-allkiri-haale-signatuur-consistency:\n\nHÄÄLE ALLKIRI, HÄÄLE SIGNATUUR, kooskõlalisus\n`````````````````````````````````````````````\n\nHääle allkirja ja hääle signatuuri kooskõlalisuse kontroll. Kontroll õnnestub,\nkui antud allkiri sisaldab antud signatuuri.\n\n.. _entity-haale-konteiner:\n\n```````````````\nHÄÄLE KONTEINER\n```````````````\n\nOlem moodustatakse valijarakenduses, luues vormingukohane konteiner, mis\nsisaldab muuhulgas krüpteeritud sedelit, hääle allkirja ja valija sertifikaati.\n\n.. _check-haale-konteiner-correctness:\n\nHÄÄLE KONTEINER, vormingu korrektsus\n````````````````````````````````````\n\nHääle konteineri vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n.. _check-haale-konteiner-haale-allkiri-valija-sertifikaat-krypteeritud-sedel-consistency:\n\nHÄÄLE KONTEINER, HÄÄLE ALLKIRI, VALIJA SERTIFIKAAT, KRÜPTEERITUD SEDEL, kooskõlalisus\n`````````````````````````````````````````````````````````````````````````````````````\n\nHääle konteineri, hääle allkirja, valija sertifikaadi ja krüpteeritud sedeli\nkooskõlalisuse kontroll. Kontroll õnnestub kui antud konteiner sisaldab\nkonkreetset hääle allkirja, valija sertifikaati ja krüpteeritud sedelit.\n\n.. _entity-kehtivuskinnitus:\n\n````````````````\nKEHTIVUSKINNITUS\n````````````````\n\nOlemi hankimise eest vastutab kogumisteenus. Tegemist on OCSP vormingus\nkinnitusega valija sertifikaadi oleku kohta.\n\n.. _check-kehtivuskinnitus-correctness:\n\nKEHTIVUSKINNITUS, vormingu korrektsus\n`````````````````````````````````````\n\nKehtivuskinnituse vormingu korrektsuse kontroll OCSP spetsifikatsiooni suhtes.\n\n\n.. _check-kehtivuskinnitus-ajatempel-order:\n\nKEHTIVUSKINNITUS, AJATEMPEL, ajaline järgnevus\n``````````````````````````````````````````````\n\nKehtivuskinnituse ja ajatempli ajalise järgnevuse kontroll. Kontroll õnnestub, kui\n\n   #. ajatempel ei ole väljastatud hiljem kui kehtivuskinnitus;\n\n   #. ajatempli ja kehtivuskinnituse väljastamise ajaline vahe on väiksem kui\n      valimise seadistustes ette nähtud aeg.\n\n\n.. _entity-ajatempel:\n\n`````````\nAJATEMPEL\n`````````\n\nOlemi hankimise eest vastutab kogumisteenus. Tegemist on PKIX vormingus ajatempliga.\n\n.. _check-ajatempel-correctness:\n\nAJATEMPEL, vormingu korrektsus\n``````````````````````````````\n\nAjatempli vormingu korrektsuse kontroll PKIX spetsifikatsiooni suhtes.\n\n\n.. _check-ajatempel-consistency-protocol-settings:\n\nAJATEMPEL, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus valimise seadistustega\n```````````````````````````````````````````````````````````````````````````````\n\nAjatempli protokollikohase kooskõlalisuse kontroll valimise seadistustega.\nKontroll õnnestub kui:\n\n   #. ajatempel on korrektselt allkirjastatud valimise seadistuses volitatud\n      ajatempliteenuse osutaja poolt;\n\n   #. ajatempel on võetud hääle allkirjale.\n\n.. _entity-registreerimisparing:\n\n````````````````````\nREGISTREERIMISPÄRING\n````````````````````\n\nOlem moodustatakse kogumisteenuses. Olemi talletamise eest vastutab\nregistreerimisteenuse osutaja.\n\n.. _check-registreerimisparing-correctness:\n\nREGISTREERIMISPÄRING, vormingu korrektsus\n`````````````````````````````````````````\n\nRegistreerimispäringu vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n\n.. _check-registreerimisparing-haale-allkiri-consistency:\n\nREGISTREERIMISPÄRING, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus valimise seadistustega\n``````````````````````````````````````````````````````````````````````````````````````````\n\nRegistreerimispäringu ja hääle allkirja protokollikohase kooskõlalisuse\nkontroll valimise seadistustega. Kontroll õnnestub kui:\n\n   #. Registreerimispäring on koostatud hääle allkirjale;\n\n   #. Registreerimispäring on korrektselt allkirjastatud valimise seadistuses\n      viidatud kogumisteenuse poolt.\n\n\n.. _check-registreerimisparing-registreerimistoend-consistency:\n\nREGISTREERIMISPÄRING, REGISTREERIMISTÕEND, kooskõlalisus\n```````````````````````````````````````````````````````````````````````\n\nRegistreerimispäringu ja registreerimistõendi protokollikohase kooskõlalisuse\nkontroll valimise seadistustega. Kontroll õnnestub kui:\n\n   #. Registreerimispäring on korrektselt allkirjastatud valimise seadistuses\n      viidatud kogumisteenuse poolt.\n\n   #. Registreerimistõend on koostatud vastuseks samale registreerimispäringule;\n\n   #. Registreerimistõend on korrektselt allkirjastatud valimise seadistuses\n      viidatud registreerimisteenuse poolt.\n\n\n.. _entity-registreerimisparing-konteiner:\n\n```````````````````````````````\nREGISTREERIMISPÄRINGU KONTEINER\n```````````````````````````````\n\nOlem moodustatakse kogumisteenuses. Olemi talletamise eest vastutab\nregistreerimisteenuse osutaja.\n\n.. _check-registreerimisparing-konteiner-correctness:\n\nREGISTREERIMISPÄRINGU KONTEINER, vormingu korrektsus\n````````````````````````````````````````````````````\n\nRegistreerimispäringu konteineri vormingu korrektsuse kontroll käesoleva\nspetsifikatsiooni suhtes.\n\n\n.. _check-registreerimisparing-konteiner-registreerimispäring-consistency:\n\nREGISTREERIMISPÄRINGU KONTEINER, REGISTREERIMISPÄRING, protokollikohane kooskõlalisus valimise seadistustega\n````````````````````````````````````````````````````````````````````````````````````````````````````````````\n\nRegistreerimispäringu konteineri ja registreerimispäringu protokollikohase kooskõlalisuse\nkontroll valimise seadistustega. Kontroll õnnestub kui:\n\n   #. Registreerimispäringu konteiner sisaldab registreerimispäringut;\n\n   #. Registreerimispäring on korrektselt allkirjastatud valimise seadistuses\n      viidatud kogumisteenuse poolt.\n\n\n.. _entity-registreerimistoend:\n\n```````````````````\nREGISTREERIMISTÕEND\n```````````````````\n\nOlemi hankimise eest vastutab kogumisteenus, olemi moodustab\nregistreerimisteenuse osutaja.\n\n.. _check-registreerimistoend-correctness:\n\nREGISTREERIMISTÕEND, vormingu korrektsus\n````````````````````````````````````````\n\nRegistreerimistõendi vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n\n.. _check-registreerimistoend-haale-allkiri-consistency:\n\nREGISTREERIMISTÕEND, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus valimise seadistustega\n`````````````````````````````````````````````````````````````````````````````````````````\n\nRegistreerimistõendi ja hääle allkirja protokollikohase kooskõlalisuse\nkontroll valimise seadistustega. Kontroll õnnestub kui:\n\n   #. Registreerimistõend on koostatud hääle allkirjale;\n\n   #. Registreerimistõend sisaldab kogumisteenuse poolt korrektselt\n      allkirjastatud registreerimispäringut;\n\n   #. Registreerimistõend on korrektselt allkirjastatud valimise seadistuses\n      viidatud registreerimisteenuse poolt.\n\n\n.. _entity-nimekirjatunnus:\n\n```````````````\nNIMEKIRJATUNNUS\n```````````````\n\nOlem moodustatakse kogumisteenuses ning identifitseerib ühe konkreetse\nversiooni valijate nimekirjast.\n\n.. _check-nimekirjatunnus-correctness:\n\nNIMEKIRJATUNNUS, vormingu korrektsus\n````````````````````````````````````\n\nNimekirjatunnuse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n.. _check-nimekirjatunnus-consistency-protocol-settings:\n\nNIMEKIRJATUNNUS, protokollikohane kooskõlalisus valimise seadistusega\n`````````````````````````````````````````````````````````````````````\n\nNimekirjatunnuse protokollikohase kooskõlalisuse kontroll valimise\nseadistusega. Kontroll õnnestub kui valimise seadistuses sisalduvate\nmuudatusnimekirjade alusel on võimalik koostada nimekirjatunnusele vastav\nnimekiri.\n\n\n.. _entity-miksitud-krypteeritud-sedel:\n\n```````````````````````````\nMIKSITUD KRÜPTEERITUD SEDEL\n```````````````````````````\n\nOlem moodustatakse miksimisrakenduses. Miksitud krüpteeritud sedel on\nsisuliselt krüpteeritud sedel ning rakenduvad samad kontrollid, mis\nkrüpteeritud sedelilegi.\n\n.. _check-miksitud-krypteeritud-sedel-correctness:\n\nMIKSITUD KRÜPTEERITUD SEDEL, vormingu korrektsus\n````````````````````````````````````````````````\n\nMiksitud krüpteeritud sedeli vormingu korrektsuse kontroll käesoleva\nspetsifikatsiooni suhtes.\n\n.. _check-miksitud-krypteeritud-sedel-public-key-consistency:\n\nMIKSITUD KRÜPTEERITUD SEDEL, kooskõlalisus avaliku võtmega\n``````````````````````````````````````````````````````````\n\nMiksitud krüpteeritud sedeli kooskõlalisuse kontroll avaliku võtmega. Kontroll\nõnnestub kui miksitud krüpteeritud sedeli komponendid on kasutatavad rühma\nliikmetena arvutusteks avaliku võtme parameetrite poolt määratud matemaatilises\nrühmas.\n\n\n.. _entity-ringkonnatunnus:\n\n```````````````\nRINGKONNATUNNUS\n```````````````\n\nOlem moodustatakse töötlemisrakenduses ning viitab ringkonnapõhisele valikute\nnimekirjale.\n\n.. _check-ringkonnatunnus-correctness:\n\nRINGKONNATUNNUS, vormingu korrektsus\n````````````````````````````````````\n\nRingkonnatunnuse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni suhtes.\n\n.. _entity-ringkonna-valikutenimekiri:\n\n`````````````````````````````````\nRINGKONNAPÕHINE VALIKUTE NIMEKIRI\n`````````````````````````````````\n\nOlem on valijale omistatud süsteemiväliselt.\n\n.. _check-ringkonna-valikutenimekiri-correctness:\n\nRINGKONNAPÕHINE VALIKUTE NIMEKIRI, vormingu korrektsus\n``````````````````````````````````````````````````````\n\nRingkonnapõhise valikute nimekirja vormingu korrektsuse kontroll VIS liideste\nspetsifikatsiooni suhtes.\n\nKontrollid kogumisteenuses\n--------------------------\n\nKogumisteenus käsitleb talletatavat häält sõltumatult teistest häältest.\nKogumisteenuse ülesandeks on hääle talletamine kogu hääletamisperioodi vältel\nja häälele töötlemise käigus kvalifitseerumiseks vajalike elementide hankimine.\n\nKogumisteenus saab valijarakenduselt järgmised olemid:\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\nKogumisteenus hangib hääle töötlemise käigus välistelt teenustelt järgmised olemid:\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimistoend`\n\nKogumisteenus tuvastab / loob ise järgmised olemid:\n\n   #. :ref:`entity-registreerimisparing`\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n   #. :ref:`entity-ringkonnatunnus`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\nKogumisteenus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-tahteavaldus`\n\n   #. :ref:`entity-juhuslikkus`\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\nKogumisteenus viib läbi järgmised tegevused ja teostab järgmised kontrollid:\n\n   #. Hääle talletamise päringu vastuvõtmine valijarakenduselt\n\n      #. :ref:`check-valija-sertifikaat-correctness`\n      #. :ref:`check-valija-sertifikaat-consistency-protocol-settings`\n      #. :ref:`check-valija-identiteet-correctness`\n      #. :ref:`check-valija-identiteet-nimekirjatunnus-eligibility`\n      #. :ref:`check-haale-signatuur-correctness`\n      #. :ref:`check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-consistency`\n      #. :ref:`check-haale-allkiri-correctness`\n      #. :ref:`check-haale-allkiri-haale-signatuur-consistency`\n      #. :ref:`check-haale-konteiner-correctness`\n      #. :ref:`check-haale-konteiner-haale-allkiri-valija-sertifikaat-krypteeritud-sedel-consistency`\n      #. :ref:`check-krypteeritud-sedel-correctness`\n      #. :ref:`check-krypteeritud-sedel-public-key-consistency`\n\n   #. Kehtivuskinnituse hankimine - :ref:`entity-kehtivuskinnitus`\n\n      #. :ref:`check-kehtivuskinnitus-correctness`\n      #. :ref:`check-valija-sertifikaat-kehtivuskinnitus-consistency`\n\n   #. Ajatempli hankimine - :ref:`entity-ajatempel`\n\n      #. :ref:`check-ajatempel-correctness`\n      #. :ref:`check-ajatempel-consistency-protocol-settings`\n\n   #. Registreerimispäringu loomine - :ref:`entity-registreerimisparing`\n\n   #. Registreerimistõendi hankimine - :ref:`entity-registreerimistoend`\n\n      #. :ref:`check-registreerimisparing-registreerimistoend-consistency`\n      #. :ref:`check-registreerimistoend-correctness`\n      #. :ref:`check-registreerimistoend-haale-allkiri-consistency`\n      #. :ref:`check-kehtivuskinnitus-ajatempel-order`\n\n   #. Hääle talletamine, kvalifitseerivate elementide ja unikaalse\n      identifikaatori tagastamine valijarakendusele.\n\nKontrollid valijarakenduses\n---------------------------\n\nValijarakendus moodustab valija avakujul tahteavalduse põhjal krüpteeritud\nsedeli ning allkirjastab selle valija allkirja andmise vahendiga.\n\nValijarakenduse rolliks peale hääle allkirjastamist on veenduda, et\nkogumisteenus käitus häält kvalifitseerivate elementide võtmisel\nprotokollikohaselt ning et hääl on talletatud selliselt, et ta saab\ntöötlemisrakenduse poolt arvesse võetud.\n\nValijarakendus viib läbi minimaalselt järgmised kontrollid:\n\n#. Kogumisteenus võttis kehtivuskinnituse valija sertifikaadile volitatud\n   kehtivuskinnitusteenuselt. Valijarakendus kontrollib allkirja\n   kehtivuskinnitusteenuse vastusel.\n\n#. Kogumisteenus registreeris valija poolt allkirjastatud hääle volitatud\n   registreerimisteenuses. Valijarakendus kontrollib, et kogumisteenuse poolt\n   moodustatud päring oli kogumisteenuse poolt signeeritud ning viitas\n   korrektselt allkirjastatud häälele. Valijarakendus kontrollib, et\n   registreerimisteenuse vastus on allkirjastatud õige registreerimisteenuse\n   osutaja poolt ning sisaldab kogumisteenuse poolt allkirjastatud päringut.\n\nKui hääle kvalifitseerimiseks vajalike elementide kontroll ei õnnestu, siis\nteavitab valijarakendus sellest kasutajat.\n\nValijarakendus loob ise järgmised olemid:\n\n   #. :ref:`entity-tahteavaldus`\n\n   #. :ref:`entity-juhuslikkus`\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\nValijarakendus saab järgmised olemid teistelt osapooltelt:\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimistoend`\n\n   #. :ref:`entity-registreerimisparing`\n\nValijarakendus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n   #. :ref:`entity-ringkonnatunnus`\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\nValijarakendus viib läbi järgmised tegevused ja teostab järgmised kontrollid:\n\n   #. eID vahendi aktiveerimine ja valija identiteedi tuvastamine\n\n      #. :ref:`entity-valija-sertifikaat`\n      #. :ref:`entity-valija-identiteet`\n\n      #. :ref:`check-valija-sertifikaat-correctness`\n      #. :ref:`check-valija-sertifikaat-consistency-protocol-settings`\n      #. :ref:`check-valija-identiteet-correctness`\n\n   #. Ringkonnapõhise valikutenimekirja tuvastamine -\n      :ref:`entity-ringkonna-valikutenimekiri`\n\n   #. Tahteavalduse moodustamine - :ref:`entity-tahteavaldus`\n\n   #. Juhuarvu genereerimine - :ref:`entity-juhuslikkus`\n\n   #. Sedeli krüpteerimine - :ref:`entity-krypteeritud-sedel`\n\n   #. Krüpteeritud sedeli signeerimine - :ref:`entity-haale-signatuur`\n\n   #. Allkirja moodustamine signatuurist - :ref:`entity-haale-allkiri`\n\n      #. :ref:`check-haale-signatuur-correctness`\n      #. :ref:`check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-consistency`\n\n   #. Allkirjastatud konteineri moodustamine - :ref:`entity-haale-konteiner`\n\n   #. Allkirjastatud konteineri edastamine kogumisteenusele.\n\n   #. Kogumisteenuse vastuse kontrollimine\n\n      #. :ref:`entity-kehtivuskinnitus`\n      #. :ref:`entity-ajatempel`\n      #. :ref:`entity-registreerimistoend`\n      #. :ref:`entity-registreerimisparing`\n\n      #. :ref:`check-kehtivuskinnitus-correctness`\n      #. :ref:`check-valija-sertifikaat-kehtivuskinnitus-consistency`\n      #. :ref:`check-ajatempel-correctness`\n      #. :ref:`check-ajatempel-consistency-protocol-settings`\n      #. :ref:`check-kehtivuskinnitus-ajatempel-order`\n\n      #. :ref:`check-registreerimisparing-correctness`\n      #. :ref:`check-registreerimisparing-haale-allkiri-consistency`\n      #. :ref:`check-registreerimisparing-registreerimistoend-consistency`\n      #. :ref:`check-registreerimistoend-correctness`\n      #. :ref:`check-registreerimistoend-haale-allkiri-consistency`\n\nKontrollid kontrollrakenduses\n-----------------------------\n\nSarnaselt valijarakendusele on kontrollrakenduse rolliks peale hääle\nallkirjastamist veenduda, et kogumisteenus käitus häält kvalifitseerivate\nelementide võtmisel protokollikohaselt ning et hääl on talletatud selliselt, et\nta saab töötlemisrakenduse poolt arvesse võetud.\n\nTäiendavalt on kontrollrakenduse ülesandeks anda valijale tagasisidet, kas tema\ntahteavaldus sai valijarakenduse poolt korrektselt hääleks vormistatud.\n\nKui hääle kvalifitseerimiseks vajalike elementide kontroll ei õnnestu, siis\nteavitab kontrollrakendus sellest kasutajat. Tahteavalduse korrektsuses peab\nvalija ise veenduma.\n\nKontrollrakendus tuvastab ise järgmised olemid:\n\n   #. :ref:`entity-tahteavaldus`\n\nKontrollrakendus saab järgmised olemid teistelt osapooltelt:\n\n   #. :ref:`entity-juhuslikkus`\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimistoend`\n\n   #. :ref:`entity-registreerimisparing`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\nKontrollrakendus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n   #. :ref:`entity-ringkonnatunnus`\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\nKontrollrakendus viib läbi järgmised tegevused ja teostab järgmised kontrollid:\n\n   #. :ref:`check-valija-sertifikaat-correctness`\n   #. :ref:`check-valija-sertifikaat-consistency-protocol-settings`\n   #. :ref:`check-valija-sertifikaat-kehtivuskinnitus-consistency`\n   #. :ref:`check-valija-identiteet-correctness`\n   #. :ref:`check-haale-signatuur-correctness`\n   #. :ref:`check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-consistency`\n   #. :ref:`check-haale-allkiri-correctness`\n   #. :ref:`check-haale-allkiri-haale-signatuur-consistency`\n   #. :ref:`check-haale-konteiner-correctness`\n   #. :ref:`check-haale-konteiner-haale-allkiri-valija-sertifikaat-krypteeritud-sedel-consistency`\n   #. :ref:`check-kehtivuskinnitus-correctness`\n   #. :ref:`check-ajatempel-correctness`\n   #. :ref:`check-ajatempel-consistency-protocol-settings`\n   #. :ref:`check-kehtivuskinnitus-ajatempel-order`\n   #. :ref:`check-registreerimisparing-correctness`\n   #. :ref:`check-registreerimisparing-haale-allkiri-consistency`\n   #. :ref:`check-registreerimisparing-registreerimistoend-consistency`\n   #. :ref:`check-registreerimistoend-correctness`\n   #. :ref:`check-registreerimistoend-haale-allkiri-consistency`\n   #. :ref:`check-ringkonna-valikutenimekiri-correctness`\n   #. :ref:`check-juhuslikkus-correctness`\n   #. :ref:`check-juhuslikkus-public-key-consistency`\n   #. :ref:`check-krypteeritud-sedel-correctness`\n   #. :ref:`check-krypteeritud-sedel-public-key-consistency`\n   #. :ref:`check-krypteeritud-sedel-juhuslikkus-consistency`\n   #. :ref:`check-tahteavaldus-correctness`\n   #. :ref:`check-tahteavaldus-ringkonna-valikutenimekiri-consistency`\n\nKontrollid töötlemisrakenduses\n------------------------------\n\nTöötlemisrakenduse sisendiks on e-valimiskast ja registreerimisteenuse\nväljavõte registreerimispäringutest. Töötlemisrakendus kontrollib mõlema\nandmehulga elemente kõigepealt eraldi ning seejärel püüab luua vastavuse nende\nvahel.\n\nTöötlemisrakendus otsustab, milline valija häältest oli viimane ning liigub\ntöötlemise järgmisesse etappi. S.t. üks häält kvalifitseerivatest elementidest\ntäidab hääle talletamise aja fikseerimise rolli ning selle elemendi põhjal\nmoodustatakse üksikute häälte ajaline järgnevus. Olenevalt IVXV profiilist võib\nsee element olla kehtivuskinnituse koosseisus (BDOC-TM), eraldi ajatemplina\n(BDOC-TS) või registreerimistõendi koosseisus (BDOC-TS).\n\nTöötlemisrakendusele tehakse kättesaadavaks järgmised olemid:\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimisparing`\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-registreerimistoend`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\nTöötlemisrakendus tuvastab/loob järgmised olemid:\n\n   #. :ref:`entity-ringkonnatunnus`\n\n\nTöötlemisrakendus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\n   #. :ref:`entity-tahteavaldus`\n\n   #. :ref:`entity-juhuslikkus`\n\nTöötlemisrakenduse töö jaguneb neljaks etapiks:\n\n   #. Kontrollimine\n\n   #. Korduvhäälte eemaldamine\n\n   #. Tühistamine/-ennistamine\n\n   #. Anonüümimine\n\n\nTöötlemisrakendus teostab kontrollimise etapis järgmised kontrollid:\n\n   #. e-valimiskasti elementide kontrollid teostatakse kõigi hääle elementide kohta:\n\n      #. :ref:`check-valija-sertifikaat-correctness`\n      #. :ref:`check-valija-sertifikaat-consistency-protocol-settings`\n      #. :ref:`check-valija-identiteet-correctness`\n      #. :ref:`check-nimekirjatunnus-correctness`\n      #. :ref:`check-nimekirjatunnus-consistency-protocol-settings`\n      #. :ref:`check-valija-identiteet-nimekirjatunnus-eligibility`\n      #. :ref:`check-ringkonnatunnus-correctness`\n      #. :ref:`check-haale-signatuur-correctness`\n      #. :ref:`check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-consistency`\n      #. :ref:`check-haale-allkiri-correctness`\n      #. :ref:`check-haale-allkiri-haale-signatuur-consistency`\n      #. :ref:`check-haale-konteiner-correctness`\n      #. :ref:`check-haale-konteiner-haale-allkiri-valija-sertifikaat-krypteeritud-sedel-consistency`\n      #. :ref:`check-kehtivuskinnitus-correctness`\n      #. :ref:`check-valija-sertifikaat-kehtivuskinnitus-consistency`\n      #. :ref:`check-ajatempel-correctness`\n      #. :ref:`check-ajatempel-consistency-protocol-settings`\n      #. :ref:`check-kehtivuskinnitus-ajatempel-order`\n      #. :ref:`check-registreerimistoend-correctness`\n      #. :ref:`check-registreerimistoend-haale-allkiri-consistency`\n\n   #. registreerimisteenuse väljavõtte kontrollid teostatakse iga\n      registreerimispäringu kohta\n\n      #. :ref:`check-registreerimisparing-correctness`\n      #. :ref:`check-registreerimisparing-konteiner-correctness`\n      #. :ref:`check-registreerimisparing-konteiner-registreerimispäring-consistency`\n\n   #. Töötlemisrakendus loob vastavuse e-valimiskasti ja registreerimisteenuse\n      väljavõtte vahel võttes aluseks olemi\n      :ref:`entity-registreerimisparing-konteiner` registreerimisteenuse\n      väljavõttest ning olemi :ref:`entity-registreerimistoend`\n      e-valimiskastist. Ühendavaks lüliks kahe vaate vahel on\n      :ref:`entity-registreerimisparing`.\n\n   #. e-valimiskasti ja registreerimisteenuse väljavõtte vastavuskontrollid\n\n      #. :ref:`check-registreerimisparing-haale-allkiri-consistency`\n      #. :ref:`check-registreerimisparing-registreerimistoend-consistency`\n\nTöötlemisrakendus teostab korduvhäälte eemaldamise etapis järgmised kontrollid:\n\n   #. Töötlemisrakendus tuvastab iga valija häälte hulgast ajaliselt viimase.\n\n   #. Töötlemisrakendus viib läbi krüpteeritud sedeli korrektsuse kontrollid.\n\n      #. :ref:`check-krypteeritud-sedel-correctness`\n      #. :ref:`check-krypteeritud-sedel-public-key-consistency`\n\n\nTöötlemisrakenduse töö järgmistes etappides täiendavaid kontrolle ei teostata.\nTühistamis-/ennistamisetapis eemaldatakse/taastatakse isikukoodile vastavaid\nhääli korduvhäältest puhastatud e-valimiskastist. Anonüümimisetapis\neemaldatakse e-häältelt kvalifitseerivad elemendid ning saadud loend\nkrüpteeritud sedelitest suunatakse miksimisrakendusse.\n\nKontrollid miksimisrakenduses\n-----------------------------\n\nMiksimisrakendusele tehakse kättesaadavaks järgmised olemid:\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-ringkonnatunnus`\n\nMiksimisrakendus loob järgmised olemid:\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\nMiksimisrakendus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-tahteavaldus`\n\n   #. :ref:`entity-juhuslikkus`\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimisparing`\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-registreerimistoend`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\n\nMiksimisrakendus viib läbi järgmised tegevused ja kontrollid:\n\n   #. :ref:`check-ringkonnatunnus-correctness`\n   #. :ref:`check-krypteeritud-sedel-correctness`\n   #. :ref:`check-krypteeritud-sedel-public-key-consistency`\n\n   #. Miksimisrakendus grupeerib sisendiks antud krüpteeritud sedelid\n      ringkonnatunnuste kaupa ning järjestab krüpteeritud sedelid ringkonnas\n      ringi.\n\n   #. Miksimisrakendus arvutab iga ümberjärjestatud krüpteeritud sedeli kohta\n      uue miksitud krüpteeritud sedeli.\n\n   #. Miksimisrakendus koostab nullteadmustõestused sedelite korrektse miksimise kohta.\n\nKontrollid võtmerakenduses\n--------------------------\n\nVõtmerakendusele tehakse kättesaadavaks järgmised olemid:\n\n   #. :ref:`entity-miksitud-krypteeritud-sedel`\n\n   #. :ref:`entity-ringkonnatunnus`\n\n   #. :ref:`entity-ringkonna-valikutenimekiri`\n\nVõtmerakendus tuvastab järgmised olemid:\n\n   #. :ref:`entity-tahteavaldus`\n\nVõtmerakendus ei puutu vahetult kokku järgmiste olemitega:\n\n   #. :ref:`entity-juhuslikkus`\n\n   #. :ref:`entity-krypteeritud-sedel`\n\n   #. :ref:`entity-valija-sertifikaat`\n\n   #. :ref:`entity-valija-identiteet`\n\n   #. :ref:`entity-haale-signatuur`\n\n   #. :ref:`entity-haale-allkiri`\n\n   #. :ref:`entity-haale-konteiner`\n\n   #. :ref:`entity-kehtivuskinnitus`\n\n   #. :ref:`entity-ajatempel`\n\n   #. :ref:`entity-registreerimisparing`\n\n   #. :ref:`entity-registreerimisparing-konteiner`\n\n   #. :ref:`entity-registreerimistoend`\n\n   #. :ref:`entity-nimekirjatunnus`\n\n\nVõtmerakendus viib läbi järgmised tegevused ja kontrollid:\n\n   #. :ref:`check-miksitud-krypteeritud-sedel-correctness`\n   #. :ref:`check-miksitud-krypteeritud-sedel-public-key-consistency`\n   #. Häälte dekrüpteerimine\n   #. :ref:`check-ringkonnatunnus-correctness`\n   #. :ref:`check-tahteavaldus-correctness`\n   #. :ref:`check-tahteavaldus-ringkonnatunnus-valikutenimekiri-consistency`\n"
  },
  {
    "path": "Documentation/public/protokollid/08-haaletamine.rst",
    "content": "..  IVXV protokollid\n\n===================\nSuhtlusprotokollid\n===================\n\nLiides\n------\n\nKogumisteenuse valijale suunatud mikroteenused suhtlevad valijarakendusega ja\nkontrollrakendusega JSON-RPC protokolli vahendusel.\n\n:id: JSON-RPC päringuidentifikaator\n:method: RPC-meetod\n:params: Konkreetse RPC-meetodi parameetrid\n\n.. literalinclude:: ../../common/examples/json.rpc.method.query.json\n   :language: json\n   :linenos:\n\n:error: Võimalik veainfo või ``null`` vea puudumisel\n:id: JSON-RPC päringuidentifikaator, peab ühtima päringus kasutatud id'ga\n:result: Meetodipõhine vastusandmestruktuur\n\n.. literalinclude:: ../../common/examples/json.rpc.method.response.json\n   :language: json\n   :linenos:\n\nEsimese päringuvahetuse käigus mõne IVXV mikroteenusega väljastatakse suhtlevale\nrakendusele HEX-kodeeritud unikaalne seansiidentifikaator (``result.SessionID``),\nmida rakendus kasutab edaspidi kõigis kogumisteenuse suunalistes päringutes\n(``params.SessionID``). Seansiidentifikaatori abil seostatakse\nhääletamisega seotud RPC-päringud üheks seansiks. Seostamine on informatiivne\nning selle eesmärk on logianalüüsi lihtsustamine, hääle ringkonnakuuluvust jm.\nsisulisi aspekte puudutavad otsused tehakse digiallkirjastatud andmete põhjal.\n\nTranspordiprotokollina on kasutusel TLS. Krüpteeritud kanali termineerimine\ntoimub konkreetses mikroteenuses. Võimaldamaks koormuse jaotamist ning\nmikroteenuste paindlikku evitamist kasutatakse TLS-SNI laiendust, mis lubab\nvahendusteenusel TLS voogu termineerimata õigesse mikroteenusinstantsi suunata.\nVahendusteenus on tüüpiliselt kättesaadav kogumisteenuse välise liidese pordis\n443.\n\nValikute nimekirja hankimine\n----------------------------\n\nValikute nimekirja hankimine tähendab valijarakenduse suhtlemist\nnimekirjateenusega (SNI ``choices.ivxv.invalid``). Valikute nimekirja hankimine\neeldab valija autentimist ning tema ringkonnakuuluvuse tuvastamist.\n\nValijarakendus teeb päringu ``RPC.VoterChoices`` nimekirjade hankimiseks.\n\n:params.AuthMethod: Toetatud valikud on meetodid ``tls`` ja ``ticket``.\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n\nPäring ``RPC.VoterChoices`` ID-kaardiga autentimise korral - autentimine toimub\nTLS-protokolli tasemel päringu töötlemise ajal kasutades ID-kaardi\nautentimissertifikaati.\n\n.. literalinclude:: ../../common/examples/id.rpc.voterchoices.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.VoterChoices`` Mobiil-ID'ga autentimise korral - päringu\nsooritamiseks tuleb eelnevalt kasutada Mobiil-ID vahendusteenuse (SNI\n``mid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\n\n:params.AuthToken: Autentimisteenuse vahendusel allkirjastatud tõend, mis\n                   sisaldab endas valija unikaalset identifikaatorit.\n\n:params.SessionID: Kuna Mobiil-ID korral on nimekirja hankimisele eelnenud\n                   interaktsioon autentimistõendi saamiseks, on olemas\n                   seansiidentifikaator, mida tuleb kasutada.\n\n.. literalinclude:: ../../common/examples/mid.rpc.voterchoices.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.VoterChoices`` Smart-ID'ga autentimise korral - päringu\nsooritamiseks tuleb eelnevalt kasutada Smart-ID vahendusteenuse (SNI\n``smartid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\n\n:params.AuthToken: Autentimisteenuse vahendusel allkirjastatud tõend, mis\n                   sisaldab endas valija unikaalset identifikaatorit.\n\n:params.SessionID: Kuna Smart-ID korral on nimekirja hankimisele eelnenud\n                   interaktsioon autentimistõendi saamiseks, on olemas\n                   seansiidentifikaator, mida tuleb kasutada.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.voterchoices.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.VoterChoices`` Web eID'ga autentimise korral - päringu\nsooritamiseks tuleb eelnevalt kasutada Web eID vahendusteenuse (SNI\n``webeid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\n\n:params.AuthToken: Autentimisteenuse vahendusel allkirjastatud tõend, mis\n                   sisaldab endas valija unikaalset identifikaatorit.\n\n:params.SessionID: Kuna Web eID korral on nimekirja hankimisele eelnenud\n                   interaktsioon autentimistõendi saamiseks, on olemas\n                   seansiidentifikaator, mida tuleb kasutada.\n\n.. literalinclude:: ../../common/examples/webeid.rpc.voterchoices.query.json\n   :language: json\n   :linenos:\n\nNimekirjateenuse vastus päringule ``RPC.VoterChoices``.\n\n:result.Choices: Valija ringkonnakuuluvuse identifikaator ``VoterDistrict``\n:result.List: BASE64-kodeeritud ringkonna valikute nimekiri ``DistrictChoices``\n:result.Voted: Kui valija on juba hääletanud, siis ``true``, vastasel juhul\n               seda välja vastuses ei ole.\n\n.. literalinclude:: ../../common/examples/id.rpc.voterchoices.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.VoterChoices`` korral.\n\n:BAD_CERTIFICATE: Viga valija isikutuvastussertifikaadiga.\n:BAD_REQUEST: Vigane päring.\n:INELIGIBLE_VOTER: Valijal pole õigust hääletada.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:UNAUTHENTICATED: Autentimata päring.\n:VOTER_TOO_YOUNG: Valija on liiga noor.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nAllkirjastatud hääle saatmine talletamiseks\n-------------------------------------------\n\nAllkirjastatud hääle saatmine talletamiseks tähendab valijarakenduse suhtlemist\nhääletamisteenusega (SNI ``voting.ivxv.invalid``).\n\nValijarakendus teeb päringu ``RPC.Vote`` allkirjastatud hääle talletamiseks\nsaatmiseks.\n\n:params.AuthMethod: Toetatud valikud on meetodid ``tls`` ja ``ticket``.\n:params.Choices: Valija ringkonnakuuluvuse identifikaator ``VoterDistrict`` mis\n                 kehtis valikute nimekirja hankimise ajal. Parameetri korrektne\n                 kasutamine võimaldab kogumisteenusel valijat hoiatada kui tema\n                 ringkonnakuuluvus on võrreldes hääletamise algushetkega\n                 muutunud.\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.Type: Allkirjastatud hääle vorming. Hetkel on ainus toetatud väärtus\n              ``bdoc``.\n:params.Vote: BASE64-kodeeritud hääl ``SignedVote`` eelpoolmääratud vormingus\n              (:ref:`signed-vote`).\n\nPäring ``RPC.Vote`` ID-kaardiga autentimise korral.\n\n.. literalinclude:: ../../common/examples/id.rpc.vote.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.Vote`` Mobiil-ID'ga autentimise korral.\n\n.. literalinclude:: ../../common/examples/mid.rpc.vote.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.Vote`` Smart-ID'ga autentimise korral.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.vote.query.json\n   :language: json\n   :linenos:\n\nPäring ``RPC.Vote`` Web eID'ga autentimise korral.\n\n.. literalinclude:: ../../common/examples/webeid.rpc.vote.query.json\n   :language: json\n   :linenos:\n\nHääletamisteenuse vastus päringule ``RPC.Vote``.\n\n:result.Qualification.ocsp:\n:result.Qualification.tspreg:\n    Kogumisteenuse poolt hangitud täiendavad tõendid valijarakenduse poolt\n    loodud hääle ``SignedVote`` (:ref:`signed-vote`) kvalifitseerimiseks ning\n    korrektseks registreerimiseks. Vastuse koosseis sõltub kogumisteenuse\n    konkreetsest seadistusest, antud juhul kasutatakse standardset OCSP\n    protokolli valija allkirjasertifikaadi kehtivuse kontrolliks ning PKIX\n    ajatempliprotokolli põhist registreerimisteenust nii hääle andmise aja\n    fikseerimiseks kui elektroonilise hääle registreerimiseks välises sõltumatus\n    teenuses.  Valijarakendusele kontrollimiseks edastatakse nii OCSP vastus kui\n    PKIX vormingus ajatempel koos registreerimisteenusele vajalike täiendustega.\n:result.TestVote: Kui hääl esitati enne hääletamise algust ning läks arvesse\n                  proovihäälena, siis ``true``, vastasel juhul seda välja\n                  vastuses ei ole. Valijarakendus kuvab valijale proovihääle\n                  korral sellekohase hoiatuse.\n:result.VoteID: Hääle identifikaator talletusteenuses, mille alusel on\n                kontrollrakendusel võimalik häält hilisemaks analüüsiks välja\n                nõuda.\n\n.. literalinclude:: ../../common/examples/id.rpc.vote.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Vote`` korral.\n\n:BAD_CERTIFICATE: Viga valija isikutuvastus- või allkirjastamissertifikaadiga.\n:BAD_REQUEST: Vigane päring.\n:IDENTITY_MISMATCH: Isikutuvastus- ning allkirjastamissertifikaadi isikukoodid\n                    ei kattu.\n:INELIGIBLE_VOTER: Valijal pole õigust hääletada.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:OUTDATED_CHOICES: Valija ringkonnakuuluvus on nimekirja hankimisest muutunud.\n:UNAUTHENTICATED: Autentimata päring.\n:VOTER_TOO_YOUNG: Valija on liiga noor.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nHääletamine Mobiil-ID'ga\n------------------------\n\nMobiil-ID kasutamine allkirjastamis- ning autentimisvahendina tingib Mobiil-ID\nteenusega liidestuva abiteenuse (SNI ``mid.ivxv.invalid``) kasutamise\nautentimistõendi hankimiseks enne valikute nimekirja hankimist ning hääle\nallkirjastamiseks enne talletamist.\n\n\nAutentimistõendi hankimine\n**************************\n\nValijarakendus teeb päringu ``RPC.Authenticate`` Mobiil-ID autentimise\nalgatamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.IDCode: Mobiil-ID kasutaja isikukood.\n:params.PhoneNo: Mobiil-ID kasutaja telefoninumber.\n\n.. literalinclude:: ../../common/examples/mid.rpc.authenticate.query.json\n   :language: json\n   :linenos:\n\n:result.Challenge: Räsi, millest arvutada Mobiil-ID kontrollkood valijarakenduses\n                   kuvamiseks\n:result.SessionCode: Mobiil-ID seansiidentifikaator edasiste poll-päringute\n                     jaoks\n\n.. literalinclude:: ../../common/examples/mid.rpc.authenticate.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Authenticate`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nValijarakendus teeb päringu ``RPC.AuthenticateStatus`` autentimisprotsessi oleku\nhindamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.SessionCode: Autentimisseansi identifikaator\n\n.. literalinclude:: ../../common/examples/mid.rpc.authenticatestatus.query.json\n   :language: json\n   :linenos:\n\n\n:result.AuthToken: Autentimistõend teistele IVXV teenustele esitamiseks või\n                   ``null``, kui päringu töötlemine alles käib.\n:result.GivenName: Eduka autentimise korral valija eesnimi\n:result.PersonalCode: Eduka autentimise korral valija isikukood\n:result.Status: Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK``\n                viitab edukale autentimisele. Vastuse muud väljad sisaldavad\n                infot vaid siis kui väärtus on ``OK``.\n:result.Surname: Eduka autentimise korral valija perekonnanimi\n\n\n.. literalinclude:: ../../common/examples/mid.rpc.authenticatestatus.response.json\n   :language: json\n   :linenos:\n\n.. literalinclude:: ../../common/examples/mid.rpc.authenticatestatus2.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.AuthenticateStatus`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:MID_BAD_CERTIFICATE: Viga valija Mobiil-ID isikutuvastussertifikaadiga.\n:MID_NOT_USER: Telefoninumber ei kuulu Mobiil-ID kliendile.\n:MID_OPERATOR: Probleem valija mobiiltelefoni SIM kaardiga, mille lahendamiseks\n               tuleb pöörduda mobiilioperaatori poole.\n:MID_ABSENT: Valija mobiiltelefon ei ole kättesaadav.\n:MID_CANCELED: Valija katkestas Mobiil-ID seansi.\n:MID_EXPIRED: Mobiil-ID seanss on aegunud.\n:MID_GENERAL: Viga Mobiil-ID teenuse töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nHääle allkirjastamine\n*********************\n\nValijarakendus teeb päringu ``RPC.GetCertificate`` allkirjastamissertifikaadi\nhankimiseks.\n\n:params.AuthMethod: Toetatud ainult autentimismeetod ``ticket``.\n:params.AuthToken: Mobiil-ID autentimistõend.\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.PhoneNo: Hääle allkirjastaja telefoninumber\n\n.. literalinclude:: ../../common/examples/mid.rpc.getcertificate.query.json\n   :language: json\n   :linenos:\n\n\n:result.Certificate: Allkirjastamissertifikaat X509-vormingus\n\n.. literalinclude:: ../../common/examples/mid.rpc.getcertificate.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.GetCertificate`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:MID_BAD_CERTIFICATE: Viga valija Mobiil-ID allkirjastamissertifikaadiga.\n:MID_GENERAL: Viga Mobiil-ID teenuse töös.\n:MID_NOT_USER: Telefoninumber ei kuulu Mobiil-ID kliendile.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nValijarakendus teeb päringu ``RPC.Sign`` hääle allkirjastamise algatamiseks.\nMobiil-ID kontrollkoodi arvutab valijarakendus andmevälja ``Hash`` väärtusest.\n\n:params.AuthMethod: Toetatud ainult autentimismeetod ``ticket``.\n:params.AuthToken: Mobiil-ID autentimistõend.\n:params.Hash: BASE64-kodeeritud elektroonilise hääle räsi\n:params.HashType: Räsifunktsiooni nimi Mobiil-ID teenusele edastamiseks, kas\n                  ``SHA256``, ``SHA384`` või  ``SHA512``\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.PhoneNo: Hääle allkirjastaja telefoninumber\n\n.. literalinclude:: ../../common/examples/mid.rpc.sign.query.json\n   :language: json\n   :linenos:\n\n:result.SessionCode: Mobiil-ID seansiidentifikaator edasiste poll-päringute\n                     jaoks.\n\n.. literalinclude:: ../../common/examples/mid.rpc.sign.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Sign`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nValijarakendus teeb päringu ``RPC.SignStatus`` allkirjastamisprotsessi seisundi\nhindamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.SessionCode: Mobiil-ID seansiidentifikaator\n\n.. literalinclude:: ../../common/examples/mid.rpc.signstatus.query.json\n   :language: json\n   :linenos:\n\n:result.Signature: Juhul kui vastuse ``Status`` väli on ``OK``, BASE-64 kodeeritud\n                   PKCS1-vormingus signatuur, vastasel juhul ``null``.\n:result.Algorithm: Juhul kui vastuse ``Status`` väli on ``OK``, Mobiil-ID teenuse\n                   poolt tagastatud signatuuri algoritm. Võimalikud väärtused on\n                   ``SHA256WithECEncryption``, ``SHA256WithRSAEncryption``,\n                   ``SHA384WithECEncryption``, ``SHA384WithRSAEncryption``,\n                   ``SHA512WithECEncryption`` ja ``SHA512WithRSAEncryption``.\n:result.Status: Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK``\n                viitab edukale allkirjastamisele. Vastuse muud väljad sisaldavad\n                infot vaid siis kui väärtus on ``OK``.\n\n.. literalinclude:: ../../common/examples/mid.rpc.signstatus.response.json\n   :language: json\n   :linenos:\n\n.. literalinclude:: ../../common/examples/mid.rpc.signstatus2.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.SignStatus`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:MID_ABSENT: Valija mobiiltelefon ei ole kättesaadav.\n:MID_BAD_CERTIFICATE: Viga valija Mobiil-ID allkirjastamissertifikaadiga.\n:MID_OPERATOR: Probleem valija mobiiltelefoni SIM kaardiga, mille lahendamiseks\n               tuleb pöörduda mobiilioperaatori poole.\n:MID_CANCELED: Valija katkestas Mobiil-ID seansi.\n:MID_EXPIRED: Mobiil-ID seanss on aegunud.\n:MID_GENERAL: Viga Mobiil-ID teenuse töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nHääletamine Smart-ID'ga\n------------------------\n\nSmart-ID kasutamine allkirjastamis- ning autentimisvahendina tingib Smart-ID\nteenusega liidestuva abiteenuse (SNI ``smartid.ivxv.invalid``) kasutamise\nautentimistõendi hankimiseks enne valikute nimekirja hankimist ning hääle\nallkirjastamiseks enne talletamist.\n\n\nAutentimistõendi hankimine\n**************************\n\nValijarakendus teeb päringu ``RPC.Challenge`` Smart-ID kontrollkoodi hankimiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.challenge.query.json\n   :language: json\n   :linenos:\n\n:result.Challenge: Räsi, millest arvutada Smart-ID kontrollkood valijarakenduses\n                   kuvamiseks\n:result.XSmartIDAuth: Päringu küpsis, kus talletatakse Smart-ID kontrollkoodi\n                      räsi, selle eluea ajatempel ja seansiidentifikaator\n\n.. literalinclude:: ../../common/examples/smartid.rpc.challenge.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Authenticate`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nValijarakendus teeb päringu ``RPC.Authenticate`` Smart-ID autentimise\nalgatamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.XSmartIDAuth: Päringu küpsis, kus talletatakse Smart-ID kontrollkoodi\n                      räsi, selle eluea ajatempel ja seansiidentifikaator\n:params.Identifier: Smart-ID kasutaja isikukood.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.authenticate.query.json\n   :language: json\n   :linenos:\n\n:result.SessionCode: Smart-ID seansiidentifikaator edasiste poll-päringute\n                     jaoks\n\n.. literalinclude:: ../../common/examples/smartid.rpc.authenticate.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Authenticate`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nValijarakendus teeb päringu ``RPC.AuthenticateStatus`` autentimisprotsessi oleku\nhindamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.XSmartIDAuth: Päringu küpsis, kus talletatakse Smart-ID kontrollkoodi\n                      räsi, selle eluea ajatempel ja seansiidentifikaator\n:params.SessionCode: Autentimisseansi identifikaator\n\n.. literalinclude:: ../../common/examples/smartid.rpc.authenticatestatus.query.json\n   :language: json\n   :linenos:\n\n\n:result.AuthToken: Autentimistõend teistele IVXV teenustele esitamiseks või\n                   ``null``, kui päringu töötlemine alles käib.\n:result.DataToken: Hääletaja Smart-ID dokumendi number või\n                   ``null``, kui päringu töötlemine alles käib.\n:result.GivenName: Eduka autentimise korral valija eesnimi\n:result.PersonalCode: Eduka autentimise korral valija isikukood\n:result.Status: Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK``\n                viitab edukale autentimisele. Vastuse muud väljad sisaldavad\n                infot vaid siis kui väärtus on ``OK``.\n:result.Surname: Eduka autentimise korral valija perekonnanimi\n\n\n.. literalinclude:: ../../common/examples/smartid.rpc.authenticatestatus.response.json\n   :language: json\n   :linenos:\n\n.. literalinclude:: ../../common/examples/smartid.rpc.authenticatestatus2.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.AuthenticateStatus`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:SMARTID_BAD_CERTIFICATE: Viga valija Smart-ID isikutuvastussertifikaadiga.\n:SMARTID_VERIFICATION: Valija valis vale verifitseerimiskoodi.\n:SMARTID_ACCOUNT: Viga valija Smart-ID kontos.\n:SMARTID_CANCELED: Valija katkestas Smart-ID seansi.\n:SMARTID_EXPIRED: Smart-ID seanss on aegunud.\n:SMARTID_GENERAL: Viga Smart-ID teenuse töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nHääle allkirjastamine\n*********************\n\nValijarakendus teeb päringu ``RPC.GetCertificateChoice`` allkirjastamissertifikaadi\nvalikuks.\n\n:params.AuthMethod: Toetatud ainult autentimismeetod ``ticket``.\n:params.AuthToken: Smart-ID autentimistõend.\n:params.DataToken: Hääletaja Smart-ID dokumendi number.\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.getcertificatechoice.query.json\n   :language: json\n   :linenos:\n\n:result.SessionCode: Smart-ID seansiidentifikaator edasiste poll-päringute\n                     jaoks\n\n.. literalinclude:: ../../common/examples/smartid.rpc.getcertificatechoice.response.json\n   :language: json\n   :linenos:\n\nValijarakendus teeb päringu ``RPC.GetCertificateChoiceStatus`` allkirjastamissertifikaadi\noleku hindamiseks.\n\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.SessionCode: Autentimisseansi identifikaator\n\n.. literalinclude:: ../../common/examples/smartid.rpc.getcertificatechoicestatus.query.json\n   :language: json\n   :linenos:\n\n:result.Certificate: Allkirjastamissertifikaat X509-vormingus\n:result.Status: Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK``\n                viitab edukale autentimisele. Vastuse muud väljad sisaldavad\n                infot vaid siis kui väärtus on ``OK``.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.getcertificatechoicestatus.response.json\n   :language: json\n   :linenos:\n\n.. literalinclude:: ../../common/examples/smartid.rpc.getcertificatechoicestatus2.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.GetCertificateChoiceStatus`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:SMARTID_BAD_CERTIFICATE: Viga valija Smart-ID allkirjastamissertifikaadiga.\n:SMARTID_GENERAL: Viga Smart-ID teenuse töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nValijarakendus teeb päringu ``RPC.Sign`` hääle allkirjastamise algatamiseks.\nSmart-ID kontrollkoodi arvutab valijarakendus andmevälja ``Hash`` väärtusest.\n\n:params.AuthMethod: Toetatud ainult autentimismeetod ``ticket``.\n:params.AuthToken: Smart-ID autentimistõend.\n:params.Hash: BASE64-kodeeritud elektroonilise hääle räsi\n:params.HashType: Räsifunktsiooni nimi Smart-ID teenusele edastamiseks, kas\n                  ``SHA256``, ``SHA384`` või  ``SHA512``\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.DataToken: Hääletaja Smart-ID dokumendi number.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.sign.query.json\n   :language: json\n   :linenos:\n\n:result.SessionCode: Smart-ID seansiidentifikaator edasiste poll-päringute\n                     jaoks.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.sign.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Sign`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nValijarakendus teeb päringu ``RPC.SignStatus`` allkirjastamisprotsessi seisundi\nhindamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.SessionCode: Smart-ID seansiidentifikaator\n\n.. literalinclude:: ../../common/examples/smartid.rpc.signstatus.query.json\n   :language: json\n   :linenos:\n\n:result.Signature: Juhul kui vastuse ``Status`` väli on ``OK``, BASE-64 kodeeritud\n                    signatuur, vastasel juhul ``null``.\n:result.Algorithm: Juhul kui vastuse ``Status`` väli on ``OK``, Smart-ID teenuse\n                   poolt tagastatud signatuuri algoritm. Võimalikud väärtused on\n                   ``sha256WithRSAEncryption``, ``sha384WithRSAEncryption``,\n                   ja ``sha512WithRSAEncryption``.\n:result.Status: Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK``\n                viitab edukale allkirjastamisele. Vastuse muud väljad sisaldavad\n                infot vaid siis kui väärtus on ``OK``.\n\n.. literalinclude:: ../../common/examples/smartid.rpc.signstatus.response.json\n   :language: json\n   :linenos:\n\n.. literalinclude:: ../../common/examples/smartid.rpc.signstatus2.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.SignStatus`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:SMARTID_BAD_CERTIFICATE: Viga valija Smart-ID isikutuvastussertifikaadiga.\n:SMARTID_VERIFICATION: Valija valis vale verifitseerimiskoodi.\n:SMARTID_ACCOUNT: Viga valija Smart-ID kontos.\n:SMARTID_CANCELED: Valija katkestas Smart-ID seansi.\n:SMARTID_EXPIRED: Smart-ID seanss on aegunud.\n:SMARTID_GENERAL: Viga Smart-ID teenuse töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nHääletamine Web eID'ga\n------------------------\n\nWeb eID kasutamine autentimisvahendina tingib Web eID\nteenusega liidestuva abiteenuse (SNI ``webeid.ivxv.invalid``) kasutamise\nautentimistõendi hankimiseks enne valikute nimekirja hankimist.\n\n\nAutentimistõendi hankimine\n**************************\n\nValijarakendus teeb päringu ``RPC.Challenge`` Web eID autentimise\nalgatamiseks.\n\n:params.OS: Operatsioonisüsteem, millel valijarakendust kasutatakse.\n\n.. literalinclude:: ../../common/examples/webeid.rpc.challenge.query.json\n   :language: json\n   :linenos:\n\n:result.Challenge: Base64 kodeeritud räsi, mille dekodeeritud väärtust peab\n                   valijarakendus kasutama autentimistõendi allkirja loomiseks.\n:params.SessionID: Seansiidentifikaator.\n:params.Bearer:    Küpsis, mida server kasutab räsi verifitseerimiseks.\n\n.. literalinclude:: ../../common/examples/webeid.rpc.challenge.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Challenge`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nValijarakendus teeb päringu ``RPC.Token`` autentimistõendi valideerimiseks.\n\n:params.OS:        Operatsioonisüsteem, millel valijarakendust kasutatakse.\n:params.SessionID: Seansiidentifikaator.\n:params.Token:     Web eID autentimistõend.\n:params.Bearer:    Küpsis, mida server kasutab räsi verifitseerimiseks.\n\n.. literalinclude:: ../../common/examples/webeid.rpc.token.query.json\n   :language: json\n   :linenos:\n\n:result.AuthToken: Autentimistõend teistele IVXV teenustele esitamiseks\n:result.GivenName: Eduka autentimise korral valija eesnimi\n:result.PersonalCode: Eduka autentimise korral valija isikukood\n:result.Status: Päringu staatus - ``OK`` viitab edukale autentimisele.\n                Vastuse muud väljad sisaldavad infot vaid siis kui\n                väärtus on ``OK``.\n:result.Surname: Eduka autentimise korral valija perekonnanimi\n\n\n.. literalinclude:: ../../common/examples/webeid.rpc.token.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Token`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:BAD_CERTIFICATE: Viga valija Web eID isikutuvastussertifikaadiga.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nHääle kontrollimine\n-------------------\n\nKontrollrakendus teeb päringu ``RPC.Verify`` allkirjastatud hääle ning häält\nkvalifitseerivate tõendite allalaadimiseks kogumisteenusest.\n\n:params.OS: Operatsioonisüsteem, millel kontrollrakendust kasutatakse.\n:params.VoteID: QR-koodi vahendusel valijarakendusest saadud hääle\n                identifikaator talletusteenuses.\n\n.. literalinclude:: ../../common/examples/ver.rpc.verify.query.json\n   :language: json\n   :linenos:\n\n:result.Qualification.ocsp:\n:result.Qualification.tspreg:\n    Vaata peatükki hääle verifitseerimisest\n\n\n:result.Type: Allkirjastatud hääle vorming. Hetkel on ainus toetatud väärtus\n              ``bdoc``.\n:result.Vote: BASE64-kodeeritud hääl ``SignedVote`` eelpoolmääratud vormingus\n              (:ref:`signed-vote`).\n:result.ChoicesList: JSON-vormingus ringkonnapõhine valikute nimekiri.\n\n.. literalinclude:: ../../common/examples/ver.rpc.verify.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Verify`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\n\nE-hääletamise jooksev nimekiri\n------------------------------\n\nX-tee teenusega(xroad-service) liidestuva abiteenuse (SNI ``votesorder.ivxv.invalid``) kasutatakse\ninformatsiooni edastamiseks X-tee turvaserverile.\n\n\nViimane järjenumber\n*******************\nX-tee teenus(xroad-service) teeb päringu ``RPC.VotesSeqNo`` viimase järjenumbri saamiseks.\n\n.. literalinclude:: ../../common/examples/votesorder.rpc.votesseqno.query.json\n   :language: json\n   :linenos:\n\n:result.SeqNo:\n    Viimane järjenumber.\n\n.. literalinclude:: ../../common/examples/votesorder.rpc.votesseqno.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.VotesSeqNo`` korral.\n\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n\nE-hääletamiste pakk\n*******************\nX-tee teenus(xroad-service) teeb päringu ``RPC.Votes`` e-hääletamise paki saamiseks.\n\n:params.VotesFrom: E-hääled alates sellest järjenumbrist.\n:params.BatchMaxSize: E-hääletamise paki suurus.\n\n.. literalinclude:: ../../common/examples/votesorder.rpc.votes.query.json\n   :language: json\n   :linenos:\n\n:result.batchRecords:\n         E-häälte loend\n:result.batchRecords.seqNo:\n         Hääle järjenumber\n:result.batchRecords.idCode:\n         Hääletaja isikukood\n:result.batchRecords.voterName:\n         Hääletaja nimi\n:result.batchRecords.kovCode:\n         KOV EHAK kood\n:result.batchRecords.electoralDistrictNo:\n         Valimisringkonna number\n\n.. literalinclude:: ../../common/examples/votesorder.rpc.votes.response.json\n   :language: json\n   :linenos:\n\nVõimalikud veateated päringu ``RPC.Votes`` korral.\n\n:BAD_REQUEST: Vigane päring.\n:INTERNAL_SERVER_ERROR: Viga serveri sisemises töös.\n:VOTING_END: Hääletusperiood on lõppenud.\n"
  },
  {
    "path": "Documentation/public/protokollid/09-tootlemine.rst",
    "content": "..  IVXV protokollid\n\n==============================================\nE-valimiskasti töötlemisetapi andmestruktuurid\n==============================================\n\n\nTühistus- ja ennistusnimekiri\n-----------------------------\n\nTühistus- ja ennistusnimekiri sisaldab andmeid isikute kohta, kelle e-hääl tuleb tühistada (ei lähe arvesse valimistulemuste kokkulugemisel) või ennistada (s.t. tühistatakse eelnev tühistamine ning häälte uuesti üle lugemisel võetakse ennistatud e-hääl arvesse). Nimekiri laaditakse süsteemi digitaalselt allkirjastatud dokumendina, mille andmefaili vorming on järgmine:\n\n.. literalinclude:: ../../common/schema/ivxv.revoke.schema\n   :language: json\n   :linenos:\n\nNäide:\n\n.. literalinclude:: ../../common/schema/ivxv.revoke.schema.example\n   :language: json\n\n\nE-hääletanute nimekiri\n----------------------\n\nE-hääletanute nimekiri on pärast e-hääletamise lõppu väljastatav nimekiri\ne-hääletanud isikutest, sordituna valimisjaoskondade kaupa. Dokument\ngenereeritakse töötlemisrakenduse poolt.\n\n.. literalinclude:: ../../common/schema/ivxv.voterlist.schema\n   :language: json\n   :linenos:\n\n\nNäide:\n\n.. literalinclude:: ../../common/schema/ivxv.voterlist.schema.example\n   :language: json\n\n\nHääletamistulemus\n-----------------\n\nVõtmerakenduse poolt dekrüpteeritud ning summeeritud hääled jagatud\nvalimisringkondade ja jaoskondade kaupa.\n\nHääletamistulemuste failis peavad iga jaoskonna kohta olema järgmised andmed.\n\n#. Rikutud ja kehtetute häälte arvu näitav kirje. Seda ka juhul, kui\n   valimisjaoskonnas polnud ühtki rikutud või kehtetut häält: sellisel juhul on\n   häälte arv null.\n\n#. Iga valiku poolt antud häälte arvu näitav kirje. Seda ka juhul, kui\n   valimisjaoskonnas ei antud selle valiku poolt ühtki häält: sellisel juhul on\n   häälte arv null.\n\n.. literalinclude:: ../../common/schema/ivxv.result.schema\n   :language: json\n   :linenos:\n\nNäide:\n\n.. literalinclude:: ../../common/schema/ivxv.result.schema.example\n   :language: json\n\n\n\nE-valimiskast\n-------------\n\nFail sisaldab kogumisteenuse poolt vastu võetud hääli koos häälte juurde\nkuuluvate andmetega.\n\nFaili vorming on Zip64 konteiner.\n\nValija-spetsiifilised kaustad asuvad vahetult juurkausta `votes` all.\n\nFaili sisu:\n\n* :file:`votes/<voter id>/`\n\n * :file:`<timestamp>.version`\n\n * :file:`<timestamp>.<vote type>`\n\n * :file:`<timestamp>.<qualifier>*`\n\nkus:\n\n* ``<voter id>`` on valija identifikaator, Eesti puhul isikukood;\n\n* ``<timestamp>`` on hääle esitamise kellaaeg vormingus\n  ``yyyymmddhhmmssmmm±zzzz``;\n\n  * see kellaaeg kajastab hetke, mil päring kogumisteenusesse tehti, ja on\n    antud lihtsalt valimiskasti inimloetavuse parandamiseks; hääle tegelik ajamärk või\n    -tempel on mõne kvalifitseeriva vastuse sees;\n\n* ``<vote type>`` on valikute konteineri tüüp, Eesti puhul BDOC;\n\n  * kusjuures BDOC ise on lihtsalt põhiprofiiliga ja ei sisalda\n    kvalifitseerivad parameetreid (kehtivuskinnitusi, ajamärgendeid,\n    ajatempleid),\n\n* ``<qualifier>`` on häält kvalifitseeriva protokolli tüüp, millest hetkel\n  võimalikud on:\n\n   * ``ocsp`` - *Online Certificate Status Protocol* (kehtivuskinnitus, `RFC\n     6960 <https://tools.ietf.org/html/rfc6960>`_) kinnitab valija\n     allkirjastamissertifikaadi kehtivust hääle andmise hetkel,\n\n   * ``tsp`` - *Time-Stamp Protocol* (ajatempel, `RFC 3161\n     <https://tools.ietf.org/html/rfc3161>`_) kinnitab, et päringu tegemise\n     hetkeks oli hääl olemas,\n\n   * ``tspreg`` - sama, mis ``tsp``, aga nonsiks pannakse kogumisteenuse\n     allkiri päringu ``MessageImprint`` elemendil, et häält registreerida.\n\n* Iga hääle kohta esinevad failid on:\n\n * ``<timestamp>.version`` - hääle andmise ajal kehtinud valijate nimekirja\n   versioon;\n\n * ``<timestamp>.<vote type>`` - valikute konteiner, mille sees on valiku\n   identifikaator kujul ``<valimise id>.<küsimuse id>.ballot``. Eesti puhul\n   BDOC-konteineris olev vastava nimega fail;\n\n * ``<timestamp>.<qualifier>`` - häält kvalifitseeriva protokolli päringu\n   vastus; neid võib esineda mitu, aga iga protokolli kohta maksimaalselt\n   üks.\n\n\nAnonüümistatud e-valimiskast\n----------------------------\n\nValimisringkondade ja jaoskondade järgi grupeeritud krüpteeritud hääled.\nAnonüümistatud e-valimiskastis puudub informatsioon valijate kohta.\n\nAnonüümistatud e-valimiskast on töötlemisrakenduse väljund ning võtmerakenduse\ndekrüpteerimise tööriista sisend.\n\n.. literalinclude:: ../../common/schema/ivxv.anon-bb.schema\n   :language: json\n   :linenos:\n\nNäide:\n\n.. literalinclude:: ../../common/schema/ivxv.anon-bb.schema.example\n   :language: json\n"
  },
  {
    "path": "Documentation/public/protokollid/11-audit.rst",
    "content": "..  IVXV protokollid\n\n========================\nHääletamistulemuse audit\n========================\n\nMiksimistõendi kontroll\n=======================\n\nMiksimistõendi kontrollimiseks kasutatakse algoritmi nagu on defineeritud\n`Verificatumi verifitseerija implementeerimise manuaalis\n<https://www.verificatum.org/files/vmnv-3.0.3.pdf>`_.\n\nMärgime, et miksimistõendi koostamisel lisatakse krüptogrammile andmed valimiste,\nringkonna, jaoskonna ja küsimuse identifikaatori kohta.  Lisamiseks kodeeritakse\nvastav väli rühma elemendina, kasutades pimendamiseks juhuslikkust 0. Näitena,\nkui esialgu on krüptogramm :math:`c_0 = (c_{00}, c_{01})`, kasutades avalikku\nvõtit :math:`pk = (g, y)`, siis Verificatumi sisendina kasutatakse laia\nkrüptogrammi :math:`C = (c_{id}, c_d, c_s, c_q, c_0)`, kus:\n\n* valimiste identifikaatori pseudokrüptogramm on antud kujul :math:`c_{id} = (1,\n  encode(id))`, kus funktsioon :math:`encode` kodeerib sõne vastava rühma\n  elemendina ja `id` on valimiste identifikaatori sõne.\n* ringkonna identifikaatori pseudokrüptogramm on antud kujul :math:`c_d = (1,\n  encode(d))`, kus `d` on ringkonna identifikaatori sõne.\n* jaoskonna identifikaatori pseudokrüptogramm on antud kujul :math:`c_s = (1,\n  encode(s))`, kus `s` on jaoskonna identifikaatori sõne.\n* küsimuse identifikaatori pseudokrüptogramm on antud kujul :math:`c_q = (1,\n  encode(q))`, kus `q` on küsimuse identifikaatori sõne.\n\nSellisel juhul defineeritakse laia krüptogrammile vastava avaliku võtmena\n:math:`((g,1), (g,1), (g,1), (g,1), (g,y))`.\n\nKorrektse dekrüpteerimise tõendi kontroll\n=========================================\n\nOlgu antud krüptogramm :math:`c = (c_0, c_1)`, mis dekrüpteeritakse väärtuseks\n:math:`d` antud avaliku võtmega :math:`pk` üle parameetrite :math:`(p,g)` ja\ndekrüpteerimistõendiga :math:`(a,b,s)`.\n\nKorrektse dekrüpteerimise kontrollimise jaoks on tarvis arvutada\nmitte-interaktiivne kontrollija väljakutse. Selle jaoks kodeeritakse\n:math:`\"DECRYPTION\" || pk || c || d || a || b` DER-kodeeringus. Baidijada\nkasutatakse deterministliku juhuarvugeneraatori initsialiseerimiseks ja selle\nväljundist loetakse rühma järgu pikkune täisarv :math:`k`.\n\nDekrüpteerimistõendi kontrolliks tuleb veenduda, et :math:`c_0^s\n= a * (c_1/d)^k` ja :math:`g^s = b * y^k`.\n\nKorrektse teisendamise kontroll\n===============================\n\nKontrollimaks, et teisendus IVXV e-valimiskasti ja Verificatumi krüptogrammide vahel on\ntehtud korrektselt, tuleb korrata teisendust sõltumatult. Pärast sõltumatut\nteisendust tuleb võrrelda saadud väljundeid. Kuna teisendamine on deterministlik\nprotseduur, siis garanteerib kordamine tegevuse õigsuse.\n"
  },
  {
    "path": "Documentation/public/protokollid/12-lisad.rst",
    "content": "..  IVXV protokollid\n\n=====\nLisad\n=====\n\nAndmestruktuuride ASN.1 spetsifikatsioon\n========================================\n\n.. literalinclude:: ivxv-elgamal-general.asn1\n   :language: asn1\n   :name: asn-general\n   :linenos:\n   :caption: IVXV ElGamal üldised andmestruktuurid\n\n.. literalinclude:: ivxv-elgamal-modp.asn1\n   :language: asn1\n   :name: asn-modp\n   :linenos:\n   :caption: IVXV ElGamal ModP spetsiifilised andmestruktuurid\n\n.. literalinclude:: ivxv-elgamal-ecc.asn1\n   :language: asn1\n   :name: asn-ecc\n   :linenos:\n   :caption: IVXV ElGamal ECC spetsiifilised andmestruktuurid\n\n\n\nElektroonilise hääle vormingu spetsifikatsioon\n==============================================\n\nKäesoleva protokolliversiooniga kooskõlaline ettepanek VVK otsuseks\nhääletamissedeli ja elektroonilise hääle vormi kehtestamiseks.\n\n.. literalinclude:: paragraph.txt\n\n\nVead töötlemisprotsessis\n========================\n\nAnname ülevaate veakoodidest töötlemisprotsessis. Veakoodid kirjeldavad vigu\nüksiku elemendi verifitseerimisel - hääl, registreerimistõend,\nregistreerimispäring - ning häälte, registreerimistõendite ja\nregistreerimispäringute vahelise vastavuse loomisel.\n\nEnamus veaolukordi on pigem hüpoteetilised (nt. `REG_NO_NONCE`), siiski on\ntegemist olukordadega, mis programselt võivad esineda ning seetõttu tuleb neid\nka käsitleda.\n\nVeaolukord tähendab, et konkreetne element ei liigu edasi töötlemise järgmisesse faasi.\n\n.. list-table:: Töötlemisprotsessi veakoodid\n   :widths: 40 60\n   :header-rows: 1\n\n   * - Veakood\n     - Selgitus\n   * - ``INVALID_FILE_NAME``\n     - Failinimi ei vasta oodatud mustrile. Viitab tundmatule failile urnis.\n   * - ``MISSING_FILE``\n     - Mõni hääle koosseisuks vajalik fail puudub. Nt. ajatempel.\n   * - ``REPEATED_FILE``\n     - Mõnda hääle koosseisuks vajalikku faili on mitu. Nt. kehtivuskinnitus.\n   * - ``UNKNOWN_FILE_TYPE``\n     - Faili tüüp on tundmatu või mittetoetatud.\n   * - ``INVALID_FILE_SIZE``\n     - Allkirjastatud hääle faili suurus ei vasta seadistustes nõutud kriteeriumitele.\n   * - ``INVALID_BALLOT_SIGNATURE``\n     - Hääl on vigase allkirjaga.\n   * - ``MISSING_VOTER_SIGNATURE``\n     - Hääl ei sisalda valija allkirja.\n   * - ``VOTER_NOT_FOUND``\n     - Valija ei olnud hääletamise hetkel valijate nimekirjas.\n   * - ``VOTERLIST_NOT_FOUND``\n     - Versioonile vastavat valijate nimekirja ei leitud.\n   * - ``TIME_BEFORE_START``\n     - Hääl on antud enne hääletamisperioodi algust. Viitab testhäälele.\n   * - ``REG_RESP_INVALID``\n     - Registreerimistõend/ajatempel on vigane. Viga ATOs või töötlejas.\n   * - ``REG_REQ_INVALID``\n     - Registreerimispäring on vigane. Viga kogumisteenuses või töötlejas.\n   * - ``REG_RESP_NOT_UNIQUE``\n     - Registreerimistõend ei ole unikaalne. Viga kogumisteenuses või töötlejas.\n   * - ``REG_REQ_NOT_UNIQUE``\n     - Registreerimispäring ei ole unikaalne. Sama räsiga häält on esitatud korduvalt.\n   * - ``REG_NO_NONCE``\n     - Registreerimistõendis puudub nonss.\n   * - ``REG_NONCE_NOT_SIG``\n     - Esitatud nonss ei ole IVXV protokolli kohaselt allkirjastatud.\n   * - ``REG_NONCE_ALG_MISMATCH``\n     - Nonssi allkirjastamisel kasutatud algoritm ei vasta oodatule.\n   * - ``REG_NONCE_SIG_INVALID``\n     - Nonssi allkiri on vigane.\n   * - ``UNKNOWN_FILE_IN_VOTE_CONTAINER``\n     - Hääle konteineris on tundmatu fail.\n   * - ``TECHNICAL_ERROR``\n     - Töötlemise ajal tekkis tehniline viga.\n   * - ``REG_RESP_REQ_UNMATCH``\n     - Registreerimistõendi andmed ei vasta registreerimispäringu andmetele.\n   * - ``REG_REQ_WITHOUT_BALLOT``\n     - Esitatud on registreerimispäring, kuid hääl puudub urnist. Kogumisteenuse viga.\n   * - ``BALLOT_WITHOUT_REG_REQ``\n     - Hääl on esitatud ilma vastava registreerimispäringuta. ATO viga.\n   * - ``SAME_TIME_AS_LATEST``\n     - Kaks häält samalt valijalt võivad olla arvesse võetavad viimasena.\n   * - ``INVALID_SIGNATURE_PROFILE``\n     - Hääle allkirjaprofiil on vigane.\n\n\nTöötlemise käigus tuvastatakse kehtetud sedelid sellisel määral, mil seda\nvõimaldavad kontrollid avaliku võtmega. Kuigi kogumisteenus ei luba kehtetuid\nsedeleid talletada, peab töötlemisrakendus kontrolle siiski kordama tagamaks\nmh. võimalike tarkvaravigade vältimise.\n\n\n.. list-table:: Krüptogrammide kehtivuse kontrolli veakoodid\n   :widths: 30 70\n   :header-rows: 1\n\n   * - Veakood\n     - Selgitus\n   * - ``INVALID_BYTES``\n     - Baidijada ei ole dekodeeritav ElGamalCiphertext'na\n   * - ``INVALID_GROUP``\n     - Väärtus ei ole oodatud rühma element\n   * - ``INVALID_RANGE``\n     - Väärtus on lubatud vahemikust väljas.\n   * - ``INVALID_QR``\n     - Väärtus ei ole ruutjääk (MODP).\n   * - ``INVALID_POINT``\n     - Väärtus ei ole kõvera punkt (ECC).\n   * - ``INVALID``\n     - Vigane šifreeritud tekst.\n\n"
  },
  {
    "path": "Documentation/public/protokollid/Makefile",
    "content": "include ../../common.mk\n"
  },
  {
    "path": "Documentation/public/protokollid/index.rst",
    "content": "..  IVXV seadistuste koostamise juhend\n\n========================================================\nIVXV protokollid\n========================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n.. toctree::\n   :maxdepth: 4\n\n   01-annotatsioon\n   02-ylevaade\n   04-seadistus\n   05-ehaal\n   06-talletamine\n   06a-regteenus\n   07-kontrollimine\n   08-haaletamine\n   09-tootlemine\n   11-audit\n   12-lisad\n"
  },
  {
    "path": "Documentation/public/protokollid/ivxv-elgamal-ecc.asn1",
    "content": "id-ivxv-ecc-elgamal OBJECT IDENTIFIER ::= { iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 99999 1 }\n\nIVXVECCElGamalParameters ::= SEQUENCE {\n    curve       GeneralString,\n    electionId  GeneralString\n}\n\nIVXVECCElGamalPublicKey ::= SEQUENCE {\n    pubY  OCTET STRING\n}\n\nIVXVECCElGamalCiphertext ::= SEQUENCE {\n    uBlind           OCTET STRING,\n    vBlindedMessage  OCTET STRING\n}\n\nIVXVECCElGamalDecryptionProof ::= SEQUENCE {\n    aMsgCommitment  OCTET STRING,\n    bKeyCommitment  OCTET STRING,\n    sResponse       INTEGER\n}\n\nIVXVECCElGamalDecryptionChallenge ::= SEQUENCE {\n    niProofDomain     GeneralString,\n    publicKey         SubjectPublicKeyInfo,\n    ciphertextInfo    IVXVElGamalCiphertextInfo,\n    encodedPlaintext  OCTET STRING,\n    aMsgCommitment    OCTET STRING,\n    bKeyCommitment    OCTET STRING\n}\n"
  },
  {
    "path": "Documentation/public/protokollid/ivxv-elgamal-general.asn1",
    "content": "IVXVElGamalCiphertextInfo ::= SEQUENCE {\n    algorithm   AlgorithmIdentifier,\n    ciphertext  ANY DEFINED BY algorithm\n}\n"
  },
  {
    "path": "Documentation/public/protokollid/ivxv-elgamal-modp.asn1",
    "content": "id-ivxv-modp-elgamal OBJECT IDENTIFIER ::= { iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) cryptlib(3029) mechanism(2) 1 }\n\nIVXVModPElGamalParameters ::= SEQUENCE {\n    modP        INTEGER,\n    gen         INTEGER,\n    electionId  GeneralString\n}\n\nIVXVModPElGamalPublicKey ::= SEQUENCE {\n    pubY  INTEGER\n}\n\nIVXVModPElGamalCiphertext ::= SEQUENCE {\n    uBlind           INTEGER,\n    vBlindedMessage  INTEGER\n}\n\nIVXVModPElGamalDecryptionProof ::= SEQUENCE {\n    aMsgCommitment  INTEGER,\n    bKeyCommitment  INTEGER,\n    sResponse       INTEGER\n}\n\nIVXVModPElGamalDecryptionChallenge ::= SEQUENCE {\n    niProofDomain     GeneralString,\n    publicKey         SubjectPublicKeyInfo,\n    ciphertextInfo    IVXVElGamalCiphertextInfo,\n    encodedPlaintext  INTEGER,\n    aMsgCommitment    INTEGER,\n    bKeyCommitment    INTEGER\n}\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/01-annotatsioon.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-02-29 16:35+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../01-annotatsioon.rst:6\nmsgid \"Annotatsioon\"\nmsgstr \"Annotation\"\n\n#: ../../01-annotatsioon.rst:8\nmsgid \"\"\n\"Käesolev dokument kirjeldab elektroonilise hääletamise infosüsteemi IVXV \"\n\"protokollistikku.\"\nmsgstr \"\"\n\"This document specifies the protocols of the online voting system IVXV.\"\n\n#: ../../01-annotatsioon.rst:11\nmsgid \"\"\n\"Dokument annab üldise ülevaate elektroonilise hääletamise süsteemi \"\n\"tehnilisest ülesehitusest ja kasutatavatest protokollidest. Dokumendis \"\n\"defineeritakse protokollides kasutatavad ühised mõisted ja andmestruktuurid.\"\nmsgstr \"\"\n\"The document gives a general overview of the technical architecture of the \"\n\"online voting system and the protocols used. It defines common concepts and \"\n\"data structures used in the protocols.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/02-ylevaade.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 08:56+0000\\n\"\n\"PO-Revision-Date: 2024-02-29 17:09+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../02-ylevaade.rst:5\nmsgid \"Ülevaade\"\nmsgstr \"Overview\"\n\n#: ../../02-ylevaade.rst:7\nmsgid \"\"\n\"Elektroonilise hääletamise protokollistik (edaspidi protokollistik) \"\n\"defineerib elektroonilise hääletamise süsteemi komponentide vahelise \"\n\"sõnumivahetuse, kasutatavad andmestruktuurid, algoritmid ning liidesed \"\n\"väliste süsteemidega. Sõnumivahetus esitatakse UML suhtlusskeemidena, mis \"\n\"üheselt defineerivad sõnumite järgnevuse. Andmestruktuuride kirjeldused on \"\n\"varustatud BNF, ASN.1 või JSON-schema notatsioonis spetsifikatsioonidega. \"\n\"Algoritmid esitatakse pseudokoodina.\"\nmsgstr \"\"\n\"The online voting protocol suite (hereafter referred to as the protocol \"\n\"suite) defines the messaging between the components of an online voting \"\n\"system, the data structures used, the algorithms, and the interfaces to \"\n\"external systems. The message exchange is represented as UML interaction \"\n\"diagrams that unambiguously define the sequence of messages. Data structure \"\n\"descriptions are provided with specifications in Backus-Naur or JSON schema \"\n\"notation. Algorithms shall be presented in pseudo-code form.\"\n\n#: ../../02-ylevaade.rst:15\nmsgid \"\"\n\"NB! Kõigis protokollistiku andmestruktuuride väljades tuleb rangelt kinni \"\n\"pidada lubatud märkidest ning väljade minimaalsetest ja maksimaalsetest \"\n\"pikkustest. Täiendavate tühikute, tabulaatorite jms. kasutamine on keelatud \"\n\"ning spetsifikatsiooni realiseerivad rakendused peavad vorminguga mitte-\"\n\"vastavate andmete töötlemisest keelduma.\"\nmsgstr \"\"\n\"NOTE: The allowed characters and the minimum and maximum lengths of the \"\n\"fields must be strictly respected in all fields of the protocol data \"\n\"structures. The use of additional spaces, tabs, etc. is prohibited, and \"\n\"applications implementing the specification must refuse to process data that\"\n\" does not conform to the format.\"\n\n#: ../../02-ylevaade.rst:21\nmsgid \"\"\n\"Protokollistik defineerib elektroonilise hääletamise protokolli ning selle \"\n\"protokolli realiseerimiseks vajalikud tugistruktuurid.\"\nmsgstr \"\"\n\"The protocol suite defines the protocol for online voting and the support \"\n\"structures needed to implement this protocol.\"\n\n#: ../../02-ylevaade.rst:25\nmsgid \"Elektroonilise hääletamise protokoll\"\nmsgstr \"Online Voting Protocol\"\n\n#: ../../02-ylevaade.rst:27\nmsgid \"Elektroonilise hääletamise protokoll spetsifitseerib:\"\nmsgstr \"The online voting protocol specifies:\"\n\n#: ../../02-ylevaade.rst:29\nmsgid \"\"\n\"elektroonilise hääle vormingu, mis võimaldab üheselt määratleda valija tahte\"\n\" konkreetsel valimisel;\"\nmsgstr \"\"\n\"the electronic ballot format that allows defining the voter’s intent \"\n\"unambiguously at a specific election;\"\n\n#: ../../02-ylevaade.rst:32\nmsgid \"elektroonilise hääle krüpteerimise hääle salajasuse tagamiseks;\"\nmsgstr \"electronic ballot encryption to ensure ballot secrecy;\"\n\n#: ../../02-ylevaade.rst:34\nmsgid \"\"\n\"elektroonilise hääle digitaalse allkirjastamise tervikluse ja valija \"\n\"identifitseerimise tagamiseks;\"\nmsgstr \"\"\n\"the digital signing of an electronic vote to ensure integrity and voter \"\n\"identification;\"\n\n#: ../../02-ylevaade.rst:37\nmsgid \"\"\n\"elektroonilise hääle kvalifitseerimise kogumisteenuse poolt, hääle \"\n\"vastuvõtmise tähistamiseks;\"\nmsgstr \"\"\n\"electronic vote qualification by the collection service, to mark the \"\n\"acceptance of a vote;\"\n\n#: ../../02-ylevaade.rst:40\nmsgid \"\"\n\"Protokoll eeldab, et valimise korraldaja defineerib valimise ning genereerib\"\n\" häälte salastamise võtmepaari, mille avalik komponent tehakse \"\n\"valijarakendusele kättesaadavaks.\"\nmsgstr \"\"\n\"The protocol assumes that the election organizer has defined the election \"\n\"and generated a key pair for vote encryption, the public component of which \"\n\"has been made available to the voting application.\"\n\n#: ../../02-ylevaade.rst:44\nmsgid \"\"\n\"Protokolli vahendusel liigub valija tahe kogumisteenuses talletatavasse \"\n\"e-valimiskasti ning võetakse tulemuse kujunemisel arvesse järgmist \"\n\"sündmusterida pidi:\"\nmsgstr \"\"\n\"Through the protocol, the voter's intent will be transferred to the digital \"\n\"ballot box stored in the collection service and will be taken into account \"\n\"in the following sequence of events:\"\n\n#: ../../02-ylevaade.rst:47\nmsgid \"\"\n\"Valija kasutab valijarakendust oma tahteavalduse elektrooniliseks \"\n\"vormistamiseks:\"\nmsgstr \"The voter uses the voting application to vote online:\"\n\n#: ../../02-ylevaade.rst:50\nmsgid \"tahteavaldus vormistatakse elektroonilise häälena;\"\nmsgstr \"voter's intent is encoded as an electronic ballot;\"\n\n#: ../../02-ylevaade.rst:52\nmsgid \"vormistatud hääl krüpteeritakse;\"\nmsgstr \"the electronic ballot is encrypted;\"\n\n#: ../../02-ylevaade.rst:54\nmsgid \"krüpteeritud hääl signeeritakse valija arvutis.\"\nmsgstr \"the encrypted ballot is digitally signed.\"\n\n#: ../../02-ylevaade.rst:56\nmsgid \"\"\n\"Kogumisteenus talletab elektroonilise hääle, moodustades selle käigus \"\n\"häälele kvalifitseeritud digitaalallkirja:\"\nmsgstr \"\"\n\"The collection service stores the electronic vote, forming a qualified \"\n\"digital signature for the vote:\"\n\n#: ../../02-ylevaade.rst:59\nmsgid \"elektrooniline hääl registreeritakse välises registreerimisteenuses;\"\nmsgstr \"\"\n\"an electronic vote is registered with an external registration service;\"\n\n#: ../../02-ylevaade.rst:61\nmsgid \"elektroonilisele häälele võetakse digitaalne ajatempel;\"\nmsgstr \"a digital timestamp is applied to the electronic vote;\"\n\n#: ../../02-ylevaade.rst:63\nmsgid \"\"\n\"elektroonilisele häälele võetakse valija sertifikaadi kehtivuskinnitus;\"\nmsgstr \"\"\n\"a digitally signed ballot will be accompanied by elements to confirm the \"\n\"validity of the voter's certificate;\"\n\n#: ../../02-ylevaade.rst:66\nmsgid \"\"\n\"elektroonilist häält kvalifitseerivad elemendid tagastatakse mh. ka \"\n\"valijarakendusele kontrollimiseks ning valija informeerimiseks \"\n\"kvalifitseerimise tulemustest;\"\nmsgstr \"\"\n\"elements qualifying an electronic vote are also returned to the voting \"\n\"application for verification and to inform the voter of the qualification \"\n\"results;\"\n\n#: ../../02-ylevaade.rst:70\nmsgid \"\"\n\"valijale võimaldatakse kvalifitseeritud elektroonilise hääle kontrollimine \"\n\"kontrollrakenduse abil.\"\nmsgstr \"\"\n\"the voter is allowed to verify a qualified electronic vote by means of a \"\n\"verification application.\"\n\n#: ../../02-ylevaade.rst:75\nmsgid \"\"\n\"Elektroonilise hääle digitaalne allkirjastamine erineb tavapärasest \"\n\"dokumentide digitaalallkirjastamisest, kus kõik allkirja kvalifitseerimiseks\"\n\" vajalikud toimingud algatatakse vahetult allkirjastaja seadmes. \"\n\"Elektroonilise hääle kvalifitseerimise kohustus on kogumisteenusel, kelle \"\n\"ülesanne on veenduda vastuvõetavate häälte korrektses allkirjastatuses. Kuna\"\n\" e-hääletamise perioodil on koormus seotud teenustele kõrge, võimaldab \"\n\"kogumisteenuse poolt juhitud kvalifitseerimine tagada paremat teenuse \"\n\"kvaliteeti.\"\nmsgstr \"\"\n\"Digital signing of electronic votes differs from traditional digital signing\"\n\" of documents, where all the operations necessary to qualify a signature are\"\n\" initiated directly on the signer's device. The onus of qualifying an \"\n\"electronic vote lies with the collection service, which is responsible for \"\n\"verifying that the votes received have been correctly signed. As the \"\n\"e-voting period is a period of high workload for the associated services, \"\n\"the qualification process managed by the collection service allows for a \"\n\"better quality of service.\"\n\n#: ../../02-ylevaade.rst:84\nmsgid \"\"\n\"Valija võib kasutada kontrollrakendust veendumaks oma hääle korrektses \"\n\"käitlemises kogumisteenuse poolt;\"\nmsgstr \"\"\n\"The voter can use the verification application to make sure their vote is \"\n\"handled correctly by the collection service;\"\n\n#: ../../02-ylevaade.rst:87\nmsgid \"\"\n\"Hääletamisperioodi lõppedes väljastab kogumisteenus valimise korraldajale \"\n\"e-valimiskasti ning registreerimisteenus väljavõtte kogumisteenuse poolt \"\n\"registreeritud häältest;\"\nmsgstr \"\"\n\"At the end of the voting period, the collection service will issue a digital\"\n\" ballot box to the election organiser and the registration service will \"\n\"issue a list of the votes registered by the collection service;\"\n\n#: ../../02-ylevaade.rst:91\nmsgid \"e-valimiskasti koosseisus antakse valimise korraldajale üle:\"\nmsgstr \"\"\n\"in the composition of the digital ballot-box following data will be handed \"\n\"over to the election organiser:\"\n\n#: ../../02-ylevaade.rst:93\nmsgid \"valija krüpteeritud tahteavaldus koos signatuuriga;\"\nmsgstr \"the voter's encrypted intent with signature;\"\n\n#: ../../02-ylevaade.rst:95\nmsgid \"registreerimisteenuse kinnitus hääle registreerimisest;\"\nmsgstr \"\"\n\"confirmation of the registration of the vote by the registration service;\"\n\n#: ../../02-ylevaade.rst:97\nmsgid \"\"\n\"ajatempliteenuse poolt väljastatud digitaalne ajatempel elektroonilisele \"\n\"häälele;\"\nmsgstr \"\"\n\"a digital timestamp issued by a timestamp service for an electronic vote;\"\n\n#: ../../02-ylevaade.rst:100\nmsgid \"\"\n\"kehtivuskinnitusteenuse poolt väljastatud kinnitus valija sertifikaadi \"\n\"kehtivuse kohta;\"\nmsgstr \"\"\n\"confirmation of the validity of the voter's certificate issued by a \"\n\"validation service;\"\n\n#: ../../02-ylevaade.rst:103\nmsgid \"\"\n\"registreerimisteenuse väljavõtte koosseisus antakse valimise korraldajale \"\n\"üle:\"\nmsgstr \"\"\n\"in the form of a registration extract shall be handed over to the organiser \"\n\"of the election:\"\n\n#: ../../02-ylevaade.rst:105\nmsgid \"\"\n\"kõik e-hääletamise perioodil kogumisteenuse poolt registreerimisteenusele \"\n\"saadetud päringud elektrooniliste häälte registreerimiseks.\"\nmsgstr \"\"\n\"all enquiries sent by the collection service to the registration service \"\n\"during the e-voting period to register electronic votes.\"\n\n#: ../../02-ylevaade.rst:109\nmsgid \"Valimise korraldaja arvutab hääletamistulemuse:\"\nmsgstr \"The election organiser calculates the tally:\"\n\n#: ../../02-ylevaade.rst:111\nmsgid \"kontrollitaks üle antud elektrooniliste häälte allkirjade kehtivust\"\nmsgstr \"verifying the validity of the electronic signatures of votes cast.\"\n\n#: ../../02-ylevaade.rst:113\nmsgid \"\"\n\"kontrollitakse, et kõik registreerimisteenuses registreeritud hääled on \"\n\"e-valimiskasti koosseisus üle antud;\"\nmsgstr \"\"\n\"making sure that all the votes registered in the registration service have \"\n\"been cast in the digital ballot box;\"\n\n#: ../../02-ylevaade.rst:116\nmsgid \"eraldatakse krüpteeritud hääled ja digitaalallkirjad;\"\nmsgstr \"separating encrypted ballots and digital signatures;\"\n\n#: ../../02-ylevaade.rst:118\nmsgid \"anonüümitakse krüpteeritud hääled krüptograafiliselt;\"\nmsgstr \"anonymizing encrypted ballots cryptographically;\"\n\n#: ../../02-ylevaade.rst:120\nmsgid \"dekrüpteeritakse krüpteeritud hääled;\"\nmsgstr \"decrypting encrypted ballots;\"\n\n#: ../../02-ylevaade.rst:122\nmsgid \"dekrüpteeritud häälte põhjal arvutatakse hääletamistulemus.\"\nmsgstr \"calculating the tally based on the decrypted votes.\"\n\n#: ../../02-ylevaade.rst:124\nmsgid \"\"\n\"Protokoll on analoogne paberil posti teel hääletamise protokolliga, kus \"\n\"valija tahe liigub valimiskomisjonini kahes ümbrikus – välimise ümbriku sees\"\n\" on sisemine ümbrik, mis omakorda sisaldab valija tahteavaldusega \"\n\"hääletussedelit. Välimine ümbrik kannab valijat identifitseerivat infot ning\"\n\" võimaldab mh. kontrollida valija õigust hääletada. Sisemine ümbrik on \"\n\"anonüümne ning kaitseb hääle salajasust. Enne häälte kokkulugemist \"\n\"eraldatakse sisemised ümbrikud välimistest.\"\nmsgstr \"\"\n\"The protocol is analogous to a postal voting protocol, where the voter's \"\n\"intent is sent to the electoral commission in two envelopes - the outer \"\n\"envelope contains the inner envelope, which in turn contains the ballot \"\n\"paper with the voter's intent. The outer envelope carries the voter's \"\n\"identifying information and allows, among other things, the voter's right to\"\n\" vote to be verified. The inner envelope is anonymous and protects the \"\n\"secrecy of the vote. The inner envelopes are separated from the outer \"\n\"envelopes before the votes are counted.\"\n\n#: ../../02-ylevaade.rst:132\nmsgid \"\"\n\"Elektroonilise hääletamise kontekstis on sisemine ümbrik vormistatud \"\n\"krüpteeritud häälena ning välimine ümbrik digitaalselt allkirjastatud \"\n\"dokumendina.\"\nmsgstr \"\"\n\"In the context of online voting, the inner envelope is an encrypted ballot \"\n\"and the outer envelope is a digitally signed document.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/04-seadistus.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-02-29 17:21+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../04-seadistus.rst:5\nmsgid \"Valimise definitsioon\"\nmsgstr \"Election definition\"\n\n#: ../../04-seadistus.rst:7\nmsgid \"\"\n\"Valimise defineerib valimise korraldaja. Eesti riiklikel valimistel \"\n\"jagunevad kõik hääleõiguslikud isikud ühte või mitmesse valimisringkonda. \"\n\"Valijal on võimalik hääletamisel valida ainult selle ringkonna kandidaatide \"\n\"vahel, kuhu ta kuulub.\"\nmsgstr \"\"\n\"The election is defined by the election organiser. In national elections in \"\n\"Estonia, all eligible voters are divided into one or more constituencies / \"\n\"electoral districts. Voters can only vote for candidates in the constituency\"\n\" to which they belong.\"\n\n#: ../../04-seadistus.rst:12\nmsgid \"Valimise defineerimiseks tuleb määratleda vähemalt\"\nmsgstr \"To define an election, at least the following have to be defined\"\n\n#: ../../04-seadistus.rst:14\nmsgid \"\"\n\"valimise unikaalne identifikaator ning küsimuste unikaalsed \"\n\"identifikaatorid;\"\nmsgstr \"\"\n\"a unique identifier for the election and unique identifiers for the \"\n\"questions;\"\n\n#: ../../04-seadistus.rst:16\nmsgid \"täielik loend valimisringkondadest ja -jaoskondadest;\"\nmsgstr \"a full list of electoral districts and polling stations;\"\n\n#: ../../04-seadistus.rst:18\nmsgid \"hääleõiguslike isikute nimekiri ja jagunemine valimisringkondadesse;\"\nmsgstr \"\"\n\"the list of persons entitled to vote and the division into electoral \"\n\"districts;\"\n\n#: ../../04-seadistus.rst:20\nmsgid \"kandidaatide nimekiri ja jagunemine valimisringkondadesse.\"\nmsgstr \"the list of candidates and the division into electoral districts.\"\n\n#: ../../04-seadistus.rst:22\nmsgid \"\"\n\"Valimise sisendandmed koostatakse Valimise Infosüsteemis (VIS), \"\n\"vormingukirjeldused on spetsifitseeritud `VIS ja EHS ühisspetsifikatsioonis \"\n\"<https://github.com/e-gov/VIS3-EHS/>`_.\"\nmsgstr \"\"\n\"Election input data is compiled in the Election Information System (VIS), \"\n\"the format specifications are specified in the 'Common Specification for VIS\"\n\" and EHS <https://github.com/e-gov/VIS3-EHS/>`_.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/05-ehaal.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 19:52+0000\\n\"\n\"PO-Revision-Date: 2024-02-29 19:50+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../05-ehaal.rst:5\nmsgid \"Elektrooniline hääl\"\nmsgstr \"Electronic vote\"\n\n#: ../../05-ehaal.rst:7\nmsgid \"\"\n\"IVXV hääletamisprotokoll põhineb topeltümbrikuskeemil, mis tähendab, et \"\n\"valija avakujul tahteavaldus krüpteeritakse valimise korraldaja poolt \"\n\"levitatud avaliku võtmega. Krüpteeritud tahteavaldus allkirjastatakse \"\n\"digitaalselt valija käsutuses oleva allkirjastamisvahendiga ning edastatakse\"\n\" kogumisteenusesse mingis kokkulepitud konteinervormingus. Kogumisteenus \"\n\"võib valija poolt allkirjastatud häält täiendavalt kvalifitseerida, \"\n\"veendudes näiteks allkirjastamissertifikaadi kehtivuses. IVXV protokollistik\"\n\" näeb mh. ette kogumisteenuse poolt vastuvõetud häälte registreerimise \"\n\"välises registreerimisteenuses.\"\nmsgstr \"\"\n\"The IVXV voting protocol is based on a double envelope scheme, which means \"\n\"that the voter's plaintext intent is encrypted with a public key distributed\"\n\" by the election organiser. The encrypted ballot is digitally signed using a\"\n\" signature device at the voter's disposal and is delivered to the collection\"\n\" service in an agreed container format. The collection service may further \"\n\"qualify the vote signed by the voter, for example by verifying the validity \"\n\"of the signature certificate. The IVXV protocol suite foresees, inter alia, \"\n\"the registration of votes received by the collection service in an external \"\n\"registration service.\"\n\n#: ../../05-ehaal.rst:17\nmsgid \"\"\n\"Kogumisteenuse poolt talletamisele võetud hääl koos kvalifitseerivate \"\n\"elementidega tehakse kättesaadavaks nii valijarakendusele kui \"\n\"kontrollrakendusele, mis teostavad üksiku hääle peal samad kontrollid, mida \"\n\"hilisem valimise korraldaja töötlemisrakendus teostab kõigi häälte peal. \"\n\"Kvalifitseerivate elementide kontrollimise võimalus annab valijale kindluse,\"\n\" et tema häält on hilisemates protsessides korrektselt menetletud.\"\nmsgstr \"\"\n\"The vote accepted to be stored by the collection service, together with the \"\n\"qualifying elements, is made available to both the voting application and \"\n\"the verification application, which perform the same checks on the \"\n\"individual vote as the subsequent election organiser's processing \"\n\"application performs on all votes. The ability to check the qualifying \"\n\"elements gives the voter confidence that their vote has been correctly \"\n\"processed in subsequent processes.\"\n\n#: ../../05-ehaal.rst:26\nmsgid \"Valija tahteavaldus avakujul\"\nmsgstr \"Voter's Plaintext Intent\"\n\n#: ../../05-ehaal.rst:28\nmsgid \"\"\n\"Valija tahteavaldus avakujul eksisteerib valijarakenduses ning hiljem ka \"\n\"kontrollrakenduses ja häälte kokkulugemisel võtmerakenduses. Tahteavaldus \"\n\"sisaldab valiku koodi ringkonnas ja ringkonna EHAK-koodi. Eraldajana \"\n\"kasutatakse ASCII kooditabeli sümbolit `0x2E` ehk \\\".\\\". Vormingus \"\n\"spetsifitseerimata sümbolite kasutamine ei ole lubatud, vormingule \"\n\"mittevastavad sedelid loetakse kehtetuks.\"\nmsgstr \"\"\n\"The voter's plaintext intent exists in the voting application and later in \"\n\"the verification application and in the key application when the votes are \"\n\"tallied. The declaration of intent includes the precinct code and the EHAK \"\n\"code of the precinct. The separator shall be the ASCII code table symbol \"\n\"`0x2E` or \\\".\\\". The use of symbols not specified in the format is not \"\n\"allowed, and non-compliant tags will be invalidated.\"\n\n#: ../../05-ehaal.rst:49\nmsgid \"Järgmised tahteavaldused on vormistatud korrektselt:\"\nmsgstr \"The following declarations of intent have been correctly drafted:\"\n\n#: ../../05-ehaal.rst:57\nmsgid \"Järgmised tahteavaldused ei ole vormistatud korrektselt:\"\nmsgstr \"The following declarations of intent have not been properly drawn up:\"\n\n#: ../../05-ehaal.rst:68\nmsgid \"Krüpteeritud sedel\"\nmsgstr \"Encrypted ballot\"\n\n#: ../../05-ehaal.rst:70\nmsgid \"\"\n\"Valija tahteavaldus avakujul :token:`ballot` krüpteeritakse valijarakenduse \"\n\"poolt valimise korraldaja genereeritud avaliku võtmega. IVXV vajab \"\n\"krüpteerimiseks mitte-deterministlikku, homomorfset avaliku võtme \"\n\"krüptosüsteemi. Selliseks süsteemiks sobib ElGamal krüptosüsteem, mida täna \"\n\"rakendatakse IVXV kontekstis nii jäägiklassiringi `Zp` multiplikatiivsel \"\n\"rühmal `Zp*` (MODP tüüpi rühmad) kui ka elliptkõveratel (ECC tüüpi rühmad).\"\nmsgstr \"\"\n\"The voter's plaintext intent :token:`ballot` is encrypted by a public key \"\n\"generated by the election organizer by the voting application. IVXV requires\"\n\" a non-deterministic, homomorphic public key cryptosystem for encryption. \"\n\"Such a system is the ElGamal cryptosystem, which is today implemented in the\"\n\" context of IVXV both on the multiplicative group `Zp*` of the residue class\"\n\" ring `Zp` (MODP type groups) and on the elliptic curves (ECC type groups).\"\n\n#: ../../05-ehaal.rst:77\nmsgid \"\"\n\"ElGamal avalik võti kodeeritakse koos ElGamal krüptosüsteemi parameetritega \"\n\"ning konkreetset valimist iseloomustava identifikaatoriga. Krüptosüsteemi \"\n\"parameetrid on osaks algoritmi identifikaatori struktuurist, avalik võti on \"\n\"kodeeritud X509 standardis kirjeldatud :token:`SubjectPublicKeyInfo` \"\n\"struktuuri :token:`subjectPublicKey` välja.\"\nmsgstr \"\"\n\"The ElGamal public key is encoded with the ElGamal cryptosystem parameters \"\n\"and an identifier for the specific election. The cryptosystem parameters are\"\n\" part of the structure of the algorithm identifier, the public key is \"\n\"encoded in the :token:`SubjectPublicKeyInfo` structure of the \"\n\":token:`subjectPublicKey` described in the X509 standard.\"\n\n#: ../../05-ehaal.rst:90\nmsgid \"\"\n\"MODP tüüpi rühmade korral identifitseerib algoritmi identifikaator \"\n\":token:`id-ivxv-modp-elgamal`. Parameetrid vastavad struktuurile \"\n\":token:`IVXVModPElGamalParameters` ning avalik võti struktuurile \"\n\":token:`IVXVModPElGamalPublicKey` (:ref:`asn-modp`).\"\nmsgstr \"\"\n\"In the case of MODP type groups, the identifier :token:`id-ivxv-modp-\"\n\"elgamal` identifies the algorithm. The parameters correspond to the \"\n\"structure :token:`IVXVModPElGamalParameters` and the public key to the \"\n\"structure :token:`IVXVModPElGamalPublicKey` (:ref:`asn-modp`).\"\n\n#: ../../05-ehaal.rst:95\nmsgid \"\"\n\"ECC tüüpi rühmade korral identifitseerib algoritmi identifikaator \"\n\":token:`id-ivxv-ecc-elgamal`. Parameetrid vastavad struktuurile \"\n\":token:`IVXVECCElGamalParameters` ning avalik võti struktuurile \"\n\":token:`IVXVECCElGamalPublicKey` (:ref:`asn-ecc`).\"\nmsgstr \"\"\n\"In the case of ECC-type groups, the identifier :token:`id-ivxv-ecc-elgamal` \"\n\"identifies the algorithm. The parameters correspond to the structure \"\n\":token:`IVXVECCElGamalParameters` and the public key to the structure \"\n\":token:`IVXVECCElGamalPublicKey` (:ref:`asn-ecc`).\"\n\n#: ../../05-ehaal.rst:100\nmsgid \"\"\n\"Valija tahteavalduse krüpteerimiseks võetakse UTF-8 kodeeringus struktuur \"\n\":token:`ballot` ning teisendatakse see ElGamal parameetrite poolt \"\n\"kirjeldatud rühma elemendiks vastavalt MODP või ECC algoritmidele. Rühma \"\n\"elemendiks teisendatud tahteavaldus krüpteeritakse ElGamal algoritmiga ning \"\n\"kodeeritakse MODP korral struktuuri :token:`IVXVModPElGamalCiphertext` \"\n\"(:ref:`asn-modp`) ja ECC korral struktuuri :token:`IVXVECCElGamalCiphertext`\"\n\" (:ref:`asn-ecc`). Lõplik krüpteeritud tahteavaldus esitatakse struktuuris \"\n\":token:`IVXVElGamalCiphertext` (:ref:`asn-general`), kus väli \"\n\":token:`algorithm` viitab rühma tüübile ning väli :token:`ciphertext` \"\n\"rühmaspetsiifilisele krüptogrammile. Andmestruktuuri \"\n\":token:`IVXVElGamalCiphertext` DER-kodeering on krüpteeritud sedel ehk \"\n\"sisemine ümbrik topeltümbriku skeemis.\"\nmsgstr \"\"\n\"To encrypt a voter's intent statement, the UTF-8 encoding takes the \"\n\"structure :token:`ballot` and converts it to a group element described by \"\n\"the ElGamal parameters, according to the MODP or ECC algorithms. The intent \"\n\"statement converted to a group element is encrypted by the ElGamal algorithm\"\n\" and encoded in the structure :token:`IVXVModPElGamalCiphertext` (:ref:`asn-\"\n\"modp`) for MODP and :token:`IVXVECCElGamalCiphertext` (:ref:`asn-ecc`) for \"\n\"ECC. The final encrypted intent statement is represented in the structure \"\n\":token:`IVXVElGamalCiphertext` (:ref:`asn-general`), where \"\n\":token:`algorithm` refers to the group type and :token:`ciphertext` refers \"\n\"to the group specific ciphertext. The :token:`IVXVElGamalCiphertext` of the \"\n\"data structure is the DER encoding of the encrypted tuple, i.e. the inner \"\n\"envelope in a double envelope scheme.\"\n\n#: ../../05-ehaal.rst:116\nmsgid \"\"\n\"Tahteavalduse krüpteerimise käigus genereeritakse valijarakenduses juhuarv, \"\n\"mida ElGamal krüpteerimisel kasutab. Sama juhuarv avalikustatakse hiljem \"\n\"kontrollrakendusele. Tulenevalt ElGamal krüptosüsteemi eripärast \"\n\"funktsioneerib see juhuarv nö. teise võtmena ning võimaldab krüptogrammi \"\n\"dekodeerimist kontrollrakenduses.\"\nmsgstr \"\"\n\"During the encryption process, a random number is generated in the voting \"\n\"application. This random number is used for encryption by the ElGamal \"\n\"method. The same random number is later exposed to the verification \"\n\"application. Due to the specificity of the ElGamal cryptosystem, this random\"\n\" number functions as a second key and allows the decryption of the \"\n\"ciphertext in the verification application.\"\n\n#: ../../05-ehaal.rst:125\nmsgid \"Valija poolt allkirjastatud hääl\"\nmsgstr \"Vote signed by the voter\"\n\n#: ../../05-ehaal.rst:127\nmsgid \"\"\n\"Krüpteeritud sedel tuleb enne kogumisteenusesse talletamisele saatmist \"\n\"digitaalselt allkirjastada, milleks on võimalik kasutada kõiki Eesti \"\n\"Vabariigis kehtivaid digitaalallkirjavahendeid – ID-kaart, Digi-ID, Mobiil-\"\n\"ID, Smart-ID. ID-kaarti saab kasutada nii TLS-CCA kui Web-eID protokolliga.\"\nmsgstr \"\"\n\"The encrypted ballot has to be digitally signed before it is sent to the \"\n\"collection service for storage, which can be done using any digital \"\n\"signature device valid in the Republic of Estonia - ID-card, Digi-ID, \"\n\"Mobile-ID, Smart-ID.\"\n\n#: ../../05-ehaal.rst:134\nmsgid \"\"\n\"Loend digitaalallkirjavahenditest ja nendega seotud protokollidest on \"\n\"tehniliselt korrektne, kuid valimiste korraldajal on võimalus kasutada vaid \"\n\"mõnda alamhulka nimetatud vahenditest.\"\nmsgstr \"\"\n\"The list of digital signature devices and associated protocols is \"\n\"technically correct, but only a subset of these devices can be used by the \"\n\"election organiser.\"\n\n#: ../../05-ehaal.rst:138\nmsgid \"\"\n\"Käesolev spetsifikatsioon näeb ette Eesti Vabariigi Standardikavandis \"\n\"[BDOC2.1] defineeritud BDOC allkirjavormingu kasutamise. BDOC \"\n\"allkirjavorming koosneb ETSI standardi TS 101 903 (XadES) profiilist ning \"\n\"OpenDocument konteineri vormingust.\"\nmsgstr \"\"\n\"This specification prescribes the use of the BDOC signature format defined \"\n\"in the draft Estonian Standard [BDOC2.1]. The BDOC signature format consists\"\n\" of the ETSI TS 101 903 (XadES) profile and the OpenDocument container \"\n\"format.\"\n\n#: ../../05-ehaal.rst:142\nmsgid \"\"\n\"Olenevalt käimasoleval valimisel esitatud küsimuste arvust võib digitaalselt\"\n\" allkirjastatud hääl sisaldada ühte või mitut andmefaili MIME-tüübiga \"\n\"``application/octet-stream``. Iga andmefaili sisuks on krüpteeritud sedel. \"\n\"Andmefaili ja teiste signeeritavate andmeobjektide räsimiseks enne \"\n\"allkirjastamist kasutatakse räsifunktsiooni SHA-256. Andmefaili nimi \"\n\"moodustatakse laiendist '``ballot``' ning valimise identifikaatorist ja \"\n\"küsimuse identifikaatorist. Kõik viidatud andmefailid peavad sisalduma \"\n\"allkirjakonteineris. Digitaalselt allkirjastatud hääl ei tohi sisaldada muid\"\n\" andmefaile kui neid, mis sisaldavad hääli mõne käimasoleva valimise \"\n\"kontekstis. Seadistusele mittevastavate häälte vastuvõtmisest, talletamisest\"\n\" ja töötlemisest peab kogumisteenus keelduma.\"\nmsgstr \"\"\n\"Depending on the number of questions in the current ballot, the digitally \"\n\"signed ballot may contain one or more data files with the MIME type \"\n\"``application/octet-stream''. The content of each data file is an encrypted \"\n\"message. The SHA-256 hash function is used to hash the data file and other \"\n\"data objects to be signed before signing. The name of the data file is \"\n\"formed by the extension ``ballot`` and the election identifier and the \"\n\"question identifier. All referenced data files must be contained in the \"\n\"signature container. A digitally signed ballot must not contain any data \"\n\"files other than those containing votes in the context of an ongoing \"\n\"election. The collection service must refuse to accept, store and process \"\n\"votes that do not conform to the configuration.\"\n\n#: ../../05-ehaal.rst:156\nmsgid \"\"\n\"Valimise identifikaator ja küsimuse identifikaator on defineeritud valimise \"\n\"korraldaja poolt loodud seadistustes ning töötlevad rakendused peavad \"\n\"lähtuma neist seadistustest otsustamaks, millised vormingule vastavad \"\n\"identifikaatorid on konkreetse sündmuse kontekstis lubatud ja millised \"\n\"mitte.\"\nmsgstr \"\"\n\"The choice identifier and the question identifier are defined in the \"\n\"settings created by the choice organiser, and processing applications must \"\n\"refer to these settings to decide which format identifiers are allowed and \"\n\"which are not in the context of a particular event.\"\n\n#: ../../05-ehaal.rst:178\nmsgid \"\"\n\"Valija poolt valijarakenduses allkirjastatud hääl moodustatakse nii, et on \"\n\"võimalik selle edasine kvalifitseerimine kogumisteenuses. Käesolev \"\n\"spetsifikatsioon näeb ette hääle kvalifitseerimiseks nii PKIX ajatempli kui \"\n\"OCSP kehtivuskinnituse võtmise. Sellisena on lõplik kvalifitseeritud hääl \"\n\"vastav BDOC-TS profiilile.\"\nmsgstr \"\"\n\"The vote signed by the voter in the voter application is formed in such a \"\n\"way that it can be further qualified in the collection service. This \"\n\"specification provides for both a PKIX timestamp and an OCSP validity \"\n\"confirmation to be taken to qualify a vote. As such, the final qualified \"\n\"vote will conform to the BDOC-TS profile.\"\n\n#: ../../05-ehaal.rst:184\nmsgid \"\"\n\"Kui hääl allkirjastatakse ID-kaardi või Digi-ID'ga, siis toimub algse \"\n\"allkirjastatud konteineri moodustamine valijarakenduses. Kui hääl \"\n\"allkirjastatakse Mobiil-ID või Smart-ID'ga, siis toimub konteineri \"\n\"moodustamine valijarakenduse ning kogumisteenuse poolt vahendatava Mobiil-\"\n\"ID/Smart-ID teenuse koostöös. Mobiil-ID/Smart-ID juhtumil kasutab \"\n\"kogumisteenus Mobiil-ID/Smart-ID teenust ainult signatuuri saamiseks \"\n\"krüpteeritud sedelile. Kõik hääle kvalifitseerimiseks vajalikud elemendid \"\n\"hangitakse vastavatelt teenustelt alles siis kui valijarakendus on saatnud \"\n\"signeeritud hääle talletamiseks. Kvalifitseeritud hääl esitatakse \"\n\"kogumisteenuse poolt valijarakendusele verifitseerimiseks, ainult \"\n\"kvalifitseeritud hääl peab vastama BDOC 2.1 standardi tingimustele -- \"\n\"valijarakenduse poolt moodustatud hääl on vaheetapp kvalifitseeritud hääleni\"\n\" jõudmiseks.\"\nmsgstr \"\"\n\"If the vote is signed with an ID-card or Digi-ID, the initial signed \"\n\"container will be formed in the voting application. If the vote is signed \"\n\"with a Mobile-ID or Smart-ID, the containerisation is done in cooperation \"\n\"between the voting application and the Mobile-ID/Smart-ID service mediated \"\n\"by the collection service. In the case of a Mobile-ID/Smart-ID, the \"\n\"collection service will only use the Mobile-ID/Smart-ID service to obtain a \"\n\"signature on an encrypted ballot. All the elements necessary to qualify the \"\n\"vote are only retrieved from the respective services once the voting \"\n\"application has sent the signed vote for storage. The qualified vote is \"\n\"submitted by the collection service to the voting application for \"\n\"verification, only the qualified vote must meet the requirements of the BDOC\"\n\" 2.1 standard -- the vote formed by the voting application is an \"\n\"intermediate step to a qualified vote.\"\n\n#: ../../05-ehaal.rst:197\nmsgid \"\"\n\"Valijarakenduses signeeritud häälel peab olema üks ja ainult üks allkiri, \"\n\"mida hoitakse signatuurifailis :file:`META-INF/signature0.xml`. Häält ja \"\n\"allkirja sisaldav konteiner (edaspidi viidatud kui ``SignedVote``) \"\n\"moodustatakse BDOC 2.1 standardis kirjeldatud meetodit kasutades.\"\nmsgstr \"\"\n\"A vote signed in the voting application must have one and only one \"\n\"signature, which is stored in the signature file :file:`META-\"\n\"INF/signature0.xml`. The container containing the vote and the signature \"\n\"(hereafter referred to as ``SignedVote``) shall be created using the method \"\n\"described in BDOC 2.1.\"\n\n#: ../../05-ehaal.rst:202\nmsgid \"\"\n\"Spetsifitseerime valijarakenduses allkirjastatud hääle vormingu ühe küsimuse\"\n\" korral.\"\nmsgstr \"\"\n\"We specify the format of a signed vote in the voting application for a \"\n\"single question.\"\n\n#: ../../05-ehaal.rst:205\nmsgid \"\"\n\"Räsialgoritmina ``DIGEST_ALG`` on kasutusel SHA-256 \"\n\"(http://www.w3.org/2001/04/xmlenc#sha256). XML kanoniseerimiseks \"\n\"(``CANON_ALG``) kasutatakse meetodit ``c14n11`` \"\n\"(http://www.w3.org/2006/12/xml-c14n11).\"\nmsgstr \"\"\n\"The ``DIGEST_ALG`` algorithm used is SHA-256 \"\n\"(http://www.w3.org/2001/04/xmlenc#sha256). XML canonicalisation \"\n\"(``CANON_ALG``) is performed using the ``c14n11`` method \"\n\"(http://www.w3.org/2006/12/xml-c14n11).\"\n\n#: ../../05-ehaal.rst:210\nmsgid \"\"\n\"ECC võtmete korral on allkirjastamismeetodiks \"\n\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256. RSA võtmeid enam ei \"\n\"kasutata.\"\nmsgstr \"\"\n\"In case of ECC keys, the signature method is \"\n\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256. RSA keys are no longer \"\n\"used.\"\n\n#: ../../05-ehaal.rst:214\nmsgid \"\"\n\"Identifikaatorite `VOTE_REF`, `SP_REF`, `SP_URI` ning `SV_URI` täpne väärtus\"\n\" ei ole fikseeritud.\"\nmsgstr \"\"\n\"The exact value of the identifiers `VOTE_REF`, `SP_REF`, `SP_URI` and \"\n\"`SV_URI` is not fixed.\"\n\n#: ../../05-ehaal.rst:218\nmsgid \"Element `SignedProperties`\"\nmsgstr \"Element `SignedProperties`\"\n\n#: ../../05-ehaal.rst:220\nmsgid \"\"\n\"Element ``SignedProperties`` moodustatakse kooskõlas BDOC 2.1 standardiga. \"\n\"Kui kvalifitseerimisel kasutatakse ajatemplit, siis elementi \"\n\"``SignaturePolicyIdentifier`` ei kasutata. Ühtegi mitte-kohustuslikku \"\n\"elementi ei kasutata. Allkirjastamise kellaaja fikseerib andmestruktuuri \"\n\"täitev arvuti ning valija X509-sertifikaat saadakse kas ID-kaardilt, Mobiil-\"\n\"ID, Smart-ID või Web eID teenuse vahendusel.\"\nmsgstr \"\"\n\"The ``SignedProperties`` element is formed in accordance with the BDOC 2.1 \"\n\"standard. If a timestamp is used for qualification, the \"\n\"``SignaturePolicyIdentifier`` element shall not be used. No non-mandatory \"\n\"element shall be used. The time of signature is fixed by the computer \"\n\"executing the data structure and the X509 certificate of the voter is \"\n\"obtained either from the ID card, Mobile-ID, Smart-ID or Web eID service.\"\n\n#: ../../05-ehaal.rst:234\nmsgid \"Element `SignedInfo`\"\nmsgstr \"Element `SignedInfo`\"\n\n#: ../../05-ehaal.rst:236\nmsgid \"\"\n\"Element ``SignedInfo`` moodustatakse kooskõlas BDOC 2.1 standardiga, \"\n\"viidates nii krüpteeritud sedelile (``VOTE_DIGEST``) kui elemendile \"\n\"``SignedProperties`` (``SP_DIGEST``).\"\nmsgstr \"\"\n\"The ``SignedInfo`` element is formed in accordance with the BDOC 2.1 \"\n\"standard, referring to both the encrypted ballot (``VOTE_DIGEST``) and the \"\n\"``SignedProperties`` element (``SP_DIGEST``).\"\n\n#: ../../05-ehaal.rst:246\nmsgid \"Element `SignatureValue`\"\nmsgstr \"Element 'SignatureValue\"\n\n#: ../../05-ehaal.rst:248\nmsgid \"\"\n\"Element ``SignatureValue`` moodustatakse kooskõlas BDOC 2.1 standardiga. \"\n\"Kanoniseeritud elemendist ``SignedInfo`` arvutatakse räsi, mis \"\n\"allkirjastatakse PKCS1-meetodiga.\"\nmsgstr \"\"\n\"The ``SignatureValue`` element shall be formed in accordance with the BDOC \"\n\"2.1 standard. A hash of the canonicalised element ``SignedInfo`` shall be \"\n\"computed and signed using the PKCS1 method.\"\n\n#: ../../05-ehaal.rst:258\nmsgid \"Element `XAdESSignatures`\"\nmsgstr \"Element `XAdESSignatures`\"\n\n#: ../../05-ehaal.rst:260\nmsgid \"\"\n\"Element ``XAdESSignatures`` sisaldab ühte ``Signature`` elementi, mis on \"\n\"koostatud lähtudes kõigist eelmistest elementidest ning valija X509 \"\n\"sertifikaadist. Elementi ``UnsignedProperties`` ei kasutata.\"\nmsgstr \"\"\n\"The ``XAdESSignatures`` element contains a single ``Signature`` element \"\n\"which is constructed from all the previous elements and the X509 certificate\"\n\" of the voter. The ``UnsignedProperties`` element is not used.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/06-talletamine.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: 2024-03-01 12:50+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\"X-Generator: Poedit 3.4.2\\n\"\n\n#: ../../06-talletamine.rst:5\nmsgid \"Elektroonilise hääle kvalifitseerimine talletamiseks\"\nmsgstr \"Qualifying an electronic vote for storage\"\n\n#: ../../06-talletamine.rst:8\nmsgid \"Kvalifitseeritud hääl\"\nmsgstr \"Qualified vote\"\n\n#: ../../06-talletamine.rst:10\nmsgid \"\"\n\"Valijarakenduse töö tulemusena saadetakse kogumisteenusesse talletamiseks \"\n\"topeltümbrik, mis sisaldab endas valija tahteavaldust krüpteeritud kujul, \"\n\"valija allkirja krüpteeritud tahteavaldusel kooskõlastatud allkirja- ja \"\n\"konteinervormingus ning valija allkirjastamissertifikaati X509-vormingus.\"\nmsgstr \"\"\n\"As a result of the work of the voting application, a double envelope \"\n\"containing the voter's intent in encrypted form, the voter's signature on \"\n\"the encrypted ballot, and the voter's signature certificate in X509 format \"\n\"is sent to the collection service for storage.\"\n\n#: ../../06-talletamine.rst:15\nmsgid \"\"\n\"Hääle edukaks talletamiseks näeb IVXV protokoll ette hääle registreerimise \"\n\"välise registreerimisteenuse osutaja juures ning registreerimistõendi \"\n\"valijarakendusele kättesaadavaks tegemise. Valimise korraldaja võib hääle \"\n\"kvalifitseerimiseks näha ette täiendavaid samme lisaks registreerimisele -- \"\n\"näiteks kehtivuskinnituse hankimist hääle allkirjastanud sertifikaadi kohta.\"\nmsgstr \"\"\n\"In order to ensure the successful storage of votes, IVXV protocol provides \"\n\"for the registration of votes with an external registration service provider\"\n\" and the availability of the registration confirmation in the voting \"\n\"application. In order to qualify a vote, the election organiser may provide \"\n\"for additional steps beyond registration -- for example, obtaining a \"\n\"validation certificate for the certificate that signed the vote.\"\n\n#: ../../06-talletamine.rst:21\nmsgid \"\"\n\"Kõik kogumisteenuse poolt hangitavad kvalifitseerivad elemendid, mis \"\n\"määravad hääle staatuse hilisemates töötlusetappides, tuleb esitada \"\n\"valijarakendusele ning nõudmise korral ka kontrollrakendusele tagamaks, et \"\n\"valija saab oma hääle korrektse menetlemise võimalikkusest õigeaegselt \"\n\"teada.\"\nmsgstr \"\"\n\"All qualifying elements obtained by the collection service that determine \"\n\"the status of the vote at subsequent stages of processing must be provided \"\n\"to the voting application and, on request, to the verification application \"\n\"to ensure that the voter is informed in a timely manner of the possibility \"\n\"of their vote being processed correctly.\"\n\n#: ../../06-talletamine.rst:27\nmsgid \"OCSP kehtivuskinnitus\"\nmsgstr \"OCSP validity confirmation\"\n\n#: ../../06-talletamine.rst:29\nmsgid \"\"\n\"OCSP (*Online Certificate Status Protocol*) on standartne protokoll \"\n\"X509-sertifikaatide kehtivusinfo pärimiseks. Kogumisteenus võib seda \"\n\"protokolli kasutada hääle allkirjastanud sertifikaadi kehtivuse \"\n\"teadasaamiseks. OCSP vastus ütleb, et sertifikaat kehtis päringu tegemise \"\n\"ajahetkel, kuid ei seosta OCSP vastust konkreetse allkirjaga.\"\nmsgstr \"\"\n\"OCSP (*Online Certificate Status Protocol*) is a standard protocol for \"\n\"requesting validity information for X509 certificates. The collection \"\n\"service can use this protocol to know the validity of the certificate that \"\n\"signed the vote. The OCSP response indicates that the certificate was valid \"\n\"at the time of the request, but does not link the OCSP response to a \"\n\"specific signature.\"\n\n#: ../../06-talletamine.rst:36\nmsgid \"RFC3161 ajatempel\"\nmsgstr \"RFC3161 timestamp\"\n\n#: ../../06-talletamine.rst:38\nmsgid \"\"\n\"RFC3161 ajatempli protokolliga saadakse usaldusteenuse pakkujalt kinnitus, \"\n\"et mingi andmekogum eksisteeris enne teatud ajahetke. BDOC-TS kontekstis \"\n\"ajatembeldatakse allkirja element ``SignatureValue`` kanoniseeritud kujul. \"\n\"Klassikaline OCSP vastus koos RFC 3161 vormingus ajatempliga \"\n\"kvalifitseerivad BDOC-TS allkirja.\"\nmsgstr \"\"\n\"The RFC3161 timestamp protocol is used to obtain confirmation from a trust \"\n\"service provider that a dataset existed before a certain point in time. In \"\n\"the context of BDOC-TS, the signature element ``SignatureValue`` is \"\n\"timestamped in canonicalized form. A classical OCSP response with a \"\n\"timestamp in RFC 3161 format qualifies a BDOC-TS signature.\"\n\n#: ../../06-talletamine.rst:47\nmsgid \"Talletamine\"\nmsgstr \"Storage\"\n\n#: ../../06-talletamine.rst:49\nmsgid \"Elektroonilise hääle talletamine kogumisteenuses tähendab:\"\nmsgstr \"Storing an electronic ballot in a collection service means:\"\n\n#: ../../06-talletamine.rst:51\nmsgid \"\"\n\"hääle vastuvõtmist valijarakenduselt ning hääletaja allkirja \"\n\"verifitseerimist;\"\nmsgstr \"\"\n\"acceptance of the vote from the voting application and verification of the \"\n\"voter's signature;\"\n\n#: ../../06-talletamine.rst:54\nmsgid \"\"\n\"hääle võimalikku kvalifitseerimist -- näiteks sertifikaadi kehtivuse \"\n\"tõendamist hääle allkirjastamisele lähedasel ajahetkel;\"\nmsgstr \"\"\n\"possible qualification of the vote -- for example, proof of the validity of \"\n\"the certificate at a point in time close to the time the vote was signed;\"\n\n#: ../../06-talletamine.rst:57\nmsgid \"hääle registreerimist sõltumatus registreerimisteenuses;\"\nmsgstr \"registering the vote with an independent registration service;\"\n\n#: ../../06-talletamine.rst:59\nmsgid \"häält kvalifitseerivate elementide vahendamist valijarakendusele.\"\nmsgstr \"\"\n\"the transmission of vote qualifying elements to the voting application.\"\n\n#: ../../06-talletamine.rst:61\nmsgid \"\"\n\"Erinevad kombinatsioonid allkirjavormingust ning hääli kvalifitseerivatest \"\n\"teenustest võivad tekitada erinevaid IVXV-profiile. Konkreetse dokumendi \"\n\"raames on IVXV profiil:\"\nmsgstr \"\"\n\"Different combinations of signature format and vote qualifying services can \"\n\"produce different IVXV profiles. In the context of a specific document, an \"\n\"IVXV profile is:\"\n\n#: ../../06-talletamine.rst:65\nmsgid \"Allkirjastatud hääle vorming on BDOC-TS;\"\nmsgstr \"The signed vote format is BDOC-TS;\"\n\n#: ../../06-talletamine.rst:67\nmsgid \"Kehtivuskinnitusprotokolliks on standartne OCSP;\"\nmsgstr \"The validation protocol is the standard OCSP;\"\n\n#: ../../06-talletamine.rst:69\nmsgid \"\"\n\"BDOC-TS kvalifitseerimiseks kasutatav RFC3161 ajatempel on kasutusel ka \"\n\"registreerimistõendina.\"\nmsgstr \"\"\n\"The RFC3161 timestamp used for BDOC-TS qualification is also used as a \"\n\"confirmation of registration.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/06a-regteenus.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 19:52+0000\\n\"\n\"PO-Revision-Date: 2024-03-01 12:42+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../06a-regteenus.rst:5\nmsgid \"Elektroonilise hääle registreerimine\"\nmsgstr \"Registration of Electronic Vote\"\n\n#: ../../06a-regteenus.rst:7\nmsgid \"\"\n\"Hääle edukaks talletamiseks näeb IVXV protokoll ette hääle registreerimise \"\n\"välise registreerimisteenuse osutaja juures ning registreerimistõendi \"\n\"valijarakendusele kättesaadavaks tegemise. Registreerimisteenus toimub \"\n\"RFC3161 ajatempli protokolli baasil. Protokolli on laiendatud selliselt, et \"\n\"kogumisteenus saab ajatempli päringule anda oma signatuuri, mis teeb \"\n\"võimalikuks hilisema võrdleva väljavõtte registreerimisteenusest. Sõltumatu \"\n\"registreerimisteenuse olemasolu vähendab häälte kogumisteenuse poolt \"\n\"\\\"kaotamise\\\" riski.\"\nmsgstr \"\"\n\"In order to ensure the successful storage of votes, IVXV protocol provides \"\n\"for the registration of votes with an external registration service provider\"\n\" and the availability of the registration confirmation in the voting \"\n\"application. The registration service is based on the RFC3161 timestamping \"\n\"protocol. The protocol has been extended in such a way that the collection \"\n\"service can provide its own signature to the timestamp request, which allows\"\n\" for a later comparative extraction from the registration service. The \"\n\"existence of an independent registration service reduces the risk of votes \"\n\"being 'lost' by the collection service.\"\n\n#: ../../06a-regteenus.rst:17 ../../06a-regteenus.rst:171\nmsgid \"Registreerimisteenus\"\nmsgstr \"Registration Service\"\n\n#: ../../06a-regteenus.rst:19\nmsgid \"\"\n\"Registreerimisteenus on teenus, mille abil Kogumisteenus registreerib kõik \"\n\"Hääletajatelt saadud hääled. Pärast hääletamisperioodi lõppu edastab \"\n\"Kogumisteenus talletatud hääled Töötlejale ning Registreerimisteenus edastab\"\n\" registreeritud hääled Töötlejale.\"\nmsgstr \"\"\n\"The Registration Service is the service through which the Collection Service\"\n\" registers all votes received from Voters. After the end of the voting \"\n\"period, the Collection Service shall transmit the stored votes to the \"\n\"Processor and the Registration Service shall transmit the registered votes \"\n\"to the Processor.\"\n\n#: ../../06a-regteenus.rst:24\nmsgid \"\"\n\"Registreerimisteenus aitab tagada e-valimiskasti terviklust. Eeldame et \"\n\"Kogumisteenusel puudub võimalus võltsida digitaalallkirja ning sellisel moel\"\n\" tekitada juurde hääli või muuta juba talletatud hääli. Riskina säilib \"\n\"võimalus, et Kogumisteenus ei anna kõiki talletatud hääli Töötlejale üle. \"\n\"Häälte valikulise üleandmise riski maandamiseks kasutab IVXV protokoll \"\n\"täiendavat Registreerimisteenust, kuhu Kogumisteenus iga talletatud hääle \"\n\"registreerib.  Hääletajal on protokollikohaselt võimalus korrektses \"\n\"registreerimises veenduda -- Registreerimisteenuse digitaalselt \"\n\"allkirjastatud kinnitus konkreetse hääle registreerimise kohta esitatakse ka\"\n\" konkreetsele hääletajale.\"\nmsgstr \"\"\n\"The registration service helps to ensure the integrity of the digital ballot\"\n\" box. We assume that the Collection Service has no possibility to forge a \"\n\"digital signature and thus generate additional votes or change votes already\"\n\" stored. As a risk, there remains the possibility that the Collection \"\n\"Service may not deliver all the stored votes to the Processor. In request to\"\n\" mitigate the risk of a selective transfer of votes, the IVXV protocol will \"\n\"use an additional Registration Service, where each stored vote will be \"\n\"registered by the Collection Service.  The voter will have the opportunity \"\n\"to verify the correct registration according to the protocol -- a digitally \"\n\"signed confirmation of the registration of a specific vote by the \"\n\"Registration Service will also be provided to the specific voter.\"\n\n#: ../../06a-regteenus.rst:35\nmsgid \"Registreerimisteenus protokollis\"\nmsgstr \"Registration Service in the protocol\"\n\n#: ../../06a-regteenus.rst:37\nmsgid \"\"\n\"Kogumisteenus saadab Registreerimisteenusele enda poolt allkirjastatud \"\n\"registreerimiskorralduse (edaspidi KORRALDUS), mis sisaldab hääle \"\n\"identifikaatorit ja allkirjastatud hääle räsi::\"\nmsgstr \"\"\n\"The Collection Service sends to the Registration Service a registration \"\n\"request (hereinafter the REQUEST) signed by the Collection Service and \"\n\"containing the vote identifier and the signed vote hash:\"\n\n#: ../../06a-regteenus.rst:43\nmsgid \"\"\n\"Registreerimisteenus talletab KORRALDUSE hilisemaks väljastamiseks ning \"\n\"vastab Kogumisteenusele oma poolt allkirjastatud registreerimiskinnitusega \"\n\"(edaspidi KINNITUS), mis allkirjastab algset KORRALDUST ning KINNITUSE \"\n\"väljastamise aega::\"\nmsgstr \"\"\n\"The Registration Service stores the REQUEST for later issuance and responds \"\n\"to the Collection Service with a registration confirmation signed by the \"\n\"Registration Service (hereinafter the CONFIRMATION), signing the original \"\n\"REQUEST and the time of issuance of the CONFIRMATION:\"\n\n#: ../../06a-regteenus.rst:49\nmsgid \"\"\n\"Kogumisteenus tagastab nii KINNITUSE kui KORRALDUSE valijarakendusele, \"\n\"Hääletaja saab teate hääle positiivsest talletamisest ainult KINNITUSE ja \"\n\"selle aluseks olnud KORRALDUSE edukal verifitseerimisel.\"\nmsgstr \"\"\n\"The collection service returns both the CONFIRMATION and the REQUEST to the \"\n\"voting application, the Voter will receive notification of a positive vote \"\n\"deposit only upon successful verification of the CONFIRMATION and the \"\n\"underlying REQUEST.\"\n\n#: ../../06a-regteenus.rst:53\nmsgid \"\"\n\"Hääletamisperioodi lõppedes edastab Registreerimisteenus Kogumisteenuse \"\n\"poolt tehtud KORRALDUSED Töötlejale.\"\nmsgstr \"\"\n\"At the end of the voting period, the REGISTRATION SERVICE forwards the \"\n\"REQUESTS made by the COLLECTION SERVICE to the Processor.\"\n\n#: ../../06a-regteenus.rst:56\nmsgid \"\"\n\"Registreerimisteenus peab Töötlejale algselt andma üle vähemalt loendi::\"\nmsgstr \"\"\n\"The registration service must initially provide the Processor with at least \"\n\"a list of:\"\n\n#: ../../06a-regteenus.rst:60\nmsgid \"\"\n\"Kui Registreerimisteenus annab üle ainult ülalkirjeldatud loendi, siis peab \"\n\"ta hiljem oleme võimeline andma loendi nõutud elemendile vastava \"\n\"korralduse::\"\nmsgstr \"\"\n\"If the Registration Service delivers only the list described above, it must \"\n\"later be able to issue the corresponding request for the requested element \"\n\"of the list::\"\n\n#: ../../06a-regteenus.rst:68\nmsgid \"Registreerimisteenuse roll hääletamisel\"\nmsgstr \"The role of the registration service in voting\"\n\n#: ../../06a-regteenus.rst:73\nmsgid \"Registreerimisteenuse roll hääle kontrollimisel\"\nmsgstr \"The role of the registration service in verifying votes\"\n\n#: ../../06a-regteenus.rst:78\nmsgid \"Registreerimisteenuse roll häälte üleandmisel\"\nmsgstr \"The role of the registration service in the transfer of votes\"\n\n#: ../../06a-regteenus.rst:82\nmsgid \"Registreerimisteenuse liidesed\"\nmsgstr \"Registration service interfaces\"\n\n#: ../../06a-regteenus.rst:84\nmsgid \"Registreerimisteenusel on kaks liidest\"\nmsgstr \"The registration service has two interfaces\"\n\n#: ../../06a-regteenus.rst:86\nmsgid \"KORRALDUSTE vastuvõtmise ja KINNITUSTE väljastamise liides;\"\nmsgstr \"\"\n\"Interface for the reception of REQUESTS and the issue of CONFIRMATIONS;\"\n\n#: ../../06a-regteenus.rst:88\nmsgid \"\"\n\"KORRALDUSE alusel väljastatud KINNITUSTE loetelu ja nende aluseks olnud \"\n\"KORRALDUSTE väljastamise liides.\"\nmsgstr \"\"\n\"List of CONFIRMATIONS issued under the REQUEST and the interface for issuing\"\n\" the underlying REQUEST.\"\n\n#: ../../06a-regteenus.rst:91\nmsgid \"\"\n\"Registreerimisteenuse funktsionaalsuseks on Kogumisteenusele KINNITUSTE \"\n\"väljastamine, väljastatud KINNITUSTE ja neile aluseks olevate KORRALDUSTE \"\n\"säilitamine ja hilisem Töötlejale üleandmine.\"\nmsgstr \"\"\n\"The functionality of the Registration Service is to issue CONFIRMATIONS to \"\n\"the Collection Service, to store the issued CONFIRMATIONS and the underlying\"\n\" REQUESTS and to later transfer them to the Processor.\"\n\n#: ../../06a-regteenus.rst:95\nmsgid \"\"\n\"Kui Registreerimisteenus osutab teenust mitmele erinevale osapoolele, siis \"\n\"peab olema garanteeritud, et konkreetse valimisega seotud Kogumisteenuse \"\n\"KORRALDUSED ja neile vastavad KINNITUSED on Valijarakenduse ja \"\n\"Kontrollrakenduse poolt kontrollitavalt eristatavad teiste osapoolte \"\n\"KORRALDUSTEST ning neile vastavatest KINNITUSTEST. Vastasel juhul võib \"\n\"tekkida olukord, kus Kogumisteenus küll registreerib hääle, kuid info \"\n\"sellest ei jõua Töötlejani.\"\nmsgstr \"\"\n\"Where the Registration Service provides a service to several different \"\n\"parties, it must be guaranteed that the REQUESTS of the Collection Service \"\n\"relating to a particular election and the corresponding CONFIRMATIONS are \"\n\"verifiably distinguishable by the Voting Application and the Verification \"\n\"Application from the REQUESTS of the other parties and the corresponding \"\n\"CONFIRMATIONS. Otherwise, a situation may arise where the Collection Service\"\n\" registers a vote but the information does not reach the Processor.\"\n\n#: ../../06a-regteenus.rst:102\nmsgid \"\"\n\"Registreerimisteenus peab olema võimeline kõiki Kogumisteenuse poolt tulnud \"\n\"KORRALDUSI üle andma.\"\nmsgstr \"\"\n\"The Registration Service must be able to deliver all the REQUESTS coming \"\n\"from the Collection Service.\"\n\n#: ../../06a-regteenus.rst:106\nmsgid \"Osapoolte nõuded registreerimisteenusele\"\nmsgstr \"Requirements of registration service\"\n\n#: ../../06a-regteenus.rst:109\nmsgid \"Töötleja\"\nmsgstr \"Processor\"\n\n#: ../../06a-regteenus.rst:111\nmsgid \"Töötleja ülesanne on muuhulgas tuvastada,\"\nmsgstr \"The task of the Processor is, inter alia, to identify,\"\n\n#: ../../06a-regteenus.rst:113\nmsgid \"millised Kogumisteenuse poolt üle antud hääled lähevad lugemisele ja\"\nmsgstr \"\"\n\"which votes handed over by the Collection Service will be counted, and\"\n\n#: ../../06a-regteenus.rst:115\nmsgid \"kas Kogumisteenus on jätnud hääli üle andmata.\"\nmsgstr \"whether the Collection Service has failed to deliver the votes.\"\n\n#: ../../06a-regteenus.rst:117\nmsgid \"\"\n\"Töötleja töö tulemusena selguvaid erisusi tuleb lahendada ning siin on \"\n\"järgmised 3 juhtumit:\"\nmsgstr \"\"\n\"The differences that emerge as a result of the processor's work need to be \"\n\"resolved, and here are 3 cases:\"\n\n#: ../../06a-regteenus.rst:120\nmsgid \"\"\n\"Kogumisteenus annab Töötlejale üle allkirjastatud hääle koos KINNITUSega, \"\n\"Registreerimisteenus annab Töötlejale üle Kogumisteenuse KORRALDUSE, mille \"\n\"alusel KINNITUS anti - vaidlust ei ole, kui konkreetne hääl oli antud \"\n\"Hääletaja jaoks viimane, siis suunatakse ta lugemisele.\"\nmsgstr \"\"\n\"The Collection Service delivers to the Processor the signed vote together \"\n\"with the CONFIRMATION, the Registration Service delivers to the Processor \"\n\"the Collection Service's REQUEST on the basis of which the CONFIRMATION was \"\n\"given - there is no dispute, if the particular vote was the last one given \"\n\"for the Voter, it will be forwarded for counting.\"\n\n#: ../../06a-regteenus.rst:125\nmsgid \"\"\n\"Kogumisteenus annab Töötlejale üle allkirjastatud hääle koos KINNITUSega, \"\n\"Registreerimisteenus ei anna Töötlejale selle hääle kohta midagi üle. Kuna \"\n\"KINNITUS on Registreerimisteenuse poolt allkirjastatud, siis on viga \"\n\"Registreerimisteenuse poolel. Kui konkreetne hääl oli antud Hääletaja jaoks \"\n\"viimane, siis suunatakse ta lugemisele.\"\nmsgstr \"\"\n\"The Collection Service will transfer the signed vote to the Processor \"\n\"together with the CONFIRMATION, the Registration Service will not transfer \"\n\"anything about the vote to the Processor. Since the CONFIRMATION is signed \"\n\"by the Registration Service, the error is on the side of the Registration \"\n\"Service. If the particular vote was the last one cast for the Voter, it will\"\n\" be forwarded for counting.\"\n\n#: ../../06a-regteenus.rst:131\nmsgid \"\"\n\"Registreerimisteenus annab Töötlejale üle Kogumisteenuse KORRALDUSE, \"\n\"Kogumisteenus ei anna Töötlejale vastava räsiga häält üle. Kuna KORRALDUS on\"\n\" Kogumisteenuse poolt allkirjastatud, siis on viga Kogumisteenuse poolel \"\n\"ning antud hääl tuleb üles otsida.\"\nmsgstr \"\"\n\"The Registration Service will transfer to the Processor the Collection \"\n\"Service's REQUEST, the Collection Service will not transfer to the Processor\"\n\" the vote with the corresponding hash. As the REQUEST is signed by the \"\n\"Collection Service, the error is on the Collection Service's side and the \"\n\"vote cast must be retrieved.\"\n\n#: ../../06a-regteenus.rst:137\nmsgid \"Hääletaja\"\nmsgstr \"Voter\"\n\n#: ../../06a-regteenus.rst:139\nmsgid \"\"\n\"Hääletaja jaoks on oht, et Kogumisteenus võib tema hääle 'unustada'. \"\n\"Nõuetekohase KINNITUSE nägemine annab Hääletajale kindluse, et väline \"\n\"osapool garanteerib tema hääle Töötlejani jõudmist. Kindluse jaoks on \"\n\"oluline:\"\nmsgstr \"\"\n\"For the voter, there is a risk that the Collection Service may 'forget' \"\n\"his/her vote. Seeing a proper CONFIRMATION gives the Voter the assurance \"\n\"that an external party will guarantee that their vote will reach the \"\n\"Processor. Following is important for the certainty:\"\n\n#: ../../06a-regteenus.rst:143 ../../06a-regteenus.rst:289\nmsgid \"KINNITUS on Registreerimisteenuse poolt allkirjastatud;\"\nmsgstr \"The CONFIRMATION is signed by the Registration Service;\"\n\n#: ../../06a-regteenus.rst:145 ../../06a-regteenus.rst:291\nmsgid \"\"\n\"KINNITUSES sisalduv algne KORRALDUS on Kogumisteenuse poolt allkirjastatud;\"\nmsgstr \"\"\n\"The original REQUEST contained in the CONFIRMATION is signed by the \"\n\"Collection Service;\"\n\n#: ../../06a-regteenus.rst:147\nmsgid \"\"\n\"usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise fakti meeles\"\n\" pidama;\"\nmsgstr \"\"\n\"confidence that the Registration Service is capable of remembering the fact \"\n\"of giving the CONFIRMATION;\"\n\n#: ../../06a-regteenus.rst:150\nmsgid \"\"\n\"usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise kohasust \"\n\"Töötlejale tõestama;\"\nmsgstr \"\"\n\"confidence that the Registration Service is able to prove the \"\n\"appropriateness of granting the CONFIRMATION to the Processor;\"\n\n#: ../../06a-regteenus.rst:153\nmsgid \"\"\n\"usaldus, et Kogumisteenusel ei ole võimalik hankida alternatiivset \"\n\"valekinnitust, mis Valijarakenduses verifitseerub, kuid Töötlejani ei jõua.\"\nmsgstr \"\"\n\"confidence that the Collection Service will not be able to obtain an \"\n\"alternative false positive CONFIRMATION that will be verified in the Voting \"\n\"Application but will not reach the Processor.\"\n\n#: ../../06a-regteenus.rst:157\nmsgid \"Kogumisteenus\"\nmsgstr \"Collection Service\"\n\n#: ../../06a-regteenus.rst:159\nmsgid \"\"\n\"Kogumisteenuse jaoks on oht, et Registreerimisteenuse poolt üleantud \"\n\"KINNITUSTE ja talletatud häälte vaated erinevad. Kogumisteenuse poolt \"\n\"KORRALDUSELE antud allkiri on Kogumisteenuse garantii, et \"\n\"Registreerimisteenuse poolt ei saa tekkida fiktiivseid KINNITUSI, mida \"\n\"Kogumisteenus tegelikult nõudnud pole.\"\nmsgstr \"\"\n\"There is a risk that, for the Collection Service, the views of the \"\n\"CONFIRMATIONS transferred by the Registration Service and the stored votes \"\n\"will differ. The signature given by the Collection Service to the REQUEST is\"\n\" the Collection Service's guarantee that no fictitious REQUESTS cannot be \"\n\"generated by the Registration Service that were not actually requested by \"\n\"the Collection Service.\"\n\n#: ../../06a-regteenus.rst:164\nmsgid \"\"\n\"Kogumisteenus talletab kõiki Registreerimisteenuse vastuseid. Kuna need on \"\n\"allkirjastatud, siis on täiendav info oluline vaid siis kui Kogumisteenus \"\n\"väidab, et mingit KORRALDUST ei ole antud, kuigi Registreerimisteenus on \"\n\"``(v_id, Hash(VOTE))`` esitanud. Sellisel juhul saab Registreerimisteenus \"\n\"esitada terve Kogumisteenuse KORRALDUSE (või vähemalt selle allkirjastatud \"\n\"komponendi)\"\nmsgstr \"\"\n\"The Collection Service stores all responses from the Registration Service. \"\n\"Since they are signed, the additional information is only relevant if the \"\n\"Collection Service claims that no REQUEST has been given, even though the \"\n\"Registration Service has provided ``(v_id, Hash(VOTE))``. In such a case, \"\n\"the Registration Service can provide the entire ORDER of the Collection \"\n\"Service (or at least a signed component of it).\"\n\n#: ../../06a-regteenus.rst:173\nmsgid \"\"\n\"Registreerimisteenus on huvitatud, et vaidlusolukordades, kus Kogumisteenus \"\n\"jätab midagi üle andmata, oleks tal võimalik oma tegevuse korrektsust \"\n\"tõestada. Oluline on tagada:\"\nmsgstr \"\"\n\"The Registration Service is interested in being able to prove the \"\n\"correctness of its actions in the event of a dispute where the Collection \"\n\"Service fails to deliver something. It is important to ensure:\"\n\n#: ../../06a-regteenus.rst:177\nmsgid \"\"\n\"Kogumisteenuse poolt konkreetse valimise raames antavad KORRALDUSED on \"\n\"teiste klientide poolt esitatud KORRALDUSTEST kontrollitavalt eristatavad.\"\nmsgstr \"\"\n\"The REQUESTS provided by the collection service in the context of a specific\"\n\" election are verifiably distinguishable from the REQUESTS provided by other\"\n\" customers.\"\n\n#: ../../06a-regteenus.rst:180\nmsgid \"\"\n\"Kogumisteenus ei saa juba esitatud KORRALDUSTE kohta väita, et ta neid ei \"\n\"esitanud.\"\nmsgstr \"\"\n\"The collection service cannot claim that it has not already submitted the \"\n\"REQUESTS.\"\n\n#: ../../06a-regteenus.rst:184\nmsgid \"Registreerimisteenuse realiseerimine RFC 3161 protokolli raamistikus\"\nmsgstr \"Registry service implementation in the RFC 3161 protocol framework\"\n\n#: ../../06a-regteenus.rst:186\nmsgid \"\"\n\"PKIX on ajatembeldusprotokoll, kus usaldatav kolmas osapool \"\n\"(ajatempliteenuse osutaja ehk ATO) kinnitab oma allkirjaga andmete \"\n\"eksisteerimist konkreetsel ajahetkel. Protokoll koosneb ühest päringust ja \"\n\"vastusest.\"\nmsgstr \"\"\n\"PKIX is a timestamping protocol, where a trusted third party (a timestamp \"\n\"authority or TSA) confirms the existence of data at a specific point in time\"\n\" with its signature. The protocol consists of a single request and response.\"\n\n#: ../../06a-regteenus.rst:190\nmsgid \"Ajatemplipäring::\"\nmsgstr \"Timestamp request::\"\n\n#: ../../06a-regteenus.rst:202\nmsgid \"\"\n\"Ajatembeldatavad andmed esitatakse teenusele ``messageImprint`` koosseisus \"\n\"räsina. ``TimeStampReq`` ei sisalda endas päringu esitaja allkirja.\"\nmsgstr \"\"\n\"The data to be timestamped shall be submitted to the ``messageImprint`` \"\n\"service as a raster. The ``TimeStampReq`` shall not include the signature of\"\n\" the requester.\"\n\n#: ../../06a-regteenus.rst:205\nmsgid \"ATO vastus ajatemplipäringule::\"\nmsgstr \"TSA response to timestamp request::\"\n\n#: ../../06a-regteenus.rst:234\nmsgid \"\"\n\"``TimeStampResp`` on ATO poolt digitaalselt allkirjastatud konteiner, mis \"\n\"sisaldab endas päringu koosseisus saadud välja ``messageImprint`` ning \"\n\"nonssi.\"\nmsgstr \"\"\n\"The ``TimeStampResp`` is a digitally signed container by the ATO containing \"\n\"the ``messageImprint`` field and the nonss received in the query \"\n\"configuration.\"\n\n#: ../../06a-regteenus.rst:237\nmsgid \"\"\n\"Registreerimisteenuse huvides on, et Kogumisteenuse päring oleks \"\n\"signeeritud. Kuna RFC 3161 ei toeta allkirjastatud päringuid on \"\n\"alternatiiviks kasutada mõnda laiendust, mis võimaldab Kogumisteenuse \"\n\"signatuuri edastamist. See laiendus tuleks teenuse poolt ajatempli \"\n\"koosseisus ka tagasi saata. Kuna RFC 3161 ei sõnasta laienduste \"\n\"tagasipeegeldamise nõuet ühemõtteliselt on reaalne võimalus kasutada \"\n\"protokolli laiendamiseks ajatemplipäringu nonssi. Nonss on ASN.1 INTEGER \"\n\"andmetüüp kuhu saab kodeerida suvalise struktuuriga andmeid, mis teeb \"\n\"võimalikuks järgmise skeemi:\"\nmsgstr \"\"\n\"It is in the interest of the registration service that the request by \"\n\"Collection Service is signed. Since RFC 3161 does not support signed \"\n\"requests, an alternative is to use an extension that allows the transmission\"\n\" of the Collection Service signature. This extension should also be returned\"\n\" by the service as part of the timestamp. Since RFC 3161 does not explicitly\"\n\" state the requirement for reflecting extensions, a real possibility is to \"\n\"use a timestamp request nonsign to extend the protocol. A nonss is an ASN.1 \"\n\"INTEGER data type into which data of arbitrary structure can be encoded, \"\n\"which makes the following scheme possible:\"\n\n#: ../../06a-regteenus.rst:246\nmsgid \"Enne hääletamist:\"\nmsgstr \"Before the vote:\"\n\n#: ../../06a-regteenus.rst:248\nmsgid \"Kogumisteenus genereerib allkirjastamisvõtme ja sertifikaadi.\"\nmsgstr \"The Collection Service generates a signature key and a certificate.\"\n\n#: ../../06a-regteenus.rst:250\nmsgid \"Kogumisteenus annab sertifikaadi Korraldajale üle.\"\nmsgstr \"The Collection Service will deliver the certificate to the Organiser.\"\n\n#: ../../06a-regteenus.rst:252\nmsgid \"Kogumisteenus seadistab ennast ATO'd kasutama.\"\nmsgstr \"The Collection Service configures itself to use the TSA.\"\n\n#: ../../06a-regteenus.rst:254\nmsgid \"Hääletamise ajal:\"\nmsgstr \"During the voting:\"\n\n#: ../../06a-regteenus.rst:256\nmsgid \"Valija saadab hääle talletamiseks.\"\nmsgstr \"The voter sends the vote to be recorded.\"\n\n#: ../../06a-regteenus.rst:258\nmsgid \"\"\n\"Kogumisteenus räsib hääle, allkirjastab räsi ning võtab räsile ajatempli, \"\n\"kasutades ajatemplipäringu TimeStampReq nonsina oma allkirja sellel räsil.\"\nmsgstr \"\"\n\"The Collection Service casts a vote, signs the hash and takes a timestamp on\"\n\" the hash, using your signature on the hash as a TimeStampReq noun.\"\n\n#: ../../06a-regteenus.rst:261\nmsgid \"\"\n\"ATO töötleb ajatemplipäringut kooskõlas RFC 3161 nõuetega ning väljastab \"\n\"allkirjastatud ajatempli.\"\nmsgstr \"\"\n\"The TSA processes the timestamp request in accordance with RFC 3161 and \"\n\"issues a signed timestamp.\"\n\n#: ../../06a-regteenus.rst:264\nmsgid \"\"\n\"Kogumisteenus vahendab ajatempli Valijarakendusele, mis teostab järgmised \"\n\"kontrollid:\"\nmsgstr \"\"\n\"The Collection Service sends a timestamp to the Voting Application, which \"\n\"performs the following checks:\"\n\n#: ../../06a-regteenus.rst:267\nmsgid \"ajatempel on ATO poolt allkirjastatud,\"\nmsgstr \"the timestamp is signed by the TSA,\"\n\n#: ../../06a-regteenus.rst:268\nmsgid \"ajatempel sisaldab nonssi,\"\nmsgstr \"the timestamp includes the nonce,\"\n\n#: ../../06a-regteenus.rst:269\nmsgid \"ajatempel sisaldab tema hääle räsi,\"\nmsgstr \"The timestamp contains the hash of her vote,\"\n\n#: ../../06a-regteenus.rst:270\nmsgid \"nonss on Kogumisteenuse poolt allkirjastatud valija hääle räsi.\"\nmsgstr \"\"\n\"nonce is a hash of voter's signed vote signed by the Collection Service.\"\n\n#: ../../06a-regteenus.rst:272\nmsgid \"Peale hääletamist:\"\nmsgstr \"After the vote:\"\n\n#: ../../06a-regteenus.rst:274\nmsgid \"Korraldaja annab ATO'le ajavahemiku ja Kogumisteenuse sertifikaadi\"\nmsgstr \"\"\n\"The Organiser will provide the TSA with a time period and a Collection \"\n\"Service certificate\"\n\n#: ../../06a-regteenus.rst:276\nmsgid \"\"\n\"ATO otsib kõigi selle ajavahemiku ajatemplipäringute ja vastuste hulgast \"\n\"neid, millel\"\nmsgstr \"\"\n\"The TSA will search for all timestamp requests and responses in this period \"\n\"for those that have\"\n\n#: ../../06a-regteenus.rst:279\nmsgid \"on nonss,\"\nmsgstr \"a nonce,\"\n\n#: ../../06a-regteenus.rst:280\nmsgid \"nonss dekodeerub kokkuleppeliseks andmestruktuuriks,\"\nmsgstr \"the nonce is decodable into a specified data structure,\"\n\n#: ../../06a-regteenus.rst:281\nmsgid \"andmestruktuur verifitseerub Kogumisteenuse sertifikaadiga.\"\nmsgstr \"the data structure is verifiable by a Collection Service certificate.\"\n\n#: ../../06a-regteenus.rst:283\nmsgid \"ATO annab üle kõik leitud ajatemplipäringud ja ajatemplid.\"\nmsgstr \"The TSA will hand over all timestamp requests and timestamps found.\"\n\n#: ../../06a-regteenus.rst:285\nmsgid \"Kogumisteenus annab üle kõik ajatemplipäringud, ajatemplid ja hääled.\"\nmsgstr \"\"\n\"The Collection Service will deliver all timestamp requests, timestamps and \"\n\"votes.\"\n\n#: ../../06a-regteenus.rst:287\nmsgid \"Töötleja analüüsib andmeid vastavalt protokollile\"\nmsgstr \"The Processor analyses the data according to the protocol\"\n\n#: ../../06a-regteenus.rst:293\nmsgid \"\"\n\"Usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise fakti meeles\"\n\" pidama;\"\nmsgstr \"\"\n\"Confirmation that the Registration Service is able to remember the fact of \"\n\"the CONFIRMATION;\"\n\n#: ../../06a-regteenus.rst:296\nmsgid \"\"\n\"Usaldus, et Kogumisteenusel ei ole võimalik hankida alternatiivset \"\n\"valekinnitust, mis Valijarakenduses verifitseerub, kuid Töötlejani ei jõua\"\nmsgstr \"\"\n\"Confirmation that it is not possible for the Collection Service to obtain an\"\n\" alternative false positive that will be verified in the Voting Application \"\n\"but will not reach the Processor\"\n\n#: ../../06a-regteenus.rst:299\nmsgid \"\"\n\"Usaldus, et Registreerimisteenus on võimeline KINNITUSE andmise kohasust \"\n\"Töötlejale tõestama;\"\nmsgstr \"\"\n\"Confirmation that the Registration Service is able to prove the \"\n\"appropriateness of granting the CONFIRMATION to the Processor;\"\n\n#: ../../06a-regteenus.rst:302\nmsgid \"\"\n\"Registreerimisteenuse poolt ei saa tekkida fiktiivseid KINNITUSI, mida \"\n\"Kogumisteenus tegelikult nõudnud pole\"\nmsgstr \"\"\n\"No fictitious CONFIRMATIONS can be generated by the Registration Service \"\n\"which are not actually required by the Collection Service\"\n\n#: ../../06a-regteenus.rst:305\nmsgid \"\"\n\"Kogumisteenus ei saa juba esitatud KORRALDUSTE kohta väita, et ta neid ei \"\n\"esitanud\"\nmsgstr \"\"\n\"The collection service cannot claim that it did not submit the REQUESTS \"\n\"already submitted\"\n\n#: ../../06a-regteenus.rst:308\nmsgid \"Nonsi vorming::\"\nmsgstr \"Nonce format::\"\n\n#: ../../06a-regteenus.rst:320\nmsgid \"Sõnumiks on TimeStampReq.messageImprint DER-kodeering::\"\nmsgstr \"The message is TimeStampReq.messageImprint DER encoding::\"\n\n#: ../../06a-regteenus.rst:327\nmsgid \"\"\n\"RSA kasutamisel allkirjastamiseks. välja \"\n\"``Signature.signingAlgorithm.algorithm`` väärtus sõltub sõnumi \"\n\"``hashAlgorithm`` välja väärtusest::\"\nmsgstr \"\"\n\"When using RSA for signing. field ``Signature.signingAlgorithm.algorithm`` \"\n\"the value depends on the value of the ``hashAlgorithm`` field of the \"\n\"message::\"\n\n#: ../../06a-regteenus.rst:339\nmsgid \"Väli ``Signature.signingAlgorithm.parameters`` puudub või on NULL.\"\nmsgstr \"\"\n\"The ``Signature.signingAlgorithm.parameters`` field is missing or NULL.\"\n\n#: ../../06a-regteenus.rst:341\nmsgid \"\"\n\"Väli ``Signature.signature`` on OCTET STRING, mis sisaldab RSA signatuuri \"\n\"sõnumil.\"\nmsgstr \"\"\n\"The ``Signature.signature`` field is an OCTET STRING containing the RSA \"\n\"signature on the message.\"\n\n#: ../../06a-regteenus.rst:344\nmsgid \"ATO väljavõte\"\nmsgstr \"TSA extract\"\n\n#: ../../06a-regteenus.rst:346\nmsgid \"\"\n\"ATO väljavõte Kogumisteenuse poolt esitatud TimeStampReq vormingus \"\n\"päringutest esitatakse ZIP failina, kus iga päring on salvestatud ühte \"\n\"faili, mis vastab järgmistele tingimustele:\"\nmsgstr \"\"\n\"The TSA extract of the requests submitted by the Collection Service in \"\n\"TimeStampReq format will be presented as a ZIP file, where each request is \"\n\"stored in a single file that meets the following conditions:\"\n\n#: ../../06a-regteenus.rst:350\nmsgid \"Kaustastruktuur puudub, ükski fail ei paikne kaustas.\"\nmsgstr \"There is no folder structure, no files are located in a folder.\"\n\n#: ../../06a-regteenus.rst:351\nmsgid \"Failinimed on unikaalsed (failinimedele samas tähendust ei omistata).\"\nmsgstr \"\"\n\"File names are unique (file names are not given any meaning at the same \"\n\"time).\"\n\n#: ../../06a-regteenus.rst:353\nmsgid \"Üleantav andmekomplekt on:\"\nmsgstr \"The data set to be transferred is:\"\n\n#: ../../06a-regteenus.rst:355\nmsgid \"Andmefail: `<andmefailinimi>.zip`\"\nmsgstr \"Data file: `<datafilename>.zip`\"\n\n#: ../../06a-regteenus.rst:356\nmsgid \"Kontrollsummafail: `<andmefailinimi>.zip.sha256sum.asice`\"\nmsgstr \"Checksum file: `<datafilename>.zip.sha256sum.asice`\"\n\n#: ../../06a-regteenus.rst:358\nmsgid \"\"\n\"Üherealise kontrollsummafaili sisu on HEX kodeeringus SHA256 räsi \"\n\"andmefailist.\"\nmsgstr \"\"\n\"The contents of the single line checksum file are HEX encoded from the \"\n\"SHA256 hash data file.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/07-kontrollimine.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-05 11:29+0000\\n\"\n\"PO-Revision-Date: 2024-03-01 17:59+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../07-kontrollimine.rst:5\nmsgid \"Elektroonilise hääle kontrollimine eraldi ja e-valimiskasti koosseisus\"\nmsgstr \"\"\n\"Verification of electronic votes separately and as part of a digital \"\n\"ballot box\"\n\n#: ../../07-kontrollimine.rst:7\nmsgid \"\"\n\"Elektroonilist häält kontrollitakse töötlemisrakenduses, kogumisteenuses,\"\n\" valijarakenduses ja kontrollrakenduses.\"\nmsgstr \"\"\n\"The electronic vote is verified in the processing application, the \"\n\"collection service, the voting application and the verification \"\n\"application.\"\n\n#: ../../07-kontrollimine.rst:10\nmsgid \"\"\n\"Kõige põhjalikuma kontrolli läbib elektrooniline hääl e-valimiskasti \"\n\"koosseisus töötlemisrakenduses, kus otsustatakse konkreetse hääle \"\n\"lugemisele saatmine või mittesaatmine. Nende kontrollide käigus \"\n\"vaadeldakse e-häält nii eraldi kui ka suhtes kõigi teiste sama valija \"\n\"poolt antud häältega. Täiendavalt kõrvutatakse e-valimiskasti \"\n\"registreerimisteenuse väljavõttega.\"\nmsgstr \"\"\n\"The most thorough check of an electronic vote takes place in the \"\n\"processing application within the e-voting box, where the decision to \"\n\"send or not to send a particular vote is made. During these checks, the \"\n\"e-vote is examined both individually and in relation to all other votes \"\n\"cast by the same voter. A further comparison is made with the e-voting \"\n\"box registration service extract.\"\n\n#: ../../07-kontrollimine.rst:16\nmsgid \"\"\n\"Iga üksiku hääle kohta läbitakse töötlemisrakendusega analoogsel tasemel \"\n\"kontroll valijarakenduses, kus veendutakse, et kogumisteenus on hääle \"\n\"kvalifitseerinud selliselt, et töötlemisrakenduses tehtavad kontrollid \"\n\"õnnestuvad. Valijarakenduse kontrollidega samaväärsed kontrollid viib \"\n\"läbi kontrollrakendus.\"\nmsgstr \"\"\n\"For each individual vote, a check is performed in the voting application \"\n\"at a level analogous to that in the processing application, to ensure \"\n\"that the collection service has qualified the vote in such a way that the\"\n\" checks in the processing application are successful. Checks equivalent \"\n\"to those carried out in the Voting Application shall be carried out by \"\n\"the Verification Application.\"\n\n#: ../../07-kontrollimine.rst:22\nmsgid \"\"\n\"Kogumisteenus on lisaks vastutav mitme hääle lõplikuks \"\n\"kvalifitseerumiseks vajaliku elemendi hankimise eest ning teostab ka \"\n\"nende hankimise järgselt kontrollid.\"\nmsgstr \"\"\n\"In addition, the collection service is responsible for the procurement of\"\n\" the elements necessary for the final qualification of several votes, and\"\n\" also carries out post-qualification checks.\"\n\n#: ../../07-kontrollimine.rst:27\nmsgid \"Lõpliku elektroonilise hääle komponendid\"\nmsgstr \"Components of the final electronic voice\"\n\n#: ../../07-kontrollimine.rst:29\nmsgid \"\"\n\"Elemendid, mis on kättesaadavad otsustamise hetkel, kas hääl \"\n\"kvalifitseerub lugemisele saatmiseks või mitte:\"\nmsgstr \"\"\n\"Elements available at the moment of deciding whether or not a vote \"\n\"qualifies to be sent for counting:\"\n\n#: ../../07-kontrollimine.rst:32\nmsgid \"elektroonilist häält sisaldav konteiner - :ref:`entity-haale-konteiner`;\"\nmsgstr \"container containing electronic voice - :ref:`entity-haale-konteiner`;\"\n\n#: ../../07-kontrollimine.rst:34\nmsgid \"krüpteeritud sedel - :ref:`entity-krypteeritud-sedel`;\"\nmsgstr \"encrypted ballot - :ref:`entity-krypteeritud-sedel`;\"\n\n#: ../../07-kontrollimine.rst:36\nmsgid \"valija signatuur krüpteeritud sedelil - :ref:`entity-haale-signatuur`;\"\nmsgstr \"voter signature on encrypted ballot - :ref:`entity-haale-signatuur`;\"\n\n#: ../../07-kontrollimine.rst:38\nmsgid \"valija allkiri krüpteeritud sedelil - :ref:`entity-haale-allkiri`;\"\nmsgstr \"voter signature on encrypted ballot - :ref:`entity-haale-allkiri`;\"\n\n#: ../../07-kontrollimine.rst:40\nmsgid \"valija allkirjastamissertifikaat - :ref:`entity-valija-sertifikaat`;\"\nmsgstr \"voter signing certificate - :ref:`entity-valija-sertifikaat`;\"\n\n#: ../../07-kontrollimine.rst:42\nmsgid \"valija isikukood - :ref:`entity-valija-identiteet`;\"\nmsgstr \"voter identity code - :ref:`entity-valija-identiteet`;\"\n\n#: ../../07-kontrollimine.rst:44\nmsgid \"\"\n\"kvalifitseeriv element - valija sertifikaadi kehtivuskinnitus - :ref\"\n\":`entity-kehtivuskinnitus`;\"\nmsgstr \"\"\n\"qualifying element - confirmation of the validity of the voter's \"\n\"certificate - :ref:`entity-kehtivuskinnitus`;\"\n\n#: ../../07-kontrollimine.rst:47\nmsgid \"\"\n\"kvalifitseeriv element - ajatempel allkirjastatud krüpteeritud sedelile -\"\n\" :ref:`entity-ajatempel`;\"\nmsgstr \"\"\n\"qualifying element - timestamp on a signed encrypted ballot - :ref\"\n\":`entity-ajatempel`;\"\n\n#: ../../07-kontrollimine.rst:50\nmsgid \"\"\n\"kvalifitseeriv element - registreerimispäringu konteiner :ref:`entity-\"\n\"registreerimisparing-konteiner`;\"\nmsgstr \"\"\n\"Creating a registration request - :ref:`entity-registreerimisparing-\"\n\"konteiner`\"\n\n#: ../../07-kontrollimine.rst:53\nmsgid \"\"\n\"kvalifitseeriv element - registreerimispäring allkirjastatud krüpteeritud\"\n\" sedelile - :ref:`entity-registreerimisparing`;\"\nmsgstr \"\"\n\"qualifying element - registration certificate for signed and encrypted \"\n\"vote - :ref:`entity-registreerimisparing`;\"\n\n#: ../../07-kontrollimine.rst:56\nmsgid \"\"\n\"kvalifitseeriv element - registreerimistõend allkirjastatud krüpteeritud \"\n\"sedeli kohta - :ref:`entity-registreerimistoend`;\"\nmsgstr \"\"\n\"qualifying element - registration certificate for signed and encrypted \"\n\"vote - :ref:`entity-registreerimistoend`;\"\n\n#: ../../07-kontrollimine.rst:59\nmsgid \"\"\n\"valija ringkonnakuuluvuse tõend hääle andmise hetkel - :ref:`entity-\"\n\"nimekirjatunnus`.\"\nmsgstr \"\"\n\"proof of the voter's district eligibility at the time of voting - :ref\"\n\":`entity-nimekirjatunnus`.\"\n\n#: ../../07-kontrollimine.rst:63\nmsgid \"\"\n\"Elemendid, mis on kättesaadavad otsustamise hetkel, kuidas häält \"\n\"kokkulugemisel arvesse võtta:\"\nmsgstr \"\"\n\"The elements available at the moment of deciding how to take the vote \"\n\"into account in the count:\"\n\n#: ../../07-kontrollimine.rst:66\nmsgid \"miksitud krüpteeritud sedel - :ref:`entity-miksitud-krypteeritud-sedel`;\"\nmsgstr \"mixed encrypted ballot - :ref:`entity-miksitud-krypteeritud-sedel`;\"\n\n#: ../../07-kontrollimine.rst:68\nmsgid \"valija tahteavaldus avakujul - :ref:`entity-tahteavaldus`;\"\nmsgstr \"voter's plaintext intent - :ref:`entity-tahteavaldus`;\"\n\n#: ../../07-kontrollimine.rst:70\nmsgid \"ringkonnakuuluvuse tunnus - :ref:`entity-ringkonnatunnus`;\"\nmsgstr \"district membership - :ref:`entity-ringkonnatunnus`;\"\n\n#: ../../07-kontrollimine.rst:72\nmsgid \"\"\n\"ringkonnapõhine valikute nimekiri - :ref:`entity-ringkonna-\"\n\"valikutenimekiri`.\"\nmsgstr \"district-based candidate list - :ref:`entity-ringkonna-valikutenimekiri`.\"\n\n#: ../../07-kontrollimine.rst:75\nmsgid \"Täiendavad elemendid:\"\nmsgstr \"Additional elements:\"\n\n#: ../../07-kontrollimine.rst:77\nmsgid \"\"\n\"krüpteerimisel kasutatud juhuslikkus - :ref:`entity-juhuslikkus`; - \"\n\"luuakse valijarakenduses hääle krüpteerimisel ning vahendatakse ainult \"\n\"kontrollrakendusele.\"\nmsgstr \"\"\n\"randomness used in encryption - :ref:`entity-juhuslikkus`; - is generated\"\n\" in the voter application when the vote is encrypted and is only \"\n\"transmitted to the verification application.\"\n\n#: ../../07-kontrollimine.rst:86\nmsgid \"TAHTEAVALDUS\"\nmsgstr \"INTENT\"\n\n#: ../../07-kontrollimine.rst:88\nmsgid \"\"\n\"Olem moodustatakse valijarakenduses ning tuvastatakse võtmerakenduses. \"\n\"Tegemist on EHS spetsiifilises vormingus UTF-8 kodeeringus baidijadaga.\"\nmsgstr \"\"\n\"The item is created in the voter application and identified in the key \"\n\"application. It is an EHS specific format UTF-8 encoded byte string.\"\n\n#: ../../07-kontrollimine.rst:94\nmsgid \"TAHTEAVALDUS, vormingu korrektsus\"\nmsgstr \"INTENT, correctness of the format\"\n\n#: ../../07-kontrollimine.rst:96\nmsgid \"\"\n\"Tahteavalduse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni \"\n\"suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the intent format against this \"\n\"specification.\"\n\n#: ../../07-kontrollimine.rst:102\nmsgid \"TAHTEAVALDUS, RINGKONNATUNNUS, RINGKONNA VALIKUTENIMEKIRI kooskõlalisus\"\nmsgstr \"INTENT, DISTRICT MEMBERSHIP, DISTRICT CHOICES consistency \"\n\n#: ../../07-kontrollimine.rst:104\nmsgid \"\"\n\"Tahteavalduses sisalduva valiku kontroll ringkonnatunnuse ja valikute \"\n\"nimekirja suhtes. Kontroll õnnestub, kui tuvastatud valik on kättesaadav \"\n\"antud ringkonnas.\"\nmsgstr \"\"\n\"Checking the choice identified by the intent against the district \"\n\"membership and the district choices. The check succeeds if the identified\"\n\" option is available in the given district.\"\n\n#: ../../07-kontrollimine.rst:111\nmsgid \"TAHTEAVALDUS, RINGKONNA VALIKUTENIMEKIRI, kooskõlalisus\"\nmsgstr \"INTENT, DISTRICT CHOICES, consistency\"\n\n#: ../../07-kontrollimine.rst:113\nmsgid \"\"\n\"Tahteavalduses sisalduva valiku kontroll ringkonnapõhise valikute \"\n\"nimekirja suhtes. Kontroll õnnestub, kui tuvastatud valik on kättesaadav \"\n\"antud ringkonnas.\"\nmsgstr \"\"\n\"Checking the choice identified by the intent against the list of \"\n\"district-based choices. The check succeeds if the identified option is \"\n\"available in the given district.\"\n\n#: ../../07-kontrollimine.rst:121\nmsgid \"JUHUSLIKKUS\"\nmsgstr \"RANDOMNESS\"\n\n#: ../../07-kontrollimine.rst:123\nmsgid \"\"\n\"Olem moodustatakse valijarakenduses ning kasutatakse ka \"\n\"kontrollrakenduses. Olem esitatakse EHS spetsiifilises vormingus \"\n\"baidijadana ning peab olema ühilduv avaliku võtme parameetrite poolt \"\n\"määratud matemaatilise rühmaga.\"\nmsgstr \"\"\n\"The item is created in the voter application and is also used in the \"\n\"verification application. The element shall be presented in an EHS \"\n\"specific format as a byte string and shall be compatible with the \"\n\"mathematical group specified by the public key parameters.\"\n\n#: ../../07-kontrollimine.rst:130\nmsgid \"JUHUSLIKKUS, vormingu korrektsus\"\nmsgstr \"RANDOMNESS, correctness of formatting\"\n\n#: ../../07-kontrollimine.rst:132\nmsgid \"\"\n\"Juhuslikkuse vormingu korrektsuse kontroll käesoleva spetsifikatsiooni \"\n\"suhtes.\"\nmsgstr \"Randomness format correctness check against this specification.\"\n\n#: ../../07-kontrollimine.rst:137\nmsgid \"JUHUSLIKKUS, kooskõlalisus avaliku võtmega\"\nmsgstr \"RANDOMNESS, consistency with the public key\"\n\n#: ../../07-kontrollimine.rst:139\nmsgid \"\"\n\"Juhuslikkuse kooskõlalisuse kontroll avaliku võtmega. Kontroll õnnestub \"\n\"kui juhuslikkus on kasutatav skalaarina arvutusteks avaliku võtme \"\n\"parameetrite poolt määratud matemaatilises rühmas.\"\nmsgstr \"\"\n\"Randomness consistency check with public key. The check succeeds if the \"\n\"randomness can be used as a scalar for calculations in the mathematical \"\n\"group specified by the parameters of the public key.\"\n\n#: ../../07-kontrollimine.rst:147\nmsgid \"KRÜPTEERITUD SEDEL\"\nmsgstr \"ENCRYPTED BALLOT\"\n\n#: ../../07-kontrollimine.rst:149\nmsgid \"\"\n\"Olem moodustatakse valijarakenduses. Tegemist on EHS spetsiifilises \"\n\"vormingus DER-kodeeritud andmestruktuuriga, mille kontrollimise aluseks \"\n\"on EHS avalik võti, mis muuhulgas määratleb krüpteerimisel kasutatud \"\n\"matemaatilise rühma.\"\nmsgstr \"\"\n\"The item is formed in the voting application. It is a DER encoded data \"\n\"structure in a specific format of the EHS, which is verified by the EHS \"\n\"public key, which, among other things, defines the mathematical group \"\n\"used for encryption.\"\n\n#: ../../07-kontrollimine.rst:156\nmsgid \"KRÜPTEERITUD SEDEL, vormingu korrektsus\"\nmsgstr \"ENCRYPTED BALLOT, format correctness\"\n\n#: ../../07-kontrollimine.rst:158\nmsgid \"\"\n\"Krüpteeritud sedeli vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"Verification of the encrypted ballot format against this specification.\"\n\n#: ../../07-kontrollimine.rst:164\nmsgid \"KRÜPTEERITUD SEDEL, kooskõlalisus avaliku võtmega\"\nmsgstr \"ENCRYPTED BALLOT, consistency with public key\"\n\n#: ../../07-kontrollimine.rst:166\nmsgid \"\"\n\"Krüpteeritud sedeli kooskõlalisuse kontroll avaliku võtmega. Kontroll \"\n\"õnnestub kui krüpteeritud sedeli komponendid on kasutatavad rühma \"\n\"liikmetena arvutusteks avaliku võtme parameetrite poolt määratud \"\n\"matemaatilises rühmas.\"\nmsgstr \"\"\n\"Checking the consistency of an encrypted ballot with a public key. The \"\n\"check succeeds if the components of the encrypted ballot are usable as \"\n\"members of a group for computations in the mathematical group specified \"\n\"by the public key parameters.\"\n\n#: ../../07-kontrollimine.rst:173\nmsgid \"KRÜPTEERITUD SEDEL, JUHUSLIKKUS, kooskõlalisus\"\nmsgstr \"ENCRYPTED BALLOT, RANDOMNESS, consistency\"\n\n#: ../../07-kontrollimine.rst:175\nmsgid \"\"\n\"Krüpteeritud sedeli kooskõlalisuse kontroll juhuslikkusega lähtudes rühma\"\n\" parameetritest. Kontroll õnnestub kui õnnestub näidata, et antud \"\n\"juhuslikkust on kasutatud krüpteeritud sedeli komponendi `uBlind` \"\n\"arvutamiseks antud rühma parameetrite järgi.\"\nmsgstr \"\"\n\"Checking the consistency of an encrypted ballot with randomness based on \"\n\"group parameters. A check succeeds if it can be shown that the given \"\n\"randomness has been used to compute the `uBlind` component of the \"\n\"ciphertext from the given group parameters.\"\n\n#: ../../07-kontrollimine.rst:185\nmsgid \"VALIJA SERTIFIKAAT\"\nmsgstr \"VOTER CERTIFICATE\"\n\n#: ../../07-kontrollimine.rst:187\nmsgid \"\"\n\"Olem on valijale omistatud süsteemiväliselt. Tegemist on X.509 vormingus \"\n\"sertifikaadiga, mille kehtivuse kontrolli aluseks on samasse avaliku \"\n\"võtme infrastruktuuri kuuluvad juursertifikaadid ja \"\n\"kehtivuskinnitusteenuse osutaja.\"\nmsgstr \"\"\n\"The item is assigned to the voter externally. It is an X.509 format \"\n\"certificate whose validity is checked by the root certificates and the \"\n\"validation service provider, which are part of the same public key \"\n\"infrastructure.\"\n\n#: ../../07-kontrollimine.rst:194\nmsgid \"VALIJA SERTIFIKAAT, vormingu korrektsus\"\nmsgstr \"VOTER CERTIFICATE, correct formatting\"\n\n#: ../../07-kontrollimine.rst:196\nmsgid \"\"\n\"Valija sertifikaadi vormingu korrektsuse kontroll lähtudes X.509 \"\n\"spetsifikatsioonist.\"\nmsgstr \"\"\n\"Verification of the correctness of the format of the voter certificate \"\n\"based on the X.509 specification.\"\n\n#: ../../07-kontrollimine.rst:201\nmsgid \"VALIJA SERTIFIKAAT, protokollikohane kooskõlalisus valimise seadistustega\"\nmsgstr \"VOTER CERTIFICATE, protocol consistency with election settings\"\n\n#: ../../07-kontrollimine.rst:203\nmsgid \"\"\n\"Valija sertifikaadi protokollikohase kooskõlalisuse kontroll valimise \"\n\"seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Checking the conformance of the voter certificate with the election \"\n\"settings. The check succeeds if:\"\n\n#: ../../07-kontrollimine.rst:206\nmsgid \"\"\n\"Valija sertifikaat on kehtiv lähtudes sertifikaadis sisalduvast \"\n\"kehtivusajast;\"\nmsgstr \"\"\n\"The voter's certificate is valid for the period of validity contained in \"\n\"the certificate;\"\n\n#: ../../07-kontrollimine.rst:208\nmsgid \"\"\n\"Valija sertifikaat kuulub mõnda valimise seadistustes kirjeldatud \"\n\"sertifitseerimishierarhiatest.\"\nmsgstr \"\"\n\"The voter certificate belongs to one of the certification hierarchies \"\n\"described in the election settings.\"\n\n#: ../../07-kontrollimine.rst:215\nmsgid \"\"\n\"VALIJA SERTIFIKAAT, KEHTIVUSKINNITUS, protokollikohane kooskõlalisus \"\n\"valimise seadistustega\"\nmsgstr \"\"\n\"VOTER CERTIFICATE, CERTIFICATE VALIDITY PROOF, protocol compliance with \"\n\"the election settings.\"\n\n#: ../../07-kontrollimine.rst:217\nmsgid \"\"\n\"Valija sertifikaadi ja kehtivuskinnituse protokollikohase kooskõlalisuse \"\n\"kontroll valimise seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Checking that the voter certificate and the certificate validity proof \"\n\"are in accordance with the protocol and in line with the election \"\n\"settings. The check succeeds if:\"\n\n#: ../../07-kontrollimine.rst:220\nmsgid \"\"\n\"Kehtivuskinnitus on korrektselt allkirjastatud kehtivuskinnituse teenuse \"\n\"osutaja poolt, kellel on seadistuste kohaselt voli valija sertifikaadi \"\n\"kehtivust tõendada\"\nmsgstr \"\"\n\"A certificate validity proof is correctly signed by validation service \"\n\"provider who has the power, according to the settings, to verify the \"\n\"validity of the voter's certificate.\"\n\n#: ../../07-kontrollimine.rst:224\nmsgid \"Kehtivuskinnitus on väljastatud valija sertifikaadi kohta;\"\nmsgstr \"A validation certificate is issued for a voter certificate;\"\n\n#: ../../07-kontrollimine.rst:226\nmsgid \"Kehtivuskinnitus näitab valija sertifikaadi OCSP olekuks 'Kehtiv'.\"\nmsgstr \"\"\n\"The validity confirmation indicates the OCSP status of the voter's \"\n\"certificate as 'Valid'.\"\n\n#: ../../07-kontrollimine.rst:233\nmsgid \"VALIJA IDENTITEET\"\nmsgstr \"VOTER IDENTITY\"\n\n#: ../../07-kontrollimine.rst:235\nmsgid \"\"\n\"Olem on valijale omistatud süsteemiväliselt. Tegemist on isikukoodiga, \"\n\"mille kontrollimise aluseks on VALIJA SERTIFIKAAT.\"\nmsgstr \"\"\n\"The item is assigned to the voter externally. It is a personal \"\n\"identification code, which is verified by the VOTER CERTIFICATE.\"\n\n#: ../../07-kontrollimine.rst:241\nmsgid \"VALIJA IDENTITEET, vormingu korrektsus\"\nmsgstr \"VOTER IDENTITY, correctness of formatting\"\n\n#: ../../07-kontrollimine.rst:243\nmsgid \"\"\n\"Valija identiteedi vormingu korrektsuse kontroll lähtudes Eesti Vabariigi\"\n\" isikukoodi vormingust.\"\nmsgstr \"\"\n\"Verification of the correctness of the format of the voter's identity \"\n\"based on the format of the personal identity code of the Republic of \"\n\"Estonia.\"\n\n#: ../../07-kontrollimine.rst:249\nmsgid \"VALIJA IDENTITEET, NIMEKIRJATUNNUS, hääleõigus\"\nmsgstr \"VOTER IDENTITY, DISTRICT ID, eligibility\"\n\n#: ../../07-kontrollimine.rst:251\nmsgid \"\"\n\"Hääleõiguse kontroll valija identiteedi ja nimekirjatunnuse alusel. \"\n\"Kontroll õnnestub, kui valija identiteet on kantud nimekirjatunnusele \"\n\"vastavasse valijate nimekirja. Sellisel juhul on identiteedile omistatud \"\n\"ka ringkonnatunnus, mis määrab ringkonnaspetsiifilise valikute nimekirja.\"\nmsgstr \"\"\n\"Verification of the right to vote based on voter identity and district \"\n\"id. The check is successful if the voter's identity is entered in the \"\n\"electoral roll corresponding to the district id. In this case, the \"\n\"identity is also assigned a precinct identifier, which determines the \"\n\"precinct-specific list of choices.\"\n\n#: ../../07-kontrollimine.rst:260\nmsgid \"HÄÄLE SIGNATUUR\"\nmsgstr \"VOTE SIGNATURE\"\n\n#: ../../07-kontrollimine.rst:262\nmsgid \"\"\n\"Olem arvutatakse allkirjastamise vahendi poolt lähtudes valijarakenduse \"\n\"poolt sisendiks antud KRÜPTEERITUD SEDELI räsist.\"\nmsgstr \"\"\n\"The element is calculated by the signing tool on the basis of the \"\n\"ENCRYPTED BALLOT given as input by the voter application.\"\n\n#: ../../07-kontrollimine.rst:268\nmsgid \"HÄÄLE SIGNATUUR, vormingu korrektsus\"\nmsgstr \"VOTE SIGNATURE, correctness of the format\"\n\n#: ../../07-kontrollimine.rst:270\nmsgid \"\"\n\"Hääle signatuuri vormingu korrektsuse kontroll lähtudes konkreetsest \"\n\"signeerimismeetodist.\"\nmsgstr \"\"\n\"Verification of the correctness of the vote signature format based on a \"\n\"specific signing method.\"\n\n#: ../../07-kontrollimine.rst:276\nmsgid \"HÄÄLE SIGNATUUR, KRÜPTEERITUD SEDEL, VALIJA SERTIFIKAAT, kooskõlalisus\"\nmsgstr \"VOTE SIGNATURE, ENCRYPTED BALLOT, VOTER CERTIFICATE, consistency\"\n\n#: ../../07-kontrollimine.rst:278\nmsgid \"\"\n\"Kontroll, mis õnnestub siis ja ainult siis kui õnnestub verifitseerida, \"\n\"et hääle signatuur on arvutatud krüpteeritud sedeli räsist kasutades \"\n\"valija sertifikaadis leiduvale avalikule võtmele vastavat privaatvõtit.\"\nmsgstr \"\"\n\"A check that succeeds if and only if it can be verified that the \"\n\"signature of the vote is computed from a hash of the encrypted ciphertext\"\n\" using the private key corresponding to the public key in the voter's \"\n\"certificate.\"\n\n#: ../../07-kontrollimine.rst:286\nmsgid \"HÄÄLE ALLKIRI\"\nmsgstr \"SIGNED VOTE\"\n\n#: ../../07-kontrollimine.rst:288\nmsgid \"\"\n\"Olem moodustatakse valijarakenduses, talletades hääle signatuuri allkirja\"\n\" vormingusse.\"\nmsgstr \"\"\n\"An item is formed in the voter application by storing the signature of \"\n\"the vote in a signature format.\"\n\n#: ../../07-kontrollimine.rst:293\nmsgid \"HÄÄLE ALLKIRI, vormingu korrektsus\"\nmsgstr \"SIGNED VOTE, correct formatting\"\n\n#: ../../07-kontrollimine.rst:295\nmsgid \"\"\n\"Hääle allkirja vormingu korrektsuse kontroll käesoleva spetsifikatsiooni \"\n\"suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the signed vote format against this \"\n\"specification.\"\n\n#: ../../07-kontrollimine.rst:300\nmsgid \"HÄÄLE ALLKIRI, HÄÄLE SIGNATUUR, kooskõlalisus\"\nmsgstr \"SIGNED VOTE, VOTE SIGNATURE, consistency\"\n\n#: ../../07-kontrollimine.rst:302\nmsgid \"\"\n\"Hääle allkirja ja hääle signatuuri kooskõlalisuse kontroll. Kontroll \"\n\"õnnestub, kui antud allkiri sisaldab antud signatuuri.\"\nmsgstr \"\"\n\"Verification of the consistency of the signed vote and the vote \"\n\"signature. The check succeeds if the signed vote contains the given \"\n\"signature.\"\n\n#: ../../07-kontrollimine.rst:309\nmsgid \"HÄÄLE KONTEINER\"\nmsgstr \"VOTE CONTAINER\"\n\n#: ../../07-kontrollimine.rst:311\nmsgid \"\"\n\"Olem moodustatakse valijarakenduses, luues vormingukohane konteiner, mis \"\n\"sisaldab muuhulgas krüpteeritud sedelit, hääle allkirja ja valija \"\n\"sertifikaati.\"\nmsgstr \"\"\n\"The item is formed in the voter application by creating a formatted \"\n\"container containing, among other things, the encrypted ballot, the \"\n\"voter's signature and the voter's certificate.\"\n\n#: ../../07-kontrollimine.rst:317\nmsgid \"HÄÄLE KONTEINER, vormingu korrektsus\"\nmsgstr \"VOTE CONTAINER, format correctness\"\n\n#: ../../07-kontrollimine.rst:319\nmsgid \"\"\n\"Hääle konteineri vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the vote container format with respect\"\n\" to this specification.\"\n\n#: ../../07-kontrollimine.rst:324\nmsgid \"\"\n\"HÄÄLE KONTEINER, HÄÄLE ALLKIRI, VALIJA SERTIFIKAAT, KRÜPTEERITUD SEDEL, \"\n\"kooskõlalisus\"\nmsgstr \"\"\n\"VOTE CONTAINER, SIGNED VOTE, VOTER CERTIFICATE, ENCRPYPTED BALLOT, \"\n\"consistency\"\n\n#: ../../07-kontrollimine.rst:326\nmsgid \"\"\n\"Hääle konteineri, hääle allkirja, valija sertifikaadi ja krüpteeritud \"\n\"sedeli kooskõlalisuse kontroll. Kontroll õnnestub kui antud konteiner \"\n\"sisaldab konkreetset hääle allkirja, valija sertifikaati ja krüpteeritud \"\n\"sedelit.\"\nmsgstr \"\"\n\"Verification of the consistency of the vote container, the signed vote, \"\n\"the voter certificate and the encrypted ballot. The check succeeds if the\"\n\" given container contains a specific vote signature, voter certificate \"\n\"and encrypted ballot.\"\n\n#: ../../07-kontrollimine.rst:334\nmsgid \"KEHTIVUSKINNITUS\"\nmsgstr \"CERTIFICATE VALIDITY PROOF\"\n\n#: ../../07-kontrollimine.rst:336\nmsgid \"\"\n\"Olemi hankimise eest vastutab kogumisteenus. Tegemist on OCSP vormingus \"\n\"kinnitusega valija sertifikaadi oleku kohta.\"\nmsgstr \"\"\n\"The collection service is responsible for the procurement of the item. \"\n\"This is a confirmation of the status of the voter's certificate in OCSP \"\n\"format.\"\n\n#: ../../07-kontrollimine.rst:342\nmsgid \"KEHTIVUSKINNITUS, vormingu korrektsus\"\nmsgstr \"CERTIFICATE VALIDITY PROOF, correctness of the format\"\n\n#: ../../07-kontrollimine.rst:344\nmsgid \"\"\n\"Kehtivuskinnituse vormingu korrektsuse kontroll OCSP spetsifikatsiooni \"\n\"suhtes.\"\nmsgstr \"\"\n\"Validation of the correctness of the validation format against the OCSP \"\n\"specification.\"\n\n#: ../../07-kontrollimine.rst:350\nmsgid \"KEHTIVUSKINNITUS, AJATEMPEL, ajaline järgnevus\"\nmsgstr \"CERTIFICATE VALIDITY PROOF, TIMESTAMP, chronological sequence\"\n\n#: ../../07-kontrollimine.rst:352\nmsgid \"\"\n\"Kehtivuskinnituse ja ajatempli ajalise järgnevuse kontroll. Kontroll \"\n\"õnnestub, kui\"\nmsgstr \"\"\n\"Verification of the chronological sequence of the certificate validity \"\n\"proof and the timestamp. The check is successful if\"\n\n#: ../../07-kontrollimine.rst:354\nmsgid \"ajatempel ei ole väljastatud hiljem kui kehtivuskinnitus;\"\nmsgstr \"\"\n\"the time stamp is issued no later than the date of validity of the \"\n\"certificate validity proof;\"\n\n#: ../../07-kontrollimine.rst:356\nmsgid \"\"\n\"ajatempli ja kehtivuskinnituse väljastamise ajaline vahe on väiksem kui \"\n\"valimise seadistustes ette nähtud aeg.\"\nmsgstr \"\"\n\"the time difference between the issue of the timestamp and the \"\n\"certificate validity proof is less than determined in election settings.\"\n\n#: ../../07-kontrollimine.rst:364\nmsgid \"AJATEMPEL\"\nmsgstr \"TIMESTAMP\"\n\n#: ../../07-kontrollimine.rst:366\nmsgid \"\"\n\"Olemi hankimise eest vastutab kogumisteenus. Tegemist on PKIX vormingus \"\n\"ajatempliga.\"\nmsgstr \"\"\n\"The collection service is responsible for the procurement of the item. \"\n\"This is a timestamp in PKIX format.\"\n\n#: ../../07-kontrollimine.rst:371\nmsgid \"AJATEMPEL, vormingu korrektsus\"\nmsgstr \"TIMESTAMP, correctness of formatting\"\n\n#: ../../07-kontrollimine.rst:373\nmsgid \"Ajatempli vormingu korrektsuse kontroll PKIX spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Checking the correctness of the timestamp format against the PKIX \"\n\"specification.\"\n\n#: ../../07-kontrollimine.rst:379\nmsgid \"\"\n\"AJATEMPEL, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus valimise \"\n\"seadistustega\"\nmsgstr \"TIMESTAMP, VOTE SIGNATURE, protocol consistency with election settings\"\n\n#: ../../07-kontrollimine.rst:381\nmsgid \"\"\n\"Ajatempli protokollikohase kooskõlalisuse kontroll valimise \"\n\"seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Checking the timestamp protocol consistency with the election settings. \"\n\"The check succeeds if:\"\n\n#: ../../07-kontrollimine.rst:384\nmsgid \"\"\n\"ajatempel on korrektselt allkirjastatud valimise seadistuses volitatud \"\n\"ajatempliteenuse osutaja poolt;\"\nmsgstr \"\"\n\"the timestamp is correctly signed by the provider authorized in the \"\n\"election settings\"\n\n#: ../../07-kontrollimine.rst:387\nmsgid \"ajatempel on võetud hääle allkirjale.\"\nmsgstr \"The timestamp has been taken to the signature of the vote.\"\n\n#: ../../07-kontrollimine.rst:393\nmsgid \"REGISTREERIMISPÄRING\"\nmsgstr \"REGISTRATION REQUEST\"\n\n#: ../../07-kontrollimine.rst:395 ../../07-kontrollimine.rst:443\nmsgid \"\"\n\"Olem moodustatakse kogumisteenuses. Olemi talletamise eest vastutab \"\n\"registreerimisteenuse osutaja.\"\nmsgstr \"\"\n\"The item is formed in the collection service. The storage of the item is \"\n\"the responsibility of the registration service provider.\"\n\n#: ../../07-kontrollimine.rst:401\nmsgid \"REGISTREERIMISPÄRING, vormingu korrektsus\"\nmsgstr \"REGISTRATION REQUEST, correctness of format\"\n\n#: ../../07-kontrollimine.rst:403\nmsgid \"\"\n\"Registreerimispäringu vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the registration request format \"\n\"against this specification.\"\n\n#: ../../07-kontrollimine.rst:409\nmsgid \"\"\n\"REGISTREERIMISPÄRING, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus \"\n\"valimise seadistustega\"\nmsgstr \"\"\n\"REGISTRATION REQUEST, VOTE SIGNATURE, protocol consistency with election \"\n\"settings\"\n\n#: ../../07-kontrollimine.rst:411\nmsgid \"\"\n\"Registreerimispäringu ja hääle allkirja protokollikohase kooskõlalisuse \"\n\"kontroll valimise seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Verification of the consistency of the registration request and the vote \"\n\"signature with the protocol settings for the election. The check succeeds\"\n\" if:\"\n\n#: ../../07-kontrollimine.rst:414\nmsgid \"Registreerimispäring on koostatud hääle allkirjale;\"\nmsgstr \"\"\n\"The registration request is drawn up on the basis of the signature of the\"\n\" vote;\"\n\n#: ../../07-kontrollimine.rst:416 ../../07-kontrollimine.rst:428\n#: ../../07-kontrollimine.rst:465\nmsgid \"\"\n\"Registreerimispäring on korrektselt allkirjastatud valimise seadistuses \"\n\"viidatud kogumisteenuse poolt.\"\nmsgstr \"\"\n\"The registration request has been correctly signed by the collection \"\n\"service shown in the election settings.\"\n\n#: ../../07-kontrollimine.rst:423\nmsgid \"REGISTREERIMISPÄRING, REGISTREERIMISTÕEND, kooskõlalisus\"\nmsgstr \"REGISTRATION REQUEST, REGISTRATION CERTIFICATE, consistency\"\n\n#: ../../07-kontrollimine.rst:425\nmsgid \"\"\n\"Registreerimispäringu ja registreerimistõendi protokollikohase \"\n\"kooskõlalisuse kontroll valimise seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Verification of the consistency of the registration request and the \"\n\"registration certificate with the protocol election settings. The check \"\n\"succeeds if:\"\n\n#: ../../07-kontrollimine.rst:431\nmsgid \"Registreerimistõend on koostatud vastuseks samale registreerimispäringule;\"\nmsgstr \"\"\n\"The registration certificate is issued in response to the same \"\n\"registration request;\"\n\n#: ../../07-kontrollimine.rst:433 ../../07-kontrollimine.rst:499\nmsgid \"\"\n\"Registreerimistõend on korrektselt allkirjastatud valimise seadistuses \"\n\"viidatud registreerimisteenuse poolt.\"\nmsgstr \"\"\n\"The certificate of registration is correctly signed by the service \"\n\"provider defined in the election settings.\"\n\n#: ../../07-kontrollimine.rst:441\nmsgid \"REGISTREERIMISPÄRINGU KONTEINER\"\nmsgstr \"REGISTRATION REQUEST CONTAINER\"\n\n#: ../../07-kontrollimine.rst:449\nmsgid \"REGISTREERIMISPÄRINGU KONTEINER, vormingu korrektsus\"\nmsgstr \"REGISTRATION REQUEST CONTAINER, correctness of format\"\n\n#: ../../07-kontrollimine.rst:451\nmsgid \"\"\n\"Registreerimispäringu konteineri vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the registration request container \"\n\"format with respect to this specification.\"\n\n#: ../../07-kontrollimine.rst:458\nmsgid \"\"\n\"REGISTREERIMISPÄRINGU KONTEINER, REGISTREERIMISPÄRING, protokollikohane \"\n\"kooskõlalisus valimise seadistustega\"\nmsgstr \"\"\n\"REGISTRATION REQUEST CONTAINER, REGISTRATION REQUEST, protocol \"\n\"consistency with election settings\"\n\n#: ../../07-kontrollimine.rst:460\nmsgid \"\"\n\"Registreerimispäringu konteineri ja registreerimispäringu \"\n\"protokollikohase kooskõlalisuse kontroll valimise seadistustega. Kontroll\"\n\" õnnestub kui:\"\nmsgstr \"\"\n\"Check the consistency of the registration request container and the \"\n\"registration request protocol with the election settings. The check \"\n\"succeeds if:\"\n\n#: ../../07-kontrollimine.rst:463\nmsgid \"Registreerimispäringu konteiner sisaldab registreerimispäringut;\"\nmsgstr \"The registration request container contains the registration request;\"\n\n#: ../../07-kontrollimine.rst:473\nmsgid \"REGISTREERIMISTÕEND\"\nmsgstr \"REGISTRATION CERTIFICATE\"\n\n#: ../../07-kontrollimine.rst:475\nmsgid \"\"\n\"Olemi hankimise eest vastutab kogumisteenus, olemi moodustab \"\n\"registreerimisteenuse osutaja.\"\nmsgstr \"\"\n\"The collection service is responsible for the procurement of the item, \"\n\"the item is formed by the registration service provider.\"\n\n#: ../../07-kontrollimine.rst:481\nmsgid \"REGISTREERIMISTÕEND, vormingu korrektsus\"\nmsgstr \"REGISTRATION CERTIFICATE, correctness of format\"\n\n#: ../../07-kontrollimine.rst:483\nmsgid \"\"\n\"Registreerimistõendi vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the format of the registration \"\n\"certificate against this specification.\"\n\n#: ../../07-kontrollimine.rst:489\nmsgid \"\"\n\"REGISTREERIMISTÕEND, HÄÄLE ALLKIRI, protokollikohane kooskõlalisus \"\n\"valimise seadistustega\"\nmsgstr \"\"\n\"REGISTRATION CERTIFICATE, VOTE SIGNATURE, protocol consistency with \"\n\"election settings\"\n\n#: ../../07-kontrollimine.rst:491\nmsgid \"\"\n\"Registreerimistõendi ja hääle allkirja protokollikohase kooskõlalisuse \"\n\"kontroll valimise seadistustega. Kontroll õnnestub kui:\"\nmsgstr \"\"\n\"Verification of the consistency of the certificate of registration and \"\n\"the signature of the vote with the protocol settings for the election. \"\n\"The check succeeds if:\"\n\n#: ../../07-kontrollimine.rst:494\nmsgid \"Registreerimistõend on koostatud hääle allkirjale;\"\nmsgstr \"The registration certificate is drawn up on the signature of the vote;\"\n\n#: ../../07-kontrollimine.rst:496\nmsgid \"\"\n\"Registreerimistõend sisaldab kogumisteenuse poolt korrektselt \"\n\"allkirjastatud registreerimispäringut;\"\nmsgstr \"\"\n\"The registration certificate shall contain the following information, \"\n\"duly completed by the collection service.\"\n\n#: ../../07-kontrollimine.rst:507\nmsgid \"NIMEKIRJATUNNUS\"\nmsgstr \"LIST IDENTIFIER\"\n\n#: ../../07-kontrollimine.rst:509\nmsgid \"\"\n\"Olem moodustatakse kogumisteenuses ning identifitseerib ühe konkreetse \"\n\"versiooni valijate nimekirjast.\"\nmsgstr \"\"\n\"An item is formed in the collection service and identifies one specific \"\n\"version of the voter list.\"\n\n#: ../../07-kontrollimine.rst:515\nmsgid \"NIMEKIRJATUNNUS, vormingu korrektsus\"\nmsgstr \"LIST IDENTIFIER, correctness of formatting\"\n\n#: ../../07-kontrollimine.rst:517\nmsgid \"\"\n\"Nimekirjatunnuse vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the list identifier format with \"\n\"respect to this specification.\"\n\n#: ../../07-kontrollimine.rst:522\nmsgid \"NIMEKIRJATUNNUS, protokollikohane kooskõlalisus valimise seadistusega\"\nmsgstr \"LIST IDENTIFIER, protocol consistency with the selection setting\"\n\n#: ../../07-kontrollimine.rst:524\nmsgid \"\"\n\"Nimekirjatunnuse protokollikohase kooskõlalisuse kontroll valimise \"\n\"seadistusega. Kontroll õnnestub kui valimise seadistuses sisalduvate \"\n\"muudatusnimekirjade alusel on võimalik koostada nimekirjatunnusele vastav\"\n\" nimekiri.\"\nmsgstr \"\"\n\"Checking the protocol consistency of the list identifier with the \"\n\"election setting. The check succeeds if the voter list updates contained \"\n\"in the election configuration can be used to generate a voterlist \"\n\"corresponding to the list identifier.\"\n\n#: ../../07-kontrollimine.rst:534\nmsgid \"MIKSITUD KRÜPTEERITUD SEDEL\"\nmsgstr \"MIXED ENCRYPTED BALLOT\"\n\n#: ../../07-kontrollimine.rst:536\nmsgid \"\"\n\"Olem moodustatakse miksimisrakenduses. Miksitud krüpteeritud sedel on \"\n\"sisuliselt krüpteeritud sedel ning rakenduvad samad kontrollid, mis \"\n\"krüpteeritud sedelilegi.\"\nmsgstr \"\"\n\"The element is formed in the mixing application. A mixed encrypted ballot\"\n\" is essentially an encrypted ballot and the same controls apply as for an\"\n\" encrypted ballot.\"\n\n#: ../../07-kontrollimine.rst:543\nmsgid \"MIKSITUD KRÜPTEERITUD SEDEL, vormingu korrektsus\"\nmsgstr \"MIXED ENCRYPTED BALLOT, format correctness\"\n\n#: ../../07-kontrollimine.rst:545\nmsgid \"\"\n\"Miksitud krüpteeritud sedeli vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the format of the mixed encrypted \"\n\"ballot with respect to this specification.\"\n\n#: ../../07-kontrollimine.rst:551\nmsgid \"MIKSITUD KRÜPTEERITUD SEDEL, kooskõlalisus avaliku võtmega\"\nmsgstr \"MIXED ENCRYPTED BALLOT, consistency with public key\"\n\n#: ../../07-kontrollimine.rst:553\nmsgid \"\"\n\"Miksitud krüpteeritud sedeli kooskõlalisuse kontroll avaliku võtmega. \"\n\"Kontroll õnnestub kui miksitud krüpteeritud sedeli komponendid on \"\n\"kasutatavad rühma liikmetena arvutusteks avaliku võtme parameetrite poolt\"\n\" määratud matemaatilises rühmas.\"\nmsgstr \"\"\n\"Checking the consistency of a mixed encrypted ballot with a public key. \"\n\"The check succeeds if the components of the mixed encrypted ballot are \"\n\"usable as members of a group for computations in the mathematical group \"\n\"specified by the public key parameters.\"\n\n#: ../../07-kontrollimine.rst:563\nmsgid \"RINGKONNATUNNUS\"\nmsgstr \"DISTRICT IDENTIFIER\"\n\n#: ../../07-kontrollimine.rst:565\nmsgid \"\"\n\"Olem moodustatakse töötlemisrakenduses ning viitab ringkonnapõhisele \"\n\"valikute nimekirjale.\"\nmsgstr \"\"\n\"The item is created in the processing application and refers to a \"\n\"district-based list of choices.\"\n\n#: ../../07-kontrollimine.rst:571\nmsgid \"RINGKONNATUNNUS, vormingu korrektsus\"\nmsgstr \"DISTRICT IDENTIFIER, correctness of formatting\"\n\n#: ../../07-kontrollimine.rst:573\nmsgid \"\"\n\"Ringkonnatunnuse vormingu korrektsuse kontroll käesoleva \"\n\"spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the district identifier format with \"\n\"respect to this specification.\"\n\n#: ../../07-kontrollimine.rst:579\nmsgid \"RINGKONNAPÕHINE VALIKUTE NIMEKIRI\"\nmsgstr \"DISTRICT CHOICES\"\n\n#: ../../07-kontrollimine.rst:581\nmsgid \"Olem on valijale omistatud süsteemiväliselt.\"\nmsgstr \"The item is assigned to the voter externally.\"\n\n#: ../../07-kontrollimine.rst:586\nmsgid \"RINGKONNAPÕHINE VALIKUTE NIMEKIRI, vormingu korrektsus\"\nmsgstr \"DISTRICT CHOICES, format correctness\"\n\n#: ../../07-kontrollimine.rst:588\nmsgid \"\"\n\"Ringkonnapõhise valikute nimekirja vormingu korrektsuse kontroll VIS \"\n\"liideste spetsifikatsiooni suhtes.\"\nmsgstr \"\"\n\"Verification of the correctness of the format of the district-based list \"\n\"of choices against the VIS interface specification.\"\n\n#: ../../07-kontrollimine.rst:592\nmsgid \"Kontrollid kogumisteenuses\"\nmsgstr \"Checks in the Collection Service\"\n\n#: ../../07-kontrollimine.rst:594\nmsgid \"\"\n\"Kogumisteenus käsitleb talletatavat häält sõltumatult teistest häältest. \"\n\"Kogumisteenuse ülesandeks on hääle talletamine kogu hääletamisperioodi \"\n\"vältel ja häälele töötlemise käigus kvalifitseerumiseks vajalike \"\n\"elementide hankimine.\"\nmsgstr \"\"\n\"The collection service treats the vote to be stored independently of \"\n\"other votes. The role of the collection service is to store votes \"\n\"throughout the voting period and to obtain the elements necessary to \"\n\"qualify a vote during the processing.\"\n\n#: ../../07-kontrollimine.rst:598\nmsgid \"Kogumisteenus saab valijarakenduselt järgmised olemid:\"\nmsgstr \"A collection service receives following items from the voter application:\"\n\n#: ../../07-kontrollimine.rst:600 ../../07-kontrollimine.rst:712\n#: ../../07-kontrollimine.rst:819 ../../07-kontrollimine.rst:898\n#: ../../07-kontrollimine.rst:1013 ../../07-kontrollimine.rst:1086\nmsgid \":ref:`entity-krypteeritud-sedel`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:602 ../../07-kontrollimine.rst:720\n#: ../../07-kontrollimine.rst:750 ../../07-kontrollimine.rst:821\n#: ../../07-kontrollimine.rst:900 ../../07-kontrollimine.rst:1027\n#: ../../07-kontrollimine.rst:1088\nmsgid \":ref:`entity-valija-sertifikaat`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:604 ../../07-kontrollimine.rst:722\n#: ../../07-kontrollimine.rst:751 ../../07-kontrollimine.rst:823\n#: ../../07-kontrollimine.rst:902 ../../07-kontrollimine.rst:1029\n#: ../../07-kontrollimine.rst:1090\nmsgid \":ref:`entity-valija-identiteet`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:606 ../../07-kontrollimine.rst:726\n#: ../../07-kontrollimine.rst:825 ../../07-kontrollimine.rst:904\n#: ../../07-kontrollimine.rst:1031 ../../07-kontrollimine.rst:1092\nmsgid \":ref:`entity-haale-signatuur`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:608 ../../07-kontrollimine.rst:714\n#: ../../07-kontrollimine.rst:827 ../../07-kontrollimine.rst:906\n#: ../../07-kontrollimine.rst:1033 ../../07-kontrollimine.rst:1094\nmsgid \":ref:`entity-haale-allkiri`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:610 ../../07-kontrollimine.rst:716\n#: ../../07-kontrollimine.rst:829 ../../07-kontrollimine.rst:908\n#: ../../07-kontrollimine.rst:1035 ../../07-kontrollimine.rst:1096\nmsgid \":ref:`entity-haale-konteiner`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:612\nmsgid \"\"\n\"Kogumisteenus hangib hääle töötlemise käigus välistelt teenustelt \"\n\"järgmised olemid:\"\nmsgstr \"\"\n\"In the course of vote processing, the Collection Service obtains the \"\n\"following entities from external services:\"\n\n#: ../../07-kontrollimine.rst:614 ../../07-kontrollimine.rst:728\n#: ../../07-kontrollimine.rst:779 ../../07-kontrollimine.rst:831\n#: ../../07-kontrollimine.rst:910 ../../07-kontrollimine.rst:1037\n#: ../../07-kontrollimine.rst:1098\nmsgid \":ref:`entity-kehtivuskinnitus`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:616 ../../07-kontrollimine.rst:730\n#: ../../07-kontrollimine.rst:780 ../../07-kontrollimine.rst:833\n#: ../../07-kontrollimine.rst:912 ../../07-kontrollimine.rst:1039\n#: ../../07-kontrollimine.rst:1100\nmsgid \":ref:`entity-ajatempel`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:618 ../../07-kontrollimine.rst:732\n#: ../../07-kontrollimine.rst:781 ../../07-kontrollimine.rst:835\n#: ../../07-kontrollimine.rst:918 ../../07-kontrollimine.rst:1045\n#: ../../07-kontrollimine.rst:1106\nmsgid \":ref:`entity-registreerimistoend`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:620\nmsgid \"Kogumisteenus tuvastab / loob ise järgmised olemid:\"\nmsgstr \"The collection service detects/creates the following entities:\"\n\n#: ../../07-kontrollimine.rst:622 ../../07-kontrollimine.rst:734\n#: ../../07-kontrollimine.rst:782 ../../07-kontrollimine.rst:837\n#: ../../07-kontrollimine.rst:914 ../../07-kontrollimine.rst:1041\n#: ../../07-kontrollimine.rst:1102\nmsgid \":ref:`entity-registreerimisparing`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:624 ../../07-kontrollimine.rst:738\n#: ../../07-kontrollimine.rst:843 ../../07-kontrollimine.rst:916\n#: ../../07-kontrollimine.rst:1043 ../../07-kontrollimine.rst:1104\nmsgid \":ref:`entity-registreerimisparing-konteiner`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:626 ../../07-kontrollimine.rst:740\n#: ../../07-kontrollimine.rst:845 ../../07-kontrollimine.rst:920\n#: ../../07-kontrollimine.rst:1047 ../../07-kontrollimine.rst:1108\nmsgid \":ref:`entity-nimekirjatunnus`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:628 ../../07-kontrollimine.rst:742\n#: ../../07-kontrollimine.rst:847 ../../07-kontrollimine.rst:926\n#: ../../07-kontrollimine.rst:1015 ../../07-kontrollimine.rst:1074\nmsgid \":ref:`entity-ringkonnatunnus`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:630 ../../07-kontrollimine.rst:724\n#: ../../07-kontrollimine.rst:839 ../../07-kontrollimine.rst:922\n#: ../../07-kontrollimine.rst:1049 ../../07-kontrollimine.rst:1076\nmsgid \":ref:`entity-ringkonna-valikutenimekiri`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:632\nmsgid \"Kogumisteenus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The collection service does not come into direct contact with:\"\n\n#: ../../07-kontrollimine.rst:634 ../../07-kontrollimine.rst:708\n#: ../../07-kontrollimine.rst:813 ../../07-kontrollimine.rst:933\n#: ../../07-kontrollimine.rst:1023 ../../07-kontrollimine.rst:1080\nmsgid \":ref:`entity-tahteavaldus`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:636 ../../07-kontrollimine.rst:710\n#: ../../07-kontrollimine.rst:817 ../../07-kontrollimine.rst:935\n#: ../../07-kontrollimine.rst:1025 ../../07-kontrollimine.rst:1084\nmsgid \":ref:`entity-juhuslikkus`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:638 ../../07-kontrollimine.rst:744\n#: ../../07-kontrollimine.rst:849 ../../07-kontrollimine.rst:931\n#: ../../07-kontrollimine.rst:1019 ../../07-kontrollimine.rst:1072\nmsgid \":ref:`entity-miksitud-krypteeritud-sedel`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:640\nmsgid \"\"\n\"Kogumisteenus viib läbi järgmised tegevused ja teostab järgmised \"\n\"kontrollid:\"\nmsgstr \"The collection service will carry out the following activities and checks:\"\n\n#: ../../07-kontrollimine.rst:642\nmsgid \"Hääle talletamise päringu vastuvõtmine valijarakenduselt\"\nmsgstr \"Receiving a vote storage request from a voter application\"\n\n#: ../../07-kontrollimine.rst:644 ../../07-kontrollimine.rst:753\n#: ../../07-kontrollimine.rst:853 ../../07-kontrollimine.rst:952\nmsgid \":ref:`check-valija-sertifikaat-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:645 ../../07-kontrollimine.rst:754\n#: ../../07-kontrollimine.rst:854 ../../07-kontrollimine.rst:953\nmsgid \":ref:`check-valija-sertifikaat-consistency-protocol-settings`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:646 ../../07-kontrollimine.rst:755\n#: ../../07-kontrollimine.rst:856 ../../07-kontrollimine.rst:954\nmsgid \":ref:`check-valija-identiteet-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:647 ../../07-kontrollimine.rst:957\nmsgid \":ref:`check-valija-identiteet-nimekirjatunnus-eligibility`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:648 ../../07-kontrollimine.rst:770\n#: ../../07-kontrollimine.rst:857 ../../07-kontrollimine.rst:959\nmsgid \":ref:`check-haale-signatuur-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:649 ../../07-kontrollimine.rst:771\n#: ../../07-kontrollimine.rst:858 ../../07-kontrollimine.rst:960\nmsgid \"\"\n\":ref:`check-haale-signatuur-krypteeritud-sedel-valija-sertifikaat-\"\n\"consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:650 ../../07-kontrollimine.rst:859\n#: ../../07-kontrollimine.rst:961\nmsgid \":ref:`check-haale-allkiri-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:651 ../../07-kontrollimine.rst:860\n#: ../../07-kontrollimine.rst:962\nmsgid \":ref:`check-haale-allkiri-haale-signatuur-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:652 ../../07-kontrollimine.rst:861\n#: ../../07-kontrollimine.rst:963\nmsgid \":ref:`check-haale-konteiner-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:653 ../../07-kontrollimine.rst:862\n#: ../../07-kontrollimine.rst:964\nmsgid \"\"\n\":ref:`check-haale-konteiner-haale-allkiri-valija-sertifikaat-\"\n\"krypteeritud-sedel-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:654 ../../07-kontrollimine.rst:875\n#: ../../07-kontrollimine.rst:998 ../../07-kontrollimine.rst:1055\nmsgid \":ref:`check-krypteeritud-sedel-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:655 ../../07-kontrollimine.rst:876\n#: ../../07-kontrollimine.rst:999 ../../07-kontrollimine.rst:1056\nmsgid \":ref:`check-krypteeritud-sedel-public-key-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:657\nmsgid \"Kehtivuskinnituse hankimine - :ref:`entity-kehtivuskinnitus`\"\nmsgstr \"Obtaining certificate validation proof - :ref:`entity-kehtivuskinnitus`\"\n\n#: ../../07-kontrollimine.rst:659 ../../07-kontrollimine.rst:784\n#: ../../07-kontrollimine.rst:863 ../../07-kontrollimine.rst:965\nmsgid \":ref:`check-kehtivuskinnitus-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:660 ../../07-kontrollimine.rst:785\n#: ../../07-kontrollimine.rst:855 ../../07-kontrollimine.rst:966\nmsgid \":ref:`check-valija-sertifikaat-kehtivuskinnitus-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:662\nmsgid \"Ajatempli hankimine - :ref:`entity-ajatempel`\"\nmsgstr \"Getting a timestamp - :ref:`entity-ajatempel`\"\n\n#: ../../07-kontrollimine.rst:664 ../../07-kontrollimine.rst:786\n#: ../../07-kontrollimine.rst:864 ../../07-kontrollimine.rst:967\nmsgid \":ref:`check-ajatempel-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:665 ../../07-kontrollimine.rst:787\n#: ../../07-kontrollimine.rst:865 ../../07-kontrollimine.rst:968\nmsgid \":ref:`check-ajatempel-consistency-protocol-settings`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:667\nmsgid \"Registreerimispäringu loomine - :ref:`entity-registreerimisparing`\"\nmsgstr \"Creating a registration request - :ref:`entity-registreerimisparing`\"\n\n#: ../../07-kontrollimine.rst:669\nmsgid \"Registreerimistõendi hankimine - :ref:`entity-registreerimistoend`\"\nmsgstr \"Obtaining a registration certificate - :ref:`entity-registreerimistoend`\"\n\n#: ../../07-kontrollimine.rst:671 ../../07-kontrollimine.rst:792\n#: ../../07-kontrollimine.rst:869 ../../07-kontrollimine.rst:990\nmsgid \":ref:`check-registreerimisparing-registreerimistoend-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:672 ../../07-kontrollimine.rst:793\n#: ../../07-kontrollimine.rst:870 ../../07-kontrollimine.rst:970\nmsgid \":ref:`check-registreerimistoend-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:673 ../../07-kontrollimine.rst:794\n#: ../../07-kontrollimine.rst:871 ../../07-kontrollimine.rst:971\nmsgid \":ref:`check-registreerimistoend-haale-allkiri-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:674 ../../07-kontrollimine.rst:788\n#: ../../07-kontrollimine.rst:866 ../../07-kontrollimine.rst:969\nmsgid \":ref:`check-kehtivuskinnitus-ajatempel-order`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:676\nmsgid \"\"\n\"Hääle talletamine, kvalifitseerivate elementide ja unikaalse \"\n\"identifikaatori tagastamine valijarakendusele.\"\nmsgstr \"\"\n\"Vote storage, return of qualifiers and unique identifiers to the voting \"\n\"application.\"\n\n#: ../../07-kontrollimine.rst:680\nmsgid \"Kontrollid valijarakenduses\"\nmsgstr \"Checks in the Voting Application\"\n\n#: ../../07-kontrollimine.rst:682\nmsgid \"\"\n\"Valijarakendus moodustab valija avakujul tahteavalduse põhjal \"\n\"krüpteeritud sedeli ning allkirjastab selle valija allkirja andmise \"\n\"vahendiga.\"\nmsgstr \"\"\n\"The voting application forms an encrypted ballot based on the voter's \"\n\"plaintext intent and signs it using the voter's signature device.\"\n\n#: ../../07-kontrollimine.rst:685\nmsgid \"\"\n\"Valijarakenduse rolliks peale hääle allkirjastamist on veenduda, et \"\n\"kogumisteenus käitus häält kvalifitseerivate elementide võtmisel \"\n\"protokollikohaselt ning et hääl on talletatud selliselt, et ta saab \"\n\"töötlemisrakenduse poolt arvesse võetud.\"\nmsgstr \"\"\n\"The role of the voting application, after the vote has been signed, is to\"\n\" make sure that the collection service acted according to protocol in \"\n\"taking the elements that qualify the vote and that the vote has been \"\n\"stored in such a way that it can be taken into account by the processing \"\n\"application.\"\n\n#: ../../07-kontrollimine.rst:690\nmsgid \"Valijarakendus viib läbi minimaalselt järgmised kontrollid:\"\nmsgstr \"\"\n\"As a minimum, the following checks will be carried out by the voting \"\n\"application:\"\n\n#: ../../07-kontrollimine.rst:692\nmsgid \"\"\n\"Kogumisteenus võttis kehtivuskinnituse valija sertifikaadile volitatud \"\n\"kehtivuskinnitusteenuselt. Valijarakendus kontrollib allkirja \"\n\"kehtivuskinnitusteenuse vastusel.\"\nmsgstr \"\"\n\"The collection service validated the voter's certificate at the \"\n\"authorised validation service. The voting application verifies the \"\n\"signature in the validation service response.\"\n\n#: ../../07-kontrollimine.rst:696\nmsgid \"\"\n\"Kogumisteenus registreeris valija poolt allkirjastatud hääle volitatud \"\n\"registreerimisteenuses. Valijarakendus kontrollib, et kogumisteenuse \"\n\"poolt moodustatud päring oli kogumisteenuse poolt signeeritud ning viitas\"\n\" korrektselt allkirjastatud häälele. Valijarakendus kontrollib, et \"\n\"registreerimisteenuse vastus on allkirjastatud õige registreerimisteenuse\"\n\" osutaja poolt ning sisaldab kogumisteenuse poolt allkirjastatud \"\n\"päringut.\"\nmsgstr \"\"\n\"The collection service registered the vote signed by the voter in the \"\n\"authorised registration service. The Voting Application verifies that the\"\n\" request generated by the Collection Service was signed by the Collection\"\n\" Service and referred to a correctly signed vote. The Voting Application \"\n\"checks that the confirmation from the registration service is signed by \"\n\"the correct registration service provider and includes the request signed\"\n\" by the collection service.\"\n\n#: ../../07-kontrollimine.rst:703\nmsgid \"\"\n\"Kui hääle kvalifitseerimiseks vajalike elementide kontroll ei õnnestu, \"\n\"siis teavitab valijarakendus sellest kasutajat.\"\nmsgstr \"\"\n\"If the verification of the elements necessary to qualify a vote fails, \"\n\"the voting application will notify the user.\"\n\n#: ../../07-kontrollimine.rst:706\nmsgid \"Valijarakendus loob ise järgmised olemid:\"\nmsgstr \"The voting application itself creates the following entities:\"\n\n#: ../../07-kontrollimine.rst:718\nmsgid \"Valijarakendus saab järgmised olemid teistelt osapooltelt:\"\nmsgstr \"The voting application receives the following entities from other parties:\"\n\n#: ../../07-kontrollimine.rst:736\nmsgid \"Valijarakendus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The voting application does not come into direct contact with:\"\n\n#: ../../07-kontrollimine.rst:746\nmsgid \"\"\n\"Valijarakendus viib läbi järgmised tegevused ja teostab järgmised \"\n\"kontrollid:\"\nmsgstr \"The voting application performs the following actions and checks:\"\n\n#: ../../07-kontrollimine.rst:748\nmsgid \"eID vahendi aktiveerimine ja valija identiteedi tuvastamine\"\nmsgstr \"activation of the eID tool and identification of the voter's identity\"\n\n#: ../../07-kontrollimine.rst:757\nmsgid \"\"\n\"Ringkonnapõhise valikutenimekirja tuvastamine - :ref:`entity-ringkonna-\"\n\"valikutenimekiri`\"\nmsgstr \"district-based candidate list - :ref:`entity-ringkonna-valikutenimekiri`.\"\n\n#: ../../07-kontrollimine.rst:760\nmsgid \"Tahteavalduse moodustamine - :ref:`entity-tahteavaldus`\"\nmsgstr \"Formation of an intent - :ref:`entity-tahteavaldus`\"\n\n#: ../../07-kontrollimine.rst:762\nmsgid \"Juhuarvu genereerimine - :ref:`entity-juhuslikkus`\"\nmsgstr \"Random number generation - :ref:`entity-juhuslikkus`\"\n\n#: ../../07-kontrollimine.rst:764\nmsgid \"Sedeli krüpteerimine - :ref:`entity-krypteeritud-sedel`\"\nmsgstr \"Encrypting ballot - :ref:`entity-krypteeritud-sedel`\"\n\n#: ../../07-kontrollimine.rst:766\nmsgid \"Krüpteeritud sedeli signeerimine - :ref:`entity-haale-signatuur`\"\nmsgstr \"Signing an encrypted ballot - :ref:`entity-haale-signatuur`\"\n\n#: ../../07-kontrollimine.rst:768\nmsgid \"Allkirja moodustamine signatuurist - :ref:`entity-haale-allkiri`\"\nmsgstr \"Forming a signed vote from a signature - :ref:`entity-haale-allkiri`\"\n\n#: ../../07-kontrollimine.rst:773\nmsgid \"Allkirjastatud konteineri moodustamine - :ref:`entity-haale-konteiner`\"\nmsgstr \"Formation of a signed container - :ref:`entity-haale-konteiner`\"\n\n#: ../../07-kontrollimine.rst:775\nmsgid \"Allkirjastatud konteineri edastamine kogumisteenusele.\"\nmsgstr \"Forwarding the signed container to the collection service.\"\n\n#: ../../07-kontrollimine.rst:777\nmsgid \"Kogumisteenuse vastuse kontrollimine\"\nmsgstr \"Checking the collection service response\"\n\n#: ../../07-kontrollimine.rst:790 ../../07-kontrollimine.rst:867\n#: ../../07-kontrollimine.rst:976\nmsgid \":ref:`check-registreerimisparing-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:791 ../../07-kontrollimine.rst:868\n#: ../../07-kontrollimine.rst:989\nmsgid \":ref:`check-registreerimisparing-haale-allkiri-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:797\nmsgid \"Kontrollid kontrollrakenduses\"\nmsgstr \"Checks in the Verification Application\"\n\n#: ../../07-kontrollimine.rst:799\nmsgid \"\"\n\"Sarnaselt valijarakendusele on kontrollrakenduse rolliks peale hääle \"\n\"allkirjastamist veenduda, et kogumisteenus käitus häält kvalifitseerivate\"\n\" elementide võtmisel protokollikohaselt ning et hääl on talletatud \"\n\"selliselt, et ta saab töötlemisrakenduse poolt arvesse võetud.\"\nmsgstr \"\"\n\"Similar to the voter application, the role of the verification \"\n\"application, once the vote has been signed, is to verify that the \"\n\"collection service acted in a protocol-compliant manner in capturing the \"\n\"elements that qualify the vote and that the vote has been stored in such \"\n\"a way that it can be taken into account by the processing application.\"\n\n#: ../../07-kontrollimine.rst:804\nmsgid \"\"\n\"Täiendavalt on kontrollrakenduse ülesandeks anda valijale tagasisidet, \"\n\"kas tema tahteavaldus sai valijarakenduse poolt korrektselt hääleks \"\n\"vormistatud.\"\nmsgstr \"\"\n\"In addition, it is the responsibility of the verification application to \"\n\"provide feedback to the voter on whether their intent was correctly \"\n\"formatted as a vote by the voter application.\"\n\n#: ../../07-kontrollimine.rst:807\nmsgid \"\"\n\"Kui hääle kvalifitseerimiseks vajalike elementide kontroll ei õnnestu, \"\n\"siis teavitab kontrollrakendus sellest kasutajat. Tahteavalduse \"\n\"korrektsuses peab valija ise veenduma.\"\nmsgstr \"\"\n\"If the verification of the elements necessary to qualify the vote fails, \"\n\"the verification application will inform the user. It is up to the voter \"\n\"to verify the correctness of the intent.\"\n\n#: ../../07-kontrollimine.rst:811\nmsgid \"Kontrollrakendus tuvastab ise järgmised olemid:\"\nmsgstr \"The verification application itself detects the following entities:\"\n\n#: ../../07-kontrollimine.rst:815\nmsgid \"Kontrollrakendus saab järgmised olemid teistelt osapooltelt:\"\nmsgstr \"\"\n\"A verification application receives the following entities from other \"\n\"parties:\"\n\n#: ../../07-kontrollimine.rst:841\nmsgid \"Kontrollrakendus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The verification application does not directly interact with:\"\n\n#: ../../07-kontrollimine.rst:851\nmsgid \"\"\n\"Kontrollrakendus viib läbi järgmised tegevused ja teostab järgmised \"\n\"kontrollid:\"\nmsgstr \"\"\n\"The verification application will carry out the following activities and \"\n\"perform the following checks:\"\n\n#: ../../07-kontrollimine.rst:872\nmsgid \":ref:`check-ringkonna-valikutenimekiri-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:873\nmsgid \":ref:`check-juhuslikkus-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:874\nmsgid \":ref:`check-juhuslikkus-public-key-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:877\nmsgid \":ref:`check-krypteeritud-sedel-juhuslikkus-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:878 ../../07-kontrollimine.rst:1117\nmsgid \":ref:`check-tahteavaldus-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:879\nmsgid \":ref:`check-tahteavaldus-ringkonna-valikutenimekiri-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:882\nmsgid \"Kontrollid töötlemisrakenduses\"\nmsgstr \"Checks in the Processing Application\"\n\n#: ../../07-kontrollimine.rst:884\nmsgid \"\"\n\"Töötlemisrakenduse sisendiks on e-valimiskast ja registreerimisteenuse \"\n\"väljavõte registreerimispäringutest. Töötlemisrakendus kontrollib mõlema \"\n\"andmehulga elemente kõigepealt eraldi ning seejärel püüab luua vastavuse \"\n\"nende vahel.\"\nmsgstr \"\"\n\"The input to the processing application is the e-voting box and the \"\n\"registration service extract of registration requests. The processing \"\n\"application first checks the elements of both data sets separately and \"\n\"then tries to establish a correspondence between them.\"\n\n#: ../../07-kontrollimine.rst:889\nmsgid \"\"\n\"Töötlemisrakendus otsustab, milline valija häältest oli viimane ning \"\n\"liigub töötlemise järgmisesse etappi. S.t. üks häält kvalifitseerivatest \"\n\"elementidest täidab hääle talletamise aja fikseerimise rolli ning selle \"\n\"elemendi põhjal moodustatakse üksikute häälte ajaline järgnevus. \"\n\"Olenevalt IVXV profiilist võib see element olla kehtivuskinnituse \"\n\"koosseisus (BDOC-TM), eraldi ajatemplina (BDOC-TS) või \"\n\"registreerimistõendi koosseisus (BDOC-TS).\"\nmsgstr \"\"\n\"The processing application decides which voter's vote was the last and \"\n\"moves to the next stage of processing. That is, one of the elements \"\n\"qualifying a vote performs the role of fixing the time at which the vote \"\n\"was recorded, and the temporal sequence of individual votes is formed \"\n\"based on this element. Depending on the IVXV profile, this element may be\"\n\" part of a validity confirmation (BDOC-TM), a separate time stamp (BDOC-\"\n\"TS) or a registration certificate (BDOC-TS).\"\n\n#: ../../07-kontrollimine.rst:896\nmsgid \"Töötlemisrakendusele tehakse kättesaadavaks järgmised olemid:\"\nmsgstr \"\"\n\"The following entities will be made available to the processing \"\n\"application:\"\n\n#: ../../07-kontrollimine.rst:924\nmsgid \"Töötlemisrakendus tuvastab/loob järgmised olemid:\"\nmsgstr \"The processing application detects/creates the following entities:\"\n\n#: ../../07-kontrollimine.rst:929\nmsgid \"Töötlemisrakendus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The processing application does not come into direct contact with:\"\n\n#: ../../07-kontrollimine.rst:937\nmsgid \"Töötlemisrakenduse töö jaguneb neljaks etapiks:\"\nmsgstr \"The work of the processing application is divided into four phases:\"\n\n#: ../../07-kontrollimine.rst:939\nmsgid \"Kontrollimine\"\nmsgstr \"Check\"\n\n#: ../../07-kontrollimine.rst:941\nmsgid \"Korduvhäälte eemaldamine\"\nmsgstr \"Removal of repeat votes\"\n\n#: ../../07-kontrollimine.rst:943\nmsgid \"Tühistamine/-ennistamine\"\nmsgstr \"Revocation/restoration\"\n\n#: ../../07-kontrollimine.rst:945\nmsgid \"Anonüümimine\"\nmsgstr \"Anonymization\"\n\n#: ../../07-kontrollimine.rst:948\nmsgid \"Töötlemisrakendus teostab kontrollimise etapis järgmised kontrollid:\"\nmsgstr \"\"\n\"The processing application performs the following checks during the check\"\n\" phase:\"\n\n#: ../../07-kontrollimine.rst:950\nmsgid \"\"\n\"e-valimiskasti elementide kontrollid teostatakse kõigi hääle elementide \"\n\"kohta:\"\nmsgstr \"\"\n\"checks on the elements of the digital ballot box are carried out for all \"\n\"elements of the vote:\"\n\n#: ../../07-kontrollimine.rst:955\nmsgid \":ref:`check-nimekirjatunnus-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:956\nmsgid \":ref:`check-nimekirjatunnus-consistency-protocol-settings`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:958 ../../07-kontrollimine.rst:1054\n#: ../../07-kontrollimine.rst:1116\nmsgid \":ref:`check-ringkonnatunnus-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:973\nmsgid \"\"\n\"registreerimisteenuse väljavõtte kontrollid teostatakse iga \"\n\"registreerimispäringu kohta\"\nmsgstr \"\"\n\"checks on the extract from the registration service are carried out for \"\n\"every registration request\"\n\n#: ../../07-kontrollimine.rst:977\nmsgid \":ref:`check-registreerimisparing-konteiner-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:978\nmsgid \"\"\n\":ref:`check-registreerimisparing-konteiner-registreerimispäring-\"\n\"consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:980\nmsgid \"\"\n\"Töötlemisrakendus loob vastavuse e-valimiskasti ja registreerimisteenuse \"\n\"väljavõtte vahel võttes aluseks olemi :ref:`entity-registreerimisparing-\"\n\"konteiner` registreerimisteenuse väljavõttest ning olemi :ref:`entity-\"\n\"registreerimistoend` e-valimiskastist. Ühendavaks lüliks kahe vaate vahel\"\n\" on :ref:`entity-registreerimisparing`.\"\nmsgstr \"\"\n\"The processing application creates a match between the digital ballot box\"\n\" and the registration service extract based on the :ref:`entity-\"\n\"registreerimisparing-konteiner` from the registration service extract and\"\n\" the :ref:`entity-registreerimistoend` from the digital ballot box. The \"\n\"connecting link between the two views is :ref:`entity-\"\n\"registreerimisparing`.\"\n\n#: ../../07-kontrollimine.rst:987\nmsgid \"e-valimiskasti ja registreerimisteenuse väljavõtte vastavuskontrollid\"\nmsgstr \"digital ballot box and registration service extract compliance checks\"\n\n#: ../../07-kontrollimine.rst:992\nmsgid \"\"\n\"Töötlemisrakendus teostab korduvhäälte eemaldamise etapis järgmised \"\n\"kontrollid:\"\nmsgstr \"\"\n\"The processing application performs the following checks during the \"\n\"squash phase:\"\n\n#: ../../07-kontrollimine.rst:994\nmsgid \"Töötlemisrakendus tuvastab iga valija häälte hulgast ajaliselt viimase.\"\nmsgstr \"\"\n\"The processing application identifies the last of each voter's votes in \"\n\"time.\"\n\n#: ../../07-kontrollimine.rst:996\nmsgid \"Töötlemisrakendus viib läbi krüpteeritud sedeli korrektsuse kontrollid.\"\nmsgstr \"\"\n\"The processing application performs correctness checks on the encrypted \"\n\"sender.\"\n\n#: ../../07-kontrollimine.rst:1002\nmsgid \"\"\n\"Töötlemisrakenduse töö järgmistes etappides täiendavaid kontrolle ei \"\n\"teostata. Tühistamis-/ennistamisetapis eemaldatakse/taastatakse \"\n\"isikukoodile vastavaid hääli korduvhäältest puhastatud e-valimiskastist. \"\n\"Anonüümimisetapis eemaldatakse e-häältelt kvalifitseerivad elemendid ning\"\n\" saadud loend krüpteeritud sedelitest suunatakse miksimisrakendusse.\"\nmsgstr \"\"\n\"No further checks will be carried out at subsequent stages of the \"\n\"processing application. In the cancellation/pre-cancelling phase, the \"\n\"votes corresponding to the personal identification code are \"\n\"removed/restored from the e-voting box cleaned of duplicate votes. In the\"\n\" anonymisation phase, the qualifying elements are removed from the \"\n\"e-votes and the resulting list of encrypted ballots is passed to the \"\n\"mixing application.\"\n\n#: ../../07-kontrollimine.rst:1009\nmsgid \"Kontrollid miksimisrakenduses\"\nmsgstr \"Controls in the mixing application\"\n\n#: ../../07-kontrollimine.rst:1011\nmsgid \"Miksimisrakendusele tehakse kättesaadavaks järgmised olemid:\"\nmsgstr \"The following entities will be made available to the mixing application:\"\n\n#: ../../07-kontrollimine.rst:1017\nmsgid \"Miksimisrakendus loob järgmised olemid:\"\nmsgstr \"The mixing application creates the following entities:\"\n\n#: ../../07-kontrollimine.rst:1021\nmsgid \"Miksimisrakendus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The mixing application does not directly interact with:\"\n\n#: ../../07-kontrollimine.rst:1052\nmsgid \"Miksimisrakendus viib läbi järgmised tegevused ja kontrollid:\"\nmsgstr \"The mixing application performs the following actions and checks:\"\n\n#: ../../07-kontrollimine.rst:1058\nmsgid \"\"\n\"Miksimisrakendus grupeerib sisendiks antud krüpteeritud sedelid \"\n\"ringkonnatunnuste kaupa ning järjestab krüpteeritud sedelid ringkonnas \"\n\"ringi.\"\nmsgstr \"\"\n\"The mixing application groups the encrypted ballots given as input by \"\n\"district identifiers and reorders the encrypted ballots within a \"\n\"district.\"\n\n#: ../../07-kontrollimine.rst:1062\nmsgid \"\"\n\"Miksimisrakendus arvutab iga ümberjärjestatud krüpteeritud sedeli kohta \"\n\"uue miksitud krüpteeritud sedeli.\"\nmsgstr \"\"\n\"The mixing application for each reordered ciphertext calculates a new \"\n\"ciphertext.\"\n\n#: ../../07-kontrollimine.rst:1065\nmsgid \"\"\n\"Miksimisrakendus koostab nullteadmustõestused sedelite korrektse \"\n\"miksimise kohta.\"\nmsgstr \"\"\n\"The mixing application generates zero-knowledge proofs of correct mixing \"\n\"of ballots.\"\n\n#: ../../07-kontrollimine.rst:1068\nmsgid \"Kontrollid võtmerakenduses\"\nmsgstr \"Checks in the key application\"\n\n#: ../../07-kontrollimine.rst:1070\nmsgid \"Võtmerakendusele tehakse kättesaadavaks järgmised olemid:\"\nmsgstr \"The following entities will be made available to the key application:\"\n\n#: ../../07-kontrollimine.rst:1078\nmsgid \"Võtmerakendus tuvastab järgmised olemid:\"\nmsgstr \"The key application identifies the following entities:\"\n\n#: ../../07-kontrollimine.rst:1082\nmsgid \"Võtmerakendus ei puutu vahetult kokku järgmiste olemitega:\"\nmsgstr \"The key application does not directly interact with:\"\n\n#: ../../07-kontrollimine.rst:1111\nmsgid \"Võtmerakendus viib läbi järgmised tegevused ja kontrollid:\"\nmsgstr \"The Key Application performs the following activities and checks:\"\n\n#: ../../07-kontrollimine.rst:1113\nmsgid \":ref:`check-miksitud-krypteeritud-sedel-correctness`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:1114\nmsgid \":ref:`check-miksitud-krypteeritud-sedel-public-key-consistency`\"\nmsgstr \"\"\n\n#: ../../07-kontrollimine.rst:1115\nmsgid \"Häälte dekrüpteerimine\"\nmsgstr \"Decryption of votes\"\n\n#: ../../07-kontrollimine.rst:1118\nmsgid \":ref:`check-tahteavaldus-ringkonnatunnus-valikutenimekiri-consistency`\"\nmsgstr \"\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/08-haaletamine.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-25 12:59+0000\\n\"\n\"PO-Revision-Date: 2024-03-01 18:24+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../08-haaletamine.rst:5\nmsgid \"Suhtlusprotokollid\"\nmsgstr \"Communication protocols\"\n\n#: ../../08-haaletamine.rst:8\nmsgid \"Liides\"\nmsgstr \"Interface\"\n\n#: ../../08-haaletamine.rst:10\nmsgid \"\"\n\"Kogumisteenuse valijale suunatud mikroteenused suhtlevad \"\n\"valijarakendusega ja kontrollrakendusega JSON-RPC protokolli vahendusel.\"\nmsgstr \"\"\n\"The micro-services at the collection service communicate with the voting \"\n\"application and the verification application via the JSON-RPC protocol.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"id\"\nmsgstr \"id\"\n\n#: ../../08-haaletamine.rst:13\nmsgid \"JSON-RPC päringuidentifikaator\"\nmsgstr \"JSON-RPC query identifier\"\n\n#: ../../08-haaletamine.rst\nmsgid \"method\"\nmsgstr \"method\"\n\n#: ../../08-haaletamine.rst:14\nmsgid \"RPC-meetod\"\nmsgstr \"RPC method\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params\"\nmsgstr \"params\"\n\n#: ../../08-haaletamine.rst:15\nmsgid \"Konkreetse RPC-meetodi parameetrid\"\nmsgstr \"Parameters of a specific RPC method\"\n\n#: ../../08-haaletamine.rst\nmsgid \"error\"\nmsgstr \"error\"\n\n#: ../../08-haaletamine.rst:21\nmsgid \"Võimalik veainfo või ``null`` vea puudumisel\"\nmsgstr \"Possible error information or \\\"null\\\" in the absence of error\"\n\n#: ../../08-haaletamine.rst:22\nmsgid \"JSON-RPC päringuidentifikaator, peab ühtima päringus kasutatud id'ga\"\nmsgstr \"JSON-RPC query identifier, must match the id used in the query.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result\"\nmsgstr \"result\"\n\n#: ../../08-haaletamine.rst:23\nmsgid \"Meetodipõhine vastusandmestruktuur\"\nmsgstr \"Method-dependent response data structure\"\n\n#: ../../08-haaletamine.rst:29\nmsgid \"\"\n\"Esimese päringuvahetuse käigus mõne IVXV mikroteenusega väljastatakse \"\n\"suhtlevale rakendusele HEX-kodeeritud unikaalne seansiidentifikaator \"\n\"(``result.SessionID``), mida rakendus kasutab edaspidi kõigis \"\n\"kogumisteenuse suunalistes päringutes (``params.SessionID``). \"\n\"Seansiidentifikaatori abil seostatakse hääletamisega seotud RPC-päringud \"\n\"üheks seansiks. Seostamine on informatiivne ning selle eesmärk on \"\n\"logianalüüsi lihtsustamine, hääle ringkonnakuuluvust jm. sisulisi aspekte\"\n\" puudutavad otsused tehakse digiallkirjastatud andmete põhjal.\"\nmsgstr \"\"\n\"During the first query exchange with one of the IVXV microservices, a \"\n\"HEX-encoded unique session identifier (``result.SessionID``) is issued to\"\n\" the communicating application, which is used by the application in all \"\n\"subsequent queries to the collection service (``params.SessionID``). The \"\n\"session identifier is used to associate RPC requests related to voting \"\n\"into a single session. The association is informative and is intended to \"\n\"simplify log analysis, vote circularity, etc. Decisions on substantive \"\n\"aspects are made on the basis of the digitally signed data.\"\n\n#: ../../08-haaletamine.rst:37\nmsgid \"\"\n\"Transpordiprotokollina on kasutusel TLS. Krüpteeritud kanali \"\n\"termineerimine toimub konkreetses mikroteenuses. Võimaldamaks koormuse \"\n\"jaotamist ning mikroteenuste paindlikku evitamist kasutatakse TLS-SNI \"\n\"laiendust, mis lubab vahendusteenusel TLS voogu termineerimata õigesse \"\n\"mikroteenusinstantsi suunata. Vahendusteenus on tüüpiliselt kättesaadav \"\n\"kogumisteenuse välise liidese pordis 443.\"\nmsgstr \"\"\n\"TLS is used as the transport protocol. The termination of the encrypted \"\n\"channel takes place in a specific microservice. To enable load balancing \"\n\"and flexible provisioning of microservices, the TLS-SNI extension is \"\n\"used, which allows the proxy service to route the TLS stream to the \"\n\"correct microservice instance without termination. The broker service is \"\n\"typically available on port 443 of the external interface to the \"\n\"collection service.\"\n\n#: ../../08-haaletamine.rst:45\nmsgid \"Valikute nimekirja hankimine\"\nmsgstr \"Retrieving the List of Choices\"\n\n#: ../../08-haaletamine.rst:47\nmsgid \"\"\n\"Valikute nimekirja hankimine tähendab valijarakenduse suhtlemist \"\n\"nimekirjateenusega (SNI ``choices.ivxv.invalid``). Valikute nimekirja \"\n\"hankimine eeldab valija autentimist ning tema ringkonnakuuluvuse \"\n\"tuvastamist.\"\nmsgstr \"\"\n\"Retrieving the list of choices means the interaction of the voting \"\n\"application with the choices service (SNI ``choices.ivxv.invalid``). \"\n\"Getting a list of choices requires authentication of the voter and \"\n\"verification of the voter's electoral district.\"\n\n#: ../../08-haaletamine.rst:51\nmsgid \"Valijarakendus teeb päringu ``RPC.VoterChoices`` nimekirjade hankimiseks.\"\nmsgstr \"The voting application will query ``RPC.VoterChoices`` to retrieve lists.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.AuthMethod\"\nmsgstr \"params.AuthMethod\"\n\n#: ../../08-haaletamine.rst:53 ../../08-haaletamine.rst:140\nmsgid \"Toetatud valikud on meetodid ``tls`` ja ``ticket``.\"\nmsgstr \"The supported options are methods ``tls`` and ``ticket``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.OS\"\nmsgstr \"params.OS\"\n\n#: ../../08-haaletamine.rst:54 ../../08-haaletamine.rst:146\n#: ../../08-haaletamine.rst:230 ../../08-haaletamine.rst:256\n#: ../../08-haaletamine.rst:305 ../../08-haaletamine.rst:337\n#: ../../08-haaletamine.rst:361 ../../08-haaletamine.rst:415\n#: ../../08-haaletamine.rst:439 ../../08-haaletamine.rst:464\n#: ../../08-haaletamine.rst:516 ../../08-haaletamine.rst:533\n#: ../../08-haaletamine.rst:570 ../../08-haaletamine.rst:594\n#: ../../08-haaletamine.rst:645 ../../08-haaletamine.rst:668\nmsgid \"Operatsioonisüsteem, millel valijarakendust kasutatakse.\"\nmsgstr \"The operating system on which the voting application is used.\"\n\n#: ../../08-haaletamine.rst:56\nmsgid \"\"\n\"Päring ``RPC.VoterChoices`` ID-kaardiga autentimise korral - autentimine \"\n\"toimub TLS-protokolli tasemel päringu töötlemise ajal kasutades ID-kaardi\"\n\" autentimissertifikaati.\"\nmsgstr \"\"\n\"Query ``RPC.VoterChoices`` in case of ID-card authentication - \"\n\"authentication is performed at the TLS protocol level during query \"\n\"processing using the ID-card authentication certificate.\"\n\n#: ../../08-haaletamine.rst:64\nmsgid \"\"\n\"Päring ``RPC.VoterChoices`` Mobiil-ID'ga autentimise korral - päringu \"\n\"sooritamiseks tuleb eelnevalt kasutada Mobiil-ID vahendusteenuse (SNI \"\n\"``mid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\"\nmsgstr \"\"\n\"Query ``RPC.VoterChoices`` in case of Mobile-ID authentication - to \"\n\"perform the query, the Mobile-ID Intermediary Service (SNI \"\n\"``mid.ivxv.invalid``) must be used in advance to obtain a signed \"\n\"authentication certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.AuthToken\"\nmsgstr \"params.AuthToken\"\n\n#: ../../08-haaletamine.rst:68 ../../08-haaletamine.rst:83\n#: ../../08-haaletamine.rst:98\nmsgid \"\"\n\"Autentimisteenuse vahendusel allkirjastatud tõend, mis sisaldab endas \"\n\"valija unikaalset identifikaatorit.\"\nmsgstr \"\"\n\"A certificate signed by an authentication service containing a unique \"\n\"identifier of the voter.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.SessionID\"\nmsgstr \"params.SessionID\"\n\n#: ../../08-haaletamine.rst:71\nmsgid \"\"\n\"Kuna Mobiil-ID korral on nimekirja hankimisele eelnenud interaktsioon \"\n\"autentimistõendi saamiseks, on olemas seansiidentifikaator, mida tuleb \"\n\"kasutada.\"\nmsgstr \"\"\n\"Since the Mobile ID has been preceded by an interaction to obtain an \"\n\"authentication certificate, there is a session identifier that must be \"\n\"used.\"\n\n#: ../../08-haaletamine.rst:79\nmsgid \"\"\n\"Päring ``RPC.VoterChoices`` Smart-ID'ga autentimise korral - päringu \"\n\"sooritamiseks tuleb eelnevalt kasutada Smart-ID vahendusteenuse (SNI \"\n\"``smartid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\"\nmsgstr \"\"\n\"Query ``RPC.VoterChoices`` in case of authentication with Smart-ID - to \"\n\"perform the query, the Smart-ID Intermediary Service (SNI \"\n\"``smartid.ivxv.invalid``) must be used beforehand to obtain a signed \"\n\"authentication certificate.\"\n\n#: ../../08-haaletamine.rst:86\nmsgid \"\"\n\"Kuna Smart-ID korral on nimekirja hankimisele eelnenud interaktsioon \"\n\"autentimistõendi saamiseks, on olemas seansiidentifikaator, mida tuleb \"\n\"kasutada.\"\nmsgstr \"\"\n\"Since the Smart-ID has an interaction to obtain an authentication proof \"\n\"prior to the list acquisition, there is a session identifier to be used.\"\n\n#: ../../08-haaletamine.rst:94\nmsgid \"\"\n\"Päring ``RPC.VoterChoices`` Web eID'ga autentimise korral - päringu \"\n\"sooritamiseks tuleb eelnevalt kasutada Web eID vahendusteenuse (SNI \"\n\"``webeid.ivxv.invalid``) abi allkirjastatud autentimistõendi saamiseks.\"\nmsgstr \"\"\n\"Query ``RPC.VoterChoices`` in case of authentication with ID-card using \"\n\"Web eID protocol - to perform the query, the Web eID Intermediary Service\"\n\" (SNI ``webeid.ivxv.invalid``) must be used beforehand to obtain a signed\"\n\" authentication certificate.\"\n\n#: ../../08-haaletamine.rst:101\nmsgid \"\"\n\"Kuna Web eID korral on nimekirja hankimisele eelnenud interaktsioon \"\n\"autentimistõendi saamiseks, on olemas seansiidentifikaator, mida tuleb \"\n\"kasutada.\"\nmsgstr \"\"\n\"Since in the case of a Web eID, the list acquisition is preceded by an \"\n\"interaction to obtain an authentication certificate, there is a session \"\n\"identifier that must be used.\"\n\n#: ../../08-haaletamine.rst:109\nmsgid \"Nimekirjateenuse vastus päringule ``RPC.VoterChoices``.\"\nmsgstr \"Choices service response to ``RPC.VoterChoices``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Choices\"\nmsgstr \"result.Choices\"\n\n#: ../../08-haaletamine.rst:111\nmsgid \"Valija ringkonnakuuluvuse identifikaator ``VoterDistrict``\"\nmsgstr \"Voter district identifier ``VoterDistrict``\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.List\"\nmsgstr \"result.List\"\n\n#: ../../08-haaletamine.rst:112\nmsgid \"BASE64-kodeeritud ringkonna valikute nimekiri ``DistrictChoices``\"\nmsgstr \"List of BASE64 encoded district options ``DistrictChoices``\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Voted\"\nmsgstr \"result.Voted\"\n\n#: ../../08-haaletamine.rst:113\nmsgid \"\"\n\"Kui valija on juba hääletanud, siis ``true``, vastasel juhul seda välja \"\n\"vastuses ei ole.\"\nmsgstr \"\"\n\"If the voter has already voted, then ``true``, otherwise it will not \"\n\"appear in the response.\"\n\n#: ../../08-haaletamine.rst:120\nmsgid \"Võimalikud veateated päringu ``RPC.VoterChoices`` korral.\"\nmsgstr \"Possible error messages for the query ``RPC.VoterChoices``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"BAD_CERTIFICATE\"\nmsgstr \"BAD_CERTIFICATE\"\n\n#: ../../08-haaletamine.rst:122\nmsgid \"Viga valija isikutuvastussertifikaadiga.\"\nmsgstr \"An error with the voter's identity certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"BAD_REQUEST\"\nmsgstr \"BAD_REQUEST\"\n\n#: ../../08-haaletamine.rst:123 ../../08-haaletamine.rst:204\n#: ../../08-haaletamine.rst:249 ../../08-haaletamine.rst:284\n#: ../../08-haaletamine.rst:321 ../../08-haaletamine.rst:353\n#: ../../08-haaletamine.rst:389 ../../08-haaletamine.rst:432\n#: ../../08-haaletamine.rst:457 ../../08-haaletamine.rst:496\n#: ../../08-haaletamine.rst:555 ../../08-haaletamine.rst:586\n#: ../../08-haaletamine.rst:621 ../../08-haaletamine.rst:662\n#: ../../08-haaletamine.rst:692 ../../08-haaletamine.rst:728\n#: ../../08-haaletamine.rst:790\nmsgid \"Vigane päring.\"\nmsgstr \"Malformed query.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"INELIGIBLE_VOTER\"\nmsgstr \"INELIGIBLE_VOTER\"\n\n#: ../../08-haaletamine.rst:124 ../../08-haaletamine.rst:207\nmsgid \"Valijal pole õigust hääletada.\"\nmsgstr \"The voter has no right to vote.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"INTERNAL_SERVER_ERROR\"\nmsgstr \"INTERNAL_SERVER_ERROR\"\n\n#: ../../08-haaletamine.rst:125 ../../08-haaletamine.rst:208\n#: ../../08-haaletamine.rst:250 ../../08-haaletamine.rst:285\n#: ../../08-haaletamine.rst:322 ../../08-haaletamine.rst:354\n#: ../../08-haaletamine.rst:390 ../../08-haaletamine.rst:433\n#: ../../08-haaletamine.rst:458 ../../08-haaletamine.rst:497\n#: ../../08-haaletamine.rst:556 ../../08-haaletamine.rst:587\n#: ../../08-haaletamine.rst:622 ../../08-haaletamine.rst:663\n#: ../../08-haaletamine.rst:693 ../../08-haaletamine.rst:729\n#: ../../08-haaletamine.rst:757 ../../08-haaletamine.rst:791\nmsgid \"Viga serveri sisemises töös.\"\nmsgstr \"An error in the internal workings of the server.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"UNAUTHENTICATED\"\nmsgstr \"UNAUTHENTICATED\"\n\n#: ../../08-haaletamine.rst:126 ../../08-haaletamine.rst:210\nmsgid \"Autentimata päring.\"\nmsgstr \"Unauthenticated request.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"VOTER_TOO_YOUNG\"\nmsgstr \"VOTER_TOO_YOUNG\"\n\n#: ../../08-haaletamine.rst:127 ../../08-haaletamine.rst:211\nmsgid \"Valija on liiga noor.\"\nmsgstr \"The voter is too young.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"VOTING_END\"\nmsgstr \"VOTING_END\"\n\n#: ../../08-haaletamine.rst:128 ../../08-haaletamine.rst:212\n#: ../../08-haaletamine.rst:251 ../../08-haaletamine.rst:294\n#: ../../08-haaletamine.rst:326 ../../08-haaletamine.rst:355\n#: ../../08-haaletamine.rst:398 ../../08-haaletamine.rst:434\n#: ../../08-haaletamine.rst:459 ../../08-haaletamine.rst:504\n#: ../../08-haaletamine.rst:559 ../../08-haaletamine.rst:588\n#: ../../08-haaletamine.rst:629 ../../08-haaletamine.rst:664\n#: ../../08-haaletamine.rst:695 ../../08-haaletamine.rst:730\n#: ../../08-haaletamine.rst:758 ../../08-haaletamine.rst:792\nmsgid \"Hääletusperiood on lõppenud.\"\nmsgstr \"The voting period has ended.\"\n\n#: ../../08-haaletamine.rst:132\nmsgid \"Allkirjastatud hääle saatmine talletamiseks\"\nmsgstr \"Sending a signed vote for storage\"\n\n#: ../../08-haaletamine.rst:134\nmsgid \"\"\n\"Allkirjastatud hääle saatmine talletamiseks tähendab valijarakenduse \"\n\"suhtlemist hääletamisteenusega (SNI ``voting.ivxv.invalid``).\"\nmsgstr \"\"\n\"Sending a signed vote to be stored implies the interaction of the voter \"\n\"application with the voting service (SNI ``voting.ivxv.invalid``).\"\n\n#: ../../08-haaletamine.rst:137\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Vote`` allkirjastatud hääle \"\n\"talletamiseks saatmiseks.\"\nmsgstr \"\"\n\"The voting application will query ``RPC.Vote`` to send the signed vote \"\n\"for storage.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Choices\"\nmsgstr \"params.Choices\"\n\n#: ../../08-haaletamine.rst:141\nmsgid \"\"\n\"Valija ringkonnakuuluvuse identifikaator ``VoterDistrict`` mis kehtis \"\n\"valikute nimekirja hankimise ajal. Parameetri korrektne kasutamine \"\n\"võimaldab kogumisteenusel valijat hoiatada kui tema ringkonnakuuluvus on \"\n\"võrreldes hääletamise algushetkega muutunud.\"\nmsgstr \"\"\n\"The voter's district identifier ``VoterDistrict`` that was in effect at \"\n\"the time the list of choices was obtained. Correct use of this parameter \"\n\"allows the collection service to alert the voter if his/her district \"\n\"affiliation has changed since the start of the voting process.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Type\"\nmsgstr \"params.Type\"\n\n#: ../../08-haaletamine.rst:147 ../../08-haaletamine.rst:716\nmsgid \"Allkirjastatud hääle vorming. Hetkel on ainus toetatud väärtus ``bdoc``.\"\nmsgstr \"Signed ballot format. Currently the only supported value is ``bdoc``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Vote\"\nmsgstr \"params.Vote\"\n\n#: ../../08-haaletamine.rst:149 ../../08-haaletamine.rst:718\nmsgid \"\"\n\"BASE64-kodeeritud hääl ``SignedVote`` eelpoolmääratud vormingus (:ref\"\n\":`signed-vote`).\"\nmsgstr \"\"\n\"BASE64-encoded vote in ``SignedVote`` predefined format (:ref:`signed-\"\n\"vote`).\"\n\n#: ../../08-haaletamine.rst:152\nmsgid \"Päring ``RPC.Vote`` ID-kaardiga autentimise korral.\"\nmsgstr \"Query ``RPC.Vote`` in case of ID-card authentication.\"\n\n#: ../../08-haaletamine.rst:158\nmsgid \"Päring ``RPC.Vote`` Mobiil-ID'ga autentimise korral.\"\nmsgstr \"Query ``RPC.Vote`` in case of authentication with Mobile-ID.\"\n\n#: ../../08-haaletamine.rst:164\nmsgid \"Päring ``RPC.Vote`` Smart-ID'ga autentimise korral.\"\nmsgstr \"Query ``RPC.Vote`` for authentication with Smart-ID.\"\n\n#: ../../08-haaletamine.rst:170\nmsgid \"Päring ``RPC.Vote`` Web eID'ga autentimise korral.\"\nmsgstr \"Query ``RPC.Vote`` for authentication with Web eID.\"\n\n#: ../../08-haaletamine.rst:176\nmsgid \"Hääletamisteenuse vastus päringule ``RPC.Vote``.\"\nmsgstr \"Voting service response to the query ``RPC.Vote``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Qualification.ocsp\"\nmsgstr \"result.Qualification.ocsp\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Qualification.tspreg\"\nmsgstr \"result.Qualification.tspreg\"\n\n#: ../../08-haaletamine.rst:180\nmsgid \"\"\n\"Kogumisteenuse poolt hangitud täiendavad tõendid valijarakenduse poolt \"\n\"loodud hääle ``SignedVote`` (:ref:`signed-vote`) kvalifitseerimiseks ning\"\n\" korrektseks registreerimiseks. Vastuse koosseis sõltub kogumisteenuse \"\n\"konkreetsest seadistusest, antud juhul kasutatakse standardset OCSP \"\n\"protokolli valija allkirjasertifikaadi kehtivuse kontrolliks ning PKIX \"\n\"ajatempliprotokolli põhist registreerimisteenust nii hääle andmise aja \"\n\"fikseerimiseks kui elektroonilise hääle registreerimiseks välises \"\n\"sõltumatus teenuses.  Valijarakendusele kontrollimiseks edastatakse nii \"\n\"OCSP vastus kui PKIX vormingus ajatempel koos registreerimisteenusele \"\n\"vajalike täiendustega.\"\nmsgstr \"\"\n\"Additional evidence obtained by the collection service for the \"\n\"qualification and correct registration of the ``SignedVote`` (:ref\"\n\":`signed-vote`) generated by the voting application. The composition of \"\n\"the response depends on the specific configuration of the collection \"\n\"service, in this case the standard OCSP protocol is used to verify the \"\n\"validity of the voter's signature certificate and the PKIX timestamp \"\n\"protocol based registration service is used both to record the time of \"\n\"casting the vote and to register the electronic vote in an external \"\n\"independent service.  Both the OCSP response and the timestamp in PKIX \"\n\"format, with the necessary additions to the registration service, are \"\n\"transmitted to the voting application for verification.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.TestVote\"\nmsgstr \"result.TestVote\"\n\n#: ../../08-haaletamine.rst:189\nmsgid \"\"\n\"Kui hääl esitati enne hääletamise algust ning läks arvesse proovihäälena,\"\n\" siis ``true``, vastasel juhul seda välja vastuses ei ole. Valijarakendus\"\n\" kuvab valijale proovihääle korral sellekohase hoiatuse.\"\nmsgstr \"\"\n\"If the vote was cast before voting started and counted as a test vote, \"\n\"then ``true``, otherwise it is not included in the response. The voting \"\n\"application will display a warning to the voter in the case of a test \"\n\"vote.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.VoteID\"\nmsgstr \"result.VoteID\"\n\n#: ../../08-haaletamine.rst:193\nmsgid \"\"\n\"Hääle identifikaator talletusteenuses, mille alusel on kontrollrakendusel\"\n\" võimalik häält hilisemaks analüüsiks välja nõuda.\"\nmsgstr \"\"\n\"The identifier of the ballot in the storage service, which allows the \"\n\"verification application to retrieve the ballot for later analysis.\"\n\n#: ../../08-haaletamine.rst:201\nmsgid \"Võimalikud veateated päringu ``RPC.Vote`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Vote`` query.\"\n\n#: ../../08-haaletamine.rst:203\nmsgid \"Viga valija isikutuvastus- või allkirjastamissertifikaadiga.\"\nmsgstr \"An error with the voter's identification or signature certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"IDENTITY_MISMATCH\"\nmsgstr \"IDENTITY_MISMATCH\"\n\n#: ../../08-haaletamine.rst:205\nmsgid \"Isikutuvastus- ning allkirjastamissertifikaadi isikukoodid ei kattu.\"\nmsgstr \"The ID codes of the ID and signature certificate do not match.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"OUTDATED_CHOICES\"\nmsgstr \"OUTDATED_CHOICES\"\n\n#: ../../08-haaletamine.rst:209\nmsgid \"Valija ringkonnakuuluvus on nimekirja hankimisest muutunud.\"\nmsgstr \"\"\n\"The district eligibility of a candidate has changed since the list was \"\n\"obtained.\"\n\n#: ../../08-haaletamine.rst:216\nmsgid \"Hääletamine Mobiil-ID'ga\"\nmsgstr \"Voting with Mobile-ID\"\n\n#: ../../08-haaletamine.rst:218\nmsgid \"\"\n\"Mobiil-ID kasutamine allkirjastamis- ning autentimisvahendina tingib \"\n\"Mobiil-ID teenusega liidestuva abiteenuse (SNI ``mid.ivxv.invalid``) \"\n\"kasutamise autentimistõendi hankimiseks enne valikute nimekirja hankimist\"\n\" ning hääle allkirjastamiseks enne talletamist.\"\nmsgstr \"\"\n\"The use of Mobile-ID as a signature and authentication tool implies the \"\n\"use of an auxiliary service (SNI ``mid.ivxv.invalid``) that interfaces \"\n\"with the Mobile-ID service to obtain an authentication certificate before\"\n\" retrieving the list of options and to sign the vote before storing it.\"\n\n#: ../../08-haaletamine.rst:225 ../../08-haaletamine.rst:411\n#: ../../08-haaletamine.rst:640\nmsgid \"Autentimistõendi hankimine\"\nmsgstr \"Retrieving an Authentication Certificate\"\n\n#: ../../08-haaletamine.rst:227\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Authenticate`` Mobiil-ID autentimise \"\n\"algatamiseks.\"\nmsgstr \"\"\n\"The voting application will make a request ``RPC.Authenticate`` to \"\n\"initiate Mobile ID authentication.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.IDCode\"\nmsgstr \"params.IDCode\"\n\n#: ../../08-haaletamine.rst:231\nmsgid \"Mobiil-ID kasutaja isikukood.\"\nmsgstr \"Mobile-ID user ID.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.PhoneNo\"\nmsgstr \"params.PhoneNo\"\n\n#: ../../08-haaletamine.rst:232\nmsgid \"Mobiil-ID kasutaja telefoninumber.\"\nmsgstr \"Mobile ID user's phone number.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Challenge\"\nmsgstr \"result.Challenge\"\n\n#: ../../08-haaletamine.rst:238\nmsgid \"Räsi, millest arvutada Mobiil-ID kontrollkood valijarakenduses kuvamiseks\"\nmsgstr \"\"\n\"Hash from which to calculate the Mobile ID verification code to display \"\n\"in the voting application\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.SessionCode\"\nmsgstr \"result.SessionCode\"\n\n#: ../../08-haaletamine.rst:240\nmsgid \"Mobiil-ID seansiidentifikaator edasiste poll-päringute jaoks\"\nmsgstr \"Mobile-ID session identifier for further poll requests\"\n\n#: ../../08-haaletamine.rst:247 ../../08-haaletamine.rst:430\n#: ../../08-haaletamine.rst:455\nmsgid \"Võimalikud veateated päringu ``RPC.Authenticate`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Authenticate`` request.\"\n\n#: ../../08-haaletamine.rst:253 ../../08-haaletamine.rst:461\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.AuthenticateStatus`` \"\n\"autentimisprotsessi oleku hindamiseks.\"\nmsgstr \"\"\n\"The voting application queries ``RPC.AuthenticateStatus`` to assess the \"\n\"status of the authentication process.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.SessionCode\"\nmsgstr \"params.SessionCode\"\n\n#: ../../08-haaletamine.rst:257 ../../08-haaletamine.rst:467\n#: ../../08-haaletamine.rst:534\nmsgid \"Autentimisseansi identifikaator\"\nmsgstr \"Authentication session identifier\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.AuthToken\"\nmsgstr \"result.AuthToken\"\n\n#: ../../08-haaletamine.rst:264 ../../08-haaletamine.rst:474\nmsgid \"\"\n\"Autentimistõend teistele IVXV teenustele esitamiseks või ``null``, kui \"\n\"päringu töötlemine alles käib.\"\nmsgstr \"\"\n\"Authentication certificate for submission to other IVXV services, or \"\n\"``null`` if the request is still being processed.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.GivenName\"\nmsgstr \"result.GivenName\"\n\n#: ../../08-haaletamine.rst:266 ../../08-haaletamine.rst:478\n#: ../../08-haaletamine.rst:678\nmsgid \"Eduka autentimise korral valija eesnimi\"\nmsgstr \"First name of voter in case of successful authentication\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.PersonalCode\"\nmsgstr \"result.PersonalCode\"\n\n#: ../../08-haaletamine.rst:267 ../../08-haaletamine.rst:479\n#: ../../08-haaletamine.rst:679\nmsgid \"Eduka autentimise korral valija isikukood\"\nmsgstr \"\"\n\"Voter's personal identification number in case of successful \"\n\"authentication\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Status\"\nmsgstr \"result.Status\"\n\n#: ../../08-haaletamine.rst:268 ../../08-haaletamine.rst:480\n#: ../../08-haaletamine.rst:541\nmsgid \"\"\n\"Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK`` \"\n\"viitab edukale autentimisele. Vastuse muud väljad sisaldavad infot vaid \"\n\"siis kui väärtus on ``OK``.\"\nmsgstr \"\"\n\"Query status - ``POLL`` indicates the need to repeat the query, ``OK`` \"\n\"indicates successful authentication. The other fields in the response \"\n\"contain information only if the value is ``OK``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Surname\"\nmsgstr \"result.Surname\"\n\n#: ../../08-haaletamine.rst:271 ../../08-haaletamine.rst:483\n#: ../../08-haaletamine.rst:683\nmsgid \"Eduka autentimise korral valija perekonnanimi\"\nmsgstr \"In case of successful authentication, the voter's surname\"\n\n#: ../../08-haaletamine.rst:282 ../../08-haaletamine.rst:494\nmsgid \"Võimalikud veateated päringu ``RPC.AuthenticateStatus`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.AuthenticateStatus`` request.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_BAD_CERTIFICATE\"\nmsgstr \"MID_BAD_CERTIFICATE\"\n\n#: ../../08-haaletamine.rst:286\nmsgid \"Viga valija Mobiil-ID isikutuvastussertifikaadiga.\"\nmsgstr \"An error with the voter's Mobile ID certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_NOT_USER\"\nmsgstr \"MID_NOT_USER\"\n\n#: ../../08-haaletamine.rst:287 ../../08-haaletamine.rst:325\nmsgid \"Telefoninumber ei kuulu Mobiil-ID kliendile.\"\nmsgstr \"The phone number does not belong to a Mobile ID customer.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_OPERATOR\"\nmsgstr \"MID_OPERATOR\"\n\n#: ../../08-haaletamine.rst:288 ../../08-haaletamine.rst:393\nmsgid \"\"\n\"Probleem valija mobiiltelefoni SIM kaardiga, mille lahendamiseks tuleb \"\n\"pöörduda mobiilioperaatori poole.\"\nmsgstr \"\"\n\"A problem with the SIM card of the voter's mobile phone, which needs to \"\n\"be solved by contacting the mobile operator.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_ABSENT\"\nmsgstr \"MID_ABSENT\"\n\n#: ../../08-haaletamine.rst:290 ../../08-haaletamine.rst:391\nmsgid \"Valija mobiiltelefon ei ole kättesaadav.\"\nmsgstr \"The voter's mobile phone is not available.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_CANCELED\"\nmsgstr \"MID_CANCELED\"\n\n#: ../../08-haaletamine.rst:291 ../../08-haaletamine.rst:395\nmsgid \"Valija katkestas Mobiil-ID seansi.\"\nmsgstr \"The voter has cancelled the Mobile-ID session.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_EXPIRED\"\nmsgstr \"MID_EXPIRED\"\n\n#: ../../08-haaletamine.rst:292 ../../08-haaletamine.rst:396\nmsgid \"Mobiil-ID seanss on aegunud.\"\nmsgstr \"The Mobile ID session has expired.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"MID_GENERAL\"\nmsgstr \"MID_GENERAL\"\n\n#: ../../08-haaletamine.rst:293 ../../08-haaletamine.rst:324\n#: ../../08-haaletamine.rst:397\nmsgid \"Viga Mobiil-ID teenuse töös.\"\nmsgstr \"An error in the operation of the Mobile ID service.\"\n\n#: ../../08-haaletamine.rst:298 ../../08-haaletamine.rst:508\nmsgid \"Hääle allkirjastamine\"\nmsgstr \"Signing the vote\"\n\n#: ../../08-haaletamine.rst:300\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.GetCertificate`` \"\n\"allkirjastamissertifikaadi hankimiseks.\"\nmsgstr \"\"\n\"The voting application makes a query ``RPC.GetCertificate`` to retrieve \"\n\"the signing certificate.\"\n\n#: ../../08-haaletamine.rst:303 ../../08-haaletamine.rst:332\n#: ../../08-haaletamine.rst:513 ../../08-haaletamine.rst:565\nmsgid \"Toetatud ainult autentimismeetod ``ticket``.\"\nmsgstr \"Only the authentication method ``ticket`` is supported.\"\n\n#: ../../08-haaletamine.rst:304 ../../08-haaletamine.rst:333\nmsgid \"Mobiil-ID autentimistõend.\"\nmsgstr \"Mobile-ID authentication certificate.\"\n\n#: ../../08-haaletamine.rst:306 ../../08-haaletamine.rst:338\nmsgid \"Hääle allkirjastaja telefoninumber\"\nmsgstr \"Telephone number of the person who signed the vote\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Certificate\"\nmsgstr \"result.Certificate\"\n\n#: ../../08-haaletamine.rst:313 ../../08-haaletamine.rst:540\nmsgid \"Allkirjastamissertifikaat X509-vormingus\"\nmsgstr \"Signature certificate in X509 format\"\n\n#: ../../08-haaletamine.rst:319\nmsgid \"Võimalikud veateated päringu ``RPC.GetCertificate`` korral.\"\nmsgstr \"Possible error messages for ``RPC.GetCertificate``.\"\n\n#: ../../08-haaletamine.rst:323 ../../08-haaletamine.rst:392\nmsgid \"Viga valija Mobiil-ID allkirjastamissertifikaadiga.\"\nmsgstr \"Error with the voter's Mobile-ID signature certificate.\"\n\n#: ../../08-haaletamine.rst:329\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Sign`` hääle allkirjastamise \"\n\"algatamiseks. Mobiil-ID kontrollkoodi arvutab valijarakendus andmevälja \"\n\"``Hash`` väärtusest.\"\nmsgstr \"\"\n\"The voting application will make a query ``RPC.Sign`` to initiate the \"\n\"signing of the vote. The mobile ID verification code is calculated by the\"\n\" voter application from the value of the ``Hash`` field.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Hash\"\nmsgstr \"params.hash\"\n\n#: ../../08-haaletamine.rst:334 ../../08-haaletamine.rst:567\nmsgid \"BASE64-kodeeritud elektroonilise hääle räsi\"\nmsgstr \"BASE64-encoded hash of the ballot\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.HashType\"\nmsgstr \"params.HashType\"\n\n#: ../../08-haaletamine.rst:335\nmsgid \"\"\n\"Räsifunktsiooni nimi Mobiil-ID teenusele edastamiseks, kas ``SHA256``, \"\n\"``SHA384`` või  ``SHA512``\"\nmsgstr \"\"\n\"The name of the hash function to be forwarded to the Mobile-ID service, \"\n\"either ``SHA256``, ``SHA384`` or ``SHA512``\"\n\n#: ../../08-haaletamine.rst:344\nmsgid \"Mobiil-ID seansiidentifikaator edasiste poll-päringute jaoks.\"\nmsgstr \"Mobile ID session identifier for further poll requests.\"\n\n#: ../../08-haaletamine.rst:351 ../../08-haaletamine.rst:584\nmsgid \"Võimalikud veateated päringu ``RPC.Sign`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Sign`` query.\"\n\n#: ../../08-haaletamine.rst:358 ../../08-haaletamine.rst:591\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.SignStatus`` allkirjastamisprotsessi \"\n\"seisundi hindamiseks.\"\nmsgstr \"\"\n\"The voting application queries ``RPC.SignStatus`` to assess the status of\"\n\" the signing process.\"\n\n#: ../../08-haaletamine.rst:362\nmsgid \"Mobiil-ID seansiidentifikaator\"\nmsgstr \"Mobile-ID session identifier\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Signature\"\nmsgstr \"result.Signature\"\n\n#: ../../08-haaletamine.rst:368\nmsgid \"\"\n\"Juhul kui vastuse ``Status`` väli on ``OK``, BASE-64 kodeeritud \"\n\"PKCS1-vormingus signatuur, vastasel juhul ``null``.\"\nmsgstr \"\"\n\"If the ``Status`` field of the response is ``OK``, the BASE-64 encoded \"\n\"PKCS1 format signature, otherwise ``zero``.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Algorithm\"\nmsgstr \"result.Algorithm\"\n\n#: ../../08-haaletamine.rst:370\nmsgid \"\"\n\"Juhul kui vastuse ``Status`` väli on ``OK``, Mobiil-ID teenuse poolt \"\n\"tagastatud signatuuri algoritm. Võimalikud väärtused on \"\n\"``SHA256WithECEncryption``, ``SHA256WithRSAEncryption``, \"\n\"``SHA384WithECEncryption``, ``SHA384WithRSAEncryption``, \"\n\"``SHA512WithECEncryption`` ja ``SHA512WithRSAEncryption``.\"\nmsgstr \"\"\n\"If the ``Status`` field of the response is ``OK``, the signature \"\n\"algorithm returned by the Mobile-ID service. The possible values are \"\n\"``SHA256WithECEncryption``, ``SHA256WithRSAEncryption``, \"\n\"``SHA384WithECEncryption``, ``SHA384WithRSAEncryption``, \"\n\"``SHA512WithECEncryption`` and ``SHA512WithRSAEncryption``.\"\n\n#: ../../08-haaletamine.rst:375 ../../08-haaletamine.rst:607\nmsgid \"\"\n\"Päringu staatus - ``POLL`` viitab vajadusele päringut korrata, ``OK`` \"\n\"viitab edukale allkirjastamisele. Vastuse muud väljad sisaldavad infot \"\n\"vaid siis kui väärtus on ``OK``.\"\nmsgstr \"\"\n\"Query status - ``POLL`` indicates the need to repeat the query, ``OK`` \"\n\"indicates a successful signature. The other fields in the response \"\n\"contain information only if the value is ``OK``.\"\n\n#: ../../08-haaletamine.rst:387 ../../08-haaletamine.rst:619\nmsgid \"Võimalikud veateated päringu ``RPC.SignStatus`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.SignStatus`` query.\"\n\n#: ../../08-haaletamine.rst:402\nmsgid \"Hääletamine Smart-ID'ga\"\nmsgstr \"Voting with Smart-ID\"\n\n#: ../../08-haaletamine.rst:404\nmsgid \"\"\n\"Smart-ID kasutamine allkirjastamis- ning autentimisvahendina tingib \"\n\"Smart-ID teenusega liidestuva abiteenuse (SNI ``smartid.ivxv.invalid``) \"\n\"kasutamise autentimistõendi hankimiseks enne valikute nimekirja hankimist\"\n\" ning hääle allkirjastamiseks enne talletamist.\"\nmsgstr \"\"\n\"The use of Smart-ID as a signature and authentication tool implies the \"\n\"use of an auxiliary service (SNI ``smartid.ivxv.invalid``) interfacing \"\n\"with the Smart-ID service to obtain an authentication certificate before \"\n\"retrieving the list of choices and to sign the vote before storing it.\"\n\n#: ../../08-haaletamine.rst:413\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Challenge`` Smart-ID kontrollkoodi \"\n\"hankimiseks.\"\nmsgstr \"\"\n\"The voter application will query ``RPC.Challenge`` to retrieve the Smart-\"\n\"ID verification code.\"\n\n#: ../../08-haaletamine.rst:421\nmsgid \"Räsi, millest arvutada Smart-ID kontrollkood valijarakenduses kuvamiseks\"\nmsgstr \"\"\n\"Hash from which to calculate the Smart-ID verification code to display in\"\n\" the voting application\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.XSmartIDAuth\"\nmsgstr \"result.XSmartIDAuth\"\n\n#: ../../08-haaletamine.rst:423 ../../08-haaletamine.rst:440\n#: ../../08-haaletamine.rst:465\nmsgid \"\"\n\"Päringu küpsis, kus talletatakse Smart-ID kontrollkoodi räsi, selle eluea\"\n\" ajatempel ja seansiidentifikaator\"\nmsgstr \"\"\n\"A query cookie storing a hash of the Smart-ID verification code, a \"\n\"timestamp of its lifetime and a session identifier.\"\n\n#: ../../08-haaletamine.rst:436\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Authenticate`` Smart-ID autentimise \"\n\"algatamiseks.\"\nmsgstr \"\"\n\"The voting application will query ``RPC.Authenticate`` to initiate Smart-\"\n\"ID authentication.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.XSmartIDAuth\"\nmsgstr \"params.XSmartIDAuth\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Identifier\"\nmsgstr \"params.Identifier\"\n\n#: ../../08-haaletamine.rst:442\nmsgid \"Smart-ID kasutaja isikukood.\"\nmsgstr \"Smart-ID user ID.\"\n\n#: ../../08-haaletamine.rst:448 ../../08-haaletamine.rst:522\nmsgid \"Smart-ID seansiidentifikaator edasiste poll-päringute jaoks\"\nmsgstr \"Smart-ID session identifier for further poll requests\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.DataToken\"\nmsgstr \"\"\n\n#: ../../08-haaletamine.rst:476\nmsgid \"\"\n\"Hääletaja Smart-ID dokumendi number või ``null``, kui päringu töötlemine \"\n\"alles käib.\"\nmsgstr \"\"\n\"Voter's Smart-ID document number or ``null``, when the query is pending.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_BAD_CERTIFICATE\"\nmsgstr \"SMARTID_BAD_CERTIFICATE\"\n\n#: ../../08-haaletamine.rst:498 ../../08-haaletamine.rst:623\nmsgid \"Viga valija Smart-ID isikutuvastussertifikaadiga.\"\nmsgstr \"Error with the voter's Smart-ID identity certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_VERIFICATION\"\nmsgstr \"SMARTID_VERIFICATION\"\n\n#: ../../08-haaletamine.rst:499 ../../08-haaletamine.rst:624\nmsgid \"Valija valis vale verifitseerimiskoodi.\"\nmsgstr \"The voter selected the wrong verification code.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_ACCOUNT\"\nmsgstr \"SMARTID_ACCOUNT\"\n\n#: ../../08-haaletamine.rst:500 ../../08-haaletamine.rst:625\nmsgid \"Viga valija Smart-ID kontos.\"\nmsgstr \"An error in the voter's Smart-ID account.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_CANCELED\"\nmsgstr \"SMARTID_CANCELED\"\n\n#: ../../08-haaletamine.rst:501 ../../08-haaletamine.rst:626\nmsgid \"Valija katkestas Smart-ID seansi.\"\nmsgstr \"The Smart-ID session has been interrupted by the voter.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_EXPIRED\"\nmsgstr \"SMARTID_EXPIRED\"\n\n#: ../../08-haaletamine.rst:502 ../../08-haaletamine.rst:627\nmsgid \"Smart-ID seanss on aegunud.\"\nmsgstr \"The Smart-ID session has expired.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"SMARTID_GENERAL\"\nmsgstr \"SMARTID_GENERAL\"\n\n#: ../../08-haaletamine.rst:503 ../../08-haaletamine.rst:558\n#: ../../08-haaletamine.rst:628\nmsgid \"Viga Smart-ID teenuse töös.\"\nmsgstr \"Error in the Smart-ID service.\"\n\n#: ../../08-haaletamine.rst:510\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.GetCertificateChoice`` \"\n\"allkirjastamissertifikaadi valikuks.\"\nmsgstr \"\"\n\"The voting application makes a query ``RPC.GetCertificateChoice`` to \"\n\"select the signing certificate.\"\n\n#: ../../08-haaletamine.rst:514 ../../08-haaletamine.rst:566\nmsgid \"Smart-ID autentimistõend.\"\nmsgstr \"Smart-ID authentication certificate.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.DataToken\"\nmsgstr \"\"\n\n#: ../../08-haaletamine.rst:515 ../../08-haaletamine.rst:571\nmsgid \"Hääletaja Smart-ID dokumendi number.\"\nmsgstr \"Voter's Smart-ID documentnumber\"\n\n#: ../../08-haaletamine.rst:529\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.GetCertificateChoiceStatus`` \"\n\"allkirjastamissertifikaadi oleku hindamiseks.\"\nmsgstr \"\"\n\"The voting application makes a query ``RPC.GetCertificateChoiceStatus`` \"\n\"to evaluate the status of the signing certificate.\"\n\n#: ../../08-haaletamine.rst:553\nmsgid \"Võimalikud veateated päringu ``RPC.GetCertificateChoiceStatus`` korral.\"\nmsgstr \"Possible error messages for the query ``RPC.GetCertificateChoiceStatus``.\"\n\n#: ../../08-haaletamine.rst:557\nmsgid \"Viga valija Smart-ID allkirjastamissertifikaadiga.\"\nmsgstr \"Error with the voter's Smart-ID signature certificate.\"\n\n#: ../../08-haaletamine.rst:562\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Sign`` hääle allkirjastamise \"\n\"algatamiseks. Smart-ID kontrollkoodi arvutab valijarakendus andmevälja \"\n\"``Hash`` väärtusest.\"\nmsgstr \"\"\n\"The voting application will make a query ``RPC.Sign`` to initiate the \"\n\"signing of the vote. The Smart-ID verification code is calculated by the \"\n\"voter application from the value of the ``Hash`` field.\"\n\n#: ../../08-haaletamine.rst:568\nmsgid \"\"\n\"Räsifunktsiooni nimi Smart-ID teenusele edastamiseks, kas ``SHA256``, \"\n\"``SHA384`` või  ``SHA512``\"\nmsgstr \"\"\n\"The name of the hash function to transmit to the Smart-ID service, either\"\n\" ``SHA256``, ``SHA384`` or ``SHA512``\"\n\n#: ../../08-haaletamine.rst:577\nmsgid \"Smart-ID seansiidentifikaator edasiste poll-päringute jaoks.\"\nmsgstr \"Smart-ID session identifier for further poll requests.\"\n\n#: ../../08-haaletamine.rst:595\nmsgid \"Smart-ID seansiidentifikaator\"\nmsgstr \"Smart-ID session identifier\"\n\n#: ../../08-haaletamine.rst:601\nmsgid \"\"\n\"Juhul kui vastuse ``Status`` väli on ``OK``, BASE-64 kodeeritud \"\n\"signatuur, vastasel juhul ``null``.\"\nmsgstr \"\"\n\"If the ``Status`` field of the response is ``OK``, BASE-64 encoded \"\n\"signature, otherwise ``null``.\"\n\n#: ../../08-haaletamine.rst:603\nmsgid \"\"\n\"Juhul kui vastuse ``Status`` väli on ``OK``, Smart-ID teenuse poolt \"\n\"tagastatud signatuuri algoritm. Võimalikud väärtused on \"\n\"``sha256WithRSAEncryption``, ``sha384WithRSAEncryption``, ja \"\n\"``sha512WithRSAEncryption``.\"\nmsgstr \"\"\n\"If the ``Status`` field of the response is ``OK``, the signature \"\n\"algorithm returned by the Smart-ID service. Possible values are \"\n\"``sha256WithRSAEncryption``, ``sha384WithRSAEncryption``, and \"\n\"``sha512WithRSAEncryption``.\"\n\n#: ../../08-haaletamine.rst:632\nmsgid \"Hääletamine Web eID'ga\"\nmsgstr \"Voting with Web eID\"\n\n#: ../../08-haaletamine.rst:634\nmsgid \"\"\n\"Web eID kasutamine autentimisvahendina tingib Web eID teenusega \"\n\"liidestuva abiteenuse (SNI ``webeid.ivxv.invalid``) kasutamise \"\n\"autentimistõendi hankimiseks enne valikute nimekirja hankimist.\"\nmsgstr \"\"\n\"The use of Web eID as an authentication tool implies the use of an \"\n\"auxiliary service (SNI ``webeid.ivxv.invalid``) interfacing with the Web \"\n\"eID service to obtain an authentication certificate before retrieving the\"\n\" list of choices.\"\n\n#: ../../08-haaletamine.rst:642\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Challenge`` Web eID autentimise \"\n\"algatamiseks.\"\nmsgstr \"\"\n\"The voting application makes a request to ``RPC.Challenge`` to initiate \"\n\"Web eID authentication.\"\n\n#: ../../08-haaletamine.rst:651\nmsgid \"\"\n\"Base64 kodeeritud räsi, mille dekodeeritud väärtust peab valijarakendus \"\n\"kasutama autentimistõendi allkirja loomiseks.\"\nmsgstr \"\"\n\"A Base64 encoded hash whose decoded value must be used by the voter \"\n\"application to generate the authentication proof signature.\"\n\n#: ../../08-haaletamine.rst:653 ../../08-haaletamine.rst:669\nmsgid \"Seansiidentifikaator.\"\nmsgstr \"Session identifier.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Bearer\"\nmsgstr \"params.Bearer\"\n\n#: ../../08-haaletamine.rst:654 ../../08-haaletamine.rst:671\nmsgid \"Küpsis, mida server kasutab räsi verifitseerimiseks.\"\nmsgstr \"A cookie used by the server to verify hashes.\"\n\n#: ../../08-haaletamine.rst:660\nmsgid \"Võimalikud veateated päringu ``RPC.Challenge`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Challenge`` query.\"\n\n#: ../../08-haaletamine.rst:666\nmsgid \"\"\n\"Valijarakendus teeb päringu ``RPC.Token`` autentimistõendi \"\n\"valideerimiseks.\"\nmsgstr \"\"\n\"The voting application will query ``RPC.Token`` to validate the \"\n\"authentication token.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.Token\"\nmsgstr \"params.Token\"\n\n#: ../../08-haaletamine.rst:670\nmsgid \"Web eID autentimistõend.\"\nmsgstr \"Web eID authentication certificate.\"\n\n#: ../../08-haaletamine.rst:677\nmsgid \"Autentimistõend teistele IVXV teenustele esitamiseks\"\nmsgstr \"Proof of authentication for submission to other IVXV services\"\n\n#: ../../08-haaletamine.rst:680\nmsgid \"\"\n\"Päringu staatus - ``OK`` viitab edukale autentimisele. Vastuse muud \"\n\"väljad sisaldavad infot vaid siis kui väärtus on ``OK``.\"\nmsgstr \"\"\n\"Query status - ``OK`` indicates successful authentication. The other \"\n\"fields in the response contain information only if the value is ``OK``.\"\n\n#: ../../08-haaletamine.rst:690\nmsgid \"Võimalikud veateated päringu ``RPC.Token`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Token`` query.\"\n\n#: ../../08-haaletamine.rst:694\nmsgid \"Viga valija Web eID isikutuvastussertifikaadiga.\"\nmsgstr \"Error with the voter's Web eID identity certificate.\"\n\n#: ../../08-haaletamine.rst:698\nmsgid \"Hääle kontrollimine\"\nmsgstr \"Vote verification\"\n\n#: ../../08-haaletamine.rst:700\nmsgid \"\"\n\"Kontrollrakendus teeb päringu ``RPC.Verify`` allkirjastatud hääle ning \"\n\"häält kvalifitseerivate tõendite allalaadimiseks kogumisteenusest.\"\nmsgstr \"\"\n\"The Verification Application will query ``RPC.Verify`` to download the \"\n\"signed vote and the evidence that qualifies the vote from the collection \"\n\"service.\"\n\n#: ../../08-haaletamine.rst:703\nmsgid \"Operatsioonisüsteem, millel kontrollrakendust kasutatakse.\"\nmsgstr \"The operating system on which the verification application is used.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.VoteID\"\nmsgstr \"params.VoteID\"\n\n#: ../../08-haaletamine.rst:704\nmsgid \"\"\n\"QR-koodi vahendusel valijarakendusest saadud hääle identifikaator \"\n\"talletusteenuses.\"\nmsgstr \"\"\n\"The identifier of the vote received from the voting application via QR \"\n\"code in the storage service.\"\n\n#: ../../08-haaletamine.rst:713\nmsgid \"Vaata peatükki hääle verifitseerimisest\"\nmsgstr \"See chapter on vote verification\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Type\"\nmsgstr \"result.Type\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.Vote\"\nmsgstr \"result.Vote\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.ChoicesList\"\nmsgstr \"result.ChoicesList\"\n\n#: ../../08-haaletamine.rst:720\nmsgid \"JSON-vormingus ringkonnapõhine valikute nimekiri.\"\nmsgstr \"A JSON-formatted list of district-based options.\"\n\n#: ../../08-haaletamine.rst:726\nmsgid \"Võimalikud veateated päringu ``RPC.Verify`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Verify`` request.\"\n\n#: ../../08-haaletamine.rst:734\nmsgid \"E-hääletamise jooksev nimekiri\"\nmsgstr \"List of e-votes\"\n\n#: ../../08-haaletamine.rst:736\nmsgid \"\"\n\"X-tee teenusega(xroad-service) liidestuva abiteenuse (SNI \"\n\"``votesorder.ivxv.invalid``) kasutatakse informatsiooni edastamiseks \"\n\"X-tee turvaserverile.\"\nmsgstr \"\"\n\"The auxiliary service (SNI ``votesorder.ivxv.invalid``), which interfaces\"\n\" with the X-road service (xroad-service), is used to send information to \"\n\"the X-road security server.\"\n\n#: ../../08-haaletamine.rst:741\nmsgid \"Viimane järjenumber\"\nmsgstr \"Last serial number\"\n\n#: ../../08-haaletamine.rst:742\nmsgid \"\"\n\"X-tee teenus(xroad-service) teeb päringu ``RPC.VotesSeqNo`` viimase \"\n\"järjenumbri saamiseks.\"\nmsgstr \"\"\n\"The X-road service (xroad-service) makes a query ``RPC.VotesSeqNo`` for \"\n\"the last sequence number.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.SeqNo\"\nmsgstr \"result.SeqNo\"\n\n#: ../../08-haaletamine.rst:749\nmsgid \"Viimane järjenumber.\"\nmsgstr \"Latest sequence number.\"\n\n#: ../../08-haaletamine.rst:755\nmsgid \"Võimalikud veateated päringu ``RPC.VotesSeqNo`` korral.\"\nmsgstr \"Possible error messages in case of query ``RPC.VotesSeqNo``.\"\n\n#: ../../08-haaletamine.rst:761\nmsgid \"E-hääletamiste pakk\"\nmsgstr \"E-voting package\"\n\n#: ../../08-haaletamine.rst:762\nmsgid \"\"\n\"X-tee teenus(xroad-service) teeb päringu ``RPC.Votes`` e-hääletamise paki\"\n\" saamiseks.\"\nmsgstr \"\"\n\"The X-road service (xroad-service) makes a request for an ``RPC.Votes`` \"\n\"e-voting packet.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.VotesFrom\"\nmsgstr \"params.VotesFrom\"\n\n#: ../../08-haaletamine.rst:764\nmsgid \"E-hääled alates sellest järjenumbrist.\"\nmsgstr \"E-votes from this serial number.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"params.BatchMaxSize\"\nmsgstr \"params.BatchMaxSize\"\n\n#: ../../08-haaletamine.rst:765\nmsgid \"E-hääletamise paki suurus.\"\nmsgstr \"Size of e-voting package.\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords\"\nmsgstr \"result.batchRecords\"\n\n#: ../../08-haaletamine.rst:772\nmsgid \"E-häälte loend\"\nmsgstr \"List of e-votes\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords.seqNo\"\nmsgstr \"result.batchRecords.seqNo\"\n\n#: ../../08-haaletamine.rst:774\nmsgid \"Hääle järjenumber\"\nmsgstr \"Voice of the sequel\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords.idCode\"\nmsgstr \"result.batchRecords.idCode\"\n\n#: ../../08-haaletamine.rst:776\nmsgid \"Hääletaja isikukood\"\nmsgstr \"Voter identification code\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords.voterName\"\nmsgstr \"result.batchRecords.voterName\"\n\n#: ../../08-haaletamine.rst:778\nmsgid \"Hääletaja nimi\"\nmsgstr \"Name of voter\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords.kovCode\"\nmsgstr \"result.batchRecords.kovCode\"\n\n#: ../../08-haaletamine.rst:780\nmsgid \"KOV EHAK kood\"\nmsgstr \"KOV EHAK code\"\n\n#: ../../08-haaletamine.rst\nmsgid \"result.batchRecords.electoralDistrictNo\"\nmsgstr \"result.batchRecords.electoralDistrictNo\"\n\n#: ../../08-haaletamine.rst:782\nmsgid \"Valimisringkonna number\"\nmsgstr \"Constituency number\"\n\n#: ../../08-haaletamine.rst:788\nmsgid \"Võimalikud veateated päringu ``RPC.Votes`` korral.\"\nmsgstr \"Possible error messages in case of ``RPC.Votes`` query.\"\n\n#~ msgid \"result.DocumentNo\"\n#~ msgstr \"result.DocumentNo\"\n\n#~ msgid \"params.DocumentNo\"\n#~ msgstr \"params.DocumentNo\"\n\n#~ msgid \"Hääle allkirjastaja Smart-ID dokumendi number.\"\n#~ msgstr \"Smart ID document number of the person who signed the vote.\"\n\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/09-tootlemine.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 08:56+0000\\n\"\n\"PO-Revision-Date: 2024-03-01 18:35+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../09-tootlemine.rst:5\nmsgid \"E-valimiskasti töötlemisetapi andmestruktuurid\"\nmsgstr \"Data structures for the digital ballot box processing phase\"\n\n#: ../../09-tootlemine.rst:9\nmsgid \"Tühistus- ja ennistusnimekiri\"\nmsgstr \"Revocation and Restoration List\"\n\n#: ../../09-tootlemine.rst:11\nmsgid \"\"\n\"Tühistus- ja ennistusnimekiri sisaldab andmeid isikute kohta, kelle e-hääl \"\n\"tuleb tühistada (ei lähe arvesse valimistulemuste kokkulugemisel) või \"\n\"ennistada (s.t. tühistatakse eelnev tühistamine ning häälte uuesti üle \"\n\"lugemisel võetakse ennistatud e-hääl arvesse). Nimekiri laaditakse süsteemi \"\n\"digitaalselt allkirjastatud dokumendina, mille andmefaili vorming on \"\n\"järgmine:\"\nmsgstr \"\"\n\"The revocation and restoration lists contain information on the persons \"\n\"whose e-vote is to be annulled (not counted in the count) or restored (i.e. \"\n\"the previous annulment is cancelled and the restored e-vote is counted in \"\n\"the recount). The list will be uploaded to the system as a digitally signed \"\n\"document with the following data file format:\"\n\n#: ../../09-tootlemine.rst:17 ../../09-tootlemine.rst:35\n#: ../../09-tootlemine.rst:61 ../../09-tootlemine.rst:146\nmsgid \"Näide:\"\nmsgstr \"Example:\"\n\n#: ../../09-tootlemine.rst:24\nmsgid \"E-hääletanute nimekiri\"\nmsgstr \"List of e-voters\"\n\n#: ../../09-tootlemine.rst:26\nmsgid \"\"\n\"E-hääletanute nimekiri on pärast e-hääletamise lõppu väljastatav nimekiri \"\n\"e-hääletanud isikutest, sordituna valimisjaoskondade kaupa. Dokument \"\n\"genereeritakse töötlemisrakenduse poolt.\"\nmsgstr \"\"\n\"The list of e-voters is the list of e-voters issued after the end of \"\n\"e-voting, sorted by polling station. The document is generated by the \"\n\"processing application.\"\n\n#: ../../09-tootlemine.rst:42\nmsgid \"Hääletamistulemus\"\nmsgstr \"Voting results\"\n\n#: ../../09-tootlemine.rst:44\nmsgid \"\"\n\"Võtmerakenduse poolt dekrüpteeritud ning summeeritud hääled jagatud \"\n\"valimisringkondade ja jaoskondade kaupa.\"\nmsgstr \"\"\n\"Votes decrypted and summed by the key application, broken down by district \"\n\"and polling station.\"\n\n#: ../../09-tootlemine.rst:47\nmsgid \"\"\n\"Hääletamistulemuste failis peavad iga jaoskonna kohta olema järgmised \"\n\"andmed.\"\nmsgstr \"\"\n\"The voting results file must contain the following information for each \"\n\"station.\"\n\n#: ../../09-tootlemine.rst:49\nmsgid \"\"\n\"Rikutud ja kehtetute häälte arvu näitav kirje. Seda ka juhul, kui \"\n\"valimisjaoskonnas polnud ühtki rikutud või kehtetut häält: sellisel juhul on\"\n\" häälte arv null.\"\nmsgstr \"\"\n\"A record showing the number of spoilt and invalid votes. This is the case \"\n\"even if there were no spoilt or invalid votes in the polling station: in \"\n\"this case the number of votes is zero.\"\n\n#: ../../09-tootlemine.rst:53\nmsgid \"\"\n\"Iga valiku poolt antud häälte arvu näitav kirje. Seda ka juhul, kui \"\n\"valimisjaoskonnas ei antud selle valiku poolt ühtki häält: sellisel juhul on\"\n\" häälte arv null.\"\nmsgstr \"\"\n\"A record showing the number of votes cast for each option. This is the case \"\n\"even if no votes were cast for that option in the polling station: in this \"\n\"case the number of votes is zero.\"\n\n#: ../../09-tootlemine.rst:69\nmsgid \"E-valimiskast\"\nmsgstr \"Ballot box\"\n\n#: ../../09-tootlemine.rst:71\nmsgid \"\"\n\"Fail sisaldab kogumisteenuse poolt vastu võetud hääli koos häälte juurde \"\n\"kuuluvate andmetega.\"\nmsgstr \"\"\n\"The file contains the votes received by the collection service, together \"\n\"with the data associated with the votes.\"\n\n#: ../../09-tootlemine.rst:74\nmsgid \"Faili vorming on Zip64 konteiner.\"\nmsgstr \"The file format is Zip64 container.\"\n\n#: ../../09-tootlemine.rst:76\nmsgid \"Valija-spetsiifilised kaustad asuvad vahetult juurkausta `votes` all.\"\nmsgstr \"\"\n\"The voter-specific folders are located just below the `votes` root folder.\"\n\n#: ../../09-tootlemine.rst:78\nmsgid \"Faili sisu:\"\nmsgstr \"File content:\"\n\n#: ../../09-tootlemine.rst:80\nmsgid \":file:`votes/<voter id>/`\"\nmsgstr \":file:`votes/<voter id>/`\"\n\n#: ../../09-tootlemine.rst:82\nmsgid \":file:`<timestamp>.version`\"\nmsgstr \":file:`<timestamp>.version`\"\n\n#: ../../09-tootlemine.rst:84\nmsgid \":file:`<timestamp>.<vote type>`\"\nmsgstr \":file:`<timestamp>.<vote type>`\"\n\n#: ../../09-tootlemine.rst:86\nmsgid \":file:`<timestamp>.<qualifier>*`\"\nmsgstr \":file:`<timestamp>.<qualifier>*`\"\n\n#: ../../09-tootlemine.rst:88\nmsgid \"kus:\"\nmsgstr \"where:\"\n\n#: ../../09-tootlemine.rst:90\nmsgid \"``<voter id>`` on valija identifikaator, Eesti puhul isikukood;\"\nmsgstr \"\"\n\"``<voter id>`` is the voter's identifier, which in Estonia is the voter's \"\n\"personal identification number;\"\n\n#: ../../09-tootlemine.rst:92\nmsgid \"\"\n\"``<timestamp>`` on hääle esitamise kellaaeg vormingus \"\n\"``yyyymmddhhmmssmmm±zzzz``;\"\nmsgstr \"\"\n\"``<timestamp>`` is the time of day for the submission of the vote in the \"\n\"format ``yyymmddhhmmmmmmm±zzzz``;\"\n\n#: ../../09-tootlemine.rst:95\nmsgid \"\"\n\"see kellaaeg kajastab hetke, mil päring kogumisteenusesse tehti, ja on antud\"\n\" lihtsalt valimiskasti inimloetavuse parandamiseks; hääle tegelik ajamärk \"\n\"või -tempel on mõne kvalifitseeriva vastuse sees;\"\nmsgstr \"\"\n\"this time reflects the moment when the query was made to the collection \"\n\"service and is given simply to improve the human readability of the ballot \"\n\"box; the actual time stamp or time stamp of the vote is inside some \"\n\"qualifying response;\"\n\n#: ../../09-tootlemine.rst:99\nmsgid \"``<vote type>`` on valikute konteineri tüüp, Eesti puhul BDOC;\"\nmsgstr \"\"\n\"``<vote type>`` is the type of the ballot container, for Estonia BDOC;\"\n\n#: ../../09-tootlemine.rst:101\nmsgid \"\"\n\"kusjuures BDOC ise on lihtsalt põhiprofiiliga ja ei sisalda kvalifitseerivad\"\n\" parameetreid (kehtivuskinnitusi, ajamärgendeid, ajatempleid),\"\nmsgstr \"\"\n\"whereas the BDOC itself is just a basic profile and does not contain any \"\n\"qualifying parameters (validations, timemarks, timestamps),\"\n\n#: ../../09-tootlemine.rst:105\nmsgid \"\"\n\"``<qualifier>`` on häält kvalifitseeriva protokolli tüüp, millest hetkel \"\n\"võimalikud on:\"\nmsgstr \"\"\n\"``<qualifier>`` is the type of vote qualifying protocol currently possible:\"\n\n#: ../../09-tootlemine.rst:108\nmsgid \"\"\n\"``ocsp`` - *Online Certificate Status Protocol* (kehtivuskinnitus, `RFC 6960\"\n\" <https://tools.ietf.org/html/rfc6960>`_) kinnitab valija \"\n\"allkirjastamissertifikaadi kehtivust hääle andmise hetkel,\"\nmsgstr \"\"\n\"``ocsp`` - *Online Certificate Status Protocol* (`RFC 6960 \"\n\"<https://tools.ietf.org/html/rfc6960>`_) confirms the validity of the \"\n\"voter's signature certificate at the moment of casting the vote,\"\n\n#: ../../09-tootlemine.rst:112\nmsgid \"\"\n\"``tsp`` - *Time-Stamp Protocol* (ajatempel, `RFC 3161 \"\n\"<https://tools.ietf.org/html/rfc3161>`_) kinnitab, et päringu tegemise \"\n\"hetkeks oli hääl olemas,\"\nmsgstr \"\"\n\"``tsp`` - *Time-Stamp Protocol* (timestamp, `RFC 3161 \"\n\"<https://tools.ietf.org/html/rfc3161>`_) confirms that a ballot was present \"\n\"at the time the request was made,\"\n\n#: ../../09-tootlemine.rst:116\nmsgid \"\"\n\"``tspreg`` - sama, mis ``tsp``, aga nonsiks pannakse kogumisteenuse allkiri \"\n\"päringu ``MessageImprint`` elemendil, et häält registreerida.\"\nmsgstr \"\"\n\"``tspreg`` - the same as ``tsp``, but with the addition of the collection \"\n\"service signature on the ``MessageImprint`` element of the query to register\"\n\" the vote.\"\n\n#: ../../09-tootlemine.rst:119\nmsgid \"Iga hääle kohta esinevad failid on:\"\nmsgstr \"The files per vote are:\"\n\n#: ../../09-tootlemine.rst:121\nmsgid \"\"\n\"``<timestamp>.version`` - hääle andmise ajal kehtinud valijate nimekirja \"\n\"versioon;\"\nmsgstr \"\"\n\"``<timestamp>.version`` - the version of the electoral roll in force at the \"\n\"time the vote was cast;\"\n\n#: ../../09-tootlemine.rst:124\nmsgid \"\"\n\"``<timestamp>.<vote type>`` - valikute konteiner, mille sees on valiku \"\n\"identifikaator kujul ``<valimise id>.<küsimuse id>.ballot``. Eesti puhul \"\n\"BDOC-konteineris olev vastava nimega fail;\"\nmsgstr \"\"\n\"``<timestamp>.<vote type>`` - a ballot container containing a ballot \"\n\"identifier of the form ``<ballot id>.<question id>.ballot``. In the case of \"\n\"Estonia, the file with the corresponding name in the BDOC container;\"\n\n#: ../../09-tootlemine.rst:128\nmsgid \"\"\n\"``<timestamp>.<qualifier>`` - häält kvalifitseeriva protokolli päringu \"\n\"vastus; neid võib esineda mitu, aga iga protokolli kohta maksimaalselt üks.\"\nmsgstr \"\"\n\"``<timestamp>.<qualifier>`` - the response to the query of the protocol \"\n\"qualifying the vote; there may be more than one, but not more than one per \"\n\"protocol.\"\n\n#: ../../09-tootlemine.rst:134\nmsgid \"Anonüümistatud e-valimiskast\"\nmsgstr \"Anonymous ballot box\"\n\n#: ../../09-tootlemine.rst:136\nmsgid \"\"\n\"Valimisringkondade ja jaoskondade järgi grupeeritud krüpteeritud hääled. \"\n\"Anonüümistatud e-valimiskastis puudub informatsioon valijate kohta.\"\nmsgstr \"\"\n\"Encrypted votes grouped by districts and stations. No information on voters \"\n\"is available in the anonymised e-voting box.\"\n\n#: ../../09-tootlemine.rst:139\nmsgid \"\"\n\"Anonüümistatud e-valimiskast on töötlemisrakenduse väljund ning \"\n\"võtmerakenduse dekrüpteerimise tööriista sisend.\"\nmsgstr \"\"\n\"The anonymised e-voting box is the output of the processing application and \"\n\"the input of the decryption tool of the key application.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/11-audit.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 19:52+0000\\n\"\n\"PO-Revision-Date: 2024-03-01 18:43+0200\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../11-audit.rst:5\nmsgid \"Hääletamistulemuse audit\"\nmsgstr \"Audit of voting results\"\n\n#: ../../11-audit.rst:8\nmsgid \"Miksimistõendi kontroll\"\nmsgstr \"Verification of Proof-of-Shuffle\"\n\n#: ../../11-audit.rst:10\nmsgid \"\"\n\"Miksimistõendi kontrollimiseks kasutatakse algoritmi nagu on defineeritud \"\n\"`Verificatumi verifitseerija implementeerimise manuaalis \"\n\"<https://www.verificatum.org/files/vmnv-3.0.3.pdf>`_.\"\nmsgstr \"\"\n\"The verification of the proof-of-shuffle is done using the algorithm as \"\n\"defined in the `Verificatum Verifier Implementation Manual \"\n\"<https://www.verificatum.org/files/vmnv-3.0.3.pdf>`_.\"\n\n#: ../../11-audit.rst:14\nmsgid \"\"\n\"Märgime, et miksimistõendi koostamisel lisatakse krüptogrammile andmed \"\n\"valimiste, ringkonna, jaoskonna ja küsimuse identifikaatori kohta.  \"\n\"Lisamiseks kodeeritakse vastav väli rühma elemendina, kasutades \"\n\"pimendamiseks juhuslikkust 0. Näitena, kui esialgu on krüptogramm :math:`c_0\"\n\" = (c_{00}, c_{01})`, kasutades avalikku võtit :math:`pk = (g, y)`, siis \"\n\"Verificatumi sisendina kasutatakse laia krüptogrammi :math:`C = (c_{id}, \"\n\"c_d, c_s, c_q, c_0)`, kus:\"\nmsgstr \"\"\n\"Note that when the proof-of-shuffle is generated, the cryptogram is appended\"\n\" with the election, district, station and question identifier.  For the \"\n\"purpose of insertion, the corresponding field is coded as a group element \"\n\"using randomness 0 for obfuscation. As an example, if the initial cryptogram\"\n\" is :math:`c_0 = (c_{00}, c_{01})` using the public key :math:`pk = (g, y)`,\"\n\" then the input to Verificatum is a broad cryptogram :math:`C = (c_{id}, \"\n\"c_d, c_s, c_q, c_0)` where:\"\n\n#: ../../11-audit.rst:21\nmsgid \"\"\n\"valimiste identifikaatori pseudokrüptogramm on antud kujul :math:`c_{id} = \"\n\"(1, encode(id))`, kus funktsioon :math:`encode` kodeerib sõne vastava rühma \"\n\"elemendina ja `id` on valimiste identifikaatori sõne.\"\nmsgstr \"\"\n\"the pseudocryptogram of the election identifier is given by :math:`c_{id} = \"\n\"(1, encode(id))`, where :math:`encode` encodes the seed as an element of the\"\n\" corresponding group and `id` is the seed of the election identifier.\"\n\n#: ../../11-audit.rst:24\nmsgid \"\"\n\"ringkonna identifikaatori pseudokrüptogramm on antud kujul :math:`c_d = (1, \"\n\"encode(d))`, kus `d` on ringkonna identifikaatori sõne.\"\nmsgstr \"\"\n\"the pseudocryptogram of the district identifier is given by :math:`c_d = (1,\"\n\" encode(d))`, where `d` is the seed of the district identifier.\"\n\n#: ../../11-audit.rst:26\nmsgid \"\"\n\"jaoskonna identifikaatori pseudokrüptogramm on antud kujul :math:`c_s = (1, \"\n\"encode(s))`, kus `s` on jaoskonna identifikaatori sõne.\"\nmsgstr \"\"\n\"the pseudocryptogram of the station identifier is given by :math:`c_s = (1, \"\n\"encode(s))`, where `s` is the seed of the station identifier.\"\n\n#: ../../11-audit.rst:28\nmsgid \"\"\n\"küsimuse identifikaatori pseudokrüptogramm on antud kujul :math:`c_q = (1, \"\n\"encode(q))`, kus `q` on küsimuse identifikaatori sõne.\"\nmsgstr \"\"\n\"the pseudocryptogram of the question identifier is given by :math:`c_q = (1,\"\n\" encode(q))`, where `q` is the seed of the question identifier.\"\n\n#: ../../11-audit.rst:31\nmsgid \"\"\n\"Sellisel juhul defineeritakse laia krüptogrammile vastava avaliku võtmena \"\n\":math:`((g,1), (g,1), (g,1), (g,1), (g,y))`.\"\nmsgstr \"\"\n\"In this case, the public key corresponding to the wide cryptogram is defined\"\n\" as :math:`((g,1), (g,1), (g,1), (g,1), (g,y))`.\"\n\n#: ../../11-audit.rst:35\nmsgid \"Korrektse dekrüpteerimise tõendi kontroll\"\nmsgstr \"Checking the Proof-of-Decryption\"\n\n#: ../../11-audit.rst:37\nmsgid \"\"\n\"Olgu antud krüptogramm :math:`c = (c_0, c_1)`, mis dekrüpteeritakse \"\n\"väärtuseks :math:`d` antud avaliku võtmega :math:`pk` üle parameetrite \"\n\":math:`(p,g)` ja dekrüpteerimistõendiga :math:`(a,b,s)`.\"\nmsgstr \"\"\n\"Given a cryptogram :math:`c = (c_0, c_1)`, decrypted to the value :math:`d` \"\n\"with the given public key :math:`pk` over the parameters :math:`(p,g)` and \"\n\"the decryption key :math:`(a,b,s)`.\"\n\n#: ../../11-audit.rst:41\nmsgid \"\"\n\"Korrektse dekrüpteerimise kontrollimise jaoks on tarvis arvutada mitte-\"\n\"interaktiivne kontrollija väljakutse. Selle jaoks kodeeritakse \"\n\":math:`\\\"DECRYPTION\\\" || pk || c || d || a || b` DER-kodeeringus. Baidijada \"\n\"kasutatakse deterministliku juhuarvugeneraatori initsialiseerimiseks ja \"\n\"selle väljundist loetakse rühma järgu pikkune täisarv :math:`k`.\"\nmsgstr \"\"\n\"To verify the proof-of-decryption it is necessary to calculate the challenge\"\n\" for non-interactive verifier. For this purpose, :math:`\\\"DECRYPTION\\\" || pk\"\n\" || c || d || a || b` is encoded in DER encoding. The byte sequence is used \"\n\"to initialise a deterministic random number generator and its output is read\"\n\" as a group-order integer :math:`k`.\"\n\n#: ../../11-audit.rst:47\nmsgid \"\"\n\"Dekrüpteerimistõendi kontrolliks tuleb veenduda, et :math:`c_0^s = a * \"\n\"(c_1/d)^k` ja :math:`g^s = b * y^k`.\"\nmsgstr \"\"\n\"To check the decryption proof, make sure that :math:`c_0^s = a * (c_1/d)^k` \"\n\"and :math:`g^s = b * y^k`.\"\n\n#: ../../11-audit.rst:51\nmsgid \"Korrektse teisendamise kontroll\"\nmsgstr \"Correct conversion check\"\n\n#: ../../11-audit.rst:53\nmsgid \"\"\n\"Kontrollimaks, et teisendus IVXV e-valimiskasti ja Verificatumi \"\n\"krüptogrammide vahel on tehtud korrektselt, tuleb korrata teisendust \"\n\"sõltumatult. Pärast sõltumatut teisendust tuleb võrrelda saadud väljundeid. \"\n\"Kuna teisendamine on deterministlik protseduur, siis garanteerib kordamine \"\n\"tegevuse õigsuse.\"\nmsgstr \"\"\n\"To verify that the conversion between the IVXV ballot Box and the \"\n\"Verificatum cryptograms has been done correctly, the conversion must be \"\n\"repeated independently. After the independent conversion, the resulting \"\n\"outputs must be compared. Since the conversion is a deterministic procedure,\"\n\" the repetition guarantees the correctness of the operation.\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/12-lisad.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.0\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-03 19:52+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../12-lisad.rst:5\nmsgid \"Lisad\"\nmsgstr \"More\"\n\n#: ../../12-lisad.rst:8\nmsgid \"Andmestruktuuride ASN.1 spetsifikatsioon\"\nmsgstr \"ASN.1 specification of data structures\"\n\n#: ../../12-lisad.rst:10\nmsgid \"IVXV ElGamal üldised andmestruktuurid\"\nmsgstr \"IVXV ElGamal general data structures\"\n\n#: ../../12-lisad.rst:16\nmsgid \"IVXV ElGamal ModP spetsiifilised andmestruktuurid\"\nmsgstr \"IVXV ElGamal ModP specific data structures\"\n\n#: ../../12-lisad.rst:22\nmsgid \"IVXV ElGamal ECC spetsiifilised andmestruktuurid\"\nmsgstr \"IVXV ElGamal ECC specific data structures\"\n\n#: ../../12-lisad.rst:31\nmsgid \"Elektroonilise hääle vormingu spetsifikatsioon\"\nmsgstr \"Specification for electronic voice formatting\"\n\n#: ../../12-lisad.rst:33\nmsgid \"\"\n\"Käesoleva protokolliversiooniga kooskõlaline ettepanek VVK otsuseks \"\n\"hääletamissedeli ja elektroonilise hääle vormi kehtestamiseks.\"\nmsgstr \"\"\n\"Proposal for a decision of the IGC establishing the voting form and the \"\n\"electronic voting form, in conformity with this version of the minutes.\"\n\n#: ../../12-lisad.rst:40\nmsgid \"Vead töötlemisprotsessis\"\nmsgstr \"Errors in the processing\"\n\n#: ../../12-lisad.rst:42\nmsgid \"\"\n\"Anname ülevaate veakoodidest töötlemisprotsessis. Veakoodid kirjeldavad vigu\"\n\" üksiku elemendi verifitseerimisel - hääl, registreerimistõend, \"\n\"registreerimispäring - ning häälte, registreerimistõendite ja \"\n\"registreerimispäringute vahelise vastavuse loomisel.\"\nmsgstr \"\"\n\"We give an overview of error codes in the processing process. Error codes \"\n\"describe errors in the verification of a single element - vote, registration\"\n\" certificate, registration request - and in the matching of votes, \"\n\"registration certificates and registration requests.\"\n\n#: ../../12-lisad.rst:47\nmsgid \"\"\n\"Enamus veaolukordi on pigem hüpoteetilised (nt. `REG_NO_NONCE`), siiski on \"\n\"tegemist olukordadega, mis programselt võivad esineda ning seetõttu tuleb \"\n\"neid ka käsitleda.\"\nmsgstr \"\"\n\"Most of the error situations are rather hypothetical (e.g. `REG_NO_NONCE`), \"\n\"however, they are situations that can occur programmatically and therefore \"\n\"need to be addressed.\"\n\n#: ../../12-lisad.rst:51\nmsgid \"\"\n\"Veaolukord tähendab, et konkreetne element ei liigu edasi töötlemise \"\n\"järgmisesse faasi.\"\nmsgstr \"\"\n\"An error condition means that a particular element does not move on to the \"\n\"next phase of processing.\"\n\n#: ../../12-lisad.rst:53\nmsgid \"Töötlemisprotsessi veakoodid\"\nmsgstr \"Error codes for the processing process\"\n\n#: ../../12-lisad.rst:57 ../../12-lisad.rst:121\nmsgid \"Veakood\"\nmsgstr \"Error code\"\n\n#: ../../12-lisad.rst:58 ../../12-lisad.rst:122\nmsgid \"Selgitus\"\nmsgstr \"Explanation\"\n\n#: ../../12-lisad.rst:59\nmsgid \"``INVALID_FILE_NAME``\"\nmsgstr \"``INVALID_FILE_NAME``\"\n\n#: ../../12-lisad.rst:60\nmsgid \"Failinimi ei vasta oodatud mustrile. Viitab tundmatule failile urnis.\"\nmsgstr \"\"\n\"The file name does not match the expected pattern. Refers to an unknown file\"\n\" in the urn.\"\n\n#: ../../12-lisad.rst:61\nmsgid \"``MISSING_FILE``\"\nmsgstr \"``MISSING_FILE``\"\n\n#: ../../12-lisad.rst:62\nmsgid \"Mõni hääle koosseisuks vajalik fail puudub. Nt. ajatempel.\"\nmsgstr \"\"\n\"Some of the files needed to compose the votes are missing. E.g. timestamp.\"\n\n#: ../../12-lisad.rst:63\nmsgid \"``REPEATED_FILE``\"\nmsgstr \"``REPEATED_FILE``\"\n\n#: ../../12-lisad.rst:64\nmsgid \"Mõnda hääle koosseisuks vajalikku faili on mitu. Nt. kehtivuskinnitus.\"\nmsgstr \"\"\n\"There are several files needed to compose some votes. E.g. validity \"\n\"confirmation.\"\n\n#: ../../12-lisad.rst:65\nmsgid \"``UNKNOWN_FILE_TYPE``\"\nmsgstr \"``UNKNOWN_FILE_TYPE``\"\n\n#: ../../12-lisad.rst:66\nmsgid \"Faili tüüp on tundmatu või mittetoetatud.\"\nmsgstr \"The file type is unknown or unsupported.\"\n\n#: ../../12-lisad.rst:67\nmsgid \"``INVALID_FILE_SIZE``\"\nmsgstr \"``INVALID_FILE_SIZE``\"\n\n#: ../../12-lisad.rst:68\nmsgid \"\"\n\"Allkirjastatud hääle faili suurus ei vasta seadistustes nõutud \"\n\"kriteeriumitele.\"\nmsgstr \"\"\n\"The size of the signed vote file does not meet the criteria required in the \"\n\"settings.\"\n\n#: ../../12-lisad.rst:69\nmsgid \"``INVALID_BALLOT_SIGNATURE``\"\nmsgstr \"``INVALID_BALLOT_SIGNATURE``\"\n\n#: ../../12-lisad.rst:70\nmsgid \"Hääl on vigase allkirjaga.\"\nmsgstr \"The voice has an incorrect signature.\"\n\n#: ../../12-lisad.rst:71\nmsgid \"``MISSING_VOTER_SIGNATURE``\"\nmsgstr \"``MISSING_VOTER_SIGNATURE``\"\n\n#: ../../12-lisad.rst:72\nmsgid \"Hääl ei sisalda valija allkirja.\"\nmsgstr \"The vote does not include the voter's signature.\"\n\n#: ../../12-lisad.rst:73\nmsgid \"``VOTER_NOT_FOUND``\"\nmsgstr \"``VOTER_NOT_FOUND``\"\n\n#: ../../12-lisad.rst:74\nmsgid \"Valija ei olnud hääletamise hetkel valijate nimekirjas.\"\nmsgstr \"The voter was not on the electoral roll at the time of voting.\"\n\n#: ../../12-lisad.rst:75\nmsgid \"``VOTERLIST_NOT_FOUND``\"\nmsgstr \"``VOTERLIST_NOT_FOUND``\"\n\n#: ../../12-lisad.rst:76\nmsgid \"Versioonile vastavat valijate nimekirja ei leitud.\"\nmsgstr \"No voter list matching the version was found.\"\n\n#: ../../12-lisad.rst:77\nmsgid \"``TIME_BEFORE_START``\"\nmsgstr \"``TIME_BEFORE_START``\"\n\n#: ../../12-lisad.rst:78\nmsgid \"Hääl on antud enne hääletamisperioodi algust. Viitab testhäälele.\"\nmsgstr \"\"\n\"The vote is cast before the start of the voting period. Refers to a test \"\n\"vote.\"\n\n#: ../../12-lisad.rst:79\nmsgid \"``REG_RESP_INVALID``\"\nmsgstr \"``REG_RESP_INVALID``\"\n\n#: ../../12-lisad.rst:80\nmsgid \"Registreerimistõend/ajatempel on vigane. Viga ATOs või töötlejas.\"\nmsgstr \"\"\n\"The registration certificate/time stamp is invalid. Error in ATO or \"\n\"processor.\"\n\n#: ../../12-lisad.rst:81\nmsgid \"``REG_REQ_INVALID``\"\nmsgstr \"``REG_REQ_INVALID``\"\n\n#: ../../12-lisad.rst:82\nmsgid \"Registreerimispäring on vigane. Viga kogumisteenuses või töötlejas.\"\nmsgstr \"\"\n\"The registration request is invalid. Error in collection service or \"\n\"processor.\"\n\n#: ../../12-lisad.rst:83\nmsgid \"``REG_RESP_NOT_UNIQUE``\"\nmsgstr \"``REG_RESP_NOT_UNIQUE``\"\n\n#: ../../12-lisad.rst:84\nmsgid \"\"\n\"Registreerimistõend ei ole unikaalne. Viga kogumisteenuses või töötlejas.\"\nmsgstr \"\"\n\"The registration certificate is not unique. Error in collection service or \"\n\"processor.\"\n\n#: ../../12-lisad.rst:85\nmsgid \"``REG_REQ_NOT_UNIQUE``\"\nmsgstr \"``REG_REQ_NOT_UNIQUE``\"\n\n#: ../../12-lisad.rst:86\nmsgid \"\"\n\"Registreerimispäring ei ole unikaalne. Sama räsiga häält on esitatud \"\n\"korduvalt.\"\nmsgstr \"\"\n\"The registration request is not unique. The same ragged vote has been \"\n\"submitted multiple times.\"\n\n#: ../../12-lisad.rst:87\nmsgid \"``REG_NO_NONCE``\"\nmsgstr \"``REG_NO_NONCE``\"\n\n#: ../../12-lisad.rst:88\nmsgid \"Registreerimistõendis puudub nonss.\"\nmsgstr \"There is no nonss on the registration certificate.\"\n\n#: ../../12-lisad.rst:89\nmsgid \"``REG_NONCE_NOT_SIG``\"\nmsgstr \"``REG_NONCE_NOT_SIG``\"\n\n#: ../../12-lisad.rst:90\nmsgid \"Esitatud nonss ei ole IVXV protokolli kohaselt allkirjastatud.\"\nmsgstr \"\"\n\"The submitted nonss have not been signed in accordance with Protocol IVXV.\"\n\n#: ../../12-lisad.rst:91\nmsgid \"``REG_NONCE_ALG_MISMATCH``\"\nmsgstr \"``REG_NONCE_ALG_MISMATCH``\"\n\n#: ../../12-lisad.rst:92\nmsgid \"Nonssi allkirjastamisel kasutatud algoritm ei vasta oodatule.\"\nmsgstr \"\"\n\"The algorithm used to sign the nonsense does not match the expected one.\"\n\n#: ../../12-lisad.rst:93\nmsgid \"``REG_NONCE_SIG_INVALID``\"\nmsgstr \"``REG_NONCE_SIG_INVALID``\"\n\n#: ../../12-lisad.rst:94\nmsgid \"Nonssi allkiri on vigane.\"\nmsgstr \"Nonss's signature is incorrect.\"\n\n#: ../../12-lisad.rst:95\nmsgid \"``UNKNOWN_FILE_IN_VOTE_CONTAINER``\"\nmsgstr \"``UNKNOWN_FILE_IN_VOTE_CONTAINER``\"\n\n#: ../../12-lisad.rst:96\nmsgid \"Hääle konteineris on tundmatu fail.\"\nmsgstr \"There is an unknown file in the voice container.\"\n\n#: ../../12-lisad.rst:97\nmsgid \"``TECHNICAL_ERROR``\"\nmsgstr \"``TECHNICAL_ERROR``\"\n\n#: ../../12-lisad.rst:98\nmsgid \"Töötlemise ajal tekkis tehniline viga.\"\nmsgstr \"A technical error occurred during processing.\"\n\n#: ../../12-lisad.rst:99\nmsgid \"``REG_RESP_REQ_UNMATCH``\"\nmsgstr \"``REG_RESP_REQ_UNMATCH``\"\n\n#: ../../12-lisad.rst:100\nmsgid \"Registreerimistõendi andmed ei vasta registreerimispäringu andmetele.\"\nmsgstr \"\"\n\"The information on the registration certificate does not match the \"\n\"information on the registration request.\"\n\n#: ../../12-lisad.rst:101\nmsgid \"``REG_REQ_WITHOUT_BALLOT``\"\nmsgstr \"``REG_REQ_WITHOUT_BALLOT``\"\n\n#: ../../12-lisad.rst:102\nmsgid \"\"\n\"Esitatud on registreerimispäring, kuid hääl puudub urnist. Kogumisteenuse \"\n\"viga.\"\nmsgstr \"\"\n\"A registration request has been submitted, but there is no vote in the \"\n\"ballot box. Collection service error.\"\n\n#: ../../12-lisad.rst:103\nmsgid \"``BALLOT_WITHOUT_REG_REQ``\"\nmsgstr \"``BALLOT_WITHOUT_REG_REQ``\"\n\n#: ../../12-lisad.rst:104\nmsgid \"Hääl on esitatud ilma vastava registreerimispäringuta. ATO viga.\"\nmsgstr \"\"\n\"The vote is submitted without a corresponding registration request. ATO \"\n\"error.\"\n\n#: ../../12-lisad.rst:105\nmsgid \"``SAME_TIME_AS_LATEST``\"\nmsgstr \"``SAME_TIME_AS_LATEST``\"\n\n#: ../../12-lisad.rst:106\nmsgid \"Kaks häält samalt valijalt võivad olla arvesse võetavad viimasena.\"\nmsgstr \"Two votes from the same voter may be counted last.\"\n\n#: ../../12-lisad.rst:107\nmsgid \"``INVALID_SIGNATURE_PROFILE``\"\nmsgstr \"``INVALID_SIGNATURE_PROFILE``\"\n\n#: ../../12-lisad.rst:108\nmsgid \"Hääle allkirjaprofiil on vigane.\"\nmsgstr \"The signature profile of the vote is incorrect.\"\n\n#: ../../12-lisad.rst:111\nmsgid \"\"\n\"Töötlemise käigus tuvastatakse kehtetud sedelid sellisel määral, mil seda \"\n\"võimaldavad kontrollid avaliku võtmega. Kuigi kogumisteenus ei luba \"\n\"kehtetuid sedeleid talletada, peab töötlemisrakendus kontrolle siiski \"\n\"kordama tagamaks mh. võimalike tarkvaravigade vältimise.\"\nmsgstr \"\"\n\"Invalid tags will be identified during the processing to the extent that \"\n\"public key checks allow. Although the collection service does not allow the \"\n\"storage of invalid tags, the processing application must still repeat the \"\n\"checks to ensure, inter alia, that possible software bugs are avoided.\"\n\n#: ../../12-lisad.rst:117\nmsgid \"Krüptogrammide kehtivuse kontrolli veakoodid\"\nmsgstr \"Cryptogram validity check error codes\"\n\n#: ../../12-lisad.rst:123\nmsgid \"``INVALID_BYTES``\"\nmsgstr \"``INVALID_BYTES``\"\n\n#: ../../12-lisad.rst:124\nmsgid \"Baidijada ei ole dekodeeritav ElGamalCiphertext'na\"\nmsgstr \"Baidijada is not decodable by ElGamalCiphertext\"\n\n#: ../../12-lisad.rst:125\nmsgid \"``INVALID_GROUP``\"\nmsgstr \"``INVALID_GROUP``\"\n\n#: ../../12-lisad.rst:126\nmsgid \"Väärtus ei ole oodatud rühma element\"\nmsgstr \"The value is not an expected group element\"\n\n#: ../../12-lisad.rst:127\nmsgid \"``INVALID_RANGE``\"\nmsgstr \"``INVALID_RANGE``\"\n\n#: ../../12-lisad.rst:128\nmsgid \"Väärtus on lubatud vahemikust väljas.\"\nmsgstr \"The value is out of range.\"\n\n#: ../../12-lisad.rst:129\nmsgid \"``INVALID_QR``\"\nmsgstr \"``INVALID_QR```\"\n\n#: ../../12-lisad.rst:130\nmsgid \"Väärtus ei ole ruutjääk (MODP).\"\nmsgstr \"The value is not a square residual (MODP).\"\n\n#: ../../12-lisad.rst:131\nmsgid \"``INVALID_POINT``\"\nmsgstr \"``INVALID_POINT``\"\n\n#: ../../12-lisad.rst:132\nmsgid \"Väärtus ei ole kõvera punkt (ECC).\"\nmsgstr \"The value is not the point of the curve (ECC).\"\n\n#: ../../12-lisad.rst:133\nmsgid \"``INVALID``\"\nmsgstr \"``INVALID``\"\n\n#: ../../12-lisad.rst:134\nmsgid \"Vigane šifreeritud tekst.\"\nmsgstr \"Erroneous shifter text.\"\n\n#~ msgid \"IVXV ElGamal ECC spetsifiilised andmestruktuurid\"\n#~ msgstr \"\"\n"
  },
  {
    "path": "Documentation/public/protokollid/locales/en/LC_MESSAGES/index.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.8.2\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\n#: ../../index.rst:5\nmsgid \"IVXV protokollid\"\nmsgstr \"IVXV Protocols\"\n"
  },
  {
    "path": "Documentation/public/protokollid/model/Makefile",
    "content": "# Source diagrams are all in different .pu files\nSRC_DIAG=\n\nPARENT=sequence\n\n# Sub-diagrams are generated from $(PARENT).pu\nSUB_DIAG=phase1 phase2 phase3\n\n\ninclude ../../../common-model.mk\n\n\n# Set environment variables for subdiagrams\nphase1.env:\n\t$(eval DIAGRAM_DEF=-DPHASE_1)\n\nphase2.env:\n\t$(eval DIAGRAM_DEF=-DPHASE_2)\n\nphase3.env:\n\t$(eval DIAGRAM_DEF=-DPHASE_3)\n"
  },
  {
    "path": "Documentation/public/protokollid/model/en/lang.pu",
    "content": "\n!define TR_A_VOTER() Voter\n!define TR_P_VOTA() Voting application\n!define TR_P_VERA() Verification app\n\n!define TR_BOX_COLLECT() Online (RIA)\n!define TR_P_COLLECT() Collection service\n\n!define TR_BOX_REG() Registration service (SK)\n!define TR_P_REG() Registration service\n\n!define TR_BOX_PROCESSOR() Offline (RVT)\n!define TR_P_PROCESSOR() Processor\n\n\n!define TR_PHASE1() Voter votes\n!define TR_PHASE2() Voter verifies the vote\n!define TR_PHASE3() Services hand over the data\n!define TR_PHASE4() Processor verifies consistency of the data\n\n"
  },
  {
    "path": "Documentation/public/protokollid/model/et/lang.pu",
    "content": "\n!define TR_A_VOTER() Hääletaja\n!define TR_P_VOTA() Valijarakendus\n!define TR_P_VERA() Kontrollrakendus\n\n!define TR_BOX_COLLECT() Online (RIA)\n!define TR_P_COLLECT() Kogumisteenus\n\n!define TR_BOX_REG() Registreerimisteenus (SK)\n!define TR_P_REG() Registreerimisteenus\n\n!define TR_BOX_PROCESSOR() Offline (RVT)\n!define TR_P_PROCESSOR() Töötlemisrakendus\n\n\n!define TR_PHASE1() Hääletaja hääletab\n!define TR_PHASE2() Hääletaja kontrollib häält\n!define TR_PHASE3() Teenused annavad andmed üle\n!define TR_PHASE4() Töötleja kontrollib teenuste vaateid\n\n"
  },
  {
    "path": "Documentation/public/protokollid/model/sequence.pu",
    "content": "@startuml\n\n\n!ifdef PHASE_1\n    !define PHASE_VOTE\n    !define PHASE_VOTER\n    !define PHASE_COLLECT\n    !define PHASE_REG\n!endif\n\n!ifdef PHASE_2\n    !define PHASE_VERIFY\n    !define PHASE_VOTER\n    !define PHASE_COLLECT\n!endif\n\n!ifdef PHASE_3\n    !define PHASE_COLLECT\n    !define PHASE_REG\n    !define PHASE_PROC\n!endif\n\n\n\n!ifdef PHASE_VOTER\nactor \"TR_A_VOTER()\" as a_voter\n!endif\n\n!ifdef PHASE_VOTE\nparticipant \"TR_P_VOTA()\" as p_vota\n!endif\n\n!ifdef PHASE_VERIFY\nparticipant \"TR_P_VERA()\" as p_vera\n!endif\n\n!ifdef PHASE_COLLECT\nbox \"TR_BOX_COLLECT()\"\n    participant \"TR_P_COLLECT()\" as p_collect\nend box\n!endif\n\n!ifdef PHASE_REG\nbox \"TR_BOX_REG()\"\nparticipant \"TR_P_REG()\" as p_reg\nend box\n!endif\n\n!ifdef PHASE_PROC\nbox \"TR_BOX_PROCESSOR()\"\n  participant \"TR_P_PROCESSOR()\" as p_processor\nend box\n!endif\n\n\n!ifdef PHASE_1\n\n== TR_PHASE1() ==\n\na_voter --> p_vota: ballot_v, key_v\n\np_vota -> p_collect: vote_v = Sign(key_v, Enc(ballot_v))\n\np_collect -> p_reg: reg_request_v = Sign(key_k, (v_id, Hash(vote_v)))\n\np_reg -> p_reg: VerifyAndStore(reg_request_v, cert_k)\n\np_reg -> p_collect: reg_confirmation_v = Sign(key_r, Hash(reg_request_v))\n\np_collect -> p_collect: VerifyAndStore(reg_confirmation_v, cert_r)\n\np_collect -> p_vota: v_id, reg_request_v, reg_confirmation_v\n\np_vota -> p_vota: Verify(reg_request_v, cert_k)\np_vota -> p_vota: Verify(reg_confirmation_v, cert_r)\np_vota -> p_vota: Verify(reg_confirmation_v, reg_request_v, vote_v)\np_vota --> a_voter: v_id\n\n!endif\n\n\n!ifdef PHASE_2\n\n== TR_PHASE2() ==\n\na_voter --> p_vera: v_id\n\np_vera -> p_collect: v_id\n\np_collect -> p_vera: vote_v, reg_request_v, reg_confirmation_v\n\np_vera -> p_vera: Verify(reg_request_v, cert_k)\np_vera -> p_vera: Verify(reg_confirmation_v, cert_r)\np_vera -> p_vera: Verify(reg_confirmation_v, reg_request_v, vote_v)\np_vera --> a_voter: ballot_v\n\n!endif\n\n\n!ifdef PHASE_3\n\n== TR_PHASE3() ==\n\np_collect -> p_processor: (v_id, vote_i, reg_request_i, reg_confirmation_i)\np_reg -> p_processor: (v_id, Hash(vote_i), reg_request_i)\n\n\n== TR_PHASE4() ==\n\n!endif\n\n\n\n@enduml\n"
  },
  {
    "path": "Documentation/public/protokollid/paragraph.txt",
    "content": "3. Kehtestada elektroonilise hääle vorm järgmiselt (skeem lisatud).\n\n  3.1. Valijarakenduses elektrooniliselt allkirjastatud elektrooniline\n    hääl on digitaalselt allkirjastatud fail, mille nimi on teksti\n    kujul e-hääletamise aeg (ajatempel). Faili sees asub punktis 3.2\n    nimetatud salastatud (krüpteeritud) elektrooniline hääl ja valija\n    elektrooniline allkiri. Elektroonilise allkirja sertifikaatide\n    kehtivuskinnitus ja ajatempel saadakse usaldusteenuse pakkujalt\n    kogujasse eraldi ning valijarakenduse faili ennast ei muudeta,\n    vaid kehtivuskinnitus ja ajatempel hoiustatakse valijarakenduses\n    loodud allkirjastatud failist lahus.\n\n  3.2. Elektrooniline hääl on krüpteeritud Riigikogu valimiste seaduse\n    §483 lõike 3 alusel hääletamiseks loodud elektrooniliste häälte\n    salastamise võtmega andmefail. Faili nimi algab valimiste või\n    rahvahääletuse identifikaatoriga ja lõpeb küsimuse\n    identifikaatoriga. Faili laiend on .ballot. Faili sees on\n    krüpteeritud kujul punktides 3.4, 3.5, 3.6 või 3.7 nimetatud\n    andmeelemendid vastavalt sellele, kas tegemist on Euroopa\n    Parlamendi, Riigikogu või kohaliku omavalitsuse volikogu valimiste\n    või rahvahääletusega.\n\n  3.3. Avatud (dekrüpteeritud) elektrooniline hääl on elektrooniliste\n    häälte avamise arvuti (võtmeprotseduurideks mõeldud arvuti) mälus\n    olev andmehulk, mis koosneb kahest andmeelemendist ja neid\n    eraldavatest märgenditest.\n\n  3.4. Euroopa Parlamendi valimiste andmeelementide ja märgendite\n    järjekord on:\n\n    3.4.1. sõne 0000 ja märgend ASCII kuueteistkümnendkood 2E;\n\n    3.4.2. kandidaadi registreerimisnumber.\n\n  3.5. Riigikogu valimiste andmeelementide ja märgendite järjekord on:\n\n    3.5.1. sõne 0000 ja märgend ASCII kuueteistkümnendkood 2E;\n\n    3.5.2. kandidaadi registreerimisnumber.\n\n  3.6. Kohaliku omavalitsuse volikogu valimiste andmeelementide ja\n    märgendite järjekord on:\n\n    3.6.1. kohaliku omavalitsuse üksuse EHAK kood ja märgend ASCII\n      kuueteistkümnendkood 2E;\n\n    3.6.2. kandidaadi registreerimisnumber.\n\n  3.7. Rahvahääletuse andmeelementide ja märgendite järjekord on:\n\n    3.7.1. sõne 0000 ja märgend ASCII kuueteistkümnendkood 2E;\n\n    3.7.2. vastuse kood.\n"
  },
  {
    "path": "Documentation/public/protokollid/spelling_wordlist.txt",
    "content": ""
  },
  {
    "path": "Documentation/public/uldsisukord/.gitignore",
    "content": "nimekiri.rst\n"
  },
  {
    "path": "Documentation/public/uldsisukord/Makefile",
    "content": "DEPENDENCIES := nimekiri.rst\ninclude ../../common.mk\n\nnimekiri.rst:\n\t../../documents.py > $@\n"
  },
  {
    "path": "Documentation/public/uldsisukord/annotatsioon.rst",
    "content": "..  IVXV dokumentatsiooni üldsisukord\n\nAnnotatsioon\n------------\n\nIVXV dokumentatsiooni üldsisukord,\ninfosüsteemi dokumentide sisukorradokument.\n"
  },
  {
    "path": "Documentation/public/uldsisukord/index.rst",
    "content": "..  IVXV dokumentatsiooni üldsisukord\n\nIVXV dokumentatsiooni üldsisukord\n=========================================================================\n\n.. raw:: html\n\n   <p style=\"background-color: #f99; padding: 20px;\">\n     <strong>NB!</strong>\n     See on HTML-versioon dokumendist.\n     Tellijale antakse üle PDF-versioon.\n   </p>\n\n\n.. toctree::\n   :maxdepth: 2\n\n   annotatsioon\n   nimekiri\n   tarned\n"
  },
  {
    "path": "Documentation/public/uldsisukord/locales/en/LC_MESSAGES/annotatsioon.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\n#: ../../annotatsioon.rst:4\nmsgid \"Annotatsioon\"\nmsgstr \"Annotation\"\n\n#: ../../annotatsioon.rst:6\nmsgid \"\"\n\"IVXV dokumentatsiooni üldsisukord, infosüsteemi dokumentide \"\n\"sisukorradokument.\"\nmsgstr \"\"\n\"Table of contents for the IVXV documentation.\"\n"
  },
  {
    "path": "Documentation/public/uldsisukord/locales/en/LC_MESSAGES/index.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2024-02-29 10:34+0200\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Language: en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"Generated-By: Babel 2.10.3\\n\"\n\n#: ../../index.rst:4\nmsgid \"IVXV dokumentatsiooni üldsisukord\"\nmsgstr \"IVXV documentation overview\"\n"
  },
  {
    "path": "Documentation/public/uldsisukord/locales/en/LC_MESSAGES/nimekiri.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2025, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-25 13:00+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../nimekiri.rst:4\nmsgid \"Dokumendid\"\nmsgstr \"Documents\"\n\n#: ../../nimekiri.rst:7\nmsgid \"Ülddokumendid\"\nmsgstr \"General documents\"\n\n#: ../../nimekiri.rst:14 ../../nimekiri.rst:32 ../../nimekiri.rst:65\n#: ../../nimekiri.rst:98\nmsgid \"Nimi\"\nmsgstr \"Name\"\n\n#: ../../nimekiri.rst:15 ../../nimekiri.rst:33 ../../nimekiri.rst:66\n#: ../../nimekiri.rst:99\nmsgid \"ID\"\nmsgstr \"ID\"\n\n#: ../../nimekiri.rst:16 ../../nimekiri.rst:34 ../../nimekiri.rst:67\n#: ../../nimekiri.rst:100\nmsgid \"Versioon\"\nmsgstr \"Version\"\n\n#: ../../nimekiri.rst:17 ../../nimekiri.rst:35 ../../nimekiri.rst:68\n#: ../../nimekiri.rst:101\nmsgid \"Kuupäev\"\nmsgstr \"Date\"\n\n#: ../../nimekiri.rst:19\nmsgid \"IVXV dokumentatsiooni üldsisukord\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:20\nmsgid \"IVXV-YS-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:21 ../../nimekiri.rst:44 ../../nimekiri.rst:49\n#: ../../nimekiri.rst:72 ../../nimekiri.rst:77 ../../nimekiri.rst:82\n#: ../../nimekiri.rst:87 ../../nimekiri.rst:105 ../../nimekiri.rst:110\n#: ../../nimekiri.rst:115 ../../nimekiri.rst:125 ../../nimekiri.rst:130\n#: ../../nimekiri.rst:135\nmsgid \"1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:22 ../../nimekiri.rst:45 ../../nimekiri.rst:50\n#: ../../nimekiri.rst:73 ../../nimekiri.rst:78 ../../nimekiri.rst:83\n#: ../../nimekiri.rst:88 ../../nimekiri.rst:106 ../../nimekiri.rst:111\n#: ../../nimekiri.rst:116 ../../nimekiri.rst:126 ../../nimekiri.rst:131\n#: ../../nimekiri.rst:136\nmsgid \"25.09.2025\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:25\nmsgid \"Spetsifikatsioonid\"\nmsgstr \"Specifications\"\n\n#: ../../nimekiri.rst:37\nmsgid \"IVXV kasutusmallid\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:38\nmsgid \"IVXV-KM-1.8.1\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:39\nmsgid \"1.8.1\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:40\nmsgid \"16.12.2022\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:42\nmsgid \"IVXV protokollide kirjeldus\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:43\nmsgid \"IVXV-PR-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:47\nmsgid \"IVXV arhitektuur\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:48\nmsgid \"IVXV-AR-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:52\nmsgid \"IVXV võtmerakendus\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:53\nmsgid \"IVXV-SVR-1.8.0\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:54 ../../nimekiri.rst:120\nmsgid \"1.8.0\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:55 ../../nimekiri.rst:121\nmsgid \"01.12.2022\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:58\nmsgid \"Ingliskeelsed dokumendid\"\nmsgstr \"Documents in english\"\n\n#: ../../nimekiri.rst:70\nmsgid \"IVXV protocols\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:71\nmsgid \"IVXV-PR-EN-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:75\nmsgid \"IVXV architecture\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:76\nmsgid \"IVXV-AR-EN-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:80\nmsgid \"VIS3-EHS interfaces\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:81\nmsgid \"IVXV-VIS-EHS-EN-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:85\nmsgid \"IVXV Backend Log Messages\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:86\nmsgid \"IVXV-LOGS-EN-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:91\nmsgid \"Juhendid\"\nmsgstr \"User guides\"\n\n#: ../../nimekiri.rst:103\nmsgid \"IVXV seadistuste koostamise juhend\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:104\nmsgid \"IVXV-JSK-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:108\nmsgid \"IVXV kogumisteenuse haldusjuhend\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:109\nmsgid \"IVXV-JSH-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:113\nmsgid \"IVXV haldusteenuse kasutusjuhend\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:114\nmsgid \"IVXV-JHT-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:118\nmsgid \"IVXV API\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:119\nmsgid \"IVXV-API-1.8.0\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:123\nmsgid \"IVXV audiitori juhend\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:124\nmsgid \"IVXV-JAJ-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:128\nmsgid \"IVXV valijarakendus\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:129\nmsgid \"IVXV-JVR-1.10\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:133\nmsgid \"IVXV valijarakenduse pakendamine\"\nmsgstr \"\"\n\n#: ../../nimekiri.rst:134\nmsgid \"IVXV-JVP-1.10\"\nmsgstr \"\"\n"
  },
  {
    "path": "Documentation/public/uldsisukord/locales/en/LC_MESSAGES/tarned.po",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) 2016-2024, Cybernetica AS\n# This file is distributed under the same license as the Elektroonilise\n# hääletamise infosüsteem package.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Elektroonilise hääletamise infosüsteem 1.10.3\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2025-09-25 13:06+0000\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: en\\n\"\n\"Language-Team: en <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.15.0\\n\"\n\n#: ../../tarned.rst:4\nmsgid \"Tarned\"\nmsgstr \"Deliveries\"\n\n#: ../../tarned.rst:7\nmsgid \"Muudatused tarne 1.10.4 koosseisus, erinevused võrreldes tarnega 1.10.3\"\nmsgstr \"\"\n\"Changes in the composition of the delivery 1.10.4, differences compared \"\n\"to delivery 1.10.3.\"\n\n#: ../../tarned.rst:10 ../../tarned.rst:78 ../../tarned.rst:146\n#: ../../tarned.rst:236 ../../tarned.rst:302 ../../tarned.rst:380\n#: ../../tarned.rst:451 ../../tarned.rst:545 ../../tarned.rst:614\n#: ../../tarned.rst:688 ../../tarned.rst:762 ../../tarned.rst:844\n#: ../../tarned.rst:921 ../../tarned.rst:1003 ../../tarned.rst:1084\n#: ../../tarned.rst:1174 ../../tarned.rst:1263\nmsgid \"Kogumisteenus\"\nmsgstr \"Collection service\"\n\n#: ../../tarned.rst:12 ../../tarned.rst:80 ../../tarned.rst:148\n#: ../../tarned.rst:238 ../../tarned.rst:304 ../../tarned.rst:382\n#: ../../tarned.rst:453 ../../tarned.rst:547 ../../tarned.rst:616\n#: ../../tarned.rst:690 ../../tarned.rst:764 ../../tarned.rst:846\n#: ../../tarned.rst:923 ../../tarned.rst:1005 ../../tarned.rst:1086\n#: ../../tarned.rst:1176\nmsgid \"Üldised muutused\"\nmsgstr \"General changes\"\n\n#: ../../tarned.rst:14\nmsgid \"Valijate nimekirjade muudatuste allalaadimise täpsustused\"\nmsgstr \"Clarify the download process for voterlist changesets\"\n\n#: ../../tarned.rst:17 ../../tarned.rst:85 ../../tarned.rst:160\n#: ../../tarned.rst:243 ../../tarned.rst:311 ../../tarned.rst:387\n#: ../../tarned.rst:470 ../../tarned.rst:555 ../../tarned.rst:625\n#: ../../tarned.rst:703 ../../tarned.rst:779 ../../tarned.rst:860\n#: ../../tarned.rst:938 ../../tarned.rst:1018 ../../tarned.rst:1102\n#: ../../tarned.rst:1193 ../../tarned.rst:1295\nmsgid \"Registreerimisteenus\"\nmsgstr \"Registration service\"\n\n#: ../../tarned.rst:19 ../../tarned.rst:26 ../../tarned.rst:30\n#: ../../tarned.rst:35 ../../tarned.rst:42 ../../tarned.rst:46\n#: ../../tarned.rst:50 ../../tarned.rst:54 ../../tarned.rst:59\n#: ../../tarned.rst:71 ../../tarned.rst:82 ../../tarned.rst:87\n#: ../../tarned.rst:94 ../../tarned.rst:98 ../../tarned.rst:103\n#: ../../tarned.rst:110 ../../tarned.rst:114 ../../tarned.rst:118\n#: ../../tarned.rst:122 ../../tarned.rst:127 ../../tarned.rst:139\n#: ../../tarned.rst:162 ../../tarned.rst:186 ../../tarned.rst:245\n#: ../../tarned.rst:261 ../../tarned.rst:268 ../../tarned.rst:272\n#: ../../tarned.rst:276 ../../tarned.rst:280 ../../tarned.rst:285\n#: ../../tarned.rst:290 ../../tarned.rst:313 ../../tarned.rst:389\n#: ../../tarned.rst:407 ../../tarned.rst:414 ../../tarned.rst:427\n#: ../../tarned.rst:472 ../../tarned.rst:491\nmsgid \"Muudatused puuduvad\"\nmsgstr \"No changes\"\n\n#: ../../tarned.rst:22 ../../tarned.rst:90 ../../tarned.rst:165\n#: ../../tarned.rst:248 ../../tarned.rst:316 ../../tarned.rst:392\n#: ../../tarned.rst:475 ../../tarned.rst:560 ../../tarned.rst:630\n#: ../../tarned.rst:708 ../../tarned.rst:784 ../../tarned.rst:865\n#: ../../tarned.rst:943 ../../tarned.rst:1023 ../../tarned.rst:1106\n#: ../../tarned.rst:1197 ../../tarned.rst:1299\nmsgid \"Kontrollrakendused\"\nmsgstr \"Verification applications\"\n\n#: ../../tarned.rst:24 ../../tarned.rst:92 ../../tarned.rst:167\n#: ../../tarned.rst:250 ../../tarned.rst:318 ../../tarned.rst:394\n#: ../../tarned.rst:477 ../../tarned.rst:562 ../../tarned.rst:632\n#: ../../tarned.rst:710 ../../tarned.rst:786 ../../tarned.rst:867\n#: ../../tarned.rst:945 ../../tarned.rst:1025 ../../tarned.rst:1108\n#: ../../tarned.rst:1199 ../../tarned.rst:1301\nmsgid \"Android\"\nmsgstr \"Android\"\n\n#: ../../tarned.rst:28 ../../tarned.rst:96 ../../tarned.rst:175\n#: ../../tarned.rst:254 ../../tarned.rst:324 ../../tarned.rst:399\n#: ../../tarned.rst:483 ../../tarned.rst:567 ../../tarned.rst:638\n#: ../../tarned.rst:714 ../../tarned.rst:791 ../../tarned.rst:871\n#: ../../tarned.rst:949 ../../tarned.rst:1029 ../../tarned.rst:1114\n#: ../../tarned.rst:1204 ../../tarned.rst:1305\nmsgid \"iOS\"\nmsgstr \"iOS\"\n\n#: ../../tarned.rst:33 ../../tarned.rst:101 ../../tarned.rst:184\n#: ../../tarned.rst:259 ../../tarned.rst:331 ../../tarned.rst:405\n#: ../../tarned.rst:489 ../../tarned.rst:572 ../../tarned.rst:645\n#: ../../tarned.rst:719 ../../tarned.rst:797 ../../tarned.rst:876\n#: ../../tarned.rst:954 ../../tarned.rst:1034 ../../tarned.rst:1119\n#: ../../tarned.rst:1211 ../../tarned.rst:1310\nmsgid \"Miksnet\"\nmsgstr \"Mixnet\"\n\n#: ../../tarned.rst:38 ../../tarned.rst:106 ../../tarned.rst:189\n#: ../../tarned.rst:264 ../../tarned.rst:336 ../../tarned.rst:410\n#: ../../tarned.rst:494 ../../tarned.rst:577 ../../tarned.rst:650\n#: ../../tarned.rst:724 ../../tarned.rst:802 ../../tarned.rst:882\n#: ../../tarned.rst:959 ../../tarned.rst:1039 ../../tarned.rst:1124\n#: ../../tarned.rst:1216 ../../tarned.rst:1314\nmsgid \"Töötleja rakendused\"\nmsgstr \"Processor applications\"\n\n#: ../../tarned.rst:40 ../../tarned.rst:108 ../../tarned.rst:191\n#: ../../tarned.rst:266 ../../tarned.rst:338 ../../tarned.rst:412\n#: ../../tarned.rst:496 ../../tarned.rst:579 ../../tarned.rst:652\n#: ../../tarned.rst:726 ../../tarned.rst:804 ../../tarned.rst:884\n#: ../../tarned.rst:961 ../../tarned.rst:1041 ../../tarned.rst:1126\nmsgid \"Üldised\"\nmsgstr \"General\"\n\n#: ../../tarned.rst:44 ../../tarned.rst:112 ../../tarned.rst:195\n#: ../../tarned.rst:270 ../../tarned.rst:343 ../../tarned.rst:416\n#: ../../tarned.rst:500 ../../tarned.rst:583 ../../tarned.rst:656\n#: ../../tarned.rst:730 ../../tarned.rst:808 ../../tarned.rst:888\n#: ../../tarned.rst:965 ../../tarned.rst:1045 ../../tarned.rst:1132\n#: ../../tarned.rst:1218 ../../tarned.rst:1315\nmsgid \"Võtmerakendus\"\nmsgstr \"Key application\"\n\n#: ../../tarned.rst:48 ../../tarned.rst:116 ../../tarned.rst:199\n#: ../../tarned.rst:274 ../../tarned.rst:348 ../../tarned.rst:421\n#: ../../tarned.rst:505 ../../tarned.rst:587 ../../tarned.rst:660\n#: ../../tarned.rst:734 ../../tarned.rst:812 ../../tarned.rst:892\n#: ../../tarned.rst:969 ../../tarned.rst:1052 ../../tarned.rst:1136\n#: ../../tarned.rst:1222 ../../tarned.rst:1319\nmsgid \"Töötlemisrakendus\"\nmsgstr \"Processing application\"\n\n#: ../../tarned.rst:52 ../../tarned.rst:120 ../../tarned.rst:203\n#: ../../tarned.rst:278 ../../tarned.rst:353 ../../tarned.rst:425\n#: ../../tarned.rst:510 ../../tarned.rst:591 ../../tarned.rst:664\n#: ../../tarned.rst:738 ../../tarned.rst:816 ../../tarned.rst:896\n#: ../../tarned.rst:973 ../../tarned.rst:1056 ../../tarned.rst:1140\n#: ../../tarned.rst:1229 ../../tarned.rst:1323\nmsgid \"Auditirakendus\"\nmsgstr \"Audit application\"\n\n#: ../../tarned.rst:57 ../../tarned.rst:125 ../../tarned.rst:208\n#: ../../tarned.rst:283 ../../tarned.rst:359 ../../tarned.rst:430\n#: ../../tarned.rst:516 ../../tarned.rst:596 ../../tarned.rst:669\n#: ../../tarned.rst:743 ../../tarned.rst:821 ../../tarned.rst:901\n#: ../../tarned.rst:979 ../../tarned.rst:1061 ../../tarned.rst:1145\n#: ../../tarned.rst:1234 ../../tarned.rst:1328\nmsgid \"Valijarakendused ja seadistusrakendus\"\nmsgstr \"Voting applications and configuration app\"\n\n#: ../../tarned.rst:62 ../../tarned.rst:130 ../../tarned.rst:217\n#: ../../tarned.rst:288 ../../tarned.rst:365 ../../tarned.rst:435\n#: ../../tarned.rst:528 ../../tarned.rst:601 ../../tarned.rst:675\n#: ../../tarned.rst:748 ../../tarned.rst:828 ../../tarned.rst:906\n#: ../../tarned.rst:985 ../../tarned.rst:1066 ../../tarned.rst:1157\n#: ../../tarned.rst:1245 ../../tarned.rst:1338\nmsgid \"Dokumentatsioon\"\nmsgstr \"Documentation\"\n\n#: ../../tarned.rst:64\nmsgid \"Numeratsiooni läbivaatus\"\nmsgstr \"\"\n\n#: ../../tarned.rst:65\nmsgid \"Võtmerakenduse seadistuste täpsustamine\"\nmsgstr \"Clarify the configuration of key app\"\n\n#: ../../tarned.rst:66\nmsgid \"Smart-ID protokolli näidete täpsustamine\"\nmsgstr \"Improved SmartID protocol examples\"\n\n#: ../../tarned.rst:69 ../../tarned.rst:137 ../../tarned.rst:224\n#: ../../tarned.rst:293 ../../tarned.rst:371 ../../tarned.rst:441\n#: ../../tarned.rst:534 ../../tarned.rst:606 ../../tarned.rst:680\n#: ../../tarned.rst:753 ../../tarned.rst:833 ../../tarned.rst:911\n#: ../../tarned.rst:992 ../../tarned.rst:1076 ../../tarned.rst:1163\n#: ../../tarned.rst:1251 ../../tarned.rst:1342\nmsgid \"Logimonitor\"\nmsgstr \"Logmonitor\"\n\n#: ../../tarned.rst:75\nmsgid \"Muudatused tarne 1.10.3 koosseisus, erinevused võrreldes tarnega 1.10.2\"\nmsgstr \"\"\n\"Changes in the composition of the delivery 1.10.3, differences compared \"\n\"to delivery 1.10.2.\"\n\n#: ../../tarned.rst:132 ../../tarned.rst:219\nmsgid \"Tõlgete läbivaatus\"\nmsgstr \"\"\n\n#: ../../tarned.rst:133 ../../tarned.rst:220\nmsgid \"Õigekirja läbivaatus\"\nmsgstr \"\"\n\n#: ../../tarned.rst:134 ../../tarned.rst:221\nmsgid \"Tarne 1.10.2 sisu dokumenteerimine\"\nmsgstr \"\"\n\n#: ../../tarned.rst:143\nmsgid \"Muudatused tarne 1.10.2 koosseisus, erinevused võrreldes tarnega 1.10.1\"\nmsgstr \"\"\n\"Changes in the composition of the delivery 1.10.2, differences compared \"\n\"to delivery 1.10.1.\"\n\n#: ../../tarned.rst:150 ../../tarned.rst:193\nmsgid \"ZIP/BDOC failide käsitluse parendamine\"\nmsgstr \"Improve handling of ZIP/BDOC files\"\n\n#: ../../tarned.rst:151\nmsgid \"Sõltuvuste uuendamine, JavaScript\"\nmsgstr \"Update dependencies, JavaScript\"\n\n#: ../../tarned.rst:152\nmsgid \"eID vahendite testimine\"\nmsgstr \"Test eID tools\"\n\n#: ../../tarned.rst:153\nmsgid \"Mälulekete parandamine\"\nmsgstr \"Fix memory leaks\"\n\n#: ../../tarned.rst:154\nmsgid \"Smart-ID voogude uuendamine vastavalt protokollile\"\nmsgstr \"Update Smart-ID flows according the protocol\"\n\n#: ../../tarned.rst:155\nmsgid \"Nimekirjade laadimise intervalli ja strateegia täpsustamine\"\nmsgstr \"Update the logic and configuration of downloading voterlists\"\n\n#: ../../tarned.rst:156\nmsgid \"Arendus- ja testkeskkonna parendused\"\nmsgstr \"Improvements to test and development tools\"\n\n#: ../../tarned.rst:157 ../../tarned.rst:197 ../../tarned.rst:201\n#: ../../tarned.rst:205\nmsgid \"Pisiparandused\"\nmsgstr \"Minor bug fixes\"\n\n#: ../../tarned.rst:169 ../../tarned.rst:177\nmsgid \"Rakenduse kolimine Riigikogu Kantselei poodi\"\nmsgstr \"Move application to Riigikogu Kantselei store\"\n\n#: ../../tarned.rst:170\nmsgid \"TargetSDK uuendamine\"\nmsgstr \"Update TargetSDK\"\n\n#: ../../tarned.rst:171 ../../tarned.rst:178\nmsgid \"Erinevad veaparandused kasutajaliideses\"\nmsgstr \"Minor UX changes/improvements\"\n\n#: ../../tarned.rst:172\nmsgid \"Täiendav obfuskeerimine\"\nmsgstr \"Improved obfuscation\"\n\n#: ../../tarned.rst:173 ../../tarned.rst:181\nmsgid \"Veateadete täiendamine\"\nmsgstr \"Improve error messages\"\n\n#: ../../tarned.rst:179\nmsgid \"Täpsustused rakenduse varundamisele\"\nmsgstr \"Update backup policy\"\n\n#: ../../tarned.rst:180\nmsgid \"Rakenduse elutsükli olekumasina täpsustused\"\nmsgstr \"Improve app lifecycle state machine\"\n\n#: ../../tarned.rst:210\nmsgid \"Sõltuvuste uuendamine, OpenSSL, PCRE\"\nmsgstr \"Update dependencies, OpenSSL, PCRE\"\n\n#: ../../tarned.rst:211\nmsgid \"Kasutajaliidese teegi FLTK uuendamine\"\nmsgstr \"Update UX library FLTK\"\n\n#: ../../tarned.rst:212\nmsgid \"Smart-ID voo uuendamine\"\nmsgstr \"Update of Smart-ID flows\"\n\n#: ../../tarned.rst:213\nmsgid \"Testimine uusima ID-kaardiga\"\nmsgstr \"Test with most recent ID-card\"\n\n#: ../../tarned.rst:214\nmsgid \"Kompileerimiskeskkonna täpsustused\"\nmsgstr \"Improvements to compilation process\"\n\n#: ../../tarned.rst:226\nmsgid \"Sõltuvuste uuendamine, Python, JavaScript\"\nmsgstr \"Update dependencies, Python, JavaScript\"\n\n#: ../../tarned.rst:227\nmsgid \"Veebiliidese täiendused\"\nmsgstr \"Updates to web-interface\"\n\n#: ../../tarned.rst:228\nmsgid \"Päringuvahendaja ``pgbouncer`` kasutuselevõtt\"\nmsgstr \"Add ``pgbouncer``\"\n\n#: ../../tarned.rst:229\nmsgid \"Veebiserveri konfiguratsiooni läbivaatus turvatesti tulemustest lähtudes\"\nmsgstr \"Web-server configuration accordig to security test feedback\"\n\n#: ../../tarned.rst:233\nmsgid \"Muudatused tarne 1.10.1 koosseisus, erinevused võrreldes tarnega 1.10.0\"\nmsgstr \"\"\n\"Changes in the composition of the delivery 1.10.1, differences compared \"\n\"to delivery 1.10.0.\"\n\n#: ../../tarned.rst:240\nmsgid \"Uuenenud SmartID serdiprofiili toetamine\"\nmsgstr \"Support for updated SmartID certificate profile\"\n\n#: ../../tarned.rst:252 ../../tarned.rst:256 ../../tarned.rst:322\n#: ../../tarned.rst:328 ../../tarned.rst:397 ../../tarned.rst:402\nmsgid \"Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\"\nmsgstr \"Clarification of vote verification, corner cases\"\n\n#: ../../tarned.rst:295 ../../tarned.rst:373 ../../tarned.rst:444\nmsgid \"Pisiparandused vastavalt muutuste logile\"\nmsgstr \"Minor changes/improvements according to `changelog` file\"\n\n#: ../../tarned.rst:299\nmsgid \"Muudatused tarne 1.10.0 koosseisus, erinevused võrreldes tarnega 1.9.10\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.10.0, differences compared to \"\n\"delivery 1.9.10\"\n\n#: ../../tarned.rst:306\nmsgid \"Funktsionaalsus kehtetute sedelite eemaldamiseks kogumisteenuses\"\nmsgstr \"Functionality to remove invalid ballots in the collection service\"\n\n#: ../../tarned.rst:307\nmsgid \"MODP ja elliptkõverate krüptograafia\"\nmsgstr \"ModP and ECC cryptography\"\n\n#: ../../tarned.rst:308 ../../tarned.rst:384 ../../tarned.rst:467\nmsgid \"Erinevad pisiparandused vastavalt muutuste logile\"\nmsgstr \"Minor changes/improvements according to `changelog` file\"\n\n#: ../../tarned.rst:320 ../../tarned.rst:326 ../../tarned.rst:333\n#: ../../tarned.rst:340 ../../tarned.rst:345 ../../tarned.rst:350\n#: ../../tarned.rst:355 ../../tarned.rst:361\nmsgid \"Elliptkõverate krüptograafia\"\nmsgstr \"Elliptic curves\"\n\n#: ../../tarned.rst:321 ../../tarned.rst:327\nmsgid \"Ligipääsetavuse parandamine\"\nmsgstr \"Accessibility improvements\"\n\n#: ../../tarned.rst:341 ../../tarned.rst:346 ../../tarned.rst:351\n#: ../../tarned.rst:356\nmsgid \"Täpsustused urni verifitseerimisel, äärejuhtumite läbivaatus\"\nmsgstr \"Clarifications of ballot box verification, corner cases\"\n\n#: ../../tarned.rst:362\nmsgid \"Proksikasutuse refaktoreerimine\"\nmsgstr \"Refactor proxy usage\"\n\n#: ../../tarned.rst:367\nmsgid \"Protokollidokumendi täiendamine\"\nmsgstr \"Improve protocol document\"\n\n#: ../../tarned.rst:368\nmsgid \"Logide dokumendi loomine\"\nmsgstr \"Create documentation for log items\"\n\n#: ../../tarned.rst:377\nmsgid \"Muudatused tarne 1.9.10 koosseisus, erinevused võrreldes tarnega 1.9.4\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.9.10, differences compared to \"\n\"delivery 1.9.4\"\n\n#: ../../tarned.rst:396\nmsgid \"FLAG_SECURE kasutuselevõtt\"\nmsgstr \"Use FLAG_SECURE\"\n\n#: ../../tarned.rst:401\nmsgid \"Ligipääsetavuse parendamine\"\nmsgstr \"Accessibility improvements\"\n\n#: ../../tarned.rst:418\nmsgid \"Krüptogrammi kehtetuks kuulutamise äärejuhtumite läbivaatus\"\nmsgstr \"Corner cases for invalid ballot detection\"\n\n#: ../../tarned.rst:423\nmsgid \"Hääle kehtetuks kuulutamise äärejuhtumite läbivaatus\"\nmsgstr \"Corner cases for invalid ballot detection\"\n\n#: ../../tarned.rst:432\nmsgid \"Windows, ligipääsetavuse veaparandused\"\nmsgstr \"Improvement of accessibility under Windows\"\n\n#: ../../tarned.rst:437 ../../tarned.rst:531\nmsgid \"Eesti- ja ingliskeelse dokumentatsiooni ühtlustamine\"\nmsgstr \"Unifying documentation in Estonian and in English\"\n\n#: ../../tarned.rst:438\nmsgid \"Arhitektuuridokumendi täiendamine\"\nmsgstr \"Improve architecture document\"\n\n#: ../../tarned.rst:443\nmsgid \"Hääletamisfaktide edastamise ebaõnnestumise tuvastamine logist\"\nmsgstr \"Detect unsucessful delivery of statistics from the logs\"\n\n#: ../../tarned.rst:448\nmsgid \"Muudatused tarne 1.9.4 koosseisus, erinevused võrreldes tarnega 1.8.2\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.9.4, differences compared to \"\n\"delivery 1.8.2\"\n\n#: ../../tarned.rst:455 ../../tarned.rst:536\nmsgid \"Ubuntu 22.04 tugi\"\nmsgstr \"Support for Ubuntu 22.04\"\n\n#: ../../tarned.rst:456\nmsgid \"Go versiooniuuendus\"\nmsgstr \"Go version upgrade\"\n\n#: ../../tarned.rst:457\nmsgid \"etcd versiooniuuendus ja seadistamine\"\nmsgstr \"etcd version upgrade and configuration\"\n\n#: ../../tarned.rst:458\nmsgid \"Sõltuvuste ajakohastamine\"\nmsgstr \"Update of dependencies\"\n\n#: ../../tarned.rst:459\nmsgid \"Web eID autentimismeetodi lisamine\"\nmsgstr \"Add Web eID authentication method\"\n\n#: ../../tarned.rst:460\nmsgid \"Session status mikroteenuse lisamine\"\nmsgstr \"Adding a session status microservice\"\n\n#: ../../tarned.rst:461\nmsgid \"EHS statistikaliidese töökindluse tõstmine\"\nmsgstr \"Improving the reliability of statistics interface\"\n\n#: ../../tarned.rst:462\nmsgid \"Valimiste järkjärguline lõpp\"\nmsgstr \"Gradual stop of an event\"\n\n#: ../../tarned.rst:463\nmsgid \"Seansiidentifikaatori muutmine kohustuslikuks\"\nmsgstr \"Mandatory SessionID\"\n\n#: ../../tarned.rst:464\nmsgid \"ASiCe vormingutäpsustus\"\nmsgstr \"ASiCe format clarifications\"\n\n#: ../../tarned.rst:465\nmsgid \"VIS tugi detailstatistikale\"\nmsgstr \"VIS detailed statistics\"\n\n#: ../../tarned.rst:466\nmsgid \"SmartID toe täpsustused\"\nmsgstr \"Improved SmartID support\"\n\n#: ../../tarned.rst:479 ../../tarned.rst:485 ../../tarned.rst:518\nmsgid \"TLS 1.3\"\nmsgstr \"TLS 1.3\"\n\n#: ../../tarned.rst:480\nmsgid \"Sertide pinnimisest loobumine\"\nmsgstr \"Deprecating certificate pinning\"\n\n#: ../../tarned.rst:481 ../../tarned.rst:486\nmsgid \"Valikute nimekirja kontroll\"\nmsgstr \"Verification of a choice list\"\n\n#: ../../tarned.rst:498\nmsgid \"Java 17\"\nmsgstr \"Java 17\"\n\n#: ../../tarned.rst:502\nmsgid \"Kehtetute sedelite korrektse dekrüpteerimise tõestamine\"\nmsgstr \"Proof of correct decryption of invalid ciphertexts\"\n\n#: ../../tarned.rst:507 ../../tarned.rst:538\nmsgid \"Statistika arvutuste täpsustamine\"\nmsgstr \"Calculation of statistics\"\n\n#: ../../tarned.rst:508\nmsgid \"Töötlemisprotseduuride auditeerimise täpsustamine\"\nmsgstr \"Clarification of auditing tools for processing steps\"\n\n#: ../../tarned.rst:512\nmsgid \"Kehtetute sedelite korrektse dekrüpteerimise kontrollimine\"\nmsgstr \"Checking the correct decryption of invalid ciphertexts\"\n\n#: ../../tarned.rst:513\nmsgid \"Sedelite kontrolli täpsustused\"\nmsgstr \"\"\n\n#: ../../tarned.rst:519\nmsgid \"Mitme PKCS11 tokeni tugi\"\nmsgstr \"Support for multiple PKCS11 tokens\"\n\n#: ../../tarned.rst:520\nmsgid \"Windowsi serdituvastuse parendamine\"\nmsgstr \"Improvement of cert handling under Windows\"\n\n#: ../../tarned.rst:521\nmsgid \"Aegunud sertide tuvastamine\"\nmsgstr \"Detection of outdated certificates\"\n\n#: ../../tarned.rst:522\nmsgid \"DLL laadimisvigade parandamine\"\nmsgstr \"Fix errors in DLL loading\"\n\n#: ../../tarned.rst:523\nmsgid \"Ligipääsetavus macOS platvormil\"\nmsgstr \"Accessibility on macOS\"\n\n#: ../../tarned.rst:524\nmsgid \"Kasutajaliidese muudatused\"\nmsgstr \"General GUI changes\"\n\n#: ../../tarned.rst:525\nmsgid \"Kuvatõmmiste automatiseerimine\"\nmsgstr \"Automated screenshots in configuration app\"\n\n#: ../../tarned.rst:530\nmsgid \"Web eID autentimismeetodiga seotud dokumentatsiooni lisamine\"\nmsgstr \"Adding documentation related to the Web eID authentication method\"\n\n#: ../../tarned.rst:537\nmsgid \"Grafana versiooni uuendamine\"\nmsgstr \"Grafana version update\"\n\n#: ../../tarned.rst:542\nmsgid \"Muudatused tarne 1.8.2 koosseisus, erinevused võrreldes tarnega 1.8.1\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.8.2, differences compared to \"\n\"delivery 1.8.1.\"\n\n#: ../../tarned.rst:549 ../../tarned.rst:581\nmsgid \"Valijate nimekirjade vormingu muutus\"\nmsgstr \"Change the voter list format\"\n\n#: ../../tarned.rst:550\nmsgid \"EHS/VIS X-tee liidese CI lisandumine\"\nmsgstr \"Addition of CI to the EHS/VIS X-road interface\"\n\n#: ../../tarned.rst:551\nmsgid \"EHS/VIS X-tee liidese dokumentatsiooni täpsustumine\"\nmsgstr \"Specification of EHS/VIS X-Road interface documentation\"\n\n#: ../../tarned.rst:552 ../../tarned.rst:598\nmsgid \"Väiksemad veaparandused\"\nmsgstr \"Minor bug fixes\"\n\n#: ../../tarned.rst:557 ../../tarned.rst:569 ../../tarned.rst:574\n#: ../../tarned.rst:585 ../../tarned.rst:593 ../../tarned.rst:608\n#: ../../tarned.rst:627 ../../tarned.rst:647 ../../tarned.rst:658\n#: ../../tarned.rst:666 ../../tarned.rst:692 ../../tarned.rst:696\n#: ../../tarned.rst:700 ../../tarned.rst:705 ../../tarned.rst:712\n#: ../../tarned.rst:716 ../../tarned.rst:728 ../../tarned.rst:732\n#: ../../tarned.rst:736 ../../tarned.rst:740 ../../tarned.rst:745\n#: ../../tarned.rst:755 ../../tarned.rst:781 ../../tarned.rst:810\n#: ../../tarned.rst:818 ../../tarned.rst:862 ../../tarned.rst:869\n#: ../../tarned.rst:873 ../../tarned.rst:894 ../../tarned.rst:898\n#: ../../tarned.rst:940 ../../tarned.rst:951 ../../tarned.rst:1020\n#: ../../tarned.rst:1027 ../../tarned.rst:1031 ../../tarned.rst:1036\n#: ../../tarned.rst:1043 ../../tarned.rst:1054 ../../tarned.rst:1058\n#: ../../tarned.rst:1103 ../../tarned.rst:1116 ../../tarned.rst:1121\n#: ../../tarned.rst:1142 ../../tarned.rst:1194 ../../tarned.rst:1220\n#: ../../tarned.rst:1231\nmsgid \"Muudatusi ei ole\"\nmsgstr \"No changes\"\n\n#: ../../tarned.rst:564\nmsgid \"RSA eemaldamine\"\nmsgstr \"RSA removal\"\n\n#: ../../tarned.rst:565\nmsgid \"Veaparandused\"\nmsgstr \"Bug fixes\"\n\n#: ../../tarned.rst:589\nmsgid \"Töövoogude konsolideerimine\"\nmsgstr \"Workflow consolidation\"\n\n#: ../../tarned.rst:603\nmsgid \"Täpsustused seoses muudatustega tarkvaras\"\nmsgstr \"Clarifications due to software changes\"\n\n#: ../../tarned.rst:611\nmsgid \"Muudatused tarne 1.8.1 koosseisus, erinevused võrreldes tarnega 1.7.7\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.8.1, differences compared to \"\n\"delivery 1.7.7\"\n\n#: ../../tarned.rst:618\nmsgid \"Smart-ID toe lisandumine\"\nmsgstr \"Addition of Smart-ID support\"\n\n#: ../../tarned.rst:619\nmsgid \"EHS/VIS X-tee liidese lisandumine\"\nmsgstr \"Addition of EHS/VIS X-road interface\"\n\n#: ../../tarned.rst:620\nmsgid \"Hääletamisfaktide järjestamine\"\nmsgstr \"Ranking of voting facts\"\n\n#: ../../tarned.rst:621 ../../tarned.rst:636 ../../tarned.rst:642\nmsgid \"Seadistatav SNI\"\nmsgstr \"Configurable SNI\"\n\n#: ../../tarned.rst:634 ../../tarned.rst:640 ../../tarned.rst:654\nmsgid \"Smart-ID\"\nmsgstr \"Smart ID\"\n\n#: ../../tarned.rst:635 ../../tarned.rst:641\nmsgid \"Täpsustatud krüptogrammi kontroll\"\nmsgstr \"Clarified ciphertext verification\"\n\n#: ../../tarned.rst:662\nmsgid \"Töövoogude konsolideerimine, täpsustused parameetrites\"\nmsgstr \"Consolidation of workflows, refinement of parameters\"\n\n#: ../../tarned.rst:671 ../../tarned.rst:682\nmsgid \"SmartID\"\nmsgstr \"SmartID\"\n\n#: ../../tarned.rst:672\nmsgid \"Uuendatud väljanägemine\"\nmsgstr \"Updated look\"\n\n#: ../../tarned.rst:677\nmsgid \"Täpsustused seoses muudatustega tarkvaras - SmartID, jne.\"\nmsgstr \"Clarifications regarding changes in software - SmartID, etc.\"\n\n#: ../../tarned.rst:685\nmsgid \"Muudatused tarne 1.7.7 koosseisus, erinevused võrreldes tarnega 1.7.6\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.7.7, differences compared to \"\n\"delivery 1.7.6\"\n\n#: ../../tarned.rst:694 ../../tarned.rst:770 ../../tarned.rst:851\n#: ../../tarned.rst:929 ../../tarned.rst:1009 ../../tarned.rst:1093\n#: ../../tarned.rst:1181 ../../tarned.rst:1265\nmsgid \"Haldusteenus\"\nmsgstr \"Management service\"\n\n#: ../../tarned.rst:698 ../../tarned.rst:774 ../../tarned.rst:855\n#: ../../tarned.rst:933 ../../tarned.rst:1013 ../../tarned.rst:1097\n#: ../../tarned.rst:1186 ../../tarned.rst:1278\nmsgid \"IVXV mikroteenused\"\nmsgstr \"IVXV microservices\"\n\n#: ../../tarned.rst:721\nmsgid \"Muudatused seoses entroopia allika valikulise tühjendamisega\"\nmsgstr \"Changes regarding selective emptying of the entropy source\"\n\n#: ../../tarned.rst:750\nmsgid \"Muudatused seoses miksneti muutustega\"\nmsgstr \"Changes due to changes in the mixnet\"\n\n#: ../../tarned.rst:759\nmsgid \"Muudatused tarne 1.7.6 koosseisus, erinevused võrreldes tarnega 1.6.0\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.7.6, differences compared to \"\n\"delivery 1.6.0.\"\n\n#: ../../tarned.rst:766\nmsgid \"Muudatused seoses sisendnimekirjade haldamisega VIS3's\"\nmsgstr \"Changes regarding the management of input lists in VIS3\"\n\n#: ../../tarned.rst:767 ../../tarned.rst:799\nmsgid \"Muudatused seoses Ubuntu 20.04 toetamisega\"\nmsgstr \"Changes related to Ubuntu 20.04 support\"\n\n#: ../../tarned.rst:768 ../../tarned.rst:772 ../../tarned.rst:776\n#: ../../tarned.rst:849 ../../tarned.rst:853 ../../tarned.rst:857\n#: ../../tarned.rst:927 ../../tarned.rst:931 ../../tarned.rst:935\n#: ../../tarned.rst:1007 ../../tarned.rst:1011 ../../tarned.rst:1015\n#: ../../tarned.rst:1078 ../../tarned.rst:1095 ../../tarned.rst:1099\n#: ../../tarned.rst:1168 ../../tarned.rst:1184 ../../tarned.rst:1190\nmsgid \"Väiksemad muutused/veaparandused vastavalt `changelog` failile\"\nmsgstr \"Minor changes/improvements according to `changelog` file\"\n\n#: ../../tarned.rst:788\nmsgid \"Muudatused seoses API versioonile 28 liikumisega\"\nmsgstr \"Changes related to the move to API version 28\"\n\n#: ../../tarned.rst:789 ../../tarned.rst:794 ../../tarned.rst:825\nmsgid \"Aegunud rakenduse tuvastamine\"\nmsgstr \"Detection of outdated application\"\n\n#: ../../tarned.rst:793\nmsgid \"Muudatused seoses iOS versioonile 12 liikumisega\"\nmsgstr \"Changes related to the move to iOS version 12\"\n\n#: ../../tarned.rst:806\nmsgid \"Muudatused seoses sisendnimekirjade haldamisega VIS3s\"\nmsgstr \"Changes to the management of input lists in VIS3\"\n\n#: ../../tarned.rst:814\nmsgid \"Töötlemise töövoogude optimeerimine\"\nmsgstr \"Optimising processing workflows\"\n\n#: ../../tarned.rst:823\nmsgid \"M1 protsessoritüübi toetamine macOS platvormil\"\nmsgstr \"M1 processor support on macOS platform\"\n\n#: ../../tarned.rst:824\nmsgid \"FLTK, OpenSSL ja teiste alusteekide versiooniuuendused\"\nmsgstr \"Updates to FLTK, OpenSSL and other libraries.\"\n\n#: ../../tarned.rst:830\nmsgid \"Muudatused seoses muutustega seadistustes ning terminoloogias\"\nmsgstr \"Changes due to configuration and terminology changes\"\n\n#: ../../tarned.rst:835\nmsgid \"Kaasatud tarnesse\"\nmsgstr \"Included in delivery\"\n\n#: ../../tarned.rst:841\nmsgid \"Muudatused tarne 1.6.0 koosseisus, erinevused võrreldes tarnega 1.5.0\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.6.0, differences compared to \"\n\"delivery 1.5.0\"\n\n#: ../../tarned.rst:848\nmsgid \"Mobiil-ID REST teenuse toetamine\"\nmsgstr \"Support for Mobile-ID REST service\"\n\n#: ../../tarned.rst:878 ../../tarned.rst:886\nmsgid \"Java versiooni 11 kasutuselevõtt\"\nmsgstr \"Java version 11 rollout\"\n\n#: ../../tarned.rst:879\nmsgid \"Verificatumi versiooniuuendus\"\nmsgstr \"Verificatum upgrade\"\n\n#: ../../tarned.rst:890\nmsgid \"RSA võtmete serialiseerimise uus vorming\"\nmsgstr \"New format for RSA key serialisation\"\n\n#: ../../tarned.rst:903 ../../tarned.rst:908\nmsgid \"Mobiil-ID REST teenuse tugi\"\nmsgstr \"Mobile-ID REST service support\"\n\n#: ../../tarned.rst:913\nmsgid \"Eemaldatud tarnest seoses litsentsi lõppemisega\"\nmsgstr \"Removed from delivery due to expiry of licence\"\n\n#: ../../tarned.rst:918\nmsgid \"Muudatused tarne 1.5.0 koosseisus, erinevused võrreldes tarnega 1.4.1\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.5.0, differences compared to \"\n\"delivery 1.4.1.\"\n\n#: ../../tarned.rst:925\nmsgid \"Kõigi päringute logimine\"\nmsgstr \"Logging all requests\"\n\n#: ../../tarned.rst:926\nmsgid \"etcd Debian buster repositooriumist, uusima golang-google-rpc saamiseks\"\nmsgstr \"\"\n\"etcd from the Debian buster repository, to get the latest golang-google-\"\n\"rpc.\"\n\n#: ../../tarned.rst:947\nmsgid \"Muudatused seoses koodiläbivaatuse ja veatöötluse parendamisega\"\nmsgstr \"Changes to improve code review and error handling\"\n\n#: ../../tarned.rst:956\nmsgid \"Muudatused seoses 300K hääle miksimisega\"\nmsgstr \"Changes to the 300K vote mixing process\"\n\n#: ../../tarned.rst:963\nmsgid \"Valimise identifikaatori kasutamine läbiva prefiksina\"\nmsgstr \"Use of a election identifier as a prefix\"\n\n#: ../../tarned.rst:967\nmsgid \"Muudatused seoses koodiläbivaatusega\"\nmsgstr \"Changes related to code review\"\n\n#: ../../tarned.rst:971 ../../tarned.rst:976\nmsgid \"Muudatused seoses 300K hääle töötlemisega\"\nmsgstr \"Changes regarding the processing of 300K votes\"\n\n#: ../../tarned.rst:975\nmsgid \"Edenemisriba\"\nmsgstr \"Progressbar\"\n\n#: ../../tarned.rst:981 ../../tarned.rst:1063\nmsgid \"MSAA toe täpsustamine\"\nmsgstr \"Specification of MSAA support\"\n\n#: ../../tarned.rst:982\nmsgid \"Süsteemsete PIN-dialoogide kasutamine Windows platvormil\"\nmsgstr \"Using system PIN dialogues on the Windows platform\"\n\n#: ../../tarned.rst:987\nmsgid \"IVXV audiitori juhendi lisamine\"\nmsgstr \"Addition of IVXV auditor's manual\"\n\n#: ../../tarned.rst:988\nmsgid \"Muudatusdokumentide lisamine tarnesse\"\nmsgstr \"Inclusion of change documents in the delivery\"\n\n#: ../../tarned.rst:994\nmsgid \"Muutused/veaparandused vastavalt `changelog` failile\"\nmsgstr \"Changes/improvements according to `changelog` file\"\n\n#: ../../tarned.rst:1000\nmsgid \"Muudatused tarne 1.4.1 koosseisus, erinevused võrreldes tarnega 1.4.0\"\nmsgstr \"\"\n\"Changes in the composition of the delivery 1.4.1, differences compared to\"\n\" delivery 1.4.0.\"\n\n#: ../../tarned.rst:1047\nmsgid \"Tööriista *init* väljundfailide muutus\"\nmsgstr \"Tool *init* change in output files\"\n\n#: ../../tarned.rst:1048\nmsgid \"Tööriista *init* SN ja CN parameetrite muutus\"\nmsgstr \"Change in tool *init* SN and CN parameters\"\n\n#: ../../tarned.rst:1049\nmsgid \"Tööriista *testkey* sisendparameetrite muutus\"\nmsgstr \"Change in input parameters of *testkey* tool\"\n\n#: ../../tarned.rst:1050\nmsgid \"Tööriista *decrypt* kasutatud kaardinumbrite kuvamine\"\nmsgstr \"Displaying the card numbers used by the *decrypt* toolbar\"\n\n#: ../../tarned.rst:1068\nmsgid \"IVXV protokollid - registreerimisprotokolli lisamine\"\nmsgstr \"Protocols IVXV - Addition to the record of registrations\"\n\n#: ../../tarned.rst:1069\nmsgid \"\"\n\"IVXV seadistuste koostejuhend - läbivad täiendused ning käsiraamatuga \"\n\"ühtlustamine.\"\nmsgstr \"IVXV configuration manual - improvements and alignment with the procedure.\"\n\n#: ../../tarned.rst:1071\nmsgid \"IVXV valijarakendus - läbivad täiendused.\"\nmsgstr \"IVXV voting application - improvements.\"\n\n#: ../../tarned.rst:1072\nmsgid \"IVXV mixnet - dokument hõlmatud seadistuste koostejuhendisse, eemaldatud.\"\nmsgstr \"IVXV mixnet - document included in the configuration manual, removed.\"\n\n#: ../../tarned.rst:1073\nmsgid \"\"\n\"IVXV registreerimisteenus - dokument hõlmatud protokollistikku, \"\n\"eemaldatud.\"\nmsgstr \"\"\n\"IVXV registration service - document included in the protocol document, \"\n\"removed.\"\n\n#: ../../tarned.rst:1081\nmsgid \"Muudatused tarne 1.4.0 koosseisus, erinevused võrreldes tarnega 1.3.0\"\nmsgstr \"\"\n\"Changes in the configuration of delivery 1.4.0, differences compared to \"\n\"delivery 1.3.0\"\n\n#: ../../tarned.rst:1088\nmsgid \"\"\n\"OCSP ja ajatemplipäringute automaatne kordamine vastavalt \"\n\"konfiguratsioonile\"\nmsgstr \"Automatic repetition of OCSP and timestamp requests as configured\"\n\n#: ../../tarned.rst:1090\nmsgid \"BDOC-TS allkirjakonteinerite toetamine\"\nmsgstr \"Support for BDOC-TS signature containers\"\n\n#: ../../tarned.rst:1091\nmsgid \"Tühjade valijanimekirjade tugi\"\nmsgstr \"Support for empty voterlists\"\n\n#: ../../tarned.rst:1110\nmsgid \"TLS 1.2 toetamine API-versioonide < 19 korral\"\nmsgstr \"TLS 1.2 support for API versions < 19\"\n\n#: ../../tarned.rst:1111\nmsgid \"Abiinfo vaate asendamine süsteemse brauseriga\"\nmsgstr \"Replacing the help view with a system browser\"\n\n#: ../../tarned.rst:1112\nmsgid \"Kuvast väljuvate tekstide automaatne lühendamine nuppude korral\"\nmsgstr \"Automatic abbreviation of texts in the image when buttons are pressed\"\n\n#: ../../tarned.rst:1128\nmsgid \"Jaoskonnanumbri unikaalsusnõuete täpsustamine\"\nmsgstr \"Specifying the uniqueness requirements for the station number\"\n\n#: ../../tarned.rst:1129\nmsgid \"Tühja valijatenimekirja tugi\"\nmsgstr \"Support for an empty voterlist\"\n\n#: ../../tarned.rst:1130\nmsgid \"Kodeeringuvigadega sertifikaatide toetamine rakendustes\"\nmsgstr \"Support for certificates with coding errors in applications\"\n\n#: ../../tarned.rst:1134\nmsgid \"Eemaldatud LOG4 ja LOG5\"\nmsgstr \"Removed LOG4 and LOG5\"\n\n#: ../../tarned.rst:1138\nmsgid \"Eemaldatud PDF vormingus valijate nimekiri faasist *revoke*\"\nmsgstr \"Removed PDF voter list from *revoke* phase.\"\n\n#: ../../tarned.rst:1147\nmsgid \"macOS 10.11 toetamine\"\nmsgstr \"macOS 10.11 support\"\n\n#: ../../tarned.rst:1148\nmsgid \"32bit Linuxi toetamine\"\nmsgstr \"32bit Linux support\"\n\n#: ../../tarned.rst:1149\nmsgid \"UPX versiooni uuendamine\"\nmsgstr \"Upgrading UPX\"\n\n#: ../../tarned.rst:1150\nmsgid \"Pinpad kaardilugejate töökindluse tõstmine (Win)\"\nmsgstr \"Improving the reliability of Pinpad card readers (Win)\"\n\n#: ../../tarned.rst:1151\nmsgid \"ID-kaardi suhtlusvigade parandamine (Win7/ECC)\"\nmsgstr \"Fix ID card communication errors (Win7/ECC)\"\n\n#: ../../tarned.rst:1152\nmsgid \"Nii PEM kui DER vormingus andmete import (Seadistaja)\"\nmsgstr \"Import data in both PEM and DER format (Configurator)\"\n\n#: ../../tarned.rst:1153\nmsgid \"Lisatud Seadistuste valideerimisvõimalus (Seadistaja)\"\nmsgstr \"Added Configuration validation option (Configurator)\"\n\n#: ../../tarned.rst:1154\nmsgid \"Parandatud käitumine liigsuurte seadistuste korral (Seadistaja)\"\nmsgstr \"Corrected behaviour in case of excessive settings (Configurator)\"\n\n#: ../../tarned.rst:1159\nmsgid \"\"\n\"Dokumentatsiooni uuendamine muudatuste kajastamiseks ning DEMO2018 \"\n\"tagasiside arvestamiseks\"\nmsgstr \"Updating documentation to reflect changes and feedback from DEMO2018.\"\n\n#: ../../tarned.rst:1165\nmsgid \"MTA sõltuvuse lisamine\"\nmsgstr \"Adding MTA dependency\"\n\n#: ../../tarned.rst:1166\nmsgid \"CSV logiväljavõtte täpsustamine, algus- ja lõpuaja lisamine\"\nmsgstr \"Specifying the CSV log field, adding start and end times\"\n\n#: ../../tarned.rst:1167\nmsgid \"Logianalüüsi optimeerimine mitmetuumalise riistvara jaoks\"\nmsgstr \"Optimising log analysis for multi-core hardware\"\n\n#: ../../tarned.rst:1171\nmsgid \"Muudatused tarne 1.3.0 koosseisus, erinevused võrreldes tarnega 1.2.0\"\nmsgstr \"\"\n\"Changes in the composition of delivery 1.3.0, differences compared to \"\n\"delivery 1.2.0\"\n\n#: ../../tarned.rst:1178 ../../tarned.rst:1253\nmsgid \"Ubuntu 18.04 LTS (Bionic Beaver) kasutuselevõtmine\"\nmsgstr \"Support for Ubuntu 18.04 LTS (Bionic Beaver)\"\n\n#: ../../tarned.rst:1179\nmsgid \"Krahhitaaste protseduuride kirjeldamine\"\nmsgstr \"Description of disaster recovery procedures\"\n\n#: ../../tarned.rst:1183\nmsgid \"Parandatud tööriistad teenuste seisundiinfo saamiseks\"\nmsgstr \"Improved tools for getting service status information\"\n\n#: ../../tarned.rst:1188\nmsgid \"golang keeleversioon 1.9 kasutuselevõtmine\"\nmsgstr \"golang language version 1.9 rollout\"\n\n#: ../../tarned.rst:1189\nmsgid \"Eesti ID-kaardi uuenenud profiili toetamine (PNOEE)\"\nmsgstr \"Supporting the renewed profile of the Estonian ID card (PNOEE)\"\n\n#: ../../tarned.rst:1201 ../../tarned.rst:1206\nmsgid \"ESTEID2018 sertifikaatide toetamine\"\nmsgstr \"Support for ESTEID2018 certificates\"\n\n#: ../../tarned.rst:1202\nmsgid \"Täpsustatud vigase ASN1-kodeeringuga avalike võtmete käitlemist\"\nmsgstr \"Improved handling of public keys with corrupt ASN1 coding\"\n\n#: ../../tarned.rst:1207\nmsgid \"iPhone 10 X muudatused\"\nmsgstr \"iPhone 10 X changes\"\n\n#: ../../tarned.rst:1208\nmsgid \"XCode 10 ja iOS 12 SDK kasutamine\"\nmsgstr \"Using XCode 10 and iOS 12 SDK\"\n\n#: ../../tarned.rst:1213\nmsgid \"Verificatumi AGPL versiooni kasutuselevõtmine\"\nmsgstr \"Deployment of the AGPL version of Verificatum\"\n\n#: ../../tarned.rst:1224\nmsgid \"Lisatud tööriist StatsTool valimiskastist statistikafaili genereerimiseks\"\nmsgstr \"Added tool StatsTool to generate a statistics file from the ballot box.\"\n\n#: ../../tarned.rst:1225\nmsgid \"Lisatud tööriist StatsDiffTool kahe statistikafaili võrdlemiseks\"\nmsgstr \"Added tool StatsDiffTool to compare two statistics files\"\n\n#: ../../tarned.rst:1226 ../../tarned.rst:1239\nmsgid \"ESTEID2018 sertifikaatide ja profiili toetamine\"\nmsgstr \"ESTEID2018 certificates and profile support\"\n\n#: ../../tarned.rst:1227\nmsgid \"digidoc4j 2.1.0 kasutamine\"\nmsgstr \"digidoc4j 2.1.0 usage\"\n\n#: ../../tarned.rst:1236\nmsgid \"Valijarakenduses kandidaatide otsingu võimaldamine\"\nmsgstr \"Enabling the search for candidates in the voter application\"\n\n#: ../../tarned.rst:1237\nmsgid \"Valijarakenduses erakondade ja kandidaatide kaustana kuvamine\"\nmsgstr \"\"\n\"Displaying political parties and candidates as a folder in the voter \"\n\"application\"\n\n#: ../../tarned.rst:1238\nmsgid \"\"\n\"Valijarakenduse ja Seadistusrakenduse üleviimine JSON-vormingus \"\n\"seadistustele\"\nmsgstr \"Migrating the Voting app and Configuration app to JSON format settings\"\n\n#: ../../tarned.rst:1240\nmsgid \"Win: IDEMIA minidraiveri toetamine\"\nmsgstr \"Win: IDEMIA minidriver support\"\n\n#: ../../tarned.rst:1241\nmsgid \"Linux/macOS: IDEMIA PKCS11 draiveri toetamine\"\nmsgstr \"Linux/macOS: IDEMIA PKCS11 driver support\"\n\n#: ../../tarned.rst:1242\nmsgid \"macOS 10.14 toetamine\"\nmsgstr \"macOS 10.14 support\"\n\n#: ../../tarned.rst:1247\nmsgid \"Dokumentatsiooni uuendamine muudatuste kajastamiseks\"\nmsgstr \"Updating documentation to reflect changes\"\n\n#: ../../tarned.rst:1248\nmsgid \"Ingliskeelse arhitektuuridokumendi ja protokollistiku lisamine\"\nmsgstr \"Creation of an English-language architecture and protocol document\"\n\n#: ../../tarned.rst:1254\nmsgid \"Võetud kasutusele Grafana 5.3.4\"\nmsgstr \"Introduced Grafana 5.3.4\"\n\n#: ../../tarned.rst:1255\nmsgid \"\"\n\"Parandatud vanusepõhise statistika genereerimine ja vanusegruppidesse \"\n\"jaotumine\"\nmsgstr \"Generation of age-specific statistics and age group breakdowns\"\n\n#: ../../tarned.rst:1257\nmsgid \"CSV väljundi võtmine seanssidest\"\nmsgstr \"Extracting CSV output from sessions\"\n\n#: ../../tarned.rst:1260\nmsgid \"\"\n\"Muudatused tarne 1.2.0 koosseisus, erinevused võrreldes KOV2017 \"\n\"valimistega.\"\nmsgstr \"\"\n\"Changes in the composition of the supply 1.2.0, differences compared to \"\n\"the KOV2017 elections.\"\n\n#: ../../tarned.rst:1267\nmsgid \"Lisatud tööriist vigaste valijanimekirjade eemaldamiseks.\"\nmsgstr \"Added a tool to remove invalid voterlists.\"\n\n#: ../../tarned.rst:1268\nmsgid \"Lisatud tööriist jaoskondade/ringkondade nimekirja lisamiseks.\"\nmsgstr \"Added a tool to add a list of wards/districts.\"\n\n#: ../../tarned.rst:1269\nmsgid \"Lisatud ringkonnapõhine statistika.\"\nmsgstr \"Statistics by district are included.\"\n\n#: ../../tarned.rst:1270\nmsgid \"Lisatud varundusteenus.\"\nmsgstr \"Added backup service.\"\n\n#: ../../tarned.rst:1271\nmsgid \"Lisatud tööriist varundatud valimiskastide konsolideerimiseks.\"\nmsgstr \"Added tool to consolidate backed up ballot boxes.\"\n\n#: ../../tarned.rst:1272\nmsgid \"Lisatud tööriist nimekirjade kooskõlalisuse kontrolliks.\"\nmsgstr \"Added a tool to check the consistency of lists.\"\n\n#: ../../tarned.rst:1273\nmsgid \"\"\n\"Lisatud võimekus seadistusfailidele valimisspetsiifiliste prefiksite \"\n\"lisamiseks.\"\nmsgstr \"Added ability to add election specific prefixes to configuration files.\"\n\n#: ../../tarned.rst:1274\nmsgid \"Täiendatud haldusliidese kasutajaliidest abiinfoga.\"\nmsgstr \"Enhanced the administrative interface with help.\"\n\n#: ../../tarned.rst:1275\nmsgid \"Eemaldatud aegunud konfiguratsiooniparameeter “stats.*”\"\nmsgstr \"Removed obsolete configuration parameter \\\"stats.*\\\"\"\n\n#: ../../tarned.rst:1276\nmsgid \"Parandatud sisendfailide vormingu kontrolli ja laadimist.\"\nmsgstr \"Improved input file format checking and loading.\"\n\n#: ../../tarned.rst:1280\nmsgid \"Uuendatud etcd versioon.\"\nmsgstr \"Updated etcd version.\"\n\n#: ../../tarned.rst:1281\nmsgid \"Lisatud võimekus etcd ajalõppude seadistamiseks keskkonnamuutujate kaudu.\"\nmsgstr \"Added ability to set etcd timeouts via environment variables.\"\n\n#: ../../tarned.rst:1282\nmsgid \"Lisatud võimekus klastri modifitseerimiseks krahhitaaste eesmärgil.\"\nmsgstr \"Added capability to modify cluster for disaster recovery\"\n\n#: ../../tarned.rst:1283\nmsgid \"\"\n\"Parandatud klastri käitumist liidrivahetuse korral, pooleliolevate \"\n\"talletamiste kordamine.\"\nmsgstr \"Improved cluster behaviour in the event of a leader-change.\"\n\n#: ../../tarned.rst:1284\nmsgid \"TLS šifrid muudetud seadistatavaks.\"\nmsgstr \"TLS ciphers made configurable.\"\n\n#: ../../tarned.rst:1285\nmsgid \"Parandatud BDOC profiili identifitseeriva konfiguratsioonivälja nimi.\"\nmsgstr \"Name of the configuration field identifying the corrected BDOC profile.\"\n\n#: ../../tarned.rst:1286\nmsgid \"\"\n\"Lisatud võimekus seadistada Mobiil-ID autentimist nõudma nii isikukoodi \"\n\"kui telefoninumbrit.\"\nmsgstr \"\"\n\"Added the ability to configure Mobile ID authentication to require both \"\n\"an ID and a phone number.\"\n\n#: ../../tarned.rst:1287\nmsgid \"Lisatud võimekus piirata korduvhääletamise sagedust.\"\nmsgstr \"Added ability to limit the frequency of re-voting.\"\n\n#: ../../tarned.rst:1288\nmsgid \"Lisatud võimekus toetada Windowsi reavahetusi konfifailides.\"\nmsgstr \"Added ability to support Windows line-breaks in configuration files.\"\n\n#: ../../tarned.rst:1289\nmsgid \"Täiustatud BDOC XML kanoniseerimist ja parsimist.\"\nmsgstr \"Improved BDOC XML canonicalisation and parsing.\"\n\n#: ../../tarned.rst:1290\nmsgid \"Karmistatud DDS päringute vormingukontrolle.\"\nmsgstr \"Tightened DDS query formatting controls.\"\n\n#: ../../tarned.rst:1291\nmsgid \"Logimine viidud üle RELP protokollile.\"\nmsgstr \"Logging migrated to RELP protocol.\"\n\n#: ../../tarned.rst:1292\nmsgid \"\"\n\"Muudetud seadistusfailide ülesehitust eristamaks Koguja ja Töötleja \"\n\"vastutusi.\"\nmsgstr \"\"\n\"Changed the structure of configuration files to distinguish between the \"\n\"responsibilities of the Collector and the Processor.\"\n\n#: ../../tarned.rst:1296 ../../tarned.rst:1307 ../../tarned.rst:1311\n#: ../../tarned.rst:1317 ../../tarned.rst:1321 ../../tarned.rst:1325\nmsgid \"Muudatusi ei ole.\"\nmsgstr \"There are no changes.\"\n\n#: ../../tarned.rst:1303\nmsgid \"\"\n\"Lisatud juhised publitseeritud kontrollrakenduse ja avalikustatud \"\n\"lähtekoodi vastavuse kontrollimiseks.\"\nmsgstr \"\"\n\"Added guidance on how to check the consistency of a published control \"\n\"implementation with the published source code.\"\n\n#: ../../tarned.rst:1329\nmsgid \"Linux ja Mac – platvormispetsiifilisi muudatusi ei ole.\"\nmsgstr \"Linux and Mac - There are no platform-specific changes.\"\n\n#: ../../tarned.rst:1330\nmsgid \"\"\n\"Windows – parandatud liidestumist Minidraiveriga, mingw64 \"\n\"kasutuselevõtmine.\"\nmsgstr \"Windows - improved interfacing with Minidraiver, introducing mingw64.\"\n\n#: ../../tarned.rst:1331\nmsgid \"Lisatud ID-kaardi ECC toetamine.\"\nmsgstr \"Added support for ID card ECC.\"\n\n#: ../../tarned.rst:1332\nmsgid \"Seadistatud Mobiil-ID nõudma vajadusel isikukoodi ja telefoninumbrit.\"\nmsgstr \"Set up Mobile ID to ask for an ID and phone number when needed.\"\n\n#: ../../tarned.rst:1333\nmsgid \"Täiendatud veakoode.\"\nmsgstr \"Updated error codes.\"\n\n#: ../../tarned.rst:1334\nmsgid \"Kohandatud seadistusrakendus muudatustega vastavusse.\"\nmsgstr \"Adjusted the configuration app to reflect the changes.\"\n\n#: ../../tarned.rst:1335\nmsgid \"Kohandatud BDOC XML mallid\"\nmsgstr \"Customized BDOC XML templates\"\n\n#: ../../tarned.rst:1339\nmsgid \"Dokumentatsioon läbivalt kaasajastatud seoses muudatustega\"\nmsgstr \"Documentation updated throughout in the light of the amendments\"\n\n#: ../../tarned.rst:1343\nmsgid \"Loobutud CrateDBst.\"\nmsgstr \"Rejected CrateDB\"\n\n#: ../../tarned.rst:1344\nmsgid \"Võetud läbivalt kasutusele PostgreSQL.\"\nmsgstr \"Adopted PostgreSQL.\"\n\n#: ../../tarned.rst:1345\nmsgid \"Võetud kasutusele Grafana 5.0.1.\"\nmsgstr \"Grafana 5.0.1 has been introduced.\"\n\n#: ../../tarned.rst:1346\nmsgid \"Seansside valideerimise parandused lähtudes KOV2017 logianalüüsist.\"\nmsgstr \"\"\n\"Improvements to the validation of the sessions based on the KOV2017 log \"\n\"analysis.\"\n\n#: ../../tarned.rst:1347\nmsgid \"Lisatud statistika genereerimine ringkondade kaupa.\"\nmsgstr \"Generation of statistics by district.\"\n\n#~ msgid \"Muudatused tarne 1.9.1 koosseisus, erinevused võrreldes tarnega 1.9.0\"\n#~ msgstr \"\"\n#~ \"Changes in the composition of delivery\"\n#~ \" 1.9.1, differences compared to delivery\"\n#~ \" 1.9.0\"\n\n#~ msgid \"???\"\n#~ msgstr \"???\"\n\n#~ msgid \"iOS versioonide täpsustamine\"\n#~ msgstr \"Update iOS versions\"\n\n"
  },
  {
    "path": "Documentation/public/uldsisukord/spelling_wordlist.txt",
    "content": "\nprotocols\narchitecture\ninterfaces\n\nAPI\n\nbackend\nbeaver\nbionic\nbit\nbuster\n\ndebian\ndigidoc\n\nECC\netcd\n\ngo\ngolang\ngoogle\ngrafana\n\niOS\niPhone\n\nlog\n\nmac\nmacOS\nmessages\nmingw\nmixnet\n\npin\npinpad\npython\n\nrpc\n\nstats\n\nubuntu\n\nWin\nWindowsi\n\nXCode\n\ningliskeelsed\n"
  },
  {
    "path": "Documentation/public/uldsisukord/tarned.rst",
    "content": "..  IVXV dokumentatsiooni üldsisukord\n\nTarned\n======\n\nMuudatused tarne 1.10.4 koosseisus, erinevused võrreldes tarnega 1.10.3\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Valijate nimekirjade muudatuste allalaadimise täpsustused\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatused puuduvad\n\niOS\n\n* Muudatused puuduvad\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatused puuduvad\n\nVõtmerakendus\n\n* Muudatused puuduvad\n\nTöötlemisrakendus\n\n* Muudatused puuduvad\n\nAuditirakendus\n\n* Muudatused puuduvad\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Numeratsiooni läbivaatus\n* Võtmerakenduse seadistuste täpsustamine\n* Smart-ID protokolli näidete täpsustamine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Muudatused puuduvad\n\n\nMuudatused tarne 1.10.3 koosseisus, erinevused võrreldes tarnega 1.10.2\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Muudatused puuduvad\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatused puuduvad\n\niOS\n\n* Muudatused puuduvad\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatused puuduvad\n\nVõtmerakendus\n\n* Muudatused puuduvad\n\nTöötlemisrakendus\n\n* Muudatused puuduvad\n\nAuditirakendus\n\n* Muudatused puuduvad\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Tõlgete läbivaatus\n* Õigekirja läbivaatus\n* Tarne 1.10.2 sisu dokumenteerimine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Muudatused puuduvad\n\n\nMuudatused tarne 1.10.2 koosseisus, erinevused võrreldes tarnega 1.10.1\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* ZIP/BDOC failide käsitluse parendamine\n* Sõltuvuste uuendamine, JavaScript\n* eID vahendite testimine\n* Mälulekete parandamine\n* Smart-ID voogude uuendamine vastavalt protokollile\n* Nimekirjade laadimise intervalli ja strateegia täpsustamine\n* Arendus- ja testkeskkonna parendused\n* Pisiparandused\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Rakenduse kolimine Riigikogu Kantselei poodi\n* TargetSDK uuendamine\n* Erinevad veaparandused kasutajaliideses\n* Täiendav obfuskeerimine\n* Veateadete täiendamine\n\niOS\n\n* Rakenduse kolimine Riigikogu Kantselei poodi\n* Erinevad veaparandused kasutajaliideses\n* Täpsustused rakenduse varundamisele\n* Rakenduse elutsükli olekumasina täpsustused\n* Veateadete täiendamine\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* ZIP/BDOC failide käsitluse parendamine\n\nVõtmerakendus\n\n* Pisiparandused\n\nTöötlemisrakendus\n\n* Pisiparandused\n\nAuditirakendus\n\n* Pisiparandused\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Sõltuvuste uuendamine, OpenSSL, PCRE\n* Kasutajaliidese teegi FLTK uuendamine\n* Smart-ID voo uuendamine\n* Testimine uusima ID-kaardiga\n* Kompileerimiskeskkonna täpsustused\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Tõlgete läbivaatus\n* Õigekirja läbivaatus\n* Tarne 1.10.2 sisu dokumenteerimine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Sõltuvuste uuendamine, Python, JavaScript\n* Veebiliidese täiendused\n* Päringuvahendaja ``pgbouncer`` kasutuselevõtt\n* Veebiserveri konfiguratsiooni läbivaatus turvatesti tulemustest lähtudes\n\n\nMuudatused tarne 1.10.1 koosseisus, erinevused võrreldes tarnega 1.10.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Uuenenud SmartID serdiprofiili toetamine\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\niOS\n\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatused puuduvad\n\nVõtmerakendus\n\n* Muudatused puuduvad\n\nTöötlemisrakendus\n\n* Muudatused puuduvad\n\nAuditirakendus\n\n* Muudatused puuduvad\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nLogimonitor\n~~~~~~~~~~~\n\n* Pisiparandused vastavalt muutuste logile\n\n\nMuudatused tarne 1.10.0 koosseisus, erinevused võrreldes tarnega 1.9.10\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Funktsionaalsus kehtetute sedelite eemaldamiseks kogumisteenuses\n* MODP ja elliptkõverate krüptograafia\n* Erinevad pisiparandused vastavalt muutuste logile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Elliptkõverate krüptograafia\n* Ligipääsetavuse parandamine\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\niOS\n\n* Elliptkõverate krüptograafia\n* Ligipääsetavuse parandamine\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\nMiksnet\n~~~~~~~\n\n* Elliptkõverate krüptograafia\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Elliptkõverate krüptograafia\n* Täpsustused urni verifitseerimisel, äärejuhtumite läbivaatus\n\nVõtmerakendus\n\n* Elliptkõverate krüptograafia\n* Täpsustused urni verifitseerimisel, äärejuhtumite läbivaatus\n\nTöötlemisrakendus\n\n* Elliptkõverate krüptograafia\n* Täpsustused urni verifitseerimisel, äärejuhtumite läbivaatus\n\nAuditirakendus\n\n* Elliptkõverate krüptograafia\n* Täpsustused urni verifitseerimisel, äärejuhtumite läbivaatus\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Elliptkõverate krüptograafia\n* Proksikasutuse refaktoreerimine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Protokollidokumendi täiendamine\n* Logide dokumendi loomine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Pisiparandused vastavalt muutuste logile\n\n\nMuudatused tarne 1.9.10 koosseisus, erinevused võrreldes tarnega 1.9.4\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Erinevad pisiparandused vastavalt muutuste logile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* FLAG_SECURE kasutuselevõtt\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\niOS\n\n* Ligipääsetavuse parendamine\n* Täpsustused hääle verifitseerimisel, äärejuhtumite läbivaatus\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatused puuduvad\n\nVõtmerakendus\n\n* Krüptogrammi kehtetuks kuulutamise äärejuhtumite läbivaatus\n\n\nTöötlemisrakendus\n\n* Hääle kehtetuks kuulutamise äärejuhtumite läbivaatus\n\nAuditirakendus\n\n* Muudatused puuduvad\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Windows, ligipääsetavuse veaparandused\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Eesti- ja ingliskeelse dokumentatsiooni ühtlustamine\n* Arhitektuuridokumendi täiendamine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Hääletamisfaktide edastamise ebaõnnestumise tuvastamine logist\n* Pisiparandused vastavalt muutuste logile\n\n\nMuudatused tarne 1.9.4 koosseisus, erinevused võrreldes tarnega 1.8.2\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Ubuntu 22.04 tugi\n* Go versiooniuuendus\n* etcd versiooniuuendus ja seadistamine\n* Sõltuvuste ajakohastamine\n* Web eID autentimismeetodi lisamine\n* Session status mikroteenuse lisamine\n* EHS statistikaliidese töökindluse tõstmine\n* Valimiste järkjärguline lõpp\n* Seansiidentifikaatori muutmine kohustuslikuks\n* ASiCe vormingutäpsustus\n* VIS tugi detailstatistikale\n* SmartID toe täpsustused\n* Erinevad pisiparandused vastavalt muutuste logile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatused puuduvad\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* TLS 1.3\n* Sertide pinnimisest loobumine\n* Valikute nimekirja kontroll\n\niOS\n\n* TLS 1.3\n* Valikute nimekirja kontroll\n\nMiksnet\n~~~~~~~\n\n* Muudatused puuduvad\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Java 17\n\nVõtmerakendus\n\n* Kehtetute sedelite korrektse dekrüpteerimise tõestamine\n\n\nTöötlemisrakendus\n\n* Statistika arvutuste täpsustamine\n* Töötlemisprotseduuride auditeerimise täpsustamine\n\nAuditirakendus\n\n* Kehtetute sedelite korrektse dekrüpteerimise kontrollimine\n* Sedelite kontrolli täpsustused\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* TLS 1.3\n* Mitme PKCS11 tokeni tugi\n* Windowsi serdituvastuse parendamine\n* Aegunud sertide tuvastamine\n* DLL laadimisvigade parandamine\n* Ligipääsetavus macOS platvormil\n* Kasutajaliidese muudatused\n* Kuvatõmmiste automatiseerimine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Web eID autentimismeetodiga seotud dokumentatsiooni lisamine\n* Eesti- ja ingliskeelse dokumentatsiooni ühtlustamine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Ubuntu 22.04 tugi\n* Grafana versiooni uuendamine\n* Statistika arvutuste täpsustamine\n\n\nMuudatused tarne 1.8.2 koosseisus, erinevused võrreldes tarnega 1.8.1\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Valijate nimekirjade vormingu muutus\n* EHS/VIS X-tee liidese CI lisandumine\n* EHS/VIS X-tee liidese dokumentatsiooni täpsustumine\n* Väiksemad veaparandused\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* RSA eemaldamine\n* Veaparandused\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Muudatusi ei ole\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Valijate nimekirjade vormingu muutus\n\nVõtmerakendus\n\n* Muudatusi ei ole\n\nTöötlemisrakendus\n\n* Töövoogude konsolideerimine\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Väiksemad veaparandused\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Täpsustused seoses muudatustega tarkvaras\n\nLogimonitor\n~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nMuudatused tarne 1.8.1 koosseisus, erinevused võrreldes tarnega 1.7.7\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Smart-ID toe lisandumine\n* EHS/VIS X-tee liidese lisandumine\n* Hääletamisfaktide järjestamine\n* Seadistatav SNI\n\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Smart-ID\n* Täpsustatud krüptogrammi kontroll\n* Seadistatav SNI\n\niOS\n\n* Smart-ID\n* Täpsustatud krüptogrammi kontroll\n* Seadistatav SNI\n\nMiksnet\n~~~~~~~\n\n* Muudatusi ei ole\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Smart-ID\n\nVõtmerakendus\n\n* Muudatusi ei ole\n\nTöötlemisrakendus\n\n* Töövoogude konsolideerimine, täpsustused parameetrites\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* SmartID\n* Uuendatud väljanägemine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Täpsustused seoses muudatustega tarkvaras - SmartID, jne.\n\nLogimonitor\n~~~~~~~~~~~\n\n* SmartID\n\nMuudatused tarne 1.7.7 koosseisus, erinevused võrreldes tarnega 1.7.6\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Muudatusi ei ole\n\nHaldusteenus\n\n* Muudatusi ei ole\n\nIVXV mikroteenused\n\n* Muudatusi ei ole\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatusi ei ole\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Muudatused seoses entroopia allika valikulise tühjendamisega\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatusi ei ole\n\nVõtmerakendus\n\n* Muudatusi ei ole\n\nTöötlemisrakendus\n\n* Muudatusi ei ole\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Muudatused seoses miksneti muutustega\n\nLogimonitor\n~~~~~~~~~~~\n\n* Muudatusi ei ole\n\n\nMuudatused tarne 1.7.6 koosseisus, erinevused võrreldes tarnega 1.6.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Muudatused seoses sisendnimekirjade haldamisega VIS3's\n* Muudatused seoses Ubuntu 20.04 toetamisega\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nHaldusteenus\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatused seoses API versioonile 28 liikumisega\n* Aegunud rakenduse tuvastamine\n\niOS\n\n* Muudatused seoses iOS versioonile 12 liikumisega\n* Aegunud rakenduse tuvastamine\n\nMiksnet\n~~~~~~~\n\n* Muudatused seoses Ubuntu 20.04 toetamisega\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatused seoses sisendnimekirjade haldamisega VIS3s\n\nVõtmerakendus\n\n* Muudatusi ei ole\n\nTöötlemisrakendus\n\n* Töötlemise töövoogude optimeerimine\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* M1 protsessoritüübi toetamine macOS platvormil\n* FLTK, OpenSSL ja teiste alusteekide versiooniuuendused\n* Aegunud rakenduse tuvastamine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Muudatused seoses muutustega seadistustes ning terminoloogias\n\nLogimonitor\n~~~~~~~~~~~\n\n* Kaasatud tarnesse\n\n\n\n\nMuudatused tarne 1.6.0 koosseisus, erinevused võrreldes tarnega 1.5.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Mobiil-ID REST teenuse toetamine\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nHaldusteenus\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatusi ei ole\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Java versiooni 11 kasutuselevõtt\n* Verificatumi versiooniuuendus\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Java versiooni 11 kasutuselevõtt\n\nVõtmerakendus\n\n* RSA võtmete serialiseerimise uus vorming\n\nTöötlemisrakendus\n\n* Muudatusi ei ole\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Mobiil-ID REST teenuse tugi\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Mobiil-ID REST teenuse tugi\n\nLogimonitor\n~~~~~~~~~~~\n\n* Eemaldatud tarnest seoses litsentsi lõppemisega\n\n\n\nMuudatused tarne 1.5.0 koosseisus, erinevused võrreldes tarnega 1.4.1\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Kõigi päringute logimine\n* etcd Debian buster repositooriumist, uusima golang-google-rpc saamiseks\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nHaldusteenus\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatused seoses koodiläbivaatuse ja veatöötluse parendamisega\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Muudatused seoses 300K hääle miksimisega\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Valimise identifikaatori kasutamine läbiva prefiksina\n\nVõtmerakendus\n\n* Muudatused seoses koodiläbivaatusega\n\nTöötlemisrakendus\n\n* Muudatused seoses 300K hääle töötlemisega\n\nAuditirakendus\n\n* Edenemisriba\n* Muudatused seoses 300K hääle töötlemisega\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* MSAA toe täpsustamine\n* Süsteemsete PIN-dialoogide kasutamine Windows platvormil\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* IVXV audiitori juhendi lisamine\n* Muudatusdokumentide lisamine tarnesse\n\n\nLogimonitor\n~~~~~~~~~~~\n\n* Muutused/veaparandused vastavalt `changelog` failile\n\n\n\n\nMuudatused tarne 1.4.1 koosseisus, erinevused võrreldes tarnega 1.4.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nHaldusteenus\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Muudatusi ei ole\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Muudatusi ei ole\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Muudatusi ei ole\n\nVõtmerakendus\n\n* Tööriista *init* väljundfailide muutus\n* Tööriista *init* SN ja CN parameetrite muutus\n* Tööriista *testkey* sisendparameetrite muutus\n* Tööriista *decrypt* kasutatud kaardinumbrite kuvamine\n\nTöötlemisrakendus\n\n* Muudatusi ei ole\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* MSAA toe täpsustamine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* IVXV protokollid - registreerimisprotokolli lisamine\n* IVXV seadistuste koostejuhend - läbivad täiendused ning käsiraamatuga\n  ühtlustamine.\n* IVXV valijarakendus - läbivad täiendused.\n* IVXV mixnet - dokument hõlmatud seadistuste koostejuhendisse, eemaldatud.\n* IVXV registreerimisteenus - dokument hõlmatud protokollistikku, eemaldatud.\n\nLogimonitor\n~~~~~~~~~~~\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nMuudatused tarne 1.4.0 koosseisus, erinevused võrreldes tarnega 1.3.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* OCSP ja ajatemplipäringute automaatne kordamine vastavalt\n  konfiguratsioonile\n* BDOC-TS allkirjakonteinerite toetamine\n* Tühjade valijanimekirjade tugi\n\nHaldusteenus\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* TLS 1.2 toetamine API-versioonide < 19 korral\n* Abiinfo vaate asendamine süsteemse brauseriga\n* Kuvast väljuvate tekstide automaatne lühendamine nuppude korral\n\niOS\n\n* Muudatusi ei ole\n\nMiksnet\n~~~~~~~\n\n* Muudatusi ei ole\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nÜldised\n\n* Jaoskonnanumbri unikaalsusnõuete täpsustamine\n* Tühja valijatenimekirja tugi\n* Kodeeringuvigadega sertifikaatide toetamine rakendustes\n\nVõtmerakendus\n\n* Eemaldatud LOG4 ja LOG5\n\nTöötlemisrakendus\n\n* Eemaldatud PDF vormingus valijate nimekiri faasist *revoke*\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* macOS 10.11 toetamine\n* 32bit Linuxi toetamine\n* UPX versiooni uuendamine\n* Pinpad kaardilugejate töökindluse tõstmine (Win)\n* ID-kaardi suhtlusvigade parandamine (Win7/ECC)\n* Nii PEM kui DER vormingus andmete import (Seadistaja)\n* Lisatud Seadistuste valideerimisvõimalus (Seadistaja)\n* Parandatud käitumine liigsuurte seadistuste korral (Seadistaja)\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Dokumentatsiooni uuendamine muudatuste kajastamiseks ning DEMO2018\n  tagasiside arvestamiseks\n\nLogimonitor\n~~~~~~~~~~~\n\n* MTA sõltuvuse lisamine\n* CSV logiväljavõtte täpsustamine, algus- ja lõpuaja lisamine\n* Logianalüüsi optimeerimine mitmetuumalise riistvara jaoks\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nMuudatused tarne 1.3.0 koosseisus, erinevused võrreldes tarnega 1.2.0\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nÜldised muutused\n\n* Ubuntu 18.04 LTS (Bionic Beaver) kasutuselevõtmine\n* Krahhitaaste protseduuride kirjeldamine\n\nHaldusteenus\n\n* Parandatud tööriistad teenuste seisundiinfo saamiseks\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nIVXV mikroteenused\n\n* golang keeleversioon 1.9 kasutuselevõtmine\n* Eesti ID-kaardi uuenenud profiili toetamine (PNOEE)\n* Väiksemad muutused/veaparandused vastavalt `changelog` failile\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n* Muudatusi ei ole\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* ESTEID2018 sertifikaatide toetamine\n* Täpsustatud vigase ASN1-kodeeringuga avalike võtmete käitlemist\n\niOS\n\n* ESTEID2018 sertifikaatide toetamine\n* iPhone 10 X muudatused\n* XCode 10 ja iOS 12 SDK kasutamine\n\nMiksnet\n~~~~~~~\n\n* Verificatumi AGPL versiooni kasutuselevõtmine\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\n\nVõtmerakendus\n\n* Muudatusi ei ole\n\nTöötlemisrakendus\n\n* Lisatud tööriist StatsTool valimiskastist statistikafaili genereerimiseks\n* Lisatud tööriist StatsDiffTool kahe statistikafaili võrdlemiseks\n* ESTEID2018 sertifikaatide ja profiili toetamine\n* digidoc4j 2.1.0 kasutamine\n\nAuditirakendus\n\n* Muudatusi ei ole\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Valijarakenduses kandidaatide otsingu võimaldamine\n* Valijarakenduses erakondade ja kandidaatide kaustana kuvamine\n* Valijarakenduse ja Seadistusrakenduse üleviimine JSON-vormingus seadistustele\n* ESTEID2018 sertifikaatide ja profiili toetamine\n* Win: IDEMIA minidraiveri toetamine\n* Linux/macOS: IDEMIA PKCS11 draiveri toetamine\n* macOS 10.14 toetamine\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n\n* Dokumentatsiooni uuendamine muudatuste kajastamiseks\n* Ingliskeelse arhitektuuridokumendi ja protokollistiku lisamine\n\nLogimonitor\n~~~~~~~~~~~\n\n* Ubuntu 18.04 LTS (Bionic Beaver) kasutuselevõtmine\n* Võetud kasutusele Grafana 5.3.4\n* Parandatud vanusepõhise statistika genereerimine ja vanusegruppidesse\n  jaotumine\n* CSV väljundi võtmine seanssidest\n\nMuudatused tarne 1.2.0 koosseisus, erinevused võrreldes KOV2017 valimistega.\n--------------------------------------------------------------------------------\n\nKogumisteenus\n~~~~~~~~~~~~~\n\nHaldusteenus\n\n* Lisatud tööriist vigaste valijanimekirjade eemaldamiseks.\n* Lisatud tööriist jaoskondade/ringkondade nimekirja lisamiseks.\n* Lisatud ringkonnapõhine statistika.\n* Lisatud varundusteenus.\n* Lisatud tööriist varundatud valimiskastide konsolideerimiseks.\n* Lisatud tööriist nimekirjade kooskõlalisuse kontrolliks.\n* Lisatud võimekus seadistusfailidele valimisspetsiifiliste prefiksite lisamiseks.\n* Täiendatud haldusliidese kasutajaliidest abiinfoga.\n* Eemaldatud aegunud konfiguratsiooniparameeter “stats.*”\n* Parandatud sisendfailide vormingu kontrolli ja laadimist.\n\nIVXV mikroteenused\n\n* Uuendatud etcd versioon.\n* Lisatud võimekus etcd ajalõppude seadistamiseks keskkonnamuutujate kaudu.\n* Lisatud võimekus klastri modifitseerimiseks krahhitaaste eesmärgil.\n* Parandatud klastri käitumist liidrivahetuse korral, pooleliolevate talletamiste kordamine.\n* TLS šifrid muudetud seadistatavaks.\n* Parandatud BDOC profiili identifitseeriva konfiguratsioonivälja nimi.\n* Lisatud võimekus seadistada Mobiil-ID autentimist nõudma nii isikukoodi kui telefoninumbrit.\n* Lisatud võimekus piirata korduvhääletamise sagedust.\n* Lisatud võimekus toetada Windowsi reavahetusi konfifailides.\n* Täiustatud BDOC XML kanoniseerimist ja parsimist.\n* Karmistatud DDS päringute vormingukontrolle.\n* Logimine viidud üle RELP protokollile.\n* Muudetud seadistusfailide ülesehitust eristamaks Koguja ja Töötleja vastutusi.\n\nRegistreerimisteenus\n~~~~~~~~~~~~~~~~~~~~\n* Muudatusi ei ole.\n\nKontrollrakendused\n~~~~~~~~~~~~~~~~~~\n\nAndroid\n\n* Lisatud juhised publitseeritud kontrollrakenduse ja avalikustatud lähtekoodi vastavuse kontrollimiseks.\n\niOS\n\n* Muudatusi ei ole.\n\nMiksnet\n~~~~~~~\n* Muudatusi ei ole.\n\nTöötleja rakendused\n~~~~~~~~~~~~~~~~~~~\nVõtmerakendus\n\n* Muudatusi ei ole.\n\nTöötlemisrakendus\n\n* Muudatusi ei ole.\n\nAuditirakendus\n\n* Muudatusi ei ole.\n\nValijarakendused ja seadistusrakendus\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n* Linux ja Mac – platvormispetsiifilisi muudatusi ei ole.\n* Windows – parandatud liidestumist Minidraiveriga, mingw64 kasutuselevõtmine.\n* Lisatud ID-kaardi ECC toetamine.\n* Seadistatud Mobiil-ID nõudma vajadusel isikukoodi ja telefoninumbrit.\n* Täiendatud veakoode.\n* Kohandatud seadistusrakendus muudatustega vastavusse.\n* Kohandatud BDOC XML mallid\n\nDokumentatsioon\n~~~~~~~~~~~~~~~\n* Dokumentatsioon läbivalt kaasajastatud seoses muudatustega\n\nLogimonitor\n~~~~~~~~~~~\n* Loobutud CrateDBst.\n* Võetud läbivalt kasutusele PostgreSQL.\n* Võetud kasutusele Grafana 5.0.1.\n* Seansside valideerimise parandused lähtudes KOV2017 logianalüüsist.\n* Lisatud statistika genereerimine ringkondade kaupa.\n"
  },
  {
    "path": "Documentation/spelling_wordlist.txt",
    "content": "\nagregeerimise\nagregeeritakse\najakohastamine\nandmeaudit\nandmeauditi\nanonüümib\nanonüümimine\nanonüümitakse\nanonüümitud\narhitektuuridokumendi\nATO'd\nATO'le\naudit\nauditi\nauditit\nauditirakendus\nauditirakenduse\nauditirakendusega\nautentimismeetod\nautentimismeetodi\nautentimismeetodiga\nautentimisprotsessi\nautentimisseansi\nautentimissertifikaadi\nautentimissertifikaat\nautentimissertifikaati\nautentimisteenuse\nautentimistõend\nautentimistõendi\nautentimisvahendina\nautentimisvahendite\nautomaatvarunduse\n\nBase\nbitise\n\nCSV\n\ndekodeerub\ndekrüpteeruvad\nDER\nDigi\ndigiallkirjastatud\ndigiallkirja\ndigiallkirjade\ndimensioneerida\n\neID\neID'ga\nelliptkõver\nelliptkõverad\nelliptkõveraid\nelliptkõveral\nelliptkõverate\nelliptkõveratel\neskaleerimiseks\n\nhaldusutiliitide\nHEX\nhomomorfset\n\nID\nID'ga\nid'ga\nidentiteedi\nidentiteedile\nidentiteet\nidentiteeti\ningliskeelse\nIP\n\nkasutuselevõtmine\nkasutuselevõtt\nkodeeringud\nkodeeringus\nkodeeringut\nkodeeringuvigadega\nkonfifailides\nkoodekfiltri\nkooskõlastatakse\nkrüpteerinud\nkrüptib\nkrüptimise\nkõrgkäideldav\nkõrgkäideldava\nkõrgkäideldavuse\nkäideldavusnõuetele\nkäimasolev\nkäimasoleval\nkäsureautiliidid\nkäsureautiliitide\nkätluse\n\nliidestuma\nliidestumist\nliidestuva\nlogiseire\nlähtestab\nlähtestamine\nlähtestatakse\n\nmh\nmiksija\nmiksijale\nmiksimine\nmiksimise\nmiksimiseelse\nmiksimisega\nmiksimiseks\nmiksimisele\nmiksimisjärgse\nmiksimisrakendus\nmiksimisrakenduse\nmiksimisrakendusele\nmiksimisrakenduses\nmiksimisrakendusse\nmiksimist\nmiksimistõend\nmiksimistõendi\nmiksimistõendit\nmiksitud\nmiksnet\nmiksneti\nmiksnetist\nminidraiveriga\nmitmelõimelist\nModP\nmonitoorida\nmonitoorimine\nmonitooring\nmonitooringu\nmonitooringulahendus\nmonitooringule\nmonitooritavas\n\nnavigeerida\nnonsiks\nnonsina\nnonss\nnonsi\nnonssi\nnotatsioon\nnotatsioonis\nnö\n\nobfuskeerimine\nolemasolev\nolemasolu\norganisatsioonilis\nosak\nosakud\n\nparametriseerimise\nparametriseeritavad\nparendused\npermuteerib\npoll\nprivaatvõti\nprivaatvõtit\nprivaatvõtme\nprivaatvõtmed\nprogramselt\nprotokollistikku\npseudojuhuslik\npseudokoodina\npseudokrüptogramm\npuhverdatavat\n\nQR\n\nrefaktoreerimine\nresponderi\nriigiti\nroad\nRPC\nräsiga\nräsil\nräsile\n\nseadistusutiliidid\nseire\nseirelahendus\nseirelahenduse\nseirelahendusest\nseirele\nseireprogramm\nseireprogrammi\nseiresse\nseiret\nseiretarkvara\nseireteenuse\nseireteenusest\nserdid\nserdiprofiili\nserdituvastuse\nserialiseerimise\nsertide\nservice\nskaleerida\nskaleerimiseks\nskaleeritavuse\nskaleerub\nsmart\nspetsifikatsioon\nspetsifikatsiooni\nstandartne\n\nteadaoleva\nteadaolevaid\nteadaolevate\nteadasaamiseks\nteavitussüsteem\nteavituste\nteenushoste\nteenushosti\nteenushostide\nteenushostidesse\ntegevusmonitooringu\ntähelepanu\nTLS\n\nunikaalsusnõuete\nutiliidi\nutiliidid\nutiliidiga\nutiliit\nUML\nURL\n\nvallasrežiimis\nvallasrežiimirakendustest\nvarundab\nvarundama\nvarundamine\nvarundamise\nvarundamisele\nvarundamiseks\nvarundamist\nvarundatakse\nvarundatavad\nvarundati\nvarundatud\nvarundus\nvarunduse\nvarundusliidese\nvarundusprotseduuride\nvarundusserver\nvarundusserveri\nvarundusserveris\nvarundusserverisse\nvarundusserveritesse\nvarundusteenus\nvarundusteenuse\nvarundusteenuses\nvarundusteenusele\nvarundusteenusesse\nvarundusteenusest\nvarundusteenust\nveebisirviku\nVerificatum\nVerificatumi\nverifitseerib\nverifitseerida\nverifitseerimine\nverifitseerimise\nverifitseerimiseks\nverifitseerimisel\nverifitseerimisest\nverifitseerimiskoodi\nverifitseerimist\nverifitseerimisteenus\nverifitseerimisteenusega\nverifitseeritakse\nverifitseeritav\nverifitseeritavuse\nverifitseerivad\nverifitseerub\nvirtualiseerimistehnoloogiate\nvisualiseerimistarkvaraga\nvõtmeosakud\nvõtmeosakut\n\nülaosas\n\nweb\n\nX\nxroad\n\nzip\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright: Vabariigi Valimisteenistus (Estonian State Electoral Office),\nwww.valimised.ee\n\nCreated in 2016-2025 by Cybernetica AS, www.cyber.ee\n\nThis work is licensed under the Creative Commons\nAttribution-NonCommercial-NoDerivs 3.0 Unported License.  To view a copy of this\nlicense, visit http://creativecommons.org/licenses/by-nc-nd/3.0/.\n"
  },
  {
    "path": "Makefile",
    "content": "JAVADIRS    := common/java key processor auditor\nGODIRS      := common/collector sessionstatus/api proxy mid smartid choices voting verification storage votesorder webeid sessionstatus\nOTHERDIRS   := systemd Documentation\n\nTESTDIRS    := $(patsubst %,test-%,$(JAVADIRS) $(GODIRS))\nINSTALLDIRS := $(patsubst %,install-%,$(JAVADIRS) $(GODIRS) systemd)\nCLEANDIRS   := $(patsubst %,clean-%,$(JAVADIRS) $(GODIRS) $(OTHERDIRS))\n\nexport ROOT_BUILD=true\n\n# Needed to include newlines in $(forach) loops. Must contain two empty lines.\ndefine NEWLINE\n\n\nendef\n\n.PHONY: help\nhelp:\n\t@echo \"usage: make all           Build all components\"\n\t@echo \"       make test          Run unit tests for all components\"\n\t@echo \"       make install       Install all collector components to \\$$DESTDIR\"\n\t@echo \"       make install-java  Install all application components to \\$$DESTDIR\"\n\t@echo \"       make clean         Clean the repository\"\n\t@echo\n\t@echo \"       make <component>          Build the component (listed below)\"\n\t@echo \"       make test-<component>     Run unit tests for the component\"\n\t@echo \"       make install-<component>  Install the component to \\$$DESTDIR\"\n\t@echo \"       make clean-<component>    Clean the component\"\n\t@echo\n\t@echo \"       make external         Checkout common/external to the expected version\"\n\t@echo \"       make update-external  Checkout common/external to the latest version\"\n\t@echo \"       make version          Update version numbers in all known locations to\"\n\t@echo \"                             the last entry in debian/changelog\"\n\t@echo\n\t@echo \"Components:\"\n\t$(foreach component,$(filter-out $(JAVADIRS) $(GODIRS),$(OTHERDIRS)),@echo \"  $(component)\"$(NEWLINE))\n\t@echo\n\t@echo \"  java  (meta-component which includes all of the following)\"\n\t$(foreach component,$(JAVADIRS),@echo \"  $(component)\"$(NEWLINE))\n\t@echo\n\t@echo \"  go  (meta-component which includes all of the following)\"\n\t$(foreach component,$(GODIRS),@echo \"  $(component)\"$(NEWLINE))\n\t@echo\n\t@echo \"All rules can be suffixed with \\\"-dev\\\" to call them in development mode instead,\"\n\t@echo \"e.g., make all-dev.\"\n\t@echo\n\t@echo \"Read README.rst in both the repository root and specific component directories\"\n\t@echo \"for more details.\"\n\n.PHONY: all\nall: $(JAVADIRS) $(GODIRS) $(OTHERDIRS)\n\n.PHONY: java\njava: $(JAVADIRS)\n\n.PHONY: go\ngo: $(GODIRS)\n\n.PHONY: $(GODIRS)\n$(GODIRS): --gotools\n\t$(MAKE) -C $@ goimports\n\t$(MAKE) -C $@ ONLINE=$(ONLINE)\n\t$(MAKE) -C $@\n\n.PHONY: $(JAVADIRS)\n$(JAVADIRS):\n\t$(MAKE) -C $@\n\n.PHONY: $(OTHERDIRS)\n$(OTHERDIRS):\n\t$(MAKE) -C $@\n\n.PHONY: test\ntest: $(TESTDIRS)\n\n.PHONY: test-java\ntest-java: $(JAVADIRS:%=test-%)\n\n.PHONY: test-go\ntest-go: $(GODIRS:%=test-%)\n\n.PHONY: test-python\ntest-python:\n\t$(MAKE) -C tests unit-tests\n\n.PHONY: $(TESTDIRS)\n$(TESTDIRS): test-%:\n\t$(MAKE) -C $* test\n\n# Only install Go services and systemd unit files by default.\n.PHONY: install\ninstall: $(patsubst %,install-%,$(GODIRS) systemd)\n\n.PHONY: install-java\ninstall-java: $(JAVADIRS:%=install-%)\n\n.PHONY: $(INSTALLDIRS)\n$(INSTALLDIRS): install-%:\n\t$(MAKE) -C $* install\n\n.PHONY: clean\nclean: $(CLEANDIRS)\n\t$(MAKE) -C tests $@\n\t$(MAKE) -C release $@\n\trm -rf build dist common/tools/go/bin\n\n.PHONY: clean-java\nclean-java: $(JAVADIRS:%=clean-%)\n\n.PHONY: release-doc\nrelease-doc: go\n\t$(MAKE) -C Documentation/common/schema validate\n\t$(MAKE) -C Documentation release\n\n.PHONY: clean-go\nclean-go: $(GODIRS:%=clean-%)\n\n.PHONY: $(CLEANDIRS)\n$(CLEANDIRS): clean-%:\n\t$(MAKE) -C $* clean\n\n.PHONY: external\nexternal:\n\tgit submodule update --init\n\n\n.PHONY: update-external\nupdate-external:\n\tgit submodule update --init --remote\n\n\n.PHONY: version\nversion:\n\tpython3 common/tools/update_project_version.py\n\n# We cannot mark this target as phony without listing all possible targets.\n%-dev:\n\t$(MAKE) $* DEVELOPMENT=1\n\n# Target prefixed with \"--\" are not seen to the call `make [target]`\n.PHONY: --gotools\n--gotools:\n\t$(MAKE) -C common/collector gotools\n"
  },
  {
    "path": "README.md",
    "content": "IVXV online voting system\n=========================\n\nThe intention behind this repository is to make source code of the Estonian\nonline voting system available for public review.\n\nThe repository is not used for active development, but will be kept up to date,\nso the code that can be found here is the code that is used for election. As the\nvoting system used for legally binding elections must strictly follow the\nlegislation, the actual development of Estonian I-voting system and I-vote\nverification application is supervised by State Electoral Office of Estonia.\nPlease refer to www.valimised.ee for further information.\n\n"
  },
  {
    "path": "README.rst",
    "content": "==============================\nIVXV Internet voting framework\n==============================\n\n<General introduction to IVXV.>\n\n----------\n Building\n----------\n\nBuilding the collector components requires Go 1.9 and the management\napplications require Java 11::\n\n        sudo apt-get install --no-install-recommends openjdk-11-jdk-headless golang-1.9-go\n\nThe submodule for external dependencies uses Git LFS. This must be locally\nsupported so make sure that it is installed::\n\n        sudo apt-get install --no-install-recommends git-lfs\n\nNext, external dependencies need to be acquired. If working off of an offline\ncopy of the IVXV repository, then they are already included and this step can\nbe skipped. Otherwise run\n\n::\n\n        make external\n\nto download the external dependecy repository.\n\nFinally, to build, test, and clean the entire codebase, just do\n\n::\n\n        make all\n        make test\n        make clean\n\nIndividual components can be built, tested, and cleaned with\n\n::\n\n        make <component>\n        make test-<component>\n        make clean-<component>\n\nwhere ``<component>`` is the name name of the components subdirectory.\n\nAll components are built in release mode by default. To build in development\nmode, which enables dummy modules and other components not safe for release,\nthe make variable ``DEVELOPMENT`` must be set either on the command line or in\nthe environment. Every target of the root Makefile can be called with a\n``-dev`` suffix, which sets this variable during the make.\n\nDebian packages can be built using ``dpkg-buildpackage`` (or ``debuild`` if\npreferred) as usual.\n\n-------------\n Development\n-------------\n\nUpdating external dependencies\n------------------------------\n\nDuring development, if changes are pushed to the external dependencies\nrepository, then the reference needs to be updated in this repository to link\nthose changes to the current revision. This is done with\n\n::\n\n        make update-external\n        git add common/external\n        git commit\n        git push\n\nAfter the reference update has been pulled by other developers, they will get\nan entry in their ``git status`` output, indicating that external has changed::\n\n        modified:   common/external (new commits)\n\nTo fetch the changes to their local working tree, developers need to rerun\n\n::\n\n        make external\n\nSetting GOPATH\n--------------\n\nThe IVXV project uses multiple GOPATH directories, one for each Go component.\nDuring ``make`` commands, the GOPATH will be automatically set, but if a\ndeveloper (or their IDE) wishes to run some manual Go commands, then this can\nbe tedious to specify. To help with this\n\n::\n\n        make gopath\n\nwill print out the GOPATH used by the build system. So for example, when using\nBash,\n\n::\n\n        export GOPATH=\"$(make gopath)\"\n\nwill set the correct GOPATH in the current shell.\n\n\n----------\n Releasing\n----------\n\nRelease builds are made using test system.\n\nTo make release build:\n\n* run ``dch --release`` to finalize the changelog for a release\n\n* run ``make release``\n\n        *Note!* This creates a new virtual environment for building the\n        release, so if you have installed a custom binaries or libraries to the\n        local machine (e.g., the patched Go standard library from ivxv-golang),\n        then those will not be used. In this case, build the Debian packages\n        manually.\n"
  },
  {
    "path": "auditor/.gitignore",
    "content": ".classpath\n.gradle/\n.idea/\n.project\nbuild/\n/log/\n/bin/\n.settings/\n"
  },
  {
    "path": "auditor/Makefile",
    "content": "include ../common/java/common.mk\n"
  },
  {
    "path": "auditor/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n---------------------\n Auditor application\n---------------------\n\nThe auditor application is an application for verifying the correctness of\noperations of other tools.\n\nThe application functionality is provided by the tools described below:\n\n* *convert* - tool for verifying that the proof of shuffle provided by\n  Verificatum corresponds to the anonymized ballot boxes. The input ballot box\n  is the anonymized ballot box output by the processor tool and the output\n  ballot box is the anonymized ballot box output by the Verificatum prover\n  application. The provided public key must be output by key generation\n  tool.\n* *mixer* - tool for verifying the correctness of a proof of shuffle. The tool\n  takes as input the protocol information file provided to Verificatum during\n  shuffle and the proof directory output by it.\n* *decrypt* - tool for verifying the correctness of decryption. The tool uses\n  public key and the proof of correct decryption output by the decryption tool.\n  The tool also verifies whether the tally has been computed correctly and\n  whether the files resulting from the decryption are consistent with one\n  another.\n  It outputs ciphertexts for which verification of the decryption proof failed.\n* *integrity* - tool for verifying the logs of the processing application. The\n  tool uses the logs output by the processing application as well as the\n  original and anonymised ballot boxes to verify the audit trail of the logs.\n\nBuilding\n--------\n\nIVXV java applications have 2 levels of build systems:\n\n* *make* - the build system facade. Must be installed on the user's machine.\n* *gradle* - the implementation of the build system. Gradle is located under\n  ``common/external/gradle-8.11``, with the executable ``bin/gradle(.bat)``.\n\nBuilding:\n\n* ``make`` or\n* ``make all`` or\n* ``gradle build installDist`` - build and test the application.\n* ``make clean`` or\n* ``gradle clean`` - clean build resources, i.e. the directory ``build``.\n\nApplication executable is ``build/install/auditor/bin/auditor(.bat)``.\n\nDistributable application packages are provided under ``build/distributions/``.\n\nSample executions\n-----------------\n\nAs the command-line arguments have to be defined precisely for correct\noperation, we strongly recommend using a parameters file. Refer to the\nconfiguration preparation documentation for the example configurations.\n\n* Verify the correctness of file format conversion::\n\n    auditor convert --conf app-conf.bdoc --params auditor-app-conf.bdoc\n\n* Verify the correcntess of the shuffle::\n\n    auditor mixer --conf app-conf.bdoc --params auditor-app-conf.bdoc\n\n* Verify the correctness of decryption::\n\n    auditor decrypt --conf app-conf.bdoc --params auditor-app-conf.bdoc\n\n* Verify the integrity of processing logs::\n\n    auditor integrity --conf app-conf.bdoc --params auditor-app-conf.bdoc\n\nSample configuration\n--------------------\n\n.. code-block:: yaml\n\n  convert:\n    input_bb: bb-4.json\n    output_bb: bb-5.json\n    pub: initout/pub.pem\n    protinfo: mixnet/ProtocolInformation.xml\n    proofdir: mixnet/\n\n  mixer:\n    protinfo: mixnet/ProtocolInformation.xml\n    proofdir: mixnet/\n    threaded: true\n\n  decrypt:\n    proofs: decout/proof\n    pub: initout/pub.pem\n    discarded: decout/invalid\n    anon_bb: bb-4.json\n    plain_bb: decout/TESTQUESTION.plain\n    tally: decout/TESTQUESTION.tally\n    candidates: choices.bdoc\n    districts: districts.bdoc\n    out: auditout/\n    invalidity_proofs: decout/proof-invalid\n\n  integrity:\n    ballotbox: votes.zip\n    anon_bb: bb-4.json\n    log_accepted: out-1/TESTQUESTION.check.log1\n    log_squashed: out-2/TESTQUESTION.squash.log2\n    log_revoked: out-3/TESTQUESTION.revoke.log2\n    log_anonymised: out-4/TESTQUESTION.anonymize.log3\n    bb_errors: out-1/ballotbox_errors.txt\n"
  },
  {
    "path": "auditor/build.gradle",
    "content": "buildscript {\n    ext.base = '../'\n    apply from: \"${base}/common/java/common-buildscript.gradle\", to: buildscript\n}\n\napply from: \"${base}/common/java/common-build.gradle\"\napply plugin: 'application'\n\ndependencies {\n    implementation project(\":common\")\n\n    testImplementation testFixtures(project(\":common\"))\n}\n\napplication {\n\tmainClass = \"ee.ivxv.audit.Audit\"\n\tapplicationDefaultJvmArgs = [\"-Xmx8G\"]\n}\n"
  },
  {
    "path": "auditor/settings.gradle",
    "content": "include \"common\"\nproject(\":common\").projectDir = file(\"../common/java\")\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/Audit.java",
    "content": "package ee.ivxv.audit;\n\nimport ee.ivxv.common.cli.AppRunner;\n\npublic class Audit {\n\n    public static void main(String[] args) {\n        AuditApp app = new AuditApp();\n        AppRunner<AuditContext> runner = new AppRunner<>(app);\n\n        if (!runner.run(args)) {\n            System.exit(1);\n        }\n    }\n\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/AuditApp.java",
    "content": "package ee.ivxv.audit;\n\nimport ee.ivxv.audit.tools.ConvertTool;\nimport ee.ivxv.audit.tools.ConvertTool.ConvertArgs;\nimport ee.ivxv.audit.tools.DecryptTool;\nimport ee.ivxv.audit.tools.DecryptTool.DecryptArgs;\nimport ee.ivxv.audit.tools.IntegrityTool;\nimport ee.ivxv.audit.tools.IntegrityTool.IntegrityArgs;\nimport ee.ivxv.audit.tools.MixerTool;\nimport ee.ivxv.audit.tools.MixerTool.MixerArgs;\nimport ee.ivxv.common.cli.App;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.conf.Conf;\nimport java.util.Arrays;\nimport java.util.List;\n\nclass AuditApp extends App<AuditContext> {\n\n    AuditApp() {\n        super(Msg.app_audit, createTools());\n    }\n\n    private static List<Tool<AuditContext, ?>> createTools() {\n        return Arrays.asList( //\n                new Tool<>(Msg.tool_convert, ConvertArgs::new, ConvertTool::new),\n                new Tool<>(Msg.tool_mixer, MixerArgs::new, MixerTool::new),\n                new Tool<>(Msg.tool_decrypt, DecryptArgs::new, DecryptTool::new),\n                new Tool<>(Msg.tool_integrity, IntegrityArgs::new, IntegrityTool::new));\n    }\n\n    @Override\n    public AuditContext createContext(InitialContext ctx, Conf conf, CommonArgs args) {\n        return new AuditContext(ctx, conf, args);\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/AuditContext.java",
    "content": "package ee.ivxv.audit;\n\nimport ee.ivxv.common.cli.AppContext;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.conf.Conf;\n\npublic class AuditContext extends AppContext<Conf> {\n\n    public AuditContext(InitialContext i, Conf conf, CommonArgs args) {\n        super(i, conf, args);\n    }\n\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/Msg.java",
    "content": "package ee.ivxv.audit;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.util.NameHolder;\n\n@BaseName(\"i18n.audit-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg implements NameHolder {\n    /*-\n     * The part of the enum name until the first '_' (including) is excluded from the getName().\n     * This is a means to provide multiple translations for the same tool or argument name.\n     */\n\n    // App\n    app_audit,\n\n    // Error messages\n    e_proof_verif_false, e_proof_verif_exception,\n    e_file_missing,\n\n    // Tools\n    tool_decrypt, tool_mixer, tool_convert, tool_integrity,\n\n    // Tool arguments\n    arg_abort_early, //\n    arg_proofs, arg_invalidity_proofs, arg_discarded, //\n    arg_hash, arg_links(\"l\"), arg_out(\"o\"), arg_pbb(\"p\"), arg_pub(\"p\"), //\n    arg_revoke(\"r\"), arg_seed(\"s\"), arg_storage(\"s\"), arg_signaturepub, arg_threads(\"t\"), //\n    arg_input_bb, arg_output_bb, arg_protinfo, arg_proofdir, arg_threaded, //\n    arg_ballotbox, arg_ballotbox_checksum, arg_anon_bb, arg_plain_bb, //\n    arg_log_accepted, arg_log_squashed, arg_log_revoked, arg_log_anonymised, arg_bb_errors, //\n    arg_tally, arg_candidates, arg_districts, arg_questioncount, //\n\n    // Messages\n    m_yes, m_no, //\n    m_pub_loading, m_pub_loaded, m_failurecount, m_verify_start, m_verify_finish, //\n    m_decrypt_bb_has_proofs, m_decrypt_bb_has_invalids, m_decrypt_one_per_file, //\n    m_decrypt_verified_valid, m_decrypt_verified_invalid, //\n    m_decrypt_consistent_valids_begin, //\n    m_decrypt_consistent_valids, m_decrypt_consistent_invalids, m_decrypt_dec_consistent, //\n    m_decrypt_success, m_decrypt_failure, //\n    m_shuffle_proof_loading, m_shuffle_proof_failed_reason, m_shuffle_proof_succeeded, //\n    m_shuffle_proof_failed, m_convert_publickey_failed, m_convert_publickey_succ, //\n    m_convert_bb_to_bt_failed, m_convert_bb_to_bt_succ, m_convert_bt_to_bb_failed, //\n    m_convert_bt_to_bb_succ, m_shuffle_step, m_shuffle_read, m_shuffle_read_prot_info, //\n    m_shuffle_read_pubkey, m_shuffle_read_pc, m_shuffle_read_posc, m_shuffle_read_posr, //\n    m_shuffle_read_ciphs, m_shuffle_read_shuffled, //\n    m_shuffle_verify, m_shuffle_verify_params, m_shuffle_verify_ni, m_shuffle_verify_permutation, //\n    m_shuffle_verify_rerandomisation, //\n    m_plain_loading, m_plain_loaded, m_plain_count, //\n    m_tally_loading, m_tally_loaded, m_tally_match, //\n    m_discarded_loading, m_discarded_loaded, m_discarded_count, //\n    m_bb_loading, m_bb_loaded, m_anon_loading, m_anon_loaded, //\n    m_log_accepted, m_log_squashed, m_log_revoked, m_log_anonymised, m_bb_errors, //\n    m_integrity_match_anon, m_integrity_match_anon_logs, //\n    m_integrity_match_valid, m_integrity_match_logs, //\n    m_integrity_bb_consistent, m_integrity_bb_inconsistent, m_integrity_recurring_ct, //\n    m_tally_start, m_tally_done, m_out_tally, m_tally_read, //\n\n    e_abb_invalid_question_count, //\n\n    ;\n\n\n    private final String shortName;\n\n    Msg() {\n        this(null);\n    }\n\n    Msg(String shortName) {\n        this.shortName = shortName;\n    }\n\n    @Override\n    public String getShortName() {\n        return shortName;\n    }\n\n    @Override\n    public String getName() {\n        return extractName(name());\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/model/PlainBallotBox.java",
    "content": "package ee.ivxv.audit.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.List;\n\n/**\n * JSON serializable structure for holding the decrypted ballot box.\n */\npublic record PlainBallotBox(String election, Map<String, Map<String, List<String>>> byParish,\n                             Map<String, List<String>> byDistrict) {\n    @JsonCreator\n    public PlainBallotBox(@JsonProperty(\"election\") String election,\n                          @JsonProperty(\"byparish\") Map<String, Map<String, List<String>>> byParish,\n                          @JsonProperty(\"bydistrict\") Map<String, List<String>> byDistrict) {\n        this.election = election;\n        this.byParish = byParish;\n        this.byDistrict = byDistrict;\n    }\n\n    public Map<String, List<String>> computeByDistrict() {\n        Map<String, List<String>> res = new LinkedHashMap<>();\n        byParish().forEach((d, sMap) -> {\n            List<String> pList = res.computeIfAbsent(d, tmp -> new ArrayList<>());\n            sMap.forEach((s, plains) -> pList.addAll(plains));\n        });\n        return res;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/model/Tally.java",
    "content": "package ee.ivxv.audit.model;\n\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * JSON serializable structure for holding the tally of the votes.\n */\npublic class Tally {\n    public static final String INVALID_VOTE_ID = \"invalid\";\n    private final String election;\n    private final Map<String, Map<String, Map<String, Integer>>> byParish;\n    private final Map<String, Map<String, Integer>> byDistrict;\n\n\n    @JsonCreator Tally(@JsonProperty(\"election\") String election,\n                       @JsonProperty(\"byparish\") Map<String, Map<String, Map<String, Integer>>> byParish,\n                       @JsonProperty(\"bydistrict\") Map<String, Map<String, Integer>> byDistrict) {\n        this.election = election;\n        this.byParish = byParish;\n        this.byDistrict = byDistrict;\n    }\n\n    /**\n     * Get the election identifier.\n     *\n     * @return\n     */\n    public String getElection() {\n        return election;\n    }\n\n    /**\n     * @return Returns a map from district id to a map from station id to a map from candidate id to\n     *         number of received votes.\n     */\n    public Map<String, Map<String, Map<String, Integer>>> getByParish() {\n        return byParish;\n    }\n\n    /**\n     * @return Returns a map from district id to a map from candidate id to number of received\n     *         votes.\n     */\n    public Map<String, Map<String, Integer>> getByDistrict() {\n        return byDistrict;\n    }\n\n    public Map<String, Map<String, Integer>> computeByDistrict() {\n        Map<String, Map<String, Integer>> res = new LinkedHashMap<>();\n        getByParish().forEach((d, sMap) -> {\n            Map<String, Integer> ccMap = res.computeIfAbsent(d, tmp -> new LinkedHashMap<>());\n            sMap.forEach((s, cMap) -> cMap.forEach((c, count) -> {\n                ccMap.compute(c, (cc, ccount) -> ccount == null ? count : ccount + count);\n            }));\n\n        });\n        return res;\n    }\n}\n\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ByteTree.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.common.math.*;\nimport ee.ivxv.common.util.Util;\nimport java.io.DataInputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.math.BigInteger;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.file.Path;\nimport java.util.HexFormat;\nimport java.util.Objects;\n\nimport org.bouncycastle.math.ec.ECFieldElement;\nimport org.bouncycastle.util.Arrays;\n\n/**\n * ByteTree decodes the ByteTree (BT) format as defined in Verificatum user manual.\n */\npublic class ByteTree {\n    public int getLength() {\n        return 0;\n    }\n\n    public int getEncodedLength() {\n        return 0;\n    }\n\n    public byte[] getEncoded() {\n        return null;\n    }\n\n    public void writeEncoded(OutputStream out) throws IOException {\n        // Overridden\n    }\n\n    public boolean isLeaf() {\n        return false;\n    }\n\n    public String getPrefix() {\n        return \"\";\n    }\n\n    public String toString(int indent) {\n        return \"\";\n    }\n\n    @Override\n    public String toString() {\n        return toString(0);\n    }\n\n    /**\n     * Node is a recursive object in a ByteTree which consists of an array of nodes and leafs.\n     */\n    public static class Node extends ByteTree {\n        private final ByteTree[] nodes;\n\n        public static final byte PREFIX = 0;\n\n        /**\n         * Initialize Node from an array of ByteTree objects.\n         *\n         * @param nodes Array of ByteTree objects.\n         */\n        public Node(ByteTree[] nodes) {\n            this.nodes = nodes;\n        }\n\n        /**\n         * Initialize Node from an array of BigInteger objects.\n         *\n         * @param ints Array of BigInteger objects\n         */\n        public Node(BigInteger[] ints) {\n            nodes = new Leaf[ints.length];\n            for (int i = 0; i < nodes.length; i++) {\n                nodes[i] = new Leaf(ints[i]);\n            }\n        }\n\n        /**\n         * Initialize Node from an array of GroupElements.\n         *\n         * @param elements Array of GroupElements\n         */\n        public Node(GroupElement[] elements) {\n            nodes = new ByteTree[elements.length];\n            for (int i = 0; i < nodes.length; i++) {\n                if (elements[i] instanceof ProductGroupElement) {\n                    nodes[i] = new Node((ProductGroupElement) elements[i]);\n                } else if (elements[i] instanceof ECGroupElement) {\n                    nodes[i] = new Node((ECGroupElement) elements[i]);\n                } else if (elements[i] instanceof ModPGroupElement) {\n                    nodes[i] = new Leaf(elements[i]);\n                } else {\n                    throw new IllegalArgumentException(\"Unknown element type: \" + elements[i].getClass().getSimpleName());\n                }\n            }\n        }\n\n        /**\n         * Initialize Node from ProductGroupElement.\n         *\n         * @param element A ProductGroupElement element\n         */\n        public Node(ProductGroupElement element) {\n            nodes = new ByteTree[element.getElements().length];\n            for (int i = 0; i < nodes.length; i++) {\n                GroupElement ge = element.getElements()[i];\n                if (ge instanceof ProductGroupElement) {\n                    nodes[i] = new Node((ProductGroupElement) ge);\n                } else if (ge instanceof ECGroupElement) {\n                    nodes[i] = new Node((ECGroupElement) ge);\n                } else if (ge instanceof ModPGroupElement) {\n                    nodes[i] = new Leaf(ge);\n                } else {\n                    throw new IllegalArgumentException(\"Unknown element type: \" + ge.getClass().getSimpleName());\n                }\n            }\n        }\n\n        /**\n         * Initialize Node from ECGroupElement.\n         *\n         * @param ec {@link ee.ivxv.common.math.ECGroupElement} instance\n         */\n        public Node(ECGroupElement ec) {\n            // Size is two, because coordinate point (X,Y) == (BigInteger, BigInteger)\n            nodes = new Leaf[2];\n\n            ECFieldElement x = ec.getPoint().getXCoord();\n            ECFieldElement y = ec.getPoint().getYCoord();\n\n            int fieldSizeBytes = ec.getPoint().getCurve().getFieldSize();\n\n            nodes[0] = new Leaf(ecFieldElementToLeaf(fieldSizeBytes, x));\n            nodes[1] = new Leaf(ecFieldElementToLeaf(fieldSizeBytes, y));\n        }\n\n        /**\n         * Return the array of ByteTree objects.\n         *\n         * @return Array of ByteTree objects.\n         */\n        public ByteTree[] getNodes() {\n            return nodes;\n        }\n\n        /**\n         * Get the number of elements in the ByteTree array.\n         *\n         * @return Length of array.\n         */\n        @Override\n        public int getLength() {\n            return nodes.length;\n        }\n\n        /**\n         * Get the byte-length of the Node.\n         *\n         * @return Byte-length of the Node.\n         */\n        @Override\n        public int getEncodedLength() {\n            int sum = 5;\n            for (ByteTree n : nodes) {\n                sum += n.getEncodedLength();\n            }\n            return sum;\n        }\n\n        /**\n         * Encode the Node as bytes.\n         *\n         * @return Node encoded as bytes\n         */\n        @Override\n        public byte[] getEncoded() {\n            byte[] ret = new byte[getEncodedLength()];\n            ret[0] = PREFIX;\n            System.arraycopy(ByteTree.parse_from_int(getLength()), 0, ret, 1, 4);\n            int pt = 5;\n            for (int i = 0; i < getLength(); i++) {\n                byte[] enc = getNodes()[i].getEncoded();\n                System.arraycopy(enc, 0, ret, pt, enc.length);\n                pt += enc.length;\n            }\n            return ret;\n        }\n\n        /**\n         * Encode the Node and write it to out.\n         *\n         * @param out Stream to write the ByteTree description of the Node.\n         * @throws IOException When writing to out fails\n         */\n        @Override\n        public void writeEncoded(OutputStream out) throws IOException {\n            out.write(PREFIX);\n            out.write(ByteTree.parse_from_int(getLength()));\n            for (int i = 0; i < getLength(); i++) {\n                getNodes()[i].writeEncoded(out);\n            }\n        }\n\n        /**\n         * @return false\n         */\n        @Override\n        public boolean isLeaf() {\n            return false;\n        }\n\n        /**\n         * Parse an array of bytes into a node, starting at offset.\n         *\n         * @param b Array of bytes to be parsed.\n         * @param offset The offset in the array to start parsing\n         * @return A decoded Node instance.\n         */\n        public static Node parse(byte[] b, int offset) {\n            int len = parse_int_fullbytes(b, offset);\n            ByteTree[] bts = new ByteTree[len];\n            int pt = 0;\n            for (int i = 0; i < len; i++) {\n                bts[i] = ByteTree.parse(b, offset + pt + 5);\n                pt += bts[i].getEncodedLength();\n            }\n            Node n = new Node(bts);\n            return n;\n        }\n\n        /**\n         * Parse a DataInputStream into a node.\n         *\n         * The given DataInputStream must be pointed to the beginning of a valid Node element.\n         *\n         * @param is DataInputStream to be used for reading a node, seeked to the beginning of Node\n         *        definition.\n         * @return A decoded Node instance.\n         * @throws IOException When reading from the stream fails.\n         */\n        public static Node parse(DataInputStream is) throws IOException {\n            int len = read_length(is);\n            ByteTree[] bts = new ByteTree[len];\n            for (int i = 0; i < len; i++) {\n                bts[i] = ByteTree.parse(is);\n            }\n            Node n = new Node(bts);\n            return n;\n        }\n\n        /**\n         * @return \"NODE\".\n         */\n        @Override\n        public String getPrefix() {\n            return \"NODE\";\n        }\n\n        /**\n         * Human-friendly representation of the Node object, with starting indentation.\n         *\n         * @param indent The indentation level\n         * @return String representation of instance\n         */\n        @Override\n        public String toString(int indent) {\n            String spaces = new String(new char[indent]).replace(\"\\0\", \" \");\n            String header = String.format(\"%s%s %d\", spaces, getPrefix(), getLength());\n            String[] subs = new String[getLength() + 1];\n            subs[0] = header;\n            for (int i = 0; i < getLength(); i++) {\n                subs[i + 1] = nodes[i].toString(indent + 1);\n            }\n            return String.join(\"\\n\", subs);\n        }\n\n        /**\n         * Convert ECFieldElement to Leaf.\n         * Mixnet Verificatum reference:\n         * <a href=\"https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ECqPGroupElement.magic#L475\">innerToByteArray</a>\n         *\n         * @param fieldSize EC field size\n         * @param ec        EC field element\n         * @return EC field element as Leaf\n         */\n        private byte[] ecFieldElementToLeaf(final int fieldSize, final ECFieldElement ec) {\n            // https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ECqPGroupElement.magic#L538\n            int fieldSizeBytesPlusOne = MathUtil.toBytesLen(fieldSize) + 1;\n\n            final byte[] leaf = new byte[fieldSizeBytesPlusOne];\n\n            // null means infinity point\n            if (Objects.isNull(ec)) {\n                java.util.Arrays.fill(leaf, (byte) 0xFF);\n            } else {\n                final byte[] ecBytes = ec.toBigInteger().toByteArray();\n                System.arraycopy(ecBytes, 0, leaf, leaf.length - ecBytes.length, ecBytes.length);\n            }\n\n            return leaf;\n        }\n    }\n\n    /**\n     * Leaf represents an abstract object.\n     */\n    public static class Leaf extends ByteTree {\n        private final byte[] value;\n        public static final byte PREFIX = 1;\n\n        /**\n         * Initialize a leaf from an abstract byte array.\n         *\n         * @param value A byte array\n         */\n        public Leaf(byte[] value) {\n            this.value = value;\n        }\n\n        /**\n         * Initialize a leaf from a String.\n         *\n         * @param value A string to initialize Leaf.\n         */\n        public Leaf(String value) {\n            byte[] encoded = null;\n            try {\n                encoded = value.getBytes(\"US-ASCII\");\n            } catch (UnsupportedEncodingException e) {\n                // this encoding is supported\n            }\n            this.value = encoded;\n        }\n\n        /**\n         * Initialize a leaf from a BigInteger.\n         *\n         * @param value A BigInteger to initialize Leaf.\n         */\n        public Leaf(BigInteger value) {\n            this.value = value.toByteArray();\n        }\n\n        /**\n         * Initialize a leaf from a GroupElement.\n         *\n         * @param value A GroupElement to initialize Leaf.\n         */\n        public Leaf(GroupElement value) {\n            if (value instanceof ModPGroupElement) {\n                this.value = getEncoded((ModPGroupElement) value);\n            } else if (value instanceof ECGroupElement) {\n                this.value = getEncoded((ECGroupElement) value);\n            } else if (value instanceof ProductGroupElement) {\n                throw new IllegalArgumentException(\"Use Node for ProductGroupElement\");\n            } else {\n                throw new IllegalArgumentException(\"Invalid group\");\n            }\n        }\n\n        /**\n         * Get the byte array used to initialize the Leaf.\n         *\n         * @return A byte array.\n         */\n        public byte[] getValue() {\n            return value;\n        }\n\n        /**\n         * Return a String representation of the underlying byte array.\n         *\n         * @return String representing byte array.\n         */\n        public String getString() {\n            return Util.toString(getValue());\n        }\n\n        /**\n         * Return a BigInteger representation of the underlying byte array.\n         *\n         * @return BigInteger representing byte array.\n         */\n        public BigInteger getBigInteger() {\n            return new BigInteger(value);\n        }\n\n        /**\n         * Get the length of the underlying byte array.\n         *\n         * @return Length of byte array.\n         */\n        @Override\n        public int getLength() {\n            return value.length;\n        }\n\n        /**\n         * Get the length of the whole Leaf object represented as byte array.\n         *\n         * @return Length of Leaf instance representation as byte array.\n         */\n        @Override\n        public int getEncodedLength() {\n            return getLength() + 5;\n        }\n\n        /**\n         * Get the value with corresponding headers.\n         *\n         * @return A byte array.\n         */\n        @Override\n        public byte[] getEncoded() {\n            byte[] ret = new byte[getEncodedLength()];\n            ret[0] = PREFIX;\n            System.arraycopy(ByteTree.parse_from_int(getLength()), 0, ret, 1, 4);\n            System.arraycopy(getValue(), 0, ret, 5, getLength());\n            return ret;\n        }\n\n        /**\n         * Encode the Leaf and write it to out.\n         *\n         * @param out Stream to write the ByteTree description of the Leaf.\n         * @throws IOException When writing to out fails\n         */\n        @Override\n        public void writeEncoded(OutputStream out) throws IOException {\n            out.write(PREFIX);\n            out.write(ByteTree.parse_from_int(getLength()));\n            out.write(getValue());\n        }\n\n        private byte[] getEncoded(ModPGroupElement value) {\n            // Verificatum verifier manual says that enough enough bytes are needed such that the\n            // element fits in. In implementation, it uses BigInteger.toByteArray().length, which is\n            // not always the same (it has a bit for magnitude).\n            byte[] ret = new byte[((ModPGroup) value.getGroup()).getOrder().toByteArray().length];\n            byte[] bvalue = value.getValue().toByteArray();\n            System.arraycopy(bvalue, 0, ret, ret.length - bvalue.length, bvalue.length);\n            return ret;\n        }\n\n        private byte[] getEncoded(ECGroupElement v) {\n            return v.getBytes();\n        }\n\n        /**\n         * @return true\n         */\n        @Override\n        public boolean isLeaf() {\n            return true;\n        }\n\n        /**\n         * Parse an array of bytes into a leaf.\n         *\n         * @param b The array of bytes to parse.\n         * @param offset Starting offset to start parsing from.\n         * @return The Leaf constructed from bytes.\n         */\n        public static Leaf parse(byte[] b, int offset) {\n            int len = parse_int_fullbytes(b, offset);\n            byte[] leafbytes = Arrays.copyOfRange(b, offset + 5, offset + 5 + len);\n            return new Leaf(leafbytes);\n        }\n\n        /**\n         * Parse a data stream into a leaf.\n         *\n         * Parses the given DataInputStream into the leaf. The given stream must be pointed to the\n         * beginning of the definition (with length).\n         *\n         * @param is The given input stream, seeked to the beginning of the Leaf definition.\n         * @return Leaf constructed from the input stream\n         * @throws IOException When reading from the stream fails.\n         */\n        public static Leaf parse(DataInputStream is) throws IOException {\n            int len = read_length(is);\n            byte[] leafbytes = new byte[len];\n            int read = is.read(leafbytes);\n            if (read == -1) {\n                throw new IOException(\"Unexpected end of stream\");\n            } else if (read != len) {\n                throw new IOException(\"Short read\");\n            }\n            return new Leaf(leafbytes);\n        }\n\n        /**\n         * @return \"LEAF\"\n         */\n        @Override\n        public String getPrefix() {\n            return \"LEAF\";\n        }\n\n        /**\n         * Return a human-friendly String representation of the Leaf with indentation.\n         *\n         * @param indent Indentation of the String.\n         */\n        @Override\n        public String toString(int indent) {\n            String spaces = new String(new char[indent]).replace(\"\\0\", \" \");\n            return String.format(\"%s%s %d %s\", spaces, getPrefix(), getLength(),\n                    HexFormat.of().formatHex(getValue()).toUpperCase());\n        }\n    }\n\n    /**\n     * Parse an array of bytes into ByteTree instance. Internally, either Node or Leaf is\n     * constructed depending on the prefix.\n     *\n     * @param b Byte array to be parsed.\n     * @param offset Starting offset of the byte array to start parsing from.\n     * @return A ByteTree representing the byte array.\n     */\n    public static ByteTree parse(byte[] b, int offset) {\n        if (offset < 0) {\n            throw new IllegalArgumentException(\"Offset must be non-negatove\");\n        }\n        if (b == null || b.length <= offset + 5) {\n            throw new IllegalArgumentException(\"Non-existing bytetree\");\n        }\n\n        if (b[offset] == Node.PREFIX) {\n            return Node.parse(b, offset);\n        } else if (b[offset] == Leaf.PREFIX) {\n            return Leaf.parse(b, offset);\n        }\n        throw new IllegalArgumentException(\"Invalid bytetree value\");\n    }\n\n    /**\n     * Parse an input stream into ByteTree instance.\n     *\n     * Depending on the prefix, either Node or Leaf instance is constructed.\n     *\n     * @param is Given input stream to construct the ByteTree instance from, seeked to the\n     *        beginning.\n     * @return A ByteTree representing the byte array.\n     * @throws IOException When reading from the stream fails or if the prefix is invalid.\n     */\n    public static ByteTree parse(DataInputStream is) throws IOException {\n        switch (is.read()) {\n            case -1:\n                throw new IOException(\"Unexpected end of stream\");\n            case Node.PREFIX:\n                return Node.parse(is);\n            case Leaf.PREFIX:\n                return Leaf.parse(is);\n            default:\n                throw new IOException(\"Unexpected token in input stream\");\n        }\n    }\n\n    /**\n     * Short-hand method for {@link #parse(byte[], int)} with {@literal offset} 0.\n     *\n     * @param b Byte array to be parsed\n     * @return A ByteTree representing the byte array.\n     */\n    public static ByteTree parse(byte[] b) {\n        return parse(b, 0);\n    }\n\n    /**\n     * Parse a file at a path into a ByteTree instance.\n     *\n     * Read a file from the given location into a ByteTree instance. In practice, depending on the\n     * prefix, either Node or Leaf is constructed. This method is useful when the files are large\n     * and do not fit into byte arrays.\n     *\n     * @param path Location of file\n     * @return A ByteTree instance representing the byte array.\n     * @throws IOException When the path is invalid, or the corresponding file is not valid ByteTree\n     *         representation.\n     */\n    public static ByteTree parse(Path path) throws IOException {\n        File file = path.toFile();\n        FileInputStream fis = new FileInputStream(file);\n        DataInputStream dis = new DataInputStream(fis);\n        return parse(dis);\n    }\n\n    /**\n     * Parse value from the byte array into integer.\n     *\n     * @param b Byte array to be parsed.\n     * @param offset Starting offset to start parsing at.\n     * @return Integer representation of the byte array.\n     */\n    private static int parse_int(byte[] b, int offset) {\n        if (b.length < offset + 4) {\n            throw new IllegalArgumentException(\"Bytetree int must be in four bytes\");\n        }\n        return ByteBuffer.wrap(b, offset, 4).order(ByteOrder.BIG_ENDIAN).getInt();\n    }\n\n    private static int parse_int_fullbytes(byte[] b, int offset) {\n        int len = parse_int(b, offset + 1);\n        return len;\n    }\n\n    private static byte[] parse_from_int(int v) {\n        return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(v).array();\n    }\n\n    private static int read_length(DataInputStream is) throws IOException {\n        return is.readInt();\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/DataParser.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.audit.shuffle.ByteTree.Leaf;\nimport ee.ivxv.audit.shuffle.ByteTree.Node;\nimport ee.ivxv.common.math.ECGroup;\nimport ee.ivxv.common.math.ECGroupElement;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.ModPGroup;\nimport ee.ivxv.common.math.ModPGroupElement;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\nimport java.math.BigInteger;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.HexFormat;\n\n/**\n * DataParser is a utility class for performing operations with ByteTree objects.\n */\npublic class DataParser {\n    /**\n     * Identifier for groups of integers modulo a value.\n     */\n    public static final String VER_MODP = \"com.verificatum.arithm.ModPGroup\";\n    /**\n     * Identifier for elliptic curve groups.\n     */\n    public static final String VER_EC = \"com.verificatum.arithm.ECqPGroup\";\n\n    /**\n     * Extract Verificatum group information node from Verificatum group description.\n     * <p>\n     * Returns list [{@literal description}, {@literal groupinfo}], where {@literal decription} is a\n     * string description of the group and {@literal groupinfo} is group-specific ByteTree\n     * description of the group.\n     * \n     * @param pgroup String group description\n     * @return Array of ByteTree elements representing group\n     * @throws ShuffleException When unmarshalling fails.\n     */\n    public static ByteTree[] unmarshalGroup(String pgroup) throws ShuffleException {\n        String[] split = pgroup.split(\"::\");\n        if (split.length != 2) {\n            throw new ShuffleException(\"Invalid pgroup description string\");\n        }\n        String groupdesc = split[1];\n        // in some cases, the pgroup description has newline in the end. omit it\n        if (groupdesc.length() % 2 == 1 && groupdesc.charAt(groupdesc.length() - 1) == '\\n') {\n            groupdesc = groupdesc.substring(0, groupdesc.length() - 1);\n        }\n        byte[] groupdescb = HexFormat.of().parseHex(groupdesc);\n        ByteTree G = ByteTree.parse(groupdescb);\n        if (G.isLeaf()) {\n            throw new ShuffleException(\"Invalid pgroup description bytetree\");\n        }\n        ByteTree.Node pginfo = (ByteTree.Node) G;\n        if (pginfo.getLength() != 2) {\n            throw new ShuffleException(\"Invalid marshalled pgroup description\");\n        }\n        return pginfo.getNodes();\n    }\n\n    /**\n     * Extract elliptic curve generator from Verificatum elliptic curve group description leaf.\n     * <p>\n     * Returns the base point corresponding to the elliptic curve. Description leaf should be a\n     * string leaf containing curve name.\n     * \n     * @param groupRoot ByteTree representation of elliptic curve group\n     * @return Elliptic curve base point\n     * @throws ShuffleException When parsing fails.\n     * @throws IllegalArgumentException When unknown elliptic curve group is defined.\n     */\n    public static ECGroupElement parseECGroupGenerator(ByteTree groupRoot)\n            throws ShuffleException, IllegalArgumentException {\n        if (!groupRoot.isLeaf()) {\n            throw new ShuffleException(\"Invalid Elliptic Curve Group description\");\n        }\n        Leaf ecgroupname = (Leaf) groupRoot;\n        ECGroup parsed_group = new ECGroup(ecgroupname.getString());\n        ECGroupElement generator = parsed_group.getBasePoint();\n        return generator;\n    }\n\n    /**\n     * Extract elliptic curve group from Verificatum elliptic curve group description node.\n     * <p>\n     * \n     * @see #parseECGroupGenerator(ByteTree) for input node format.\n     * \n     * @param groupRoot ByteTree representation of elliptic curve group.\n     * @return {@link ee.ivxv.common.math.ECGroup} instance representing the group.\n     * @throws ShuffleException When failing to parse the node\n     */\n    public static ECGroup parseECGroup(ByteTree groupRoot)\n            throws ShuffleException, IllegalArgumentException {\n        return (ECGroup) parseECGroupGenerator(groupRoot).getGroup();\n    }\n\n    /**\n     * Extract generator for group of integers modulo a safe prime from Verificatum group\n     * description node.\n     * <p>\n     * Expect as input a node [{@literal p}, {@literal q}, {@literal g}], where p is the modulus of\n     * the group, q the order of the group and g the generator of the group.\n     * \n     * @param groupRoot ByteTree representation of group of integers modulo a prime.\n     * @return Group generator\n     * @throws ShuffleException When failing to parse the node.\n     */\n    public static ModPGroupElement parseModPGroupGenerator(ByteTree groupRoot)\n            throws ShuffleException {\n        if (groupRoot.isLeaf()) {\n            throw new ShuffleException(\"Invalid ModPGroup description\");\n        }\n        ByteTree.Node marshalled = (ByteTree.Node) groupRoot;\n        if (marshalled.getLength() != 4) {\n            throw new ShuffleException(\"Invalid ModPGroup description length\");\n        }\n        ByteTree pnode = marshalled.getNodes()[0];\n        if (!pnode.isLeaf()) {\n            throw new ShuffleException(\"Invalid modulus leaf\");\n        }\n        BigInteger p = ((Leaf) pnode).getBigInteger();\n        ByteTree gnode = marshalled.getNodes()[2];\n        if (!gnode.isLeaf()) {\n            throw new ShuffleException(\"Invalid generator leaf\");\n        }\n        BigInteger g = ((Leaf) gnode).getBigInteger();\n        ModPGroup parsed_group = new ModPGroup(p);\n        ModPGroupElement generator = new ModPGroupElement(parsed_group, g);\n        return generator;\n    }\n\n    /**\n     * Extract group of integers modulo a prime from Verificatum group description node.\n     * <p>\n     * \n     * @see #parseModPGroupGenerator(ByteTree).\n     * \n     * @param groupRoot ByteTree representation of group of integers modulo a prime.\n     * @return {@link ee.ivxv.common.math.ModPGroup} representing a group\n     * @throws ShuffleException When failing to parse the node\n     */\n    public static ModPGroup parseModPGroup(ByteTree groupRoot) throws ShuffleException {\n        return (ModPGroup) parseModPGroupGenerator(groupRoot).getGroup();\n    }\n\n    /**\n     * Parse group generator from Verificatum group description node.\n     * <p>\n     * \n     * @see #parseModPGroupGenerator(ByteTree)\n     * @see #parseECGroupGenerator(ByteTree)\n     * \n     * @param pgroup ByteTree representation of group.\n     * @return {@link ee.ivxv.common.math.GroupElement} representing the group generator.\n     * @throws ShuffleException When failing to parse the node\n     */\n    public static GroupElement parseGroupGenerator(String pgroup) throws ShuffleException {\n        ByteTree[] nodes = unmarshalGroup(pgroup);\n        if (!nodes[0].isLeaf()) {\n            throw new ShuffleException(\"Invalid marshalled pgroup description string\");\n        }\n        Leaf descstring = (Leaf) nodes[0];\n        GroupElement generator = null;\n        if (descstring.getString().equals(VER_MODP)) {\n            generator = parseModPGroupGenerator(nodes[1]);\n        } else if (descstring.getString().equals(VER_EC)) {\n            generator = parseECGroupGenerator(nodes[1]);\n        } else {\n            throw new ShuffleException(\"Invalid group\");\n        }\n        return generator;\n    }\n\n    /**\n     * Get the ByteTree node as a group element.\n     * <p>\n     * The Verificatum element representation node bt must be a correct representation of the group\n     * element. See descriptions of specific group methods for more detailed structures.\n     * <p>\n     * \n     * @see #getAsElement(ECGroup, ByteTree)\n     * @see #getAsElement(ModPGroup, ByteTree)\n     * @see #getAsElement(ProductGroup, ByteTree)\n     * \n     * @param group Group where the element belongs\n     * @param bt Element representation in Verificatum ByteTree format\n     * @return Group element instance\n     * @throws IllegalArgumentException when parsing fails\n     */\n    public static GroupElement getAsElement(Group group, ByteTree bt)\n            throws IllegalArgumentException {\n        if (group instanceof ModPGroup) {\n            return getAsElement((ModPGroup) group, bt);\n        } else if (group instanceof ECGroup) {\n            return getAsElement((ECGroup) group, bt);\n        } else if (group instanceof ProductGroup) {\n            return getAsElement((ProductGroup) group, bt);\n        } else {\n            throw new IllegalArgumentException(\"Invalid group\");\n        }\n    }\n\n    /**\n     * Get the indexed node from the ByteTree nodes as a group element\n     * <p>\n     * Indexed version of {@link #getAsElement(Group, ByteTree)}.\n     * <p>\n     * \n     * @see #getAsElement(Group, ByteTree).\n     * \n     * @param group Group where the element belongs\n     * @param bt Elements node representation in Verificatum ByteTree format\n     * @param index Element index\n     * @return Group element instance\n     * @throws IllegalArgumentException when parsing fails\n     */\n    public static GroupElement getAsElement(Group group, ByteTree bt, int index)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting node\");\n        }\n        Node node = (Node) bt;\n        return getAsElement(group, node.getNodes()[index]);\n    }\n\n    /**\n     * Get the ByteTree leaf as ModPGroupElement\n     * <p>\n     * We assume that the leaf is an integer. It is represented as an element in group of modulo a\n     * prime.\n     * \n     * @param group Group where the element belongs.\n     * @param bt Integer leaf\n     * @return {@link ee.ivxv.common.math.ModPGroupElement} instance\n     * @throws IllegalArgumentException When parsing fails\n     */\n    public static ModPGroupElement getAsElement(ModPGroup group, ByteTree bt)\n            throws IllegalArgumentException {\n        if (!bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting leaf\");\n        }\n        Leaf leaf = (Leaf) bt;\n        BigInteger val = leaf.getBigInteger();\n        return new ModPGroupElement(group, val);\n    }\n\n    /**\n     * Get the ByteTree node as an ECGroupElement.\n     *\n     * @param group Group where the element belongs\n     * @param bt Node node\n     * @return {@link ee.ivxv.common.math.ECGroupElement} instance\n     */\n    public static ECGroupElement getAsElement(ECGroup group, ByteTree bt) {\n        Node point = (Node) bt;\n        Leaf x = (Leaf) point.getNodes()[0];\n        Leaf y = (Leaf) point.getNodes()[1];\n        return new ECGroupElement(group, x.getBigInteger(), y.getBigInteger());\n    }\n\n    /**\n     * Get the ByteTree node as an ProductGroupElement.\n     * <p>\n     * We assume that the node is an array of group element nodes. Every node is parsed as a\n     * suitable group element from the product group.\n     * \n     * @param group Product group instance where the element belongs\n     * @param bt Node representing the element\n     * @return {@link ee.ivxv.common.math.ProductGroupElement} instance.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static ProductGroupElement getAsElement(ProductGroup group, ByteTree bt)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting node\");\n        }\n        Group[] groups = ((ProductGroup) group).getGroups();\n        GroupElement[] el = new GroupElement[groups.length];\n        for (int i = 0; i < groups.length; i++) {\n            el[i] = getAsElement(groups[i], bt, i);\n        }\n        return new ProductGroupElement(group, el);\n    }\n\n    /**\n     * Get the ByteTree node as an array of group elements.\n     * <p>\n     * We assume that the node is an array of group element representations. See specific group\n     * element getter methods for exact structures.\n     * <p>\n     * \n     * @see #getAsElement(Group, ByteTree).\n     * \n     * @param group Group which the elements are part of.\n     * @param bt Node which is an array of elements.\n     * @return An array of {@link ee.ivxv.common.math.GroupElement} instances.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static GroupElement[] getAsElementArray(Group group, ByteTree bt)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting root node\");\n        }\n        if (group instanceof ProductGroup) {\n            // handle special case\n            return getAsElementArray((ProductGroup) group, bt);\n        }\n        Node rootnode = (Node) bt;\n        ByteTree[] nodes = rootnode.getNodes();\n        GroupElement[] ret = new GroupElement[nodes.length];\n        for (int i = 0; i < nodes.length; i++) {\n            ret[i] = getAsElement(group, nodes[i]);\n        }\n        return ret;\n    }\n\n    /**\n     * Get the ByteTree node as an array of product group elements.\n     * <p>\n     * We assume that the node is an array of group element representations. See specific group\n     * element getter methods for exact structures.\n     * <p>\n     * \n     * @see #getAsElement(ProductGroup, ByteTree)\n     * \n     * @param group Group which the elements are part of.\n     * @param bt Node which is an array of elements.\n     * @return An array of {@link ee.ivxv.common.math.GroupElement} instances.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static GroupElement[] getAsElementArray(ProductGroup group, ByteTree bt)\n            throws IllegalArgumentException {\n        Node rootnode = (Node) bt;\n        Group[] groups = group.getGroups();\n        GroupElement[][] sub = new GroupElement[groups.length][];\n        for (int i = 0; i < groups.length; i++) {\n            sub[i] = getAsElementArray(groups[i], rootnode.getNodes()[i]);\n        }\n        GroupElement[] ret = new GroupElement[sub[0].length];\n        for (int i = 0; i < ret.length; i++) {\n            GroupElement[] cons = new GroupElement[groups.length];\n            for (int j = 0; j < groups.length; j++) {\n                cons[j] = sub[j][i];\n            }\n            ret[i] = new ProductGroupElement(group, cons);\n        }\n        return ret;\n    }\n\n    /**\n     * Get the indexed ByteTree node as an array of group elements.\n     * <p>\n     * Indexed version of {@link #getAsElementArray(Group, ByteTree)}.\n     * <p>\n     * \n     * @see #getAsElementArray(Group, ByteTree)\n     * \n     * @param group Group which the elements are part of.\n     * @param bt Node which is an array of elements.\n     * @param index Index of the node to use.\n     * @return An array of {@link ee.ivxv.common.math.GroupElement} instances.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static GroupElement[] getAsElementArray(Group group, ByteTree bt, int index)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting node\");\n        }\n        Node node = (Node) bt;\n        return getAsElementArray(group, node.getNodes()[index]);\n    }\n\n    /**\n     * Get the indexed ByteTree node as an integer.\n     * <p>\n     * The index element of the node must be an integer leaf.\n     * \n     * @param bt Node\n     * @param index Index of the element\n     * @return Integer value\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static BigInteger getAsInteger(ByteTree bt, int index) throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Root should be node\");\n        }\n        ByteTree[] nodes = ((Node) bt).getNodes();\n        if (!nodes[index].isLeaf()) {\n            throw new IllegalArgumentException(\"Should be leaf\");\n        }\n        Leaf leaf = (Leaf) nodes[index];\n        BigInteger res = leaf.getBigInteger();\n        return res;\n    }\n\n    /**\n     * Get the indexed ByteTree node as an array of integers.\n     * <p>\n     * The index element of the node must be a node consisting of integer leafs.\n     * \n     * @param bt Node\n     * @param index Index of the element.\n     * @return Array of integer values.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static BigInteger[] getAsIntegerArray(ByteTree bt, int index)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Root should be node\");\n        }\n        ByteTree[] nodes = ((Node) bt).getNodes();\n        if (nodes[index].isLeaf()) {\n            throw new IllegalArgumentException(\"Should be node\");\n        }\n        ByteTree[] intnodes = ((Node) nodes[index]).getNodes();\n        BigInteger[] res = new BigInteger[intnodes.length];\n        for (int i = 0; i < intnodes.length; i++) {\n            if (!intnodes[i].isLeaf()) {\n                throw new IllegalArgumentException(\"Should be leaf\");\n            }\n            res[i] = ((Leaf) intnodes[i]).getBigInteger();\n        }\n        return res;\n    }\n\n    /**\n     * Get the indexed ByteTree node as a group scalar.\n     * <p>\n     * Indexed version of {@link #getAsScalar(Group, ByteTree, int)}.\n     * <p>\n     *\n     * @see #getAsScalar(Group, ByteTree, int)\n     *\n     * @param group Group where the scalar belongs.\n     * @param bt Scalar node representation in Verificatum ByteTree format.\n     * @param index Index of the node to use.\n     * @return An array of {@link BigInteger} instances.\n     * @throws IllegalArgumentException When parsing fails.\n     */\n    public static BigInteger[] getAsScalar(Group group, ByteTree bt, int index)\n            throws IllegalArgumentException {\n        if (bt.isLeaf()) {\n            throw new IllegalArgumentException(\"Expecting node\");\n        }\n\n        if (group instanceof ProductGroup) {\n            Node node = (Node) ((Node) bt).getNodes()[index];\n            Group[] groups = ((ProductGroup) group).getGroups();\n            BigInteger[] scalars = new BigInteger[groups.length];\n\n            for (int i = 0; i < groups.length; i++) {\n                Leaf leaf = (Leaf) node.getNodes()[i];\n                scalars[i] = leaf.getBigInteger();\n            }\n\n            return scalars;\n        } else {\n            throw new IllegalArgumentException(\"Invalid group\");\n        }\n    }\n\n    /**\n     * Get the hash corresponding to string representation.\n     * <p>\n     * The hash name is delegated to {@link java.security.MessageDigest}.\n     * \n     * @param hashname Hash name.\n     * @return Hash instance.\n     * @throws IllegalArgumentException When can not find hash with hashname.\n     */\n    public static MessageDigest getHash(String hashname) throws IllegalArgumentException {\n        MessageDigest md;\n        try {\n            md = MessageDigest.getInstance(hashname);\n\n        } catch (NoSuchAlgorithmException e) {\n            throw new IllegalArgumentException(\n                    String.format(\"Hash function %s not supported\", hashname));\n        }\n        return md;\n    }\n\n    /**\n     * Convert GroupElement array to a ProductGroupElement instance.\n     * <p>\n     * Currently only ProductGroupElement instances are supported.\n     * \n     * @param elements\n     * @return\n     * @throws IllegalArgumentException When converting fails\n     */\n    public static ProductGroupElement toArray(GroupElement[] elements)\n            throws IllegalArgumentException {\n        ProductGroupElement[] casted = new ProductGroupElement[elements.length];\n        for (int i = 0; i < casted.length; i++) {\n            if (!(elements[i] instanceof ProductGroupElement)) {\n                throw new IllegalArgumentException(\"Invalid group\");\n            }\n            casted[i] = (ProductGroupElement) elements[i];\n        }\n        return toArray(casted);\n    }\n\n    /**\n     * Convert ProductGroupElement array to a ProductGroupElement instance.\n     * <p>\n     * \n     * @see #toArray(GroupElement[]).\n     * \n     * @param elements\n     * @return\n     */\n    public static ProductGroupElement toArray(ProductGroupElement[] elements) {\n        ProductGroupElement r = toArray_first(elements[0], elements.length);\n        for (int i = 0; i < elements.length; i++) {\n            toArray_second(r, elements[i], i);\n        }\n        return r;\n    }\n\n    private static ProductGroupElement toArray_first(ProductGroupElement in, int N) {\n        GroupElement[] gs = in.getElements();\n        GroupElement[] ret = new GroupElement[gs.length];\n        Group[] retgs = new Group[gs.length];\n        for (int i = 0; i < gs.length; i++) {\n            if (gs[i] instanceof ProductGroupElement) {\n                ret[i] = toArray_first((ProductGroupElement) gs[i], N);\n            } else {\n                ret[i] = new ProductGroupElement(new ProductGroup(gs[i].getGroup(), N));\n            }\n            retgs[i] = ret[i].getGroup();\n        }\n        return new ProductGroupElement(new ProductGroup(retgs), ret);\n    }\n\n    private static void toArray_second(ProductGroupElement out, ProductGroupElement in, int N) {\n        GroupElement[] inel = in.getElements();\n        GroupElement[] outel = out.getElements();\n        for (int i = 0; i < inel.length; i++) {\n            if (inel[i] instanceof ProductGroupElement) {\n                toArray_second((ProductGroupElement) outel[i], (ProductGroupElement) inel[i], N);\n            } else {\n                ((ProductGroupElement) outel[i]).getElements()[N] = inel[i];\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/PRNG.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.common.util.Util;\nimport java.security.MessageDigest;\n\n/**\n * Class PRNG implements pseudo-random number generator as defined in Verificatum independent\n * verifier implementation description.\n */\npublic class PRNG {\n    private MessageDigest cleanhash;\n    private byte[] buf;\n    private int it;\n    private int bufp;\n    private int digestLen;\n\n    /**\n     * Initialize PRNG using a hash function and a seed.\n     * \n     * @param hashname Defined hash function.\n     * @param seed Seed to initialize PRNG with.\n     */\n    PRNG(String hashname, byte[] seed) {\n        this.cleanhash = init_hash(hashname, seed);\n        this.digestLen = cleanhash.getDigestLength();\n        this.buf = new byte[digestLen];\n        this.it = 0;\n        this.bufp = digestLen;\n    }\n\n    /**\n     * Fill the output buffer with bytes from the PRNG.\n     * \n     * @param out Output buffer to fill.\n     */\n    public void read(byte[] out) {\n        int read = 0;\n        int to_read;\n        int len = out.length;\n        while (read < len) {\n            refill();\n            to_read = Math.min(len - read, digestLen - bufp);\n            System.arraycopy(buf, bufp, out, read, to_read);\n            bufp = bufp + to_read;\n            read += to_read;\n        }\n    }\n\n    private void refill() {\n        if (bufp < digestLen) {\n            return;\n        }\n        MessageDigest h;\n        try {\n            h = (MessageDigest) cleanhash.clone();\n        } catch (CloneNotSupportedException e) {\n            // already checked\n            return;\n        }\n        buf = h.digest(Util.toBytes(it));\n        bufp = 0;\n        it += 1;\n    }\n\n    static MessageDigest init_hash(String hashname, byte[] seed) {\n        MessageDigest md = DataParser.getHash(hashname);\n        try {\n            md.clone();\n        } catch (CloneNotSupportedException e) {\n            throw new IllegalArgumentException(\n                    String.format(\"Hash function %s support is incomplete\", hashname));\n        }\n        if (seed != null) {\n            md.update(seed);\n        }\n        return md;\n    }\n\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ProtocolInformation.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.SAXException;\n\n/**\n * ProtocolInformation represents Verificatum protocol information file.\n */\npublic class ProtocolInformation {\n    private String version, sid, name, pgroup, prg, rohash, auxsid, type;\n    private int statdist, width, ebitlenro, vbitlenro, keywidth;\n    private Group pgroup_parsed;\n    private GroupElement generator_parsed;\n\n    private static final String default_auxsid = \"default\";\n    private static final String default_type = \"shuffling\";\n    private static final int default_keywidth = 5;\n\n    /**\n     * Initialize ProtocolInformation from required values.\n     * \n     * @param version\n     * @param sid\n     * @param name\n     * @param pgroup\n     * @param keywidth\n     * @param vbitlenro\n     * @param ebitlenro\n     * @param prg\n     * @param rohash\n     * @param width\n     * @param statdist\n     */\n    public ProtocolInformation(String version, String sid, String name, String pgroup, int keywidth,\n            int vbitlenro, int ebitlenro, String prg, String rohash, int width, int statdist) {\n        this.version = version;\n        this.sid = sid;\n        this.name = name;\n        this.pgroup = pgroup;\n        this.keywidth = keywidth;\n        this.vbitlenro = vbitlenro;\n        this.ebitlenro = ebitlenro;\n        this.prg = prg;\n        this.rohash = rohash;\n        this.width = width;\n        this.statdist = statdist;\n        this.auxsid = default_auxsid;\n        this.type = default_type;\n    }\n\n    /**\n     * Initialize the ProtocolInformation from a file.\n     * \n     * @param protinfo Protocol information file\n     * @throws ShuffleException When parsing fails\n     */\n    public ProtocolInformation(Path protinfo) throws ShuffleException {\n        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();\n        DocumentBuilder db;\n        Document dom;\n        try {\n            db = dbf.newDocumentBuilder();\n        } catch (ParserConfigurationException e) {\n            // there should be a default parser\n            throw new RuntimeException(\"XML parser not configured\", e);\n        }\n        try {\n            dom = db.parse(protinfo.toFile());\n        } catch (SAXException e) {\n            throw new IllegalArgumentException(\"Invalid Protocol Information file\", e);\n        } catch (IOException e) {\n            throw new IllegalArgumentException(\"Error while reading Protocol Information file\", e);\n        }\n        Element root = dom.getDocumentElement();\n        root.normalize();\n        if (!root.getNodeName().equals(\"protocol\")) {\n            throw new IllegalArgumentException(\"Root node must be 'protocol'\");\n        }\n        if (root.getNodeType() != Node.ELEMENT_NODE) {\n            throw new IllegalArgumentException(\"Protocol node must be an element node\");\n        }\n        Element rootel = (Element) root;\n        this.version = get_element(rootel, \"version\");\n        this.sid = get_element(rootel, \"sid\");\n        this.name = get_element(rootel, \"name\");\n        this.pgroup = get_element(rootel, \"pgroup\");\n        this.generator_parsed = DataParser.parseGroupGenerator(this.pgroup);\n        this.pgroup_parsed = this.generator_parsed.getGroup();\n        this.keywidth = Integer.parseInt(get_element(rootel, \"keywidth\"));\n        if (this.keywidth != default_keywidth) {\n            throw new IllegalArgumentException(\"Invalid keywidth\");\n        }\n        this.vbitlenro = Integer.parseInt(get_element(rootel, \"vbitlenro\"));\n        this.ebitlenro = Integer.parseInt(get_element(rootel, \"ebitlenro\"));\n        this.prg = get_element(rootel, \"prg\");\n        this.rohash = get_element(rootel, \"rohash\");\n        this.width = Integer.parseInt(get_element(rootel, \"width\"));\n        this.statdist = Integer.parseInt(get_element(rootel, \"statdist\"));\n        this.auxsid = default_auxsid;\n        this.type = default_type;\n        String corr = get_element(rootel, \"corr\");\n        if (!corr.equals(\"noninteractive\")) {\n            throw new IllegalArgumentException(\"Only non-interactive protocol is supported\");\n        }\n    }\n\n    private static String get_element(Element node, String name) {\n        NodeList els = node.getElementsByTagName(name);\n        String val = null;\n        if (els.getLength() < 1) {\n            throw new IllegalArgumentException(String.format(\"Element '%s' missing\", name));\n        }\n        if (els.getLength() > 1) {\n            for (int i = 0; i < els.getLength(); i++) {\n                if (els.item(i).getParentNode().equals(node)) {\n                    val = els.item(i).getTextContent();\n                }\n            }\n            if (val == null) {\n                throw new IllegalArgumentException(String.format(\"Element '%s' not found\", name));\n            }\n        } else {\n            val = els.item(0).getTextContent();\n        }\n        return val;\n    }\n\n    public String get_version() {\n        return version;\n    }\n\n    public String get_sid() {\n        return sid;\n    }\n\n    public String get_auxsid() {\n        return auxsid;\n    }\n\n    public String get_name() {\n        return name;\n    }\n\n    public String get_pgroup() {\n        return pgroup;\n    }\n\n    public Group get_parsed_pgroup() {\n        return pgroup_parsed;\n    }\n\n    public GroupElement get_parsed_generator() {\n        return generator_parsed;\n    }\n\n    public int get_keywidth() {\n        return keywidth;\n    }\n\n    public int get_vbitlenro() {\n        return vbitlenro;\n    }\n\n    public int get_ebitlenro() {\n        return ebitlenro;\n    }\n\n    public String get_prg() {\n        return prg;\n    }\n\n    public String get_rohash() {\n        return rohash;\n    }\n\n    public int get_width() {\n        return width;\n    }\n\n    public int get_statdist() {\n        return statdist;\n    }\n\n    public String get_type() {\n        return type;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/RO.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.security.MessageDigest;\n\n/**\n * Implements Random Oracle as defined in Verificatum manual for implementing independent verifier.\n */\npublic class RO extends OutputStream {\n    private String hashname;\n    private MessageDigest hash;\n    private byte[] seed;\n    private int amount;\n\n    /**\n     * Initialize RO using a hashname and a seed.\n     * \n     * @param hashname Hash function to use.\n     * @param seed Seed bytes.\n     */\n    public RO(String hashname, byte[] seed) {\n        this.hashname = hashname;\n        this.flush();\n        this.seed = seed.clone();\n    }\n\n    /**\n     * Initialize RO instance using hashname.\n     * \n     * In this mode, the user has to provide the amount to be read and the seed to seed this\n     * instance separately.\n     * \n     * @param hashname Hash function to use.\n     */\n    public RO(String hashname) {\n        this.hashname = hashname;\n        this.flush();\n    }\n\n    /**\n     * Reset the RO instance\n     * \n     */\n    public void flush() {\n        this.hash = PRNG.init_hash(hashname, null);\n        this.amount = 0;\n    }\n\n    /**\n     * Set the expected amount to be read.\n     * \n     * As the amount is written to the digest instance, then it must be set before updating the RO\n     * instance.\n     * \n     * @param amount\n     */\n    public void setAmount(int amount) {\n        this.amount = amount;\n        hash.update(Util.toBytes(amount));\n    }\n\n\n\n    private void readOut(byte[] out) {\n        if ((this.amount + 7) / 8 != out.length) {\n            throw new IllegalArgumentException(\n                    \"Output buffer length does not correspond to requested read amount\");\n        }\n        PRNG p = new PRNG(hashname, hash.digest());\n        p.read(out);\n        if (amount % 8 != 0) {\n            out[0] &= (1 << (amount % 8)) - 1;\n        }\n    }\n\n    /**\n     * Fill the output buffer with bytes from the RO.\n     * \n     * If the amount field is 0, then it is assumed that it has been set by a call to\n     * {@link #setAmount(int)}. If the seed was given during the initialization, then it is added to\n     * the input.\n     * \n     * @param out Output buffer to fill.\n     * @param amount Number of bits to fill\n     */\n    public void read(byte[] out, int amount) {\n        if (this.amount == 0) {\n            setAmount(amount);\n        }\n        if (this.seed != null) {\n            try {\n                write(seed);\n            } catch (IOException e) {\n                // checked exception\n            }\n        }\n        readOut(out);\n    }\n\n    /**\n     * Update the RO with some input.\n     * \n     * The expected amount to be read must be set.\n     * \n     * @param b The data\n     * @throws IOException When amount to be read is not set.\n     */\n    @Override\n    public void write(int b) throws IOException {\n        write(new byte[] {(byte) b});\n    }\n\n    /**\n     * Update the RO with some input.\n     * \n     * The expected amount to be read must be set.\n     * \n     * @param b The data\n     * @param off Offset of the data\n     * @param len Length of the data\n     * @throws IOException when amount to be read is not set.\n     */\n    @Override\n    public void write(byte[] b, int off, int len) throws IOException {\n        if (amount == 0) {\n            throw new IllegalArgumentException(\"Amount not set\");\n        }\n        hash.update(b, off, len);\n    }\n\n    /**\n     * Update the RO with some input.\n     * \n     * The expected amount to be read must be set.\n     * \n     * @param in A byte array to seed into the RO instance.\n     * @throws IOException when amount to be read is not set.\n     */\n    @Override\n    public void write(byte[] b) throws IOException {\n        write(b, 0, b.length);\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ShuffleConsole.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.audit.Msg;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.i18n.Translatable;\nimport ee.ivxv.common.util.I18nConsole;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class ShuffleConsole {\n    private final I18nConsole console;\n\n    public ShuffleConsole(I18nConsole console) {\n        this.console = console;\n    }\n\n    public void enter(ShuffleStep step) {\n        int totalSteps = step.parent != null ? step.parent.subSteps.size() : 0;\n        int thisStep = step.parent != null ? step.parent.nextSubStep() : 0;\n        String stepstr = \"\";\n        if (totalSteps > 0) {\n            stepstr = String.format(\"(%d/%d): \", thisStep, totalSteps);\n        }\n        if (step.i18msg != null) {\n            console.println(Msg.m_shuffle_step, step.getDepth(), stepstr, step.i18msg);\n        } else if (step.msg != null) {\n            console.println(Msg.m_shuffle_step, step.getDepth(), stepstr, step.msg);\n        } else {\n            console.println(Msg.m_shuffle_step, step.getDepth(), stepstr, \"\");\n        }\n    }\n\n    public Progress enter(ShuffleStep step, long length) {\n        enter(step);\n        return console.startProgress(length, true);\n    }\n\n    public static enum ShuffleStep {\n        READ(Msg.m_shuffle_read), //\n        READ_PROT_INFO(READ, Msg.m_shuffle_read_prot_info), //\n        READ_PUBKEY(READ, Msg.m_shuffle_read_pubkey), //\n        READ_PC(READ, Msg.m_shuffle_read_pc), READ_POSC(READ, Msg.m_shuffle_read_posc), //\n        READ_POSR(READ, Msg.m_shuffle_read_posr), //\n        READ_CIPHS(READ, Msg.m_shuffle_read_ciphs), //\n        READ_SHUFFLED(READ, Msg.m_shuffle_read_shuffled),\n\n        VERIFY(Msg.m_shuffle_verify), //\n        VERIFY_PARAMS(VERIFY, Msg.m_shuffle_verify_params), //\n        VERIFY_NI(VERIFY, Msg.m_shuffle_verify_ni), //\n        VERIFY_PERM(VERIFY, Msg.m_shuffle_verify_permutation), //\n        VERIFY_RERAND(VERIFY, Msg.m_shuffle_verify_rerandomisation);\n\n        final String msg;\n        final Translatable i18msg;\n        final ShuffleStep parent;\n        final List<ShuffleStep> subSteps = new ArrayList<ShuffleStep>();\n        final AtomicInteger completed = new AtomicInteger();\n\n        ShuffleStep(Translatable msg) {\n            this.msg = null;\n            this.i18msg = msg;\n            this.parent = null;\n        }\n\n        ShuffleStep(String msg) {\n            this.msg = msg;\n            this.i18msg = null;\n            this.parent = null;\n        }\n\n        ShuffleStep(ShuffleStep parent, Translatable msg) {\n            this.msg = null;\n            this.i18msg = msg;\n            this.parent = parent;\n            parent.appendSubStep(this);\n        }\n\n        int getDepth() {\n            int i = 0;\n            ShuffleStep next = this.parent;\n            while (next != null) {\n                next = next.parent;\n                i++;\n            }\n            return i;\n        }\n\n        void appendSubStep(ShuffleStep substep) {\n            subSteps.add(substep);\n        }\n\n        int subStepCount() {\n            return subSteps.size();\n        }\n\n        int nextSubStep() {\n            return this.completed.incrementAndGet();\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ShuffleException.java",
    "content": "package ee.ivxv.audit.shuffle;\n\n@SuppressWarnings(\"serial\")\npublic class ShuffleException extends Exception {\n    public ShuffleException(Throwable t) {\n        super(t);\n    }\n\n    public ShuffleException(String msg) {\n        super(msg);\n    }\n\n    public ShuffleException(String msg, Throwable t) {\n        super(msg, t);\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ShuffleParameters.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\n\npublic class ShuffleParameters {\n    private String version, auxsid, type;\n    int width;\n\n    /**\n     * Initialize the shuffle parameters from values.\n     * \n     * @param version\n     * @param auxsid\n     * @param width\n     * @param type\n     */\n    ShuffleParameters(String version, String auxsid, int width, String type) {\n        this.version = version;\n        this.auxsid = auxsid;\n        this.width = width;\n        this.type = type;\n    }\n\n    /**\n     * Initialize ShuffleParameters using a proof directory path. The corresponding files are\n     * assumed to be at their default locations (stored by Verificatum).\n     * \n     * @param proofdir Proof directory\n     * @throws IOException If reading parameter file fails\n     */\n    ShuffleParameters(Path proofdir) throws IOException {\n        Path versionpath = Paths.get(proofdir.toString(), \"version\");\n        Path auxsidpath = Paths.get(proofdir.toString(), \"auxsid\");\n        Path widthpath = Paths.get(proofdir.toString(), \"width\");\n        Path typepath = Paths.get(proofdir.toString(), \"type\");\n        this.version = read_parameters(versionpath);\n        this.auxsid = read_parameters(auxsidpath);\n        this.width = Integer.parseInt(read_parameters(widthpath));\n        this.type = read_parameters(typepath);\n\n    }\n\n    private static String read_parameters(Path loc) throws IOException {\n        List<String> l = Files.readAllLines(loc);\n        if (l.size() != 1) {\n            throw new IllegalArgumentException(\"Parameters file must have single line\");\n        }\n        return l.get(0);\n    }\n\n    public String get_version() {\n        return version;\n    }\n\n    public String get_auxsid() {\n        return auxsid;\n    }\n\n    public int get_width() {\n        return width;\n    }\n\n    public String get_type() {\n        return type;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ShuffleProof.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.audit.shuffle.ShuffleConsole.ShuffleStep;\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\n/**\n * Class for holding variables for non-interactive shuffle proof.\n */\npublic class ShuffleProof {\n    private final ProtocolInformation prot;\n    private final PermutationCommitment pc;\n    private final PoSCommitment posc;\n    private final PoSReply posr;\n    private final GroupElement[] ciphs, shuffled;\n    private final GroupElement pk;\n\n    /**\n     * Default filename of ciphertexts in Verificatum shuffle proof.\n     */\n    public static final String CIPHERTEXTS_PATH = \"Ciphertexts.bt\";\n    /**\n     * Default filename of shuffled ciphertexts in Verificatum shuffle proof.\n     */\n    public static final String SHUFFLED_CIPHERTEXTS_PATH = \"ShuffledCiphertexts.bt\";\n    /**\n     * Default filename of public key in Verificatum shuffle proof.\n     */\n    public static final String PUBLICKEY_PATH = \"FullPublicKey.bt\";\n    /**\n     * Default directory name for proofs in Verificatum shuffle proof.\n     */\n    public static final String PROOFS_PATH = \"proofs\";\n    /**\n     * Default filename of permutation commitment in Verificatum shuffle proof.\n     */\n    public static final String PC_PATH = \"PermutationCommitment01.bt\";\n    /**\n     * Default filename of shuffle commitment in Verificatum shuffle proof.\n     */\n    public static final String POSC_PATH = \"PoSCommitment01.bt\";\n    /**\n     * Default filename of shuffle proof reply in Verificatum shuffle proof.\n     */\n    public static final String POSR_PATH = \"PoSReply01.bt\";\n\n    /**\n     * Initialize the proof from components. Ciphertexts, shuffled ciphertexts and public key are\n     * represented as corresponding group elements.\n     * \n     * @param prot\n     * @param pc\n     * @param posc\n     * @param posr\n     * @param ciphs\n     * @param shuffled\n     * @param pk\n     */\n    public ShuffleProof(ProtocolInformation prot, PermutationCommitment pc, PoSCommitment posc,\n            PoSReply posr, GroupElement[] ciphs, GroupElement[] shuffled, GroupElement pk) {\n        this.prot = prot;\n        this.pc = pc;\n        this.posc = posc;\n        this.posr = posr;\n        this.ciphs = ciphs;\n        this.shuffled = shuffled;\n        this.pk = pk;\n    }\n\n    /**\n     * Initialize the proof from components.\n     * \n     * @param prot\n     * @param pc\n     * @param posc\n     * @param posr\n     * @param ciphs\n     * @param shuffled\n     * @param pk\n     */\n    public ShuffleProof(ProtocolInformation prot, PermutationCommitment pc, PoSCommitment posc,\n            PoSReply posr, ElGamalCiphertext[] ciphs, ElGamalCiphertext[] shuffled,\n            ElGamalPublicKey pk) {\n        if (ciphs.length != shuffled.length) {\n            throw new IllegalArgumentException(\"Ciphertext and shuffled length does not match\");\n        }\n        GroupElement[] cge = new GroupElement[ciphs.length];\n        GroupElement[] scge = new GroupElement[shuffled.length];\n        for (int i = 0; i < ciphs.length; i++) {\n            cge[i] = ciphs[i].getAsProductGroupElement();\n            scge[i] = shuffled[i].getAsProductGroupElement();\n        }\n        this.prot = prot;\n        this.pc = pc;\n        this.posc = posc;\n        this.posr = posr;\n        this.ciphs = cge;\n        this.shuffled = scge;\n        this.pk = pk.getAsProductGroupElement();\n    }\n\n    /**\n     * Initialize the proof from protocol information path and proof directory path.\n     * <p>\n     * Parses the files and constructs the components.\n     * \n     * @param protpath\n     * @param proofdir\n     * @throws IOException\n     * @throws ShuffleException\n     */\n    public ShuffleProof(Path protpath, Path proofdir, ShuffleConsole console) throws IOException, ShuffleException {\n        console.enter(ShuffleStep.READ);\n        console.enter(ShuffleStep.READ_PROT_INFO);\n        prot = new ProtocolInformation(protpath);\n\n        console.enter(ShuffleStep.READ_PC);\n        Path pcpath = Paths.get(proofdir.toString(), PROOFS_PATH, PC_PATH);\n        pc = new PermutationCommitment(prot, pcpath);\n\n        console.enter(ShuffleStep.READ_POSC);\n        Path poscpath = Paths.get(proofdir.toString(), PROOFS_PATH, POSC_PATH);\n        posc = new PoSCommitment(prot, poscpath);\n\n        console.enter(ShuffleStep.READ_POSR);\n        Path posrpath = Paths.get(proofdir.toString(), PROOFS_PATH, POSR_PATH);\n        posr = new PoSReply(prot, posrpath);\n\n        console.enter(ShuffleStep.READ_PUBKEY);\n        Path pkpath = Paths.get(proofdir.toString(), PUBLICKEY_PATH);\n        ByteTree pkbt = ByteTree.parse(pkpath);\n        Group group = prot.get_parsed_pgroup();\n        Group ciphgroup = get_ciphertext_group(prot, group);\n        pk = DataParser.getAsElement(ciphgroup, pkbt);\n\n        console.enter(ShuffleStep.READ_CIPHS);\n        Path ciphspath = Paths.get(proofdir.toString(), CIPHERTEXTS_PATH);\n        ByteTree ctsbt = ByteTree.parse(ciphspath);\n        ciphs = DataParser.getAsElementArray(ciphgroup, ctsbt);\n\n        console.enter(ShuffleStep.READ_SHUFFLED);\n        Path shuffledpath = Paths.get(proofdir.toString(), SHUFFLED_CIPHERTEXTS_PATH);\n        ByteTree sctsbt = ByteTree.parse(shuffledpath);\n        shuffled = DataParser.getAsElementArray(ciphgroup, sctsbt);\n    }\n\n    public ProtocolInformation get_ProtocolInformation() {\n        return prot;\n    }\n\n    public PermutationCommitment get_PermutationCommitment() {\n        return pc;\n    }\n\n    public PoSCommitment get_PoSCommitment() {\n        return posc;\n    }\n\n    public PoSReply get_PoSReply() {\n        return posr;\n    }\n\n    public GroupElement[] get_ciphertexts() {\n        return ciphs;\n    }\n\n    public GroupElement[] get_shuffled_ciphertexts() {\n        return shuffled;\n    }\n\n    public GroupElement get_publickey() {\n        return pk;\n    }\n\n    private static ProductGroup get_ciphertext_group(ProtocolInformation prot, Group group) {\n        ProductGroup prodgroup = new ProductGroup(group, prot.get_keywidth());\n        ProductGroup ciphgroup = new ProductGroup(prodgroup, 2);\n        return ciphgroup;\n    }\n\n    /**\n     * Permutation commitment holder.\n     */\n    public static class PermutationCommitment {\n        private final GroupElement[] u;\n\n        /**\n         * Parse permutation commitment from a bytetree.\n         * \n         * @param bt\n         */\n        PermutationCommitment(ProtocolInformation prot, byte[] bt) {\n            ByteTree root = ByteTree.parse(bt);\n            this.u = DataParser.getAsElementArray(prot.get_parsed_pgroup(), root);\n        }\n\n        /**\n         * Initialize the permutation commitment from protocol information and permutation\n         * commitment path.\n         * \n         * @param prot\n         * @param path\n         * @throws IOException\n         */\n        PermutationCommitment(ProtocolInformation prot, Path path) throws IOException {\n            this(prot, Files.readAllBytes(path));\n        }\n\n        public GroupElement[] get_u() {\n            return u;\n        }\n    }\n\n    /**\n     * Proof of shuffle commitment holder.\n     */\n    public static class PoSCommitment {\n        private GroupElement A_prim, C_prim, D_prim;\n        private GroupElement[] B, B_prim;\n        private ProductGroupElement F_prim;\n\n        /**\n         * Parse proof of shuffle commitment from bytetree.\n         * \n         * @param bt Byte array holding proof of shuffle commitment.\n         */\n        PoSCommitment(ProtocolInformation prot, byte[] bt) {\n            ByteTree root = ByteTree.parse(bt);\n            Group group = prot.get_parsed_pgroup();\n            ProductGroup ciphgroup = get_ciphertext_group(prot, group);\n            B = DataParser.getAsElementArray(group, root, 0);\n            A_prim = DataParser.getAsElement(group, root, 1);\n            B_prim = DataParser.getAsElementArray(group, root, 2);\n            C_prim = DataParser.getAsElement(group, root, 3);\n            D_prim = DataParser.getAsElement(group, root, 4);\n            F_prim = (ProductGroupElement) DataParser.getAsElement(ciphgroup, root, 5);\n        }\n\n        /**\n         * Initialize the shuffle commitment from protocol information and shuffle commitment path.\n         * \n         * @param prot\n         * @param path\n         * @throws IOException\n         */\n        PoSCommitment(ProtocolInformation prot, Path path) throws IOException {\n            this(prot, Files.readAllBytes(path));\n        }\n\n        public GroupElement get_A_prim() {\n            return A_prim;\n        }\n\n        public GroupElement get_C_prim() {\n            return C_prim;\n        }\n\n        public GroupElement get_D_prim() {\n            return D_prim;\n        }\n\n        public GroupElement[] get_B() {\n            return B;\n        }\n\n        public GroupElement[] get_B_prim() {\n            return B_prim;\n        }\n\n        public ProductGroupElement get_F_prim() {\n            return F_prim;\n        }\n    }\n\n    /**\n     * Holder for proof of shuffle reply.\n     */\n    public static class PoSReply {\n        private BigInteger kA, kC, kD;\n        private BigInteger[] kB, kE;\n        private BigInteger[] kF;\n\n        /**\n         * Parse proof of shuffle reply from bytetree.\n         * \n         * @param bt Byte array holding proof of shuffle in bytetree format.\n         */\n        PoSReply(ProtocolInformation prot, byte[] bt) {\n            ByteTree root = ByteTree.parse(bt);\n            Group group = prot.get_parsed_pgroup();\n            ProductGroup prodgroup = new ProductGroup(group, prot.get_keywidth());\n            kA = DataParser.getAsInteger(root, 0);\n            kB = DataParser.getAsIntegerArray(root, 1);\n            kC = DataParser.getAsInteger(root, 2);\n            kD = DataParser.getAsInteger(root, 3);\n            kE = DataParser.getAsIntegerArray(root, 4);\n            kF = DataParser.getAsScalar(prodgroup, root, 5);\n        }\n\n        /**\n         * Initialize the proof reply from protocol information and path to proof reply.\n         * \n         * @param prot\n         * @param path\n         * @throws IOException\n         */\n        PoSReply(ProtocolInformation prot, Path path) throws IOException {\n            this(prot, Files.readAllBytes(path));\n        }\n\n        public BigInteger get_kA() {\n            return kA;\n        }\n\n        public BigInteger get_kC() {\n            return kC;\n        }\n\n        public BigInteger get_kD() {\n            return kD;\n        }\n\n        public BigInteger[] get_kB() {\n            return kB;\n        }\n\n        public BigInteger[] get_kE() {\n            return kE;\n        }\n\n        public BigInteger[] get_kF() {\n            return kF;\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/ThreadedVerifier.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.audit.shuffle.ShuffleConsole.ShuffleStep;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.math.ModPGroupElement;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\nimport ee.ivxv.common.service.console.Progress;\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.FutureTask;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ThreadedVerifier extends Verifier {\n    static final Logger log = LoggerFactory.getLogger(ThreadedVerifier.class);\n\n    private int nothreads;\n    private ExecutorService executor;\n\n    public ThreadedVerifier(ShuffleConsole console, ShuffleProof proof, int nothreads) {\n        super(console, proof);\n        this.nothreads = nothreads;\n        this.executor = Executors.newFixedThreadPool(nothreads);\n    }\n\n    public GroupElement compute_A_threaded(Progress progress, BigInteger[] e)\n            throws MathException, InterruptedException, ExecutionException {\n        return compute_A(progress, get_proof().get_PermutationCommitment().get_u(), e, nothreads,\n                executor);\n    }\n\n    public GroupElement compute_F_threaded(Progress progress, BigInteger[] e)\n            throws MathException, InterruptedException, ExecutionException {\n        return compute_F(progress, get_proof().get_ciphertexts(), e, nothreads, executor);\n    }\n\n    public boolean verify_A_threaded(Progress progress, BigInteger v, GroupElement A,\n            GroupElement[] h) throws MathException, InterruptedException, ExecutionException {\n        return verify_A(progress, v, A, get_proof().get_PoSCommitment().get_A_prim(),\n                get_proof().get_ProtocolInformation().get_parsed_generator(), h,\n                get_proof().get_PoSReply().get_kA(), get_proof().get_PoSReply().get_kE(), nothreads,\n                executor);\n    }\n\n    public boolean verify_B_threaded(Progress progress, BigInteger v, GroupElement[] h)\n            throws MathException, InterruptedException, ExecutionException {\n        return verify_B(progress, v, get_proof().get_PoSCommitment().get_B(),\n                get_proof().get_PoSCommitment().get_B_prim(),\n                get_proof().get_ProtocolInformation().get_parsed_generator(),\n                get_proof().get_PoSReply().get_kB(), get_proof().get_PoSReply().get_kE(), h,\n                nothreads, executor);\n    }\n\n    public boolean verify_F_threaded(Progress progress, BigInteger v, GroupElement F)\n            throws MathException, InterruptedException, ExecutionException {\n        return verify_F(progress, v, F, get_proof().get_PoSCommitment().get_F_prim(),\n                get_proof().get_publickey(), get_proof().get_PoSReply().get_kE(),\n                get_proof().get_PoSReply().get_kF(), get_proof().get_shuffled_ciphertexts(),\n                nothreads, executor);\n    }\n\n    public boolean verify_all() throws ShuffleException, MathException {\n        console.enter(ShuffleStep.VERIFY);\n        console.enter(ShuffleStep.VERIFY_PARAMS);\n        byte[] rho = compute_rho();\n        GroupElement[] h = compute_h(rho);\n        console.enter(ShuffleStep.VERIFY_NI);\n        byte[] s = compute_RO_seed(rho, h);\n        BigInteger[] e = compute_e(s);\n        BigInteger v = compute_v(rho, s);\n\n        int N = get_proof().get_ciphertexts().length;\n        Progress progress = console.enter(ShuffleStep.VERIFY_PERM, 5 * N + 10 + 3 * nothreads);\n        GroupElement A;\n        try {\n            A = compute_A_threaded(progress, e);\n        } catch (InterruptedException | ExecutionException ex) {\n            executor.shutdown();\n            throw new ShuffleException(ex);\n        }\n        GroupElement C = compute_C(progress, h);\n        GroupElement D = compute_D(progress, h, e);\n        try {\n            if (!verify_A_threaded(progress, v, A, h)) {\n                throw new ShuffleException(\"A failed\");\n            }\n        } catch (InterruptedException | ExecutionException ex) {\n            executor.shutdown();\n            throw new ShuffleException(ex);\n        }\n        try {\n            if (!verify_B_threaded(progress, v, h)) {\n                throw new ShuffleException(\"B failed\");\n            }\n        } catch (InterruptedException | ExecutionException ex) {\n            executor.shutdown();\n            throw new ShuffleException(ex);\n        }\n        if (!verify_C(progress, v, C)) {\n            throw new ShuffleException(\"C failed\");\n        }\n        if (!verify_D(progress, v, D)) {\n            throw new ShuffleException(\"D failed\");\n        }\n        progress.finish();\n        progress = console.enter(ShuffleStep.VERIFY_RERAND, 2 * N\n                + get_proof().get_PoSReply().get_kF().length + 3 + 2 * nothreads);\n        GroupElement F;\n        try {\n            F = compute_F_threaded(progress, e);\n        } catch (InterruptedException | ExecutionException ex) {\n            executor.shutdown();\n            throw new ShuffleException(ex);\n        }\n        try {\n            if (!verify_F_threaded(progress, v, F)) {\n                throw new ShuffleException(\"F failed\");\n            }\n        } catch (InterruptedException | ExecutionException ex) {\n            executor.shutdown();\n            throw new ShuffleException(ex);\n        }\n        executor.shutdown();\n        progress.finish();\n        return true;\n    }\n\n    private GroupElement compute_A(Progress progress, GroupElement[] u, BigInteger[] e,\n            int nothreads, ExecutorService executor)\n            throws MathException, InterruptedException, ExecutionException {\n        // the number of computations differ in threaded and non-threaded case. In threaded case we\n        // also aggregate the per-thread results.\n        List<Future<GroupElement>> futures = new ArrayList<>();\n        for (int i = 0; i < nothreads; i++) {\n            FutureTask<GroupElement> ft =\n                    new FutureTask<>(get_compute_A_worker(progress, u, e, i, nothreads));\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        log.debug(\"Started all compute A workers\");\n        GroupElement res = u[0].getGroup().getIdentity();\n        log.debug(\"Collecting compute A worker results\");\n        for (Future<GroupElement> ft : futures) {\n            res = res.op(ft.get());\n            progress.increase(1);\n        }\n        log.debug(\"Collected all compute A worker results\");\n        return res;\n    }\n\n    private static Callable<GroupElement> get_compute_A_worker(Progress progress, GroupElement[] u,\n            BigInteger[] e, int threadid, int nothreads) {\n        return () -> {\n            log.debug(\"Compute A worker [{}/{}] started\", threadid, nothreads);\n            GroupElement res = u[0].getGroup().getIdentity();\n            for (int i = 0; i < u.length; i++) {\n                if (i % nothreads != threadid)\n                    continue;\n                GroupElement exped = u[i].scale(e[i]);\n                res = res.op(exped);\n                progress.increase(1);\n            }\n            log.debug(\"Compute A worker [{}/{}] finished\", threadid, nothreads);\n            return res;\n        };\n    }\n\n    public GroupElement compute_F(Progress progress, GroupElement[] w, BigInteger[] e,\n            int nothreads, ExecutorService executor)\n            throws MathException, InterruptedException, ExecutionException {\n        // the number of computations differ in threaded and non-threaded case. In threaded case we\n        // also aggregate the per-thread results.\n        List<Future<GroupElement>> futures = new ArrayList<>();\n        for (int i = 0; i < nothreads; i++) {\n            FutureTask<GroupElement> ft =\n                    new FutureTask<>(get_compute_F_worker(progress, w, e, i, nothreads));\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        log.debug(\"Started all compute F workers\");\n        GroupElement res = w[0].getGroup().getIdentity();\n        log.debug(\"Collecting compute F worker results\");\n        for (Future<GroupElement> ft : futures) {\n            res = res.op(ft.get());\n            progress.increase(1);\n        }\n        log.debug(\"Collected all compute F worker results\");\n        return res;\n    }\n\n    private static Callable<GroupElement> get_compute_F_worker(Progress progress, GroupElement[] w,\n            BigInteger[] e, int threadid, int nothreads) throws MathException {\n        return () -> {\n            log.debug(\"Compute F [{}/{}] worker started\", threadid, nothreads);\n            GroupElement res = w[0].getGroup().getIdentity();\n            for (int i = 0; i < w.length; i++) {\n                if (i % nothreads != threadid)\n                    continue;\n                GroupElement exped = w[i].scale(e[i]);\n                res = res.op(exped);\n                progress.increase(1);\n            }\n            log.debug(\"Compute F worker [{}/{}] finished\", threadid, nothreads);\n            return res;\n        };\n    }\n\n    private boolean verify_A(Progress progress, BigInteger v, GroupElement A, GroupElement A_prim,\n            GroupElement g, GroupElement[] h, BigInteger k_A, BigInteger[] k_E, int nothreads,\n            ExecutorService executor)\n            throws MathException, InterruptedException, ExecutionException {\n        // the number of computations differ in threaded and non-threaded case. In threaded case we\n        // also aggregate the per-thread results.\n        List<Future<GroupElement>> futures = new ArrayList<>();\n        for (int i = 0; i < nothreads; i++) {\n            FutureTask<GroupElement> ft =\n                    new FutureTask<>(get_verify_A_worker(progress, h, k_E, i, nothreads));\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        log.debug(\"Started verify A workers\");\n        GroupElement left = A.scale(v).op(A_prim);\n        progress.increase(1);\n        GroupElement right = h[0].getGroup().getIdentity();\n        log.debug(\"Collecting verify A worker results\");\n        for (Future<GroupElement> ft : futures) {\n            right = right.op(ft.get());\n            progress.increase(1);\n            log.debug(\"Collected verify A worker result\");\n        }\n        log.debug(\"Collected all verify A worker results\");\n        right = right.op(g.scale(k_A));\n        progress.increase(1);\n        return left.equals(right);\n    }\n\n    private static Callable<GroupElement> get_verify_A_worker(Progress progress, GroupElement[] h,\n            BigInteger[] k_E, int threadid, int nothreads) throws MathException {\n        return () -> {\n            log.debug(\"Verify A worker [{}/{}] started\", threadid, nothreads);\n            GroupElement right = h[0].getGroup().getIdentity();\n            for (int i = 0; i < h.length; i++) {\n                if (i % nothreads != threadid)\n                    continue;\n                right = right.op(h[i].scale(k_E[i]));\n                progress.increase(1);\n            }\n            log.debug(\"Verify A worker [{}/{}] finished\", threadid, nothreads);\n            return right;\n        };\n    }\n\n    private boolean verify_B(Progress progress, BigInteger v, GroupElement[] B,\n            GroupElement[] B_prim, GroupElement g, BigInteger[] k_B, BigInteger[] k_E,\n            GroupElement[] h, int nothreads, ExecutorService executor)\n            throws MathException, InterruptedException, ExecutionException {\n        // the number of computations is different in threaded and non-threaded case. In threaded\n        // case the thread which sees invalid proof stops and this is propagated to the controlling\n        // thread. In non-threaded case, all values are computed and then checked one-by-one.\n        GroupElement left = B[0].scale(v).op(B_prim[0]);\n        GroupElement right = h[0].scale(k_E[0]).op(g.scale(k_B[0]));\n        if (!left.equals(right)) {\n            return false;\n        }\n        log.debug(\"Verified first B value\");\n        List<Future<Boolean>> futures = new ArrayList<>();\n        for (int i = 0; i < nothreads; i++) {\n            FutureTask<Boolean> ft = new FutureTask<>(\n                    get_verify_B_worker(progress, v, B, B_prim, g, k_B, k_E, h, i, nothreads));\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        log.debug(\"Started verify B workers\");\n        log.debug(\"Collecting verify B worker results\");\n        for (Future<Boolean> ft : futures) {\n            if (!ft.get()) {\n                return false;\n            }\n            progress.increase(1);\n            log.debug(\"Collected verify B worker result\");\n        }\n        log.debug(\"Collected all verify B worker results\");\n        return true;\n    }\n\n    private static Callable<Boolean> get_verify_B_worker(Progress progress, BigInteger v,\n            GroupElement[] B, GroupElement[] B_prim, GroupElement g, BigInteger[] k_B,\n            BigInteger[] k_E, GroupElement[] h, int threadid, int nothreads) {\n        return () -> {\n            log.debug(\"Verify B worker [{}/{}] started\", threadid, nothreads);\n            GroupElement left, right;\n            for (int i = 1; i < B.length; i++) {\n                if (i % nothreads != threadid)\n                    continue;\n                left = B[i].scale(v).op(B_prim[i]);\n                right = B[i - 1].scale(k_E[i]);\n                right = right.op(g.scale(k_B[i]));\n                progress.increase(1);\n                if (!left.equals(right)) {\n                    log.debug(\"Verify B worker [{}/{}] finished early\", threadid, nothreads);\n                    return false;\n                }\n            }\n            log.debug(\"Verify B worker [{}/{}] finished\", threadid, nothreads);\n            return true;\n        };\n    }\n\n    private boolean verify_F(Progress progress, BigInteger v, GroupElement F, GroupElement F_prim,\n            GroupElement pk, BigInteger[] k_E, BigInteger[] k_F, GroupElement[] w_prim,\n            int nothreads, ExecutorService executor)\n            throws MathException, InterruptedException, ExecutionException {\n        // the number of computations differ in threaded and non-threaded case. In threaded case we\n        // also aggregate the per-thread results.\n        List<Future<GroupElement>> futures = new ArrayList<>();\n        for (int i = 0; i < nothreads; i++) {\n            FutureTask<GroupElement> ft =\n                    new FutureTask<>(get_verify_F_worker(progress, k_E, w_prim, i, nothreads));\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        log.debug(\"Started verify F workers\");\n        BigInteger[] factors = new BigInteger[k_F.length];\n        for (int i = 0; i < factors.length; i++) {\n            factors[i] = k_F[i].negate();\n            progress.increase(1);\n        }\n        GroupElement left = F.scale(v).op(F_prim);\n        GroupElement right = w_prim[0].getGroup().getIdentity();\n        log.debug(\"Collecting verify F worker results\");\n        for (Future<GroupElement> ft : futures) {\n            right = right.op(ft.get());\n            progress.increase(1);\n            log.debug(\"Collected verify F worker result\");\n        }\n        log.debug(\"Collected all verify F worker results\");\n        ProductGroupElement pkl = (ProductGroupElement) ((ProductGroupElement) pk).getElements()[0];\n        ProductGroupElement pkr = (ProductGroupElement) ((ProductGroupElement) pk).getElements()[1];\n        ProductGroupElement tmpl = pkl.scale(factors);\n        progress.increase(1);\n        ProductGroupElement tmpr = pkr.scale(factors);\n        progress.increase(1);\n        ProductGroupElement tmp = new ProductGroupElement((ProductGroup) pk.getGroup(), tmpl, tmpr);\n        right = right.op(tmp);\n        progress.increase(1);\n        return left.equals(right);\n    }\n\n    private static Callable<GroupElement> get_verify_F_worker(Progress progress, BigInteger[] k_E,\n            GroupElement[] w_prim, int threadid, int nothreads) {\n        return () -> {\n            log.debug(\"Verify F worker [{}/{}] started\", threadid, nothreads);\n            GroupElement right = w_prim[0].getGroup().getIdentity();\n            for (int i = 0; i < w_prim.length; i++) {\n                if (i % nothreads != threadid)\n                    continue;\n                right = right.op(w_prim[i].scale(k_E[i]));\n                progress.increase(1);\n            }\n            log.debug(\"Verify F worker [{}/{}] finished\", threadid, nothreads);\n            return right;\n        };\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/shuffle/Verifier.java",
    "content": "package ee.ivxv.audit.shuffle;\n\nimport ee.ivxv.audit.shuffle.ByteTree.Leaf;\nimport ee.ivxv.audit.shuffle.ByteTree.Node;\nimport ee.ivxv.audit.shuffle.ShuffleConsole.ShuffleStep;\nimport ee.ivxv.common.math.*;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.util.Util;\n\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.function.Supplier;\n\n/**\n * Verificatum proof of a shuffle verifier.\n * <p>\n * See the Verificatum manual for implementing independent verifier for the explanation of the\n * variables used in the verifier.\n */\npublic class Verifier {\n\n    protected final ShuffleProof proof;\n    protected final ShuffleConsole console;\n\n    /**\n     * Initialize the verifier using proof of a shuffle.\n     * \n     * @param proof\n     */\n    public Verifier(ShuffleProof proof) {\n        this.console = null;\n        this.proof = proof;\n    }\n\n    public Verifier(ShuffleConsole console, ShuffleProof proof) {\n        this.console = console;\n        this.proof = proof;\n    }\n\n    public ShuffleProof get_proof() {\n        return proof;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/mixnet/MixNetElGamalVerifyFiatShamirSession.java#L186\">globalPrefix</a>\n     *\n     * @return\n     */\n    public byte[] compute_rho() {\n        ProtocolInformation p = proof.get_ProtocolInformation();\n\n        String fullsid = String.format(\"%s.%s\", p.get_sid(), p.get_auxsid());\n\n        Node n = new Node(new ByteTree[]{\n                new Leaf(p.get_version()),\n                new Leaf(fullsid),\n                new Leaf(Util.toBytes(p.get_statdist())),\n                new Leaf(Util.toBytes(p.get_vbitlenro())),\n                new Leaf(Util.toBytes(p.get_ebitlenro())),\n                new Leaf(p.get_prg()),\n                new Leaf(p.get_pgroup()),\n                new Leaf(p.get_rohash())\n        });\n\n        return DataParser.getHash(p.get_rohash()).digest(n.getEncoded());\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSTW.java#L126\">prgSeed</a>\n     *\n     * @param rho\n     * @param h\n     * @return\n     */\n    public byte[] compute_RO_seed(byte[] rho, GroupElement[] h) {\n        ProductGroupElement publickey = (ProductGroupElement) get_proof().get_publickey();\n\n        ProtocolInformation p = proof.get_ProtocolInformation();\n        GroupElement generator = get_proof().get_ProtocolInformation().get_parsed_generator();\n        String pgroup = p.get_pgroup();\n\n        ByteTree[] nodes;\n        ByteTree generatorNode;\n\n        if (pgroup.contains(\"ECqPGroup\")) {\n            generatorNode = new Node((ECGroupElement) generator);\n        } else {\n            generatorNode = new Leaf(generator);\n        }\n\n        nodes = new ByteTree[]{\n                generatorNode,\n                new Node(h),\n                new Node(get_proof().get_PermutationCommitment().get_u()),\n                new Node(publickey),\n                new Node(DataParser.toArray(get_proof().get_ciphertexts()).getElements()),\n                new Node(DataParser.toArray(get_proof().get_shuffled_ciphertexts()).getElements())\n        };\n\n        Node node = new Node(nodes);\n        RO ro = new RO(get_proof().get_ProtocolInformation().get_rohash());\n        byte[] out = new byte[DataParser.getHash(get_proof().get_ProtocolInformation().get_prg()).getDigestLength()];\n        ro.setAmount(out.length * 8);\n\n        try {\n            ro.write(rho);\n            node.writeEncoded(ro);\n        } catch (IOException e) {\n            // checked\n        }\n\n        ro.read(out, out.length * 8);\n        return out;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSCBasicTW.java#L354\">e</a>\n     *\n     * @param s\n     * @return\n     */\n    public BigInteger[] compute_e(byte[] s) {\n        PRNG gen = new PRNG(get_proof().get_ProtocolInformation().get_prg(), s);\n        int n_e = get_proof().get_ProtocolInformation().get_ebitlenro();\n        int N = get_proof().get_ciphertexts().length;\n        BigInteger[] scalars = new BigInteger[N];\n        BigInteger mask = BigInteger.ONE.shiftLeft(n_e);\n\n        for (int i = 0; i < N; i++) {\n            byte[] ti = new byte[(n_e + 7) / 8];\n            gen.read(ti);\n            scalars[i] = new BigInteger(1, ti).mod(mask);\n        }\n        return scalars;\n    }\n\n    /**\n     * For ECGroup see:\n     * <a href=\"https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ECqPGroup.magic#L391\">ec</a>\n     * For ModpGroup see:\n     * <a href=\"https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ModPGroup.java#L778\">modp</a>\n     *\n     * @param rho\n     * @return\n     */\n    public GroupElement[] compute_h(byte[] rho) {\n        Group G_q = get_proof().get_ProtocolInformation().get_parsed_pgroup();\n        BigInteger p = get_proof().get_ProtocolInformation().get_parsed_pgroup().getOrder();\n        int n_r = get_proof().get_ProtocolInformation().get_statdist();\n        int n_p = p.bitLength();\n        int N = get_proof().get_ciphertexts().length;\n        String prg = get_proof().get_ProtocolInformation().get_prg();\n        String rohash = get_proof().get_ProtocolInformation().get_rohash();\n\n        Leaf l = new Leaf(\"generators\");\n\n        byte[] seed = new byte[rho.length + l.getEncodedLength()];\n\n        System.arraycopy(rho, 0, seed, 0, rho.length);\n        System.arraycopy(l.getEncoded(), 0, seed, rho.length, l.getEncodedLength());\n\n        @SuppressWarnings(\"resource\")\n        RO ro = new RO(rohash, seed);\n\n        byte[] out = new byte[DataParser.getHash(prg).getDigestLength()];\n        ro.read(out, out.length * 8);\n        PRNG gen = new PRNG(prg, out);\n\n        GroupElement[] elements = new GroupElement[N];\n        BigInteger mask = BigInteger.ONE.shiftLeft(n_p + n_r);\n\n        if (G_q instanceof ECGroup) {\n            // Each dprg.get() invocation produces new byte array\n            Supplier<byte[]> dprg = () -> {\n                byte[] rand = new byte[(n_p + n_r + 7) / 8];\n                gen.read(rand);\n                return new BigInteger(1, rand).mod(mask).toByteArray();\n            };\n\n            elements = new ECGroup().pseudoRandomElements(N, dprg);\n        } else {\n            BigInteger TWO = BigInteger.valueOf(2);\n            for (int i = 0; i < N; i++) {\n                byte[] rand = new byte[(n_p + n_r + 7) / 8];\n                gen.read(rand);\n                BigInteger value = new BigInteger(1, rand).mod(mask).modPow(TWO, p);\n                elements[i] = new ModPGroupElement((ModPGroup) G_q, value);\n            }\n        }\n\n        return elements;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSCBasicTW.java#L597\">v</a>\n     *\n     * @param rho\n     * @param s\n     * @return\n     */\n    public BigInteger compute_v(byte[] rho, byte[] s) {\n        GroupElement A_prim = get_proof().get_PoSCommitment().get_A_prim();\n        GroupElement C_prim = get_proof().get_PoSCommitment().get_C_prim();\n        GroupElement D_prim = get_proof().get_PoSCommitment().get_D_prim();\n        int n_v = get_proof().get_ProtocolInformation().get_vbitlenro();\n\n        ByteTree aPrim, cPrim, dPrim;\n\n        if (get_proof().get_ProtocolInformation().get_pgroup().contains(\"ECqPGroup\")) {\n            aPrim = new Node((ECGroupElement) A_prim);\n            cPrim = new Node((ECGroupElement) C_prim);\n            dPrim = new Node((ECGroupElement) D_prim);\n\n        } else {\n            aPrim = new Leaf(A_prim);\n            cPrim = new Leaf(C_prim);\n            dPrim = new Leaf(D_prim);\n        }\n\n        ByteTree[] nodes = new ByteTree[]{\n                new Node(get_proof().get_PoSCommitment().get_B()),\n                aPrim,\n                new Node(get_proof().get_PoSCommitment().get_B_prim()),\n                cPrim,\n                dPrim,\n                new Node(get_proof().get_PoSCommitment().get_F_prim())\n        };\n\n        Node n = new Node(new ByteTree[]{new Leaf(s), new Node(nodes)});\n\n        byte[] seed = new byte[rho.length + n.getEncodedLength()];\n        System.arraycopy(rho, 0, seed, 0, rho.length);\n        System.arraycopy(n.getEncoded(), 0, seed, rho.length, n.getEncodedLength());\n        @SuppressWarnings(\"resource\")\n        RO ro = new RO(get_proof().get_ProtocolInformation().get_rohash(), seed);\n        byte[] out = new byte[(n_v + 7) / 8];\n        ro.read(out, n_v);\n        return new BigInteger(1, out);\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L408\">A</a>\n     *\n     * @param progress\n     * @param e\n     * @return\n     * @throws MathException\n     */\n    public GroupElement compute_A(Progress progress, BigInteger[] e) throws MathException {\n        GroupElement[] u = get_proof().get_PermutationCommitment().get_u();\n        GroupElement res = u[0].getGroup().getIdentity();\n        for (int i = 0; i < u.length; i++) {\n            res = res.op(u[i].scale(e[i]));\n            progress.increase(1);\n        }\n        return res;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1013\">C</a>\n     *\n     * @param progress\n     * @param h\n     * @return\n     * @throws MathException\n     * @throws ShuffleException\n     */\n    public GroupElement compute_C(Progress progress, GroupElement[] h)\n            throws MathException, ShuffleException {\n        GroupElement[] u = get_proof().get_PermutationCommitment().get_u();\n\n        if (u.length != h.length) {\n            throw new ShuffleException(\"u and h length does not match\");\n        }\n\n        GroupElement up = u[0].getGroup().getIdentity();\n        GroupElement hp = h[0].getGroup().getIdentity();\n\n        for (int i = 0; i < u.length; i++) {\n            up = u[i].op(up);\n            hp = h[i].op(hp);\n            progress.increase(1);\n        }\n\n        GroupElement hpi = hp.inverse();\n        GroupElement res = up.op(hpi);\n\n        progress.increase(2);\n        return res;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1014\">D</a>\n     *\n     * @param progress\n     * @param h\n     * @param e\n     * @return\n     * @throws ShuffleException\n     * @throws MathException\n     */\n    public GroupElement compute_D(Progress progress, GroupElement[] h, BigInteger[] e)\n            throws ShuffleException, MathException {\n        BigInteger ep = BigInteger.ONE;\n        BigInteger q;\n        int N = get_proof().get_ciphertexts().length;\n\n        if (h[0] instanceof ModPGroupElement) {\n            ModPGroup group = (ModPGroup) h[0].getGroup();\n            q = MathUtil.safePrimeOrder(group.getOrder());\n        } else if (h[0] instanceof ECGroupElement) {\n            q = h[0].getGroup().getOrder();\n        } else {\n            throw new ShuffleException(\"Unknown element type: \" + h[0].getClass().getSimpleName());\n        }\n\n        for (BigInteger bigInteger : e) {\n            ep = ep.multiply(bigInteger).mod(q);\n            progress.increase(1);\n        }\n\n        GroupElement hp = h[0].scale(ep);\n        hp = hp.inverse();\n        GroupElement ret = get_proof().get_PoSCommitment().get_B()[N - 1].op(hp);\n\n        progress.increase(3);\n        return ret;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L409\">F</a>\n     *\n     * @param progress\n     * @param e\n     * @return\n     * @throws MathException\n     */\n    public GroupElement compute_F(Progress progress, BigInteger[] e) throws MathException {\n        GroupElement[] w = get_proof().get_ciphertexts();\n        GroupElement res = w[0].getGroup().getIdentity();\n\n        for (int i = 0; i < w.length; i++) {\n            GroupElement exped = w[i].scale(e[i]);\n            res = res.op(exped);\n            progress.increase(1);\n        }\n\n        return res;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1020\">verdictA</a>\n     *\n     * @param progress\n     * @param v\n     * @param A\n     * @param h\n     * @param e\n     * @return\n     * @throws MathException\n     */\n    public boolean verify_A(Progress progress, BigInteger v, GroupElement A, GroupElement[] h)\n            throws MathException {\n        GroupElement left = A.scale(v).op(get_proof().get_PoSCommitment().get_A_prim());\n        progress.increase(1);\n        GroupElement right = h[0].getGroup().getIdentity();\n\n        for (int i = 0; i < h.length; i++) {\n            right = right.op(h[i].scale(get_proof().get_PoSReply().get_kE()[i]));\n            progress.increase(1);\n        }\n\n        right = get_proof().get_ProtocolInformation().get_parsed_generator()\n                .scale(get_proof().get_PoSReply().get_kA())\n                .op(right);\n\n        progress.increase(1);\n        return left.equals(right);\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1035\">verdictB</a>\n     *\n     * @param progress\n     * @param v\n     * @param h\n     * @return\n     * @throws MathException\n     */\n    public boolean verify_B(Progress progress, BigInteger v, GroupElement[] h)\n            throws MathException {\n        GroupElement[] B = get_proof().get_PoSCommitment().get_B();\n        GroupElement[] Bp = get_proof().get_PoSCommitment().get_B_prim();\n        BigInteger[] kB = get_proof().get_PoSReply().get_kB();\n        BigInteger[] kE = get_proof().get_PoSReply().get_kE();\n        GroupElement g = get_proof().get_ProtocolInformation().get_parsed_generator();\n\n        for (int i = 1; i < B.length; i++) {\n            GroupElement left = B[i].scale(v).op(Bp[i]);\n            GroupElement right = B[i - 1].scale(kE[i]).op(g.scale(kB[i]));\n            if (!left.equals(right)) {\n                return false;\n            }\n            progress.increase(1);\n        }\n\n        return true;\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1048\">verdictC</a>\n     *\n     * @param progress\n     * @param v\n     * @param C\n     * @return\n     * @throws MathException\n     */\n    public boolean verify_C(Progress progress, BigInteger v, GroupElement C) throws MathException {\n        GroupElement left = C.scale(v).op(get_proof().get_PoSCommitment().get_C_prim());\n        progress.increase(1);\n\n        GroupElement right = get_proof().get_ProtocolInformation().get_parsed_generator()\n                .scale(get_proof().get_PoSReply().get_kC());\n        progress.increase(1);\n\n        return left.equals(right);\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1055\">verdictD</a>\n     *\n     * @param progress\n     * @param v\n     * @param D\n     * @return\n     * @throws MathException\n     */\n    public boolean verify_D(Progress progress, BigInteger v, GroupElement D) throws MathException {\n        GroupElement left = D.scale(v).op(get_proof().get_PoSCommitment().get_D_prim());\n        progress.increase(1);\n\n        GroupElement right = get_proof().get_ProtocolInformation().get_parsed_generator()\n                .scale(get_proof().get_PoSReply().get_kD());\n        progress.increase(1);\n\n        return left.equals(right);\n    }\n\n    /**\n     * See:\n     * <a href=\"https://github.com/verificatum/verificatum-vmn/blob/9d9c45e37fa043881e58d71901ce45e387b31271/src/java/com/verificatum/protocol/hvzk/PoSBasicTW.java#L1062\">verdictF</a>\n     *\n     * @param progress\n     * @param v\n     * @param F\n     * @return\n     * @throws MathException\n     */\n    public boolean verify_F(Progress progress, BigInteger v, GroupElement F) throws MathException {\n        // the number of computations differ in threaded and non-threaded case. In threaded case we\n        // also aggregate the per-thread results.\n        GroupElement pk = get_proof().get_publickey();\n        BigInteger[] kF = get_proof().get_PoSReply().get_kF();\n        GroupElement[] wp = get_proof().get_shuffled_ciphertexts();\n        BigInteger[] kE = get_proof().get_PoSReply().get_kE();\n        GroupElement Fp = get_proof().get_PoSCommitment().get_F_prim();\n\n        GroupElement left = F.scale(v).op(Fp);\n        GroupElement right = wp[0].getGroup().getIdentity();\n\n        for (int i = 0; i < wp.length; i++) {\n            right = right.op(wp[i].scale(kE[i]));\n            progress.increase(1);\n        }\n\n        BigInteger[] factors = new BigInteger[kF.length];\n        for (int i = 0; i < factors.length; i++) {\n            factors[i] = kF[i].negate();\n            progress.increase(1);\n\n        }\n\n        ProductGroupElement pkl = (ProductGroupElement) ((ProductGroupElement) pk).getElements()[0];\n        ProductGroupElement pkr = (ProductGroupElement) ((ProductGroupElement) pk).getElements()[1];\n        ProductGroupElement tmpl = pkl.scale(factors);\n        progress.increase(1);\n        ProductGroupElement tmpr = pkr.scale(factors);\n        progress.increase(1);\n        ProductGroupElement tmp = new ProductGroupElement((ProductGroup) pk.getGroup(), tmpl, tmpr);\n        right = right.op(tmp);\n\n        progress.increase(1);\n        return left.equals(right);\n    }\n\n    /**\n     * Verify the correctness of the shuffle.\n     * <p>\n     * Throws an exception specifying the reason for failed verification.\n     * \n     * @return Boolean True if the proof verifies. If not, then an exception is thrown.\n     * @throws ShuffleException If the verification fails, denoting a reason.\n     * @throws MathException If computation fails.\n     */\n    public boolean verify_all() throws ShuffleException, MathException {\n        console.enter(ShuffleStep.VERIFY);\n        console.enter(ShuffleStep.VERIFY_PARAMS);\n        byte[] rho = compute_rho();\n        GroupElement[] h = compute_h(rho);\n        console.enter(ShuffleStep.VERIFY_NI);\n        byte[] s = compute_RO_seed(rho, h);\n        BigInteger[] e = compute_e(s);\n        BigInteger v = compute_v(rho, s);\n\n        int N = get_proof().get_ciphertexts().length;\n        Progress progress = console.enter(ShuffleStep.VERIFY_PERM, 5 * N + 11);\n        GroupElement A = compute_A(progress, e);\n        GroupElement C = compute_C(progress, h);\n        GroupElement D = compute_D(progress, h, e);\n        if (!verify_A(progress, v, A, h)) {\n            throw new ShuffleException(\"A failed\");\n        }\n        if (!verify_B(progress, v, h)) {\n            throw new ShuffleException(\"B failed\");\n        }\n        if (!verify_C(progress, v, C)) {\n            throw new ShuffleException(\"C failed\");\n        }\n        if (!verify_D(progress, v, D)) {\n            throw new ShuffleException(\"D failed\");\n        }\n        progress.finish();\n        progress = console.enter(ShuffleStep.VERIFY_RERAND,\n                2 * N + get_proof().get_PoSReply().get_kF().length + 3);\n        GroupElement F = compute_F(progress, e);\n        if (!verify_F(progress, v, F)) {\n            throw new ShuffleException(\"F failed\");\n        }\n        progress.finish();\n        return true;\n    }\n\n    /**\n     * Verify the correctness of the shuffle.\n     * <p>\n     * If {@literal throwexception} is false, then now exceptions are thrown during computation and\n     * verification.\n     * \n     * @param throwexception Boolean in\n     * @return Boolean indicating the correctness of the shuffle.\n     */\n    public boolean verify_all(boolean throwexception) {\n        try {\n            return verify_all();\n        } catch (ShuffleException | MathException e) {\n            if (!throwexception) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/tools/ConvertTool.java",
    "content": "package ee.ivxv.audit.tools;\n\nimport ee.ivxv.audit.AuditContext;\nimport ee.ivxv.audit.Msg;\nimport ee.ivxv.audit.shuffle.DataParser;\nimport ee.ivxv.audit.shuffle.ShuffleConsole;\nimport ee.ivxv.audit.shuffle.ShuffleProof;\nimport ee.ivxv.audit.tools.ConvertTool.ConvertArgs;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.helpers.NOPLogger;\n\n/**\n * ConvertTool verifies the correctness of file format conversions.\n */\npublic class ConvertTool implements Tool.Runner<ConvertArgs> {\n    public static class ConvertArgs extends Args {\n        Arg<Path> inputBbox = Arg.aPath(Msg.arg_input_bb);\n        Arg<Path> outputBbox = Arg.aPath(Msg.arg_output_bb);\n        Arg<Path> pubPath = Arg.aPath(Msg.arg_pub);\n        Arg<Path> protPath = Arg.aPath(Msg.arg_protinfo, true, false);\n        Arg<Path> proofPath = Arg.aPath(Msg.arg_proofdir, true, true);\n\n        public ConvertArgs() {\n            super();\n            args.add(inputBbox);\n            args.add(outputBbox);\n            args.add(pubPath);\n            args.add(protPath);\n            args.add(proofPath);\n        }\n    }\n\n    // we use static NOP logger to be able to run tests without logging\n    // the logger is patched in non-test context\n    private static Logger log = NOPLogger.NOP_LOGGER;\n    private I18nConsole console;\n\n    public ConvertTool(AuditContext ctx) {\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        // patch the static logger. It is a single-instance class, so there are not any race\n        // conditions.\n        ConvertTool.log = LoggerFactory.getLogger(ConvertTool.class);\n    }\n\n    @Override\n    public boolean run(ConvertArgs args) throws Exception {\n        boolean ret = true;\n        log.debug(\"Reading shuffle proof\");\n        ShuffleConsole sc = new ShuffleConsole(this.console);\n        ShuffleProof proof = new ShuffleProof(args.protPath.value(), args.proofPath.value(), sc);\n        log.debug(\"Reading pre-shuffle ballot box\");\n        AnonymousBallotBox bb = Json.read(args.inputBbox.value(), AnonymousBallotBox.class);\n        log.debug(\"Reading post-shuffle ballot box\");\n        AnonymousBallotBox sbb = Json.read(args.outputBbox.value(), AnonymousBallotBox.class);\n        log.debug(\"Reading public key\");\n        ElGamalPublicKey pk = new ElGamalPublicKey(args.pubPath.value());\n        if (!verifyPublickey(pk, proof)) {\n            console.println(Msg.m_convert_publickey_failed);\n            ret = false;\n        } else {\n            console.println(Msg.m_convert_publickey_succ);\n        }\n        if (!verifyBallotboxToBytetree(pk, bb, proof)) {\n            console.println(Msg.m_convert_bb_to_bt_failed);\n            ret = false;\n        } else {\n            console.println(Msg.m_convert_bb_to_bt_succ);\n        }\n        if (!verifyBytetreeToBallotbox(pk, proof, sbb)) {\n            console.println(Msg.m_convert_bt_to_bb_failed);\n            ret = false;\n        } else {\n            console.println(Msg.m_convert_bt_to_bb_succ);\n        }\n        return ret;\n    }\n\n    public static boolean verifyPublickey(ElGamalPublicKey pk, ShuffleProof proof) {\n        log.debug(\"Verifying public key correct converting\");\n        log.debug(\"Getting shuffle proof public key\");\n        GroupElement btpk = proof.get_publickey();\n        log.debug(\"Converting public key to bytetree format\");\n        GroupElement gppk = convertPublickeyToBytetree(pk);\n        boolean res = btpk.equals(gppk);\n        log.debug(\"Shuffle proof public key == converted public key: {}\", res);\n        return res;\n    }\n\n    public static boolean verifyBallotboxToBytetree(ElGamalPublicKey pk, AnonymousBallotBox bb,\n            ShuffleProof proof) {\n        log.debug(\"Verifying ballot box to bytetree converting\");\n        log.debug(\"Getting shuffle ciphertext list\");\n        GroupElement[] proofciphs = proof.get_ciphertexts();\n        log.debug(\"Converting ballot box to byte tree format\");\n        GroupElement[] bbciphs;\n        try {\n            bbciphs = convertBallotboxToBytetree(pk, bb);\n        } catch (IllegalArgumentException e) {\n            log.debug(\"Converting ballot box to bytetree cipherexts failed: {}\", e);\n            return false;\n        }\n        if (bbciphs.length != proofciphs.length) {\n            log.debug(\"Ballot box and shuffle ciphertext list size differ\");\n            return false;\n        }\n        for (int i = 0; i < bbciphs.length; i++) {\n            if (!bbciphs[i].equals(proofciphs[i])) {\n                log.debug(\"Ballot box element and shuffle ciphertext item '{}' differ\", i);\n                return false;\n            }\n        }\n        log.debug(\"Shuffle proof ciphertexts == ballot box: true\");\n        return true;\n    }\n\n    public static boolean verifyBytetreeToBallotbox(ElGamalPublicKey pk, ShuffleProof proof,\n            AnonymousBallotBox bb) {\n        log.debug(\"Verifying bytetree to ballot box converting\");\n        log.debug(\"Converting shuffle ciphertext list to ballot box\");\n        AnonymousBallotBox proofbb;\n        try {\n            proofbb = convertBytetreeToBallotbox(pk, proof.get_shuffled_ciphertexts());\n        } catch (IllegalArgumentException e) {\n            log.debug(\"Converting bytetree to ballot box failed: {}\", e);\n            return false;\n        }\n        if (!bb.getElection().equals(proofbb.getElection())) {\n            log.debug(\"Converted ballot box and ballot box election identifier differ. \"\n                    + \"Expected '{}', got '{}'\", bb.getElection(), proofbb.getElection());\n            return false;\n        }\n        Map<String, Map<String, Map<String, List<byte[]>>>> proofd = proofbb.getDistricts();\n        Map<String, Map<String, Map<String, List<byte[]>>>> bbd = bb.getDistricts();\n        if (!proofd.keySet().equals(bbd.keySet())) {\n            log.debug(\"Converted ballot box and ballot box district identifier set differ\");\n            return false;\n        }\n        for (String d : proofd.keySet()) {\n            Map<String, Map<String, List<byte[]>>> proofs = proofd.get(d);\n            Map<String, Map<String, List<byte[]>>> bbs = bbd.get(d);\n            if (!proofs.keySet().equals(bbs.keySet())) {\n                log.debug(\"Converted ballot box and ballot box station identifier set differ \"\n                        + \"for district '{}'\", d);\n                return false;\n            }\n            for (String s : proofs.keySet()) {\n                Map<String, List<byte[]>> proofq = proofs.get(s);\n                Map<String, List<byte[]>> bbq = bbs.get(s);\n                if (!proofq.keySet().equals(bbq.keySet())) {\n                    log.debug(\"Converted ballot box and ballot box question identifier set differ \"\n                            + \"for district '{}' and station '{}'\", d, s);\n                    return false;\n                }\n                for (String q : proofq.keySet()) {\n                    List<byte[]> proofc = proofq.get(q);\n                    List<byte[]> bbc = bbq.get(q);\n                    if (proofc.size() != bbc.size()) {\n                        log.debug(\n                                \"Converted ballot box and ballot box ciphertext list differ \"\n                                        + \"for district '{}', station '{}' and question '{}'\",\n                                d, s, q);\n                        return false;\n                    }\n                    for (int i = 0; i < proofc.size(); i++) {\n                        if (!Arrays.equals(proofc.get(i), bbc.get(i))) {\n                            log.debug(\"Converted ballot box and ballot box ciphertext differ for \"\n                                    + \"district '{}', station '{}', question '{}' and \"\n                                    + \"index '{}'\", d, s, q, i);\n                            return false;\n                        }\n                    }\n                }\n            }\n        }\n        return true;\n    }\n\n    private static GroupElement[] convertBallotboxToBytetree(ElGamalPublicKey pk,\n            AnonymousBallotBox bb) throws IllegalArgumentException {\n        List<ProductGroupElement> res = new ArrayList<ProductGroupElement>();\n        ProductGroupElement ege = convertStringToProductGroupElement(pk, bb.getElection());\n        bb.getDistricts().forEach((d, smap) -> {\n            ProductGroupElement dge = convertStringToProductGroupElement(pk, d);\n            smap.forEach((s, qmap) -> {\n                ProductGroupElement sge = convertStringToProductGroupElement(pk, s);\n                qmap.forEach((q, clist) -> {\n                    ProductGroupElement qge = convertStringToProductGroupElement(pk, q);\n                    clist.forEach(c -> {\n                        ElGamalCiphertext ct = new ElGamalCiphertext(pk.getParameters(), c);\n                        ProductGroupElement cge = ct.getAsProductGroupElement();\n                        ProductGroupElement multict = DataParser\n                                .toArray(new ProductGroupElement[] {ege, dge, sge, qge, cge});\n                        res.add(multict);\n                    });\n                });\n            });\n        });\n        return res.toArray(new ProductGroupElement[0]);\n    }\n\n    private static ProductGroupElement convertStringToProductGroupElement(ElGamalPublicKey pk,\n            String msg) throws IllegalArgumentException {\n        Group G = pk.getParameters().getGroup();\n        Plaintext padded = G.pad(new Plaintext(msg));\n        GroupElement ge;\n        try {\n            ge = G.encode(padded);\n        } catch (MathException e) {\n            throw new IllegalArgumentException(\"Encoding failed\", e);\n        }\n        ProductGroup pG = new ProductGroup(G, 2);\n        ProductGroupElement pge = new ProductGroupElement(pG, G.getIdentity(), ge);\n        return pge;\n    }\n\n    private static AnonymousBallotBox convertBytetreeToBallotbox(ElGamalPublicKey pk,\n            GroupElement[] cts) throws IllegalArgumentException {\n        Map<String, Map<String, Map<String, List<byte[]>>>> res =\n                new LinkedHashMap<String, Map<String, Map<String, List<byte[]>>>>();\n        String election = null;\n        for (int i = 0; i < cts.length; i++) {\n            if (!(cts[i] instanceof ProductGroupElement)) {\n                throw new IllegalArgumentException(\n                        String.format(\"Ciphertext %d not ProductGroupElement\", i));\n            }\n            ProductGroupElement multict = (ProductGroupElement) cts[i];\n            if (multict.getElements().length != 2) {\n                throw new IllegalArgumentException(String.format(\"Ciphertext %d length not 2\", i));\n            }\n            if (!(multict.getElements()[0] instanceof ProductGroupElement)) {\n                throw new IllegalArgumentException(\n                        String.format(\"Ciphertext %d left side not PGE\", i));\n            }\n            if (((ProductGroupElement) multict.getElements()[0]).getElements().length != 5) {\n                throw new IllegalArgumentException(\n                        String.format(\"Ciphertext %d left side length not 5\", i));\n            }\n            if (!(multict.getElements()[1] instanceof ProductGroupElement)) {\n                throw new IllegalArgumentException(\n                        String.format(\"Ciphertext %d right side not PGE\", i));\n            }\n            if (((ProductGroupElement) multict.getElements()[1]).getElements().length != 5) {\n                throw new IllegalArgumentException(\n                        String.format(\"Ciphertext %d right side length not 5\", i));\n            }\n            String thiselection = convertProductGroupElementToString(pk, multict, 0);\n            if (election == null) {\n                election = thiselection;\n            }\n            if (!election.equals(thiselection)) {\n                throw new IllegalArgumentException(String\n                        .format(\"Ciphertext %d election differs. Expected '%s', got '%s'\", i));\n            }\n            String district = convertProductGroupElementToString(pk, multict, 1);\n            String station = convertProductGroupElementToString(pk, multict, 2);\n            String question = convertProductGroupElementToString(pk, multict, 3);\n            GroupElement blind = ((ProductGroupElement) multict.getElements()[0]).getElements()[4];\n            GroupElement blindedMessage =\n                    ((ProductGroupElement) multict.getElements()[1]).getElements()[4];\n            ElGamalCiphertext cc =\n                    new ElGamalCiphertext(blind, blindedMessage, pk.getParameters().getOID());\n            res.computeIfAbsent(district, x -> new LinkedHashMap<>())\n                    .computeIfAbsent(station, x -> new LinkedHashMap<>())\n                    .computeIfAbsent(question, x -> new ArrayList<byte[]>()).add(cc.getBytes());\n        }\n        AnonymousBallotBox bb = new AnonymousBallotBox(election, res);\n        return bb;\n    }\n\n    private static String convertProductGroupElementToString(ElGamalPublicKey pk,\n            ProductGroupElement pge, int index) throws IllegalArgumentException {\n        GroupElement[] els = pge.getElements();\n        GroupElement msgenc = ((ProductGroupElement) els[1]).getElements()[index];\n        Plaintext padded = pk.getParameters().getGroup().decode(msgenc);\n        Plaintext pt = pk.getParameters().getGroup().unpad(padded);\n        String msg = pt.getUTF8DecodedMessage();\n        return msg;\n    }\n\n    private static GroupElement convertPublickeyToBytetree(ElGamalPublicKey pk) {\n        ProductGroupElement pkpg = pk.getAsProductGroupElement();\n        ProductGroupElement gpg = new ProductGroupElement((ProductGroup) pkpg.getGroup(),\n                pk.getParameters().getGenerator(), pk.getParameters().getGroup().getIdentity());\n        ProductGroupElement res =\n                DataParser.toArray(new ProductGroupElement[] {gpg, gpg, gpg, gpg, pkpg});\n        return res;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/tools/DecryptTool.java",
    "content": "package ee.ivxv.audit.tools;\n\nimport ee.ivxv.audit.AuditContext;\nimport ee.ivxv.audit.Msg;\nimport ee.ivxv.audit.model.PlainBallotBox;\nimport ee.ivxv.audit.model.Tally;\nimport ee.ivxv.audit.tools.DecryptTool.DecryptArgs;\nimport ee.ivxv.audit.util.DiscardedBallot;\nimport ee.ivxv.audit.util.InvalidDecProofs;\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.Proof;\nimport ee.ivxv.common.service.bbox.impl.BboxHelperImpl;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.apache.commons.collections4.Bag;\nimport org.apache.commons.collections4.bag.HashBag;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\n\n/**\n * Tool for verifying the correctness and consistency of ciphertext decryption.\n */\npublic class DecryptTool implements Tool.Runner<DecryptArgs> {\n    private final Logger log = LoggerFactory.getLogger(DecryptTool.class);\n\n    private final AuditContext ctx;\n    private final I18nConsole console;\n    private final ToolHelper tool;\n\n    public DecryptTool(AuditContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        tool = new ToolHelper(console, ctx.container, new BboxHelperImpl(ctx.conf, ctx.container));\n    }\n\n    @Override\n    public boolean run(DecryptArgs args) throws Exception {\n        // Verify whether all provided files are found on the filesystem.\n        boolean allFilesExist = verifyInputFiles(args);\n        if (!allFilesExist) {\n            console.println();\n            return false;\n        }\n\n        boolean abortEarly = args.abortEarly.value();\n        boolean verifyInvalid = Objects.nonNull(args.invalidInputPath.value());\n        boolean allChecksPassed = true;\n\n        DistrictList districts = tool.readJsonDistricts(args.districts.value());\n        CandidateList candidates = tool.readJsonCandidates(args.candidates.value(), districts);\n\n        console.println();\n\n        console.println(Msg.m_anon_loading, args.anonBoxPath.value());\n        // The anonymous ballot box will subsequently be mutated!\n        AnonymousBallotBox anonBb = Json.read(args.anonBoxPath.value(), AnonymousBallotBox.class);\n        console.println(Msg.m_anon_loaded);\n\n        console.println();\n\n        console.println(Msg.m_pub_loading, args.pubPath.value());\n        ElGamalPublicKey pub = new ElGamalPublicKey(args.pubPath.value());\n        console.println(Msg.m_pub_loaded);\n\n        // Use bags since it might be that there are multiple identical ciphertexts.\n        // While these occurrences are suspicious and should be flagged, the flagging is done by\n        // the integrity tool, not the decrypt tool.\n        Bag<String> vProofCiphers = new HashBag<>(); // from proofs of ballot validity\n        Bag<String> iProofCiphers = new HashBag<>(); // from proofs of ballot invalidity\n\n        // For privacy reasons, these proofs might not exist: their generation is optional.\n        if (verifyInvalid) {\n            Proof proofs = tool.readJsonProofs(args.invalidInputPath.value());\n            // Cryptographically verify proofs and get the base64 encoded invalid ballots.\n            boolean allCorrect = processVerification(proofs, pub, ctx.args.threads.value(),\n                    args.outputPath.value(), false, iProofCiphers);\n\n            if (!allCorrect && abortEarly) {\n                printFailure();\n                return true;\n            } else allChecksPassed = allCorrect;\n\n            // Verify invalidity: plaintexts are indeed invalid.\n            // Verify consistency: all ciphertexts in the proof file have a counterpart in the anonymous BB.\n            // If this step becomes a performance bottleneck, it could be combined with the previous check.\n            // Currently, the checks are separate for implementation clarity.\n            // Note! Mutates the anonymous ballot box: removes ciphertexts of invalid votes from it.\n            boolean areConsistent = verifyPlaintexts(pub, anonBb, proofs, candidates, false);\n            if (!areConsistent && abortEarly) {\n                printFailure();\n                return true;\n            } else allChecksPassed = allChecksPassed && areConsistent;\n        }\n\n        Proof proofs = tool.readJsonProofs(args.inputPath.value());\n        // Cryptographically verify proofs and get the base64 encoded valid ballots.\n        boolean allCorrect = processVerification(proofs, pub, ctx.args.threads.value(),\n                args.outputPath.value(), true, vProofCiphers);\n\n        if (!allCorrect && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && allCorrect;\n\n        // Verify validity: plaintexts are indeed valid.\n        // Verify consistency: all ciphertexts in the proof file have a counterpart in the anonymous BB.\n        // If this step becomes a performance bottleneck, it could be combined with the previous check.\n        // Currently, the checks are separate for implementation clarity.\n        // Note! Mutates the anonymous ballot box: removes ciphertexts of valid votes from it.\n        boolean areConsistent = verifyPlaintexts(pub, anonBb, proofs, candidates, true);\n        if (!areConsistent && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && areConsistent;\n\n        // Get the ciphertexts deemed invalid.\n        // The file must always be provided, even if there are none: the file then contains none.\n        Bag<String> discardedCiphers = getDiscardedCiphers(args.discardedInputPath.value());\n\n        if (verifyInvalid) {\n            // Verify whether the invalidity proofs are consistent with the ciphertexts declared invalid:\n            // - all ciphertexts in the proofs file must have a counterpart in the invalids file\n            // - all ciphertexts in the invalids file must have a counterpart in the proofs file\n            boolean consistentInvalids = verifyInvalidConsistency(discardedCiphers, iProofCiphers);\n            console.println(Msg.m_decrypt_consistent_invalids, getYesNoMessage(consistentInvalids));\n            if (!consistentInvalids && abortEarly) {\n                printFailure();\n                return true;\n            } else allChecksPassed = allChecksPassed && consistentInvalids;\n        } else {\n            // If the invalidity proofs are not provided, verify whether all ciphertexts declared invalid\n            // have a counterpart in the anonymous BB. This is already done if the proofs are provided.\n            // Note! Mutates the anonymous ballot box: removes ciphertexts of invalid votes from it.\n            boolean invalidsInBb = verifyCiphersInBb(anonBb, discardedCiphers);\n            console.println(Msg.m_decrypt_bb_has_invalids, getYesNoMessage(invalidsInBb));\n            if (!invalidsInBb && abortEarly) {\n                printFailure();\n                return true;\n            } else allChecksPassed = allChecksPassed && invalidsInBb;\n        }\n\n        // Verify that no ciphertext is declared both valid and invalid.\n        console.println();\n        boolean disjointOutputs = Collections.disjoint(vProofCiphers, discardedCiphers);\n        console.println(Msg.m_decrypt_one_per_file, getYesNoMessage(disjointOutputs));\n        if (!disjointOutputs && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && disjointOutputs;\n\n        // The anonymous BB should now be empty if it is consistent with the proof files:\n        // all ciphertexts in the anonymous BB then have a counterpart in the proof files.\n        // If only validity proofs were provided, the counterparts were checked through the invalid\n        // ciphertexts file.\n        // The converse consistency has already been verified, i.e. that all ciphertexts have a\n        // counterpart in the anonymous BB.\n        // In summary, if the following check passes, then: anon bb = valid ciphertexts + invalid ciphertexts\n        boolean abbIsEmpty = isAnonBBEmpty(anonBb);\n        console.println(Msg.m_decrypt_dec_consistent, getYesNoMessage(abbIsEmpty));\n        if (!abbIsEmpty && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && abbIsEmpty;\n\n        // Get the plaintext ballot box.\n        console.println();\n        console.println(Msg.m_plain_loading, args.plainBoxPath.value());\n        PlainBallotBox plainBb = Json.read(args.plainBoxPath.value(), PlainBallotBox.class);\n        console.println(Msg.m_plain_loaded);\n\n        // Verify whether the plaintext ballot box is consistent with the plaintexts in the proofs file:\n        // - all plaintexts in the proofs file must have a counterpart in the plaintext ballot box\n        // - all entries in the plaintext ballot box must have a counterpart in the proofs file\n        // - the number of invalid votes should be consistent\n        // If this step becomes a performance bottleneck, it should be somehow combined with the\n        // validity proof verification stage.\n        boolean consistentValids = verifyValidConsistency(pub, proofs, plainBb, discardedCiphers.size());\n        console.println(Msg.m_decrypt_consistent_valids, getYesNoMessage(consistentValids));\n        if (!consistentValids && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && consistentValids;\n\n        console.println();\n\n        // Verify whether the plaintext ballot box matches with the computed tally.\n        boolean tallyValid = verifyTally(plainBb, args.tallyPath.value());\n        console.println(Msg.m_tally_match, getYesNoMessage(tallyValid));\n        if (!tallyValid && abortEarly) {\n            printFailure();\n            return true;\n        } else allChecksPassed = allChecksPassed && tallyValid;\n\n        console.println();\n        if (allChecksPassed) console.println(Msg.m_decrypt_success);\n        else console.println(Msg.m_decrypt_failure);\n        console.println();\n\n        return true;\n    }\n\n    private Msg getYesNoMessage(boolean success) {\n        if (success) return Msg.m_yes;\n        return Msg.m_no;\n    }\n\n    private void printFailure() {\n        console.println();\n        console.println(Msg.m_decrypt_failure);\n        console.println();\n    }\n\n    private boolean isAnonBBEmpty(AnonymousBallotBox abb) {\n        boolean isEmpty = true;\n        for (Map<String, Map<String, List<byte[]>>> smap : abb.getDistricts().values()) {\n            for (Map<String, List<byte[]>> qmap : smap.values()) {\n                for (List<byte[]> clist : qmap.values()) {\n                    if (clist.isEmpty()) continue;\n                    isEmpty = false;\n                    for (byte[] c : clist) {\n                        String b64ct = Base64.getEncoder().encodeToString(c);\n                        log.warn(\"Output files missing ballot: {}\", b64ct);\n                    }\n                }\n            }\n        }\n        return isEmpty;\n    }\n\n    /**\n     * Parses the file of discarded ciphertexts and extracts them.\n     *\n     * @param path the path of the file containing discarded ciphertexts\n     * @return a bag of base64-encoded ciphertexts\n     * @throws Exception If the file cannot be read or parsed\n     */\n    private Bag<String> getDiscardedCiphers(Path path) throws Exception {\n        Bag<String> discardedCTs = new HashBag<>();\n\n        console.println();\n        console.println(Msg.m_discarded_loading, path);\n        DiscardedBallot discarded = Json.read(path, DiscardedBallot.class);\n        console.println(Msg.m_discarded_loaded);\n\n        console.println(Msg.m_discarded_count, discarded.getCount());\n\n        discarded.getDiscarded().forEach(vote ->\n                discardedCTs.add(Base64.getEncoder().encodeToString(vote.getVote())));\n\n        return discardedCTs;\n    }\n\n    /**\n     * Verifies the proofs of correct decryption.\n     * <p>\n     * Additionally, verifies whether the decrypted results indeed are valid or invalid,\n     * thus confirming whether proofs are valid proofs of validity or invalidity.\n     * Collects also the base64-encoded ciphertexts for subsequent audit operations.\n     *\n     * @param proofs         the decryption proofs\n     * @param pub            the election public key\n     * @param threadCount    the number of threads to use for processing\n     * @param outPath        the directory where to output verification failures\n     * @param validityProofs whether the proofs prove validity or invalidity of ballots\n     * @param b64CTs         the bag where to store base64-encoded ciphertexts\n     * @return true if all proofs verify, false otherwise\n     * @throws Exception If the verification process cannot complete\n     */\n    private boolean processVerification(Proof proofs, ElGamalPublicKey pub, int threadCount,\n                                        Path outPath, boolean validityProofs,\n                                        Bag<String> b64CTs) throws Exception {\n        console.println();\n        console.println(Msg.m_verify_start);\n        InvalidDecProofs invalid = verifyDecryption(proofs, pub, threadCount, b64CTs);\n        console.println(Msg.m_verify_finish);\n\n        console.println(Msg.m_failurecount, invalid.getCount());\n        if (invalid.getCount() == 0) return true;\n\n        console.println(M.m_out_start, outPath);\n        if (!Files.exists(outPath)) Files.createDirectory(outPath);\n\n        if (validityProofs)\n            invalid.outputFailedProofsOfValidity(outPath);\n        else\n            invalid.outputFailedProofsOfInvalidity(outPath);\n\n        console.println(M.m_out_done, outPath);\n\n        return false;\n    }\n\n    /**\n     * Verifies whether plaintexts in a proof file really are valid/invalid.\n     * <p>\n     * Mutates the anonymous ballot box: removes ciphertexts for which it has\n     * checked the plaintext. This speeds up subsequent verifications and keeps\n     * track of the verification status.\n     *\n     * @param pub           the election public key\n     * @param abb           the anonymous ballot box\n     * @param proofs        the proof file\n     * @param candidates    the list of accepted candidates\n     * @param expectedValid whether the plaintexts should be valid or invalid\n     * @return the result of the checks\n     */\n    private boolean verifyPlaintexts(ElGamalPublicKey pub, AnonymousBallotBox abb, Proof proofs, CandidateList candidates,\n                                     boolean expectedValid) {\n        // Using a set is fine here even if there are repetitions since we have already verified the proofs:\n        // inconsistent repetitions should no longer be possible.\n        Map<String, GroupElement> votes = new HashMap<>();\n        // However, we still need to keep track of how many duplications there are when we compare with the\n        // ballot box later.\n        Bag<String> multiples = new HashBag<>();\n\n        // Get all ciphertext-message pairs.\n        for (Proof.ProofJson proof : proofs.getProofs()) {\n            String b64 = Base64.getEncoder().encodeToString(proof.getCiphertext());\n            GroupElement old = votes.put(b64, pub.getParameters().getGroup().getElement(proof.getMessage()));\n\n            if (Objects.isNull(old)) continue; // not a duplicate\n\n            if (!multiples.contains(b64)) multiples.add(b64, 2); // add two the first time\n            else multiples.add(b64);\n        }\n\n        boolean allCorrect = true;\n\n        for (Map.Entry<String, Map<String, Map<String, List<byte[]>>>> districtMap : abb.getDistricts().entrySet()) {\n            for (Map<String, List<byte[]>> sMap : districtMap.getValue().values()) {\n                for (List<byte[]> cList : sMap.values()) {\n                    Iterator<byte[]> i = cList.iterator();\n                    while (i.hasNext()) {\n                        byte[] c = i.next();\n                        String b64 = Base64.getEncoder().encodeToString(c);\n                        GroupElement decrypted = votes.remove(b64);\n                        multiples.remove(b64);\n                        if (Objects.isNull(decrypted)) continue;\n                        Group group = pub.getParameters().getGroup();\n                        Plaintext decoded = group.decode(decrypted);\n                        Plaintext unpadded = group.unpad(decoded);\n\n                        boolean isValid = isValidChoice(unpadded, districtMap.getKey(), candidates);\n                        if (isValid != expectedValid) {\n                            log.warn(\"Plaintext is declared {} but is not: {}\",\n                                    expectedValid ? \"valid\" : \"invalid\", unpadded.getUTF8DecodedMessage());\n                        }\n                        allCorrect = allCorrect && (isValid == expectedValid);\n                        i.remove();\n                    }\n                }\n            }\n        }\n\n        // Verify whether all proof file entries were indeed in the anonymous ballot box.\n        boolean allInBb = votes.isEmpty() && multiples.isEmpty();\n        if (!allInBb) {\n            log.warn(\"There were {} ballots missing from the input ballot box:\", votes.size() + multiples.size());\n            logMissingBallots(votes.keySet());\n            logMissingBallots(multiples);\n        }\n\n        if (expectedValid) console.println(Msg.m_decrypt_verified_valid, getYesNoMessage(allCorrect));\n        else console.println(Msg.m_decrypt_verified_invalid, getYesNoMessage(allCorrect));\n\n        console.println(Msg.m_decrypt_bb_has_proofs, getYesNoMessage(allInBb));\n        return allCorrect && allInBb;\n    }\n\n    private boolean verifyTally(PlainBallotBox pbb, Path tallyPath) throws Exception {\n        console.println(Msg.m_tally_loading, tallyPath);\n        Tally tally = Json.read(tallyPath, Tally.class);\n        console.println(Msg.m_tally_loaded);\n\n        boolean electionMatches = tally.getElection().equals(pbb.election());\n        if (!electionMatches) {\n            log.warn(\"The tally is for election '{}' while the plaintext ballot box is for election '{}'\",\n                    tally.getElection(), pbb.election());\n            return false;\n        }\n\n        boolean parishesMatch = tally.getByParish().keySet().equals(pbb.byParish().keySet());\n        if (!parishesMatch) {\n            log.warn(\"Parishes do not match between the tally and the plaintext ballot box\");\n            return false;\n        }\n\n        // This iteration is comprehensive since the parish sets must now be equal.\n        for (Map.Entry<String, Map<String, List<String>>> parishMap : pbb.byParish().entrySet()) {\n            String parishId = parishMap.getKey();\n            Map<String, List<String>> districtVotes = parishMap.getValue();\n\n            Map<String, Map<String, Integer>> parishDistricts = tally.getByParish().get(parishId);\n            boolean districtsMatch = parishDistricts.keySet().equals(districtVotes.keySet());\n            if (!districtsMatch) {\n                log.warn(\"Districts do not match between the tally and the plaintext ballot box\");\n                return false;\n            }\n\n            // This iteration is comprehensive since the district sets must now be equal.\n            for (Map.Entry<String, List<String>> votesMap : districtVotes.entrySet()) {\n                // The plaintext votes for that district.\n                List<String> votes = votesMap.getValue();\n\n                // Counter the (choice, count) pairs from the plaintext BB entry.\n                Map<String, Integer> counts = votes.stream()\n                        .collect(Collectors.groupingBy(c -> c, Collectors.summingInt(c -> 1)));\n                // The tallied (choice, count) pairs. Create a copy to avoid mutation.\n                Map<String, Integer> tallyCounts = new LinkedHashMap<>(parishDistricts.get(votesMap.getKey()));\n\n                for (Map.Entry<String, Integer> count : counts.entrySet()) {\n                    // The tally counts must match the count obtained from the plaintext BB.\n                    if (!tallyCounts.remove(count.getKey()).equals(count.getValue())) {\n                        log.warn(\"Plaintext ballot box and tally mismatch for the choice '{}'\", count.getKey());\n                        return false;\n                    }\n                }\n\n                // Whatever choice is present in the tally map should now have 0 votes.\n                // The plaintext BB choices have already been checked for validity.\n                for (Integer val : tallyCounts.values()) {\n                    if (val != 0) {\n                        log.warn(\"Vote count > 0 for a choice not present in the plaintext ballot box\");\n                        return false;\n                    }\n                }\n            }\n        }\n\n        // We have checked the byParish tally. The byDistrict tally must also be checked.\n        // Since byParish is valid, simply check whether the byDistrict tally obtained\n        // from the byParish tally matches with the byDistrict tally read form the tally.\n        return tally.getByDistrict().equals(tally.computeByDistrict());\n    }\n\n    private boolean isValidChoice(Plaintext pt, String district, CandidateList candidates) {\n        String candidateCode;\n        try {\n            candidateCode = pt.getUTF8DecodedMessage();\n        } catch (IllegalArgumentException ignored) {\n            return false;\n        }\n\n        // Get choices list\n        Map<String, Map<String, Map<String, String>>> ds = candidates.getCandidates();\n        // Ensure that vote district exists in a choices list\n        if (!ds.containsKey(district)) {\n            return false;\n        }\n\n        // Get choices for a specific district (district that vote belongs to)\n        Map<String, Map<String, String>> ps = ds.get(district);\n        // Ensure that there are choices per that district\n        if (ps.isEmpty()) {\n            return false;\n        }\n\n        // Loop over all choices per that district\n        for (Map<String, String> parties : ps.values()) {\n            // Is there a candidate code that matches candidateCode\n            if (parties.containsKey(candidateCode)) {\n                return true;\n            }\n        }\n\n        // Choice is not valid\n        return false;\n    }\n\n    private InvalidDecProofs verifyDecryption(Proof input, ElGamalPublicKey pub,\n                                              int threadCount, Bag<String> b64CTs) throws Exception {\n        InvalidDecProofs idp = new InvalidDecProofs(input.getElection());\n        ExecutorService ioExecutor = Executors.newFixedThreadPool(2);\n        CompletionService<Void> CompService = new ExecutorCompletionService<>(ioExecutor);\n\n        ExecutorService verifyExecutor;\n        threadCount = threadCount > 0 ? threadCount : 1;\n        verifyExecutor = new ThreadPoolExecutor(threadCount, threadCount, 0L, TimeUnit.MILLISECONDS,\n                new ArrayBlockingQueue<>(threadCount * 2));\n\n        WorkManager manager =\n                new WorkManager(input, getVerifyConsumer(pub, idp, b64CTs), verifyExecutor, idp);\n        CompService.submit(manager);\n        CompService.submit(idp.getResultWorker());\n\n        try {\n            for (int done = 0; done < 2; done++) {\n                CompService.take().get();\n            }\n        } finally {\n            ioExecutor.shutdown();\n            verifyExecutor.shutdown();\n        }\n        return idp;\n    }\n\n    private Consumer<Proof.ProofJson> getVerifyConsumer(ElGamalPublicKey pub,\n                                                        InvalidDecProofs out,\n                                                        Bag<String> b64CTs) {\n        return (proofJson) -> {\n            GroupElement decrypted = pub.getParameters().getGroup().getElement(proofJson.getMessage());\n            ElGamalCiphertext ct =\n                    new ElGamalCiphertext(pub.getParameters(), proofJson.getCiphertext());\n            ElGamalDecryptionProof proof =\n                    new ElGamalDecryptionProof(ct, decrypted, pub, proofJson.getProof());\n\n            // Get the base64-encoded ballot for later comparison with the ballot box.\n            b64CTs.add(Base64.getEncoder().encodeToString(proofJson.getCiphertext()));\n\n            try {\n                boolean res = proof.verifyProof();\n                if (!res) {\n                    log.warn(\"Proof verification failed: {}\", proof);\n                    out.addInvalidProof(proof);\n                }\n            } catch (MathException e) {\n                log.warn(\"Proof verification exception: {}, {}\", proof, e);\n                out.addInvalidProof(proof);\n            }\n        };\n    }\n\n    /**\n     * Verifies whether ciphertexts are part of the anonymous ballot box.\n     *\n     * @param abb    the anonymous ballot box\n     * @param b64CTs the base64-encoded ciphertexts to check inclusion for\n     * @return true if all the ciphertexts are included, false otherwise\n     */\n    private boolean verifyCiphersInBb(AnonymousBallotBox abb, Bag<String> b64CTs) {\n        // Do not mutate the ciphers.\n        Bag<String> b64copy = new HashBag<>(b64CTs);\n\n        for (Map<String, Map<String, List<byte[]>>> sMap : abb.getDistricts().values()) {\n            for (Map<String, List<byte[]>> qMap : sMap.values()) {\n                for (List<byte[]> cList : qMap.values()) {\n                    Iterator<byte[]> i = cList.iterator();\n                    while (i.hasNext()) {\n                        byte[] c = i.next();\n                        String b64ct = Base64.getEncoder().encodeToString(c);\n                        if (!b64copy.remove(b64ct, 1)) continue;\n                        i.remove();\n                    }\n                }\n            }\n        }\n\n        // Verify whether all entries were indeed in the anonymous ballot box.\n        boolean allInBb = b64copy.isEmpty();\n        if (!allInBb) {\n            log.warn(\"There were {} ballots missing from the input ballot box:\", b64copy.size());\n            logMissingBallots(b64copy);\n        }\n        return allInBb;\n    }\n\n    private boolean verifyInvalidConsistency(Bag<String> discardedCiphers, Bag<String> iProofCiphers) {\n        if (iProofCiphers.equals(discardedCiphers)) return true;\n\n        // Create copies to avoid mutating inputs.\n        Bag<String> iProofCopy = new HashBag<>(iProofCiphers);\n        Bag<String> discardedCopy = new HashBag<>(discardedCiphers);\n\n        // Obtain the differences and log them.\n        iProofCopy.removeAll(discardedCiphers);\n        discardedCopy.removeAll(iProofCiphers);\n\n        log.warn(\"There were {} ballots missing from the discarded votes file:\", iProofCopy.size());\n        logMissingBallots(iProofCopy);\n\n        log.warn(\"There were {} ballots missing from the invalid proofs:\", discardedCopy.size());\n        logMissingBallots(discardedCopy);\n\n        return false;\n    }\n\n    private void logMissingBallots(Collection<String> b64CTs) {\n        for (String b64ct : b64CTs) log.warn(\"Missing ballot: {}\", b64ct);\n    }\n\n    /**\n     * Verifies plaintext ballot box consistency with the validity proofs.\n     * <p>\n     * Carries out the verification solely based on the choice identifier, and not\n     * on the complete plaintext vote. The full validation of plaintexts in the proofs\n     * file must be carried out beforehand for stronger consistency guarantees.\n     *\n     * @param pub          the election public key\n     * @param proofs       the validity proofs\n     * @param pbb          the plaintext ballot box\n     * @param invalidCount the number of expected invalid votes\n     * @return true if the verification succeeds, false otherwise\n     */\n    private boolean verifyValidConsistency(ElGamalPublicKey pub, Proof proofs, PlainBallotBox pbb, int invalidCount) {\n        Bag<String> plainVotes = new HashBag<>(pbb.byDistrict().values().stream()\n                .flatMap(Collection::stream).toList());\n        console.println(Msg.m_plain_count, plainVotes.size());\n\n        console.println();\n        console.println(Msg.m_decrypt_consistent_valids_begin);\n\n        boolean noExtra = true;\n        try {\n            for (Proof.ProofJson proof : proofs.getProofs()) {\n                GroupElement decrypted = pub.getParameters().getGroup().getElement(proof.getMessage());\n                Group group = decrypted.getGroup();\n                Plaintext decoded = group.decode(decrypted);\n                // Note! Here we do not validate the full vote, meaning that the validation\n                // should be done prior to calling this function.\n                String voteCode = group.unpad(decoded).getUTF8DecodedMessage();\n                if (plainVotes.remove(voteCode, 1)) continue;\n                noExtra = false;\n                log.warn(\"A vote for {} is missing from the plaintext ballot box\", voteCode);\n            }\n        } catch (Exception e) {\n            // Barring software errors, this can happen only if\n            // the proofs file is manipulated/corrupted.\n            log.error(e.toString());\n            return false;\n        }\n\n        // There were proofs for votes not present in the plaintext BB.\n        if (!noExtra) {\n            log.warn(\"There were proofs for votes not present in the plaintext ballot box\");\n            return false;\n        }\n\n        // We have checked the byDistrict votes. The byParish votes must also be checked.\n        // Since byDistrict is valid, simply check whether the byDistrict votes obtained\n        // from the byParish votes matches with the byDistrict votes read form the plaintext BB.\n        boolean tallyConsistent = pbb.byDistrict().equals(pbb.computeByDistrict());\n        if (!tallyConsistent) {\n            log.warn(\"byparish inconsistent with bydistrict in the plaintext ballot box\");\n            return false;\n        }\n\n        return plainVotes.size() == invalidCount;\n    }\n\n    private boolean verifyInputFiles(DecryptArgs args) {\n        List<Path> paths = new ArrayList<>(List.of(\n                args.inputPath.value(),\n                args.pubPath.value(),\n                args.discardedInputPath.value(),\n                args.anonBoxPath.value(),\n                args.plainBoxPath.value(),\n                args.tallyPath.value(),\n                args.districts.value(),\n                args.candidates.value()));\n\n        if (Objects.nonNull(args.invalidInputPath.value()))\n            paths.add(args.invalidInputPath.value());\n\n        for (Path path : paths) {\n            if (Files.notExists(path)) {\n                console.println();\n                console.println(Msg.e_file_missing, path);\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public static class DecryptArgs extends Args {\n        Arg<Path> inputPath = Arg.aPath(Msg.arg_proofs);\n        Arg<Path> invalidInputPath = Arg.aPath(Msg.arg_invalidity_proofs).setOptional();\n        Arg<Path> pubPath = Arg.aPath(Msg.arg_pub);\n        Arg<Path> anonBoxPath = Arg.aPath(Msg.arg_anon_bb);\n        Arg<Path> plainBoxPath = Arg.aPath(Msg.arg_plain_bb);\n        Arg<Path> tallyPath = Arg.aPath(Msg.arg_tally);\n        Arg<Path> discardedInputPath = Arg.aPath(Msg.arg_discarded);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts);\n        Arg<Path> candidates = Arg.aPath(Msg.arg_candidates);\n        Arg<Path> outputPath = Arg.aPath(Msg.arg_out, false, true);\n        Arg<Boolean> abortEarly = Arg.aFlag(Msg.arg_abort_early).setDefault(true);\n\n        public DecryptArgs() {\n            super();\n            args.add(inputPath);\n            args.add(pubPath);\n            args.add(discardedInputPath);\n            args.add(anonBoxPath);\n            args.add(plainBoxPath);\n            args.add(tallyPath);\n            args.add(districts);\n            args.add(candidates);\n            args.add(outputPath);\n            args.add(invalidInputPath);\n            args.add(abortEarly);\n        }\n    }\n\n    private class WorkManager implements Callable<Void> {\n        private final Proof in;\n        private final Consumer<Proof.ProofJson> consumer;\n        private final ExecutorService verifyExecutor;\n        private final InvalidDecProofs idp;\n\n        WorkManager(Proof in, Consumer<Proof.ProofJson> consumer, ExecutorService verifyExecutor,\n                    InvalidDecProofs idp) {\n            this.in = in;\n            this.consumer = consumer;\n            this.verifyExecutor = verifyExecutor;\n            this.idp = idp;\n        }\n\n        @Override\n        public Void call() throws Exception {\n            Progress progress = console.startProgress(in.getCount());\n            in.getProofs().forEach(proof -> {\n                boolean taskAdded = false;\n                do {\n                    try {\n                        verifyExecutor.execute(() -> consumer.accept(proof));\n                        taskAdded = true;\n                    } catch (RejectedExecutionException e) {\n                        try {\n                            Thread.sleep(20);\n                        } catch (InterruptedException e1) {\n                            log.warn(\"Unexpected interruption\", e1);\n                        }\n                    }\n                } while (!taskAdded);\n                progress.increase(1);\n            });\n            verifyExecutor.shutdown();\n            verifyExecutor.awaitTermination(1, TimeUnit.DAYS);\n            idp.setEot();\n            progress.finish();\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/tools/IntegrityTool.java",
    "content": "package ee.ivxv.audit.tools;\n\nimport ee.ivxv.audit.AuditContext;\nimport ee.ivxv.audit.Msg;\nimport ee.ivxv.audit.tools.IntegrityTool.IntegrityArgs;\nimport ee.ivxv.audit.util.RawBallotWithDigest;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.hash.HashType;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.service.bbox.impl.FileSource;\nimport ee.ivxv.common.service.bbox.impl.ZipSource;\nimport ee.ivxv.common.service.bbox.impl.ZipSourceRaw;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.apache.commons.collections4.Bag;\nimport org.apache.commons.collections4.bag.TreeBag;\n\nimport java.io.BufferedReader;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class IntegrityTool implements Tool.Runner<IntegrityArgs> {\n\n    private static final String BDOC_EXTENSION = \".bdoc\";\n    private static final String ASICE_EXTENSION = \".asice\";\n    private static final String RAW_BALLOT_EXTENSION = \".ballot\";\n    private static final Character DIR_SEP = '/';\n    private static final String EXPECTED_BALLOT_REF = \"<voter-id>\" + DIR_SEP + \"<ballot-id>+XXXX\";\n\n    private static final String CSV_FIELD_SEPARATOR = \"\\t\";\n    private static final int LOG_HASH_ENTRY_FIELDS = 5;\n    private static final int BB_ERRORS_ENTRY_FIELDS = 3;\n\n    private static final String BB_ERRORS_BALLOT_REGEX = \"^\\\\d{11}/\\\\d+\\\\+\\\\d{4}$\";\n\n    private final Logger log = LoggerFactory.getLogger(IntegrityTool.class);\n    private final AuditContext ctx;\n    private final I18nConsole console;\n\n    public IntegrityTool(AuditContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    @Override\n    public boolean run(IntegrityArgs args) throws Exception {\n        // Verify whether all provided files are found on the filesystem.\n        boolean allFilesExist = verifyInputFiles(args);\n        if (!allFilesExist) {\n            console.println();\n            return false;\n        }\n\n        boolean abortEarly = args.abortEarly.value();\n\n        console.println();\n\n        console.println(Msg.m_bb_loading, args.bb.value());\n        List<RawBallotWithDigest> ballots = getBallots(args);\n        console.println(Msg.m_bb_loaded);\n        BigInteger checksumAll = sumBbHashes(ballots);\n\n        console.println();\n\n        // Checksums of ballots in the processed ballot box.\n        console.println(Msg.m_anon_loading, args.anonBoxPath.value());\n        AnonymousBallotBox anonBb = Json.read(args.anonBoxPath.value(), AnonymousBallotBox.class);\n        AnonBbDigests anonDigests = getAnonBbDigests(anonBb);\n        console.println(Msg.m_anon_loaded);\n        console.println();\n\n        console.println(Msg.m_log_accepted, args.acceptedLogPath.value());\n        console.println(Msg.m_log_squashed, args.squashedLogPath.value());\n        console.println(Msg.m_log_revoked, args.revokedLogPath.value());\n        console.println(Msg.m_log_anonymised, args.anonLogPath.value());\n        console.println(Msg.m_bb_errors, args.bbErrorsPath.value());\n\n        console.println();\n\n        // Checksums of all accepted (valid received) ballots.\n        List<byte[]> acceptedDigests = getBallotDigestsFromLog(args.acceptedLogPath.value());\n        BigInteger acceptedDigestsSum = sumHashes(acceptedDigests);\n\n        Set<String> erroredBallotNames = getErroredBallotsNames(args.bbErrorsPath.value());\n        BbDigests bbDigests = getValidInvalidSums(ballots, acceptedDigests, erroredBallotNames);\n        if (bbDigests == null) {\n            // This should happen only if ballots in the original ballot box appear in neither\n            // the error log nor the accepted log.\n            console.println(Msg.m_integrity_bb_consistent, getYesNoMessage(false));\n            if (abortEarly) {\n                console.println();\n                return true;\n            }\n        } else {\n            // This check should never fail but is here for clarity.\n            // If true: accepted ballots in the original box are consistent with the log of accepted ballots.\n            boolean validChecksumsMatch = bbDigests.validSum.equals(acceptedDigestsSum);\n\n            // This check should never fail but is here for clarity.\n            // If true: accepted ballots + refused ballots = original box.\n            boolean sumIsConsistent = checksumAll.equals(bbDigests.invalidSum.add(bbDigests.validSum));\n\n            boolean bbIsConsistent = validChecksumsMatch && sumIsConsistent;\n            console.println(Msg.m_integrity_bb_consistent, getYesNoMessage(bbIsConsistent));\n            if (!bbIsConsistent) {\n\n                if (abortEarly) {\n                    console.println();\n                    return true;\n                }\n\n                // Check if accepted ballots are included in the original box.\n                // This provides more information on the inconsistency failure.\n                boolean acceptedMatch = matchHashesToBb(acceptedDigests, ballots);\n                console.println(Msg.m_integrity_match_valid, getYesNoMessage(acceptedMatch));\n            }\n        }\n\n        // Checksums of accepted ballots that were squashed.\n        List<byte[]> squashedDigests = getBallotDigestsFromLog(args.squashedLogPath.value());\n        BigInteger squashedDigestsSum = sumHashes(squashedDigests);\n\n        // Checksums of accepted ballots that were revoked.\n        List<byte[]> revokedDigests = getBallotDigestsFromLog(args.revokedLogPath.value());\n        BigInteger revokedDigestsSum = sumHashes(revokedDigests);\n\n        // Checksums of accepted ballots forwarded for tallying (remaining after squash + revoke).\n        List<byte[]> anonLogDigests = getBallotDigestsFromLog(args.anonLogPath.value());\n        BigInteger anonLogDigestsSum = sumHashes(anonLogDigests);\n\n        // If true: anon ballots are consistent with the log of anon ballots.\n        boolean anonDigestMatches = anonDigests.checksumSum.equals(anonLogDigestsSum);\n        console.println(Msg.m_integrity_match_anon_logs, getYesNoMessage(anonDigestMatches));\n        if (!anonDigestMatches && abortEarly) {\n            console.println();\n            return true;\n        }\n\n        BigInteger postprocessDigestSum = squashedDigestsSum.add(revokedDigestsSum.add(anonLogDigestsSum));\n\n        // If true: anon + revoked + squashed = all accepted ballots.\n        boolean acceptedDigestMatches = acceptedDigestsSum.equals(postprocessDigestSum);\n        console.println(Msg.m_integrity_match_logs, getYesNoMessage(acceptedDigestMatches));\n\n        // If all checks succeed, the following audit trail is verified:\n        // - anon BB matches with anon log\n        // - invalid ballots (log) + valid ballots (log) = original BB\n        // - anon ballots (log) + squashed ballots (log) + revoked ballots (log) = valid ballots (log)\n        // Inferred: anon ballots are part of the original ballot box.\n\n        console.println();\n\n        return true;\n    }\n\n    private Msg getYesNoMessage(boolean success) {\n        if (success) return Msg.m_yes;\n        return Msg.m_no;\n    }\n\n    private List<RawBallotWithDigest> getBallots(IntegrityArgs args) {\n        List<RawBallotWithDigest> ballots = new ArrayList<>();\n\n        // Read ballot box\n        FileSource zipContainer = new ZipSource(args.bb.value());\n\n        // Start progress, counting from 0 up to the amount of ballots in a ballot box\n        int ballotCount = zipContainer.countFilesWithSuffix(Arrays.asList(BDOC_EXTENSION, ASICE_EXTENSION));\n        Progress progress = console.startProgress(ballotCount);\n\n        // Read ballots from a ballot box one by one\n        zipContainer.processFiles((fileName, fileContent) -> {\n\n            // Only look for voter signed ballots\n            if (fileName.contains(BDOC_EXTENSION) || fileName.contains(ASICE_EXTENSION)) {\n                try {\n                    // Read voter signed container as byte stream\n                    FileSource signedContainer = new ZipSourceRaw(fileContent.readAllBytes());\n\n                    // Signed container is a zip file, that contains files inside\n                    signedContainer.processFiles((ballotName, ballotContent) -> {\n\n                        // Only look for a ballot file\n                        if (ballotName.contains(RAW_BALLOT_EXTENSION)) {\n                            try {\n                                byte[] ballot = ballotContent.readAllBytes();\n                                String ballotId = getBallotContainerId(fileName);\n                                ballots.add(new RawBallotWithDigest(ballot, ballotId));\n                            } catch (Exception e) {\n                                throw new RuntimeException(e);\n                            }\n                        }\n                    });\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n                progress.increase(1);\n            }\n        });\n\n        progress.finish();\n\n        return ballots;\n    }\n\n    private static String getBallotContainerId(String fileName) throws Exception {\n        // Getting the container path in the BB is necessary\n        // for ballot identification through the error log.\n        int i = fileName.lastIndexOf(DIR_SEP);\n        if (i < 0) {\n            // It's OK to throw since we can reasonably expect a valid ballot box.\n            throw new Exception(\"Expected name \" + EXPECTED_BALLOT_REF + \" but got \" + fileName);\n        }\n        int j = fileName.lastIndexOf(DIR_SEP, i - 1);\n        int extensionLength = fileName.contains(BDOC_EXTENSION) ?\n                BDOC_EXTENSION.length() :\n                ASICE_EXTENSION.length();\n        String ballotContainerId = fileName.substring(j + 1, fileName.length() - extensionLength);\n        if (!ballotContainerId.matches(BB_ERRORS_BALLOT_REGEX)) {\n            throw new Exception(\"Expected name \" + EXPECTED_BALLOT_REF + \" but got \" + fileName);\n        }\n        return ballotContainerId;\n    }\n\n    private BbDigests getValidInvalidSums(List<RawBallotWithDigest> bb,\n                                          List<byte[]> acceptedDigests, Set<String> invalidBallotIds) {\n        BigInteger invalidBallotChecksumSum = BigInteger.ZERO;\n        BigInteger validBallotChecksumSum = BigInteger.ZERO;\n\n        // Use a tree bag since comparing arrays of bytes does not work in a regular bag.\n        // We must use a bag instead of a set since it might happen (statistically unlikely unless intentional)\n        // that there are ballots with the same ciphertext. Therefore, a set would not correctly represent the\n        // state of the accepted ballots.\n        // Use a tree bag since comparing arrays of bytes does not work in a regular bag.\n        Bag<byte[]> digests = new TreeBag<>(Arrays::compare);\n        digests.addAll(acceptedDigests);\n\n        // Notify in case of multiple matching ciphertexts so that the occurrence(s) can be investigated further.\n        int recurringCts = digests.size() - digests.uniqueSet().size();\n        if (recurringCts != 0) {\n            console.println(Msg.m_integrity_recurring_ct);\n            log.warn(\"There are {} ciphertext recurrences among the accepted ballots\", recurringCts);\n\n            Set<byte[]> recurringDigests = new HashSet<>();\n            for (byte[] c : digests) if (digests.getCount(c) != 1) recurringDigests.add(c);\n            for (byte[] c : recurringDigests) log.warn(\"Recurring ballot: {}\", Base64.getEncoder().encodeToString(c));\n            console.println();\n        }\n\n        // Avoid mutating the input set. These are IDs so they must be unique.\n        Set<String> invalids = new HashSet<>(invalidBallotIds);\n\n        boolean errored = false;\n\n        for (RawBallotWithDigest ballot : bb) {\n            // Make sure that each ballot is referenced in exactly one file.\n            int seenCount = 0;\n\n            if (invalids.remove(ballot.getId())) {\n                invalidBallotChecksumSum =\n                        invalidBallotChecksumSum.add(new BigInteger(1, ballot.getRawDigest()));\n                ++seenCount;\n            }\n\n            if (digests.remove(ballot.getRawDigest(), 1)) {\n                validBallotChecksumSum =\n                        validBallotChecksumSum.add(new BigInteger(1, ballot.getRawDigest()));\n                ++seenCount;\n            }\n\n            if (seenCount == 0) {\n                log.warn(\"Ballot '{}' not found in the acceptance/rejection logs\", ballot.getId());\n                errored = true;\n            }\n\n            if (seenCount > 1) {\n                log.warn(\"Ballot '{}' present in both the acceptance and rejection logs\", ballot.getId());\n                errored = true;\n            }\n        }\n\n        if (errored) return null;\n\n        // Make sure that there are not more log entries than there are ballots in the BB.\n        if (!digests.isEmpty() || !invalids.isEmpty()) {\n            for (String invalid : invalids)\n                log.warn(\"Ballot '{}' is missing from the original ballot box\", invalid);\n            for (byte[] digest : digests)\n                log.warn(\"Missing from the original ballot box: {}\", Base64.getEncoder().encodeToString(digest));\n            return null;\n        }\n\n        return new BbDigests(invalidBallotChecksumSum, validBallotChecksumSum);\n    }\n\n    /**\n     * Reads the digests from a processor logfile.\n     *\n     * @param path the logfile path\n     * @return a list of digests extracted from the logfile\n     * @throws Exception If the logfile does not exist or cannot be properly read\n     */\n    private List<byte[]> getBallotDigestsFromLog(Path path) throws Exception {\n        List<byte[]> hashes = new ArrayList<>();\n\n        BufferedReader br = new BufferedReader(new FileReader(path.toFile()));\n        String line;\n        while ((line = br.readLine()) != null) {\n            String[] values = line.split(CSV_FIELD_SEPARATOR);\n            if (values.length != LOG_HASH_ENTRY_FIELDS) continue;\n\n            byte[] hash = Base64.getDecoder().decode(values[1]);\n            hashes.add(hash);\n        }\n\n        return hashes;\n    }\n\n    /**\n     * Reads ballot identifiers from the ballot validation error report.\n     *\n     * @param path the report path\n     * @return a set of ballot identifiers extracted from the report\n     * @throws Exception If the report cannot be found be read\n     */\n    private Set<String> getErroredBallotsNames(Path path) throws Exception {\n        // Use a set since there may be multiple errors pertaining to the\n        // same ballot in the log.\n        Set<String> erroredBallotNames = new HashSet<>();\n\n        BufferedReader br = new BufferedReader(new FileReader(path.toFile()));\n        String line;\n        while ((line = br.readLine()) != null) {\n            String[] values = line.split(CSV_FIELD_SEPARATOR);\n            if (values.length != BB_ERRORS_ENTRY_FIELDS) continue;\n            if (!values[0].matches(BB_ERRORS_BALLOT_REGEX)) continue;\n\n            erroredBallotNames.add(values[0]);\n        }\n\n        return erroredBallotNames;\n    }\n\n    private static BigInteger sumHashes(List<byte[]> hashes) {\n        BigInteger sum = BigInteger.ZERO;\n\n        for (byte[] hash : hashes) {\n            sum = sum.add(new BigInteger(1, hash));\n        }\n\n        return sum;\n    }\n\n    private static BigInteger sumBbHashes(List<RawBallotWithDigest> bb) {\n        BigInteger sum = BigInteger.ZERO;\n\n        for (RawBallotWithDigest ballot : bb) {\n            sum = sum.add(new BigInteger(1, ballot.getRawDigest()));\n        }\n\n        return sum;\n    }\n\n    private AnonBbDigests getAnonBbDigests(AnonymousBallotBox bb) {\n        // Also extract checksums in a list to avoid looping again later.\n        List<byte[]> digests = new ArrayList<>();\n        BigInteger sum = BigInteger.ZERO;\n\n        for (Map<String, Map<String, List<byte[]>>> smap : bb.getDistricts().values()) {\n            for (Map<String, List<byte[]>> qmap : smap.values()) {\n                for (List<byte[]> clist : qmap.values()) {\n                    for (byte[] c : clist) {\n                        byte[] hash = HashType.SHA256.getFunction().digest(c);\n                        digests.add(hash);\n                        sum = sum.add(new BigInteger(1, hash));\n                    }\n                }\n            }\n        }\n\n        return new AnonBbDigests(digests, sum);\n    }\n\n    /**\n     * Verifies whether digests correspond to ballots in a ballot box.\n     *\n     * @param digests the digests of some set of ballots\n     * @param bb      the ballot box to match against\n     * @return whether all ballots were present in the box\n     */\n    private boolean matchHashesToBb(List<byte[]> digests, List<RawBallotWithDigest> bb) {\n        boolean allOk = true;\n        // Potential perf improvement: would it be faster to iterate over the full ballot box\n        // and try to remove them from a set of digests? The return value is then whether\n        // the set is empty. During normal execution this should never be invoked in any case.\n        for (byte[] digest : digests) {\n            boolean matches = bb.stream().anyMatch(b -> Arrays.equals(digest, b.getRawDigest()));\n            if (!matches) {\n                log.warn(\"Missing from the original ballot box: {}\", Base64.getEncoder().encodeToString(digest));\n                allOk = false;\n            }\n        }\n        return allOk;\n    }\n\n    record BbDigests(BigInteger invalidSum, BigInteger validSum) {\n    }\n\n    record AnonBbDigests(List<byte[]> digests, BigInteger checksumSum) {\n    }\n\n    private boolean verifyInputFiles(IntegrityArgs args) {\n        List<Path> paths = new ArrayList<>(List.of(\n                args.bb.value(),\n                args.anonBoxPath.value(),\n                args.acceptedLogPath.value(),\n                args.squashedLogPath.value(),\n                args.revokedLogPath.value(),\n                args.anonLogPath.value(),\n                args.bbErrorsPath.value()));\n\n        for (Path path : paths) {\n            if (Files.notExists(path)) {\n                console.println();\n                console.println(Msg.e_file_missing, path);\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n\n    public static class IntegrityArgs extends Args {\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> anonBoxPath = Arg.aPath(Msg.arg_anon_bb);\n        Arg<Path> acceptedLogPath = Arg.aPath(Msg.arg_log_accepted);\n        Arg<Path> squashedLogPath = Arg.aPath(Msg.arg_log_squashed);\n        Arg<Path> revokedLogPath = Arg.aPath(Msg.arg_log_revoked);\n        Arg<Path> anonLogPath = Arg.aPath(Msg.arg_log_anonymised);\n        Arg<Path> bbErrorsPath = Arg.aPath(Msg.arg_bb_errors);\n        Arg<Boolean> abortEarly = Arg.aFlag(Msg.arg_abort_early).setDefault(true);\n\n        public IntegrityArgs() {\n            super();\n            args.add(bb);\n            args.add(anonBoxPath);\n            args.add(acceptedLogPath);\n            args.add(squashedLogPath);\n            args.add(revokedLogPath);\n            args.add(anonLogPath);\n            args.add(bbErrorsPath);\n            args.add(abortEarly);\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/tools/MixerTool.java",
    "content": "package ee.ivxv.audit.tools;\n\nimport ee.ivxv.audit.AuditContext;\nimport ee.ivxv.audit.Msg;\nimport ee.ivxv.audit.shuffle.ShuffleConsole;\nimport ee.ivxv.audit.shuffle.ShuffleException;\nimport ee.ivxv.audit.shuffle.ShuffleProof;\nimport ee.ivxv.audit.shuffle.ThreadedVerifier;\nimport ee.ivxv.audit.shuffle.Verifier;\nimport ee.ivxv.audit.tools.MixerTool.MixerArgs;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.util.I18nConsole;\nimport java.nio.file.Path;\n\n/**\n * Tool for verifying the correctness of the shuffle.\n */\npublic class MixerTool implements Tool.Runner<MixerArgs> {\n    private final I18nConsole console;\n    private AuditContext ctx;\n\n    public static class MixerArgs extends Args {\n        Arg<Path> protPath = Arg.aPath(Msg.arg_protinfo, true, false);\n        Arg<Path> proofPath = Arg.aPath(Msg.arg_proofdir, true, true);\n        Arg<Boolean> threaded = Arg.aFlag(Msg.arg_threaded);\n\n        public MixerArgs() {\n            super();\n            args.add(protPath);\n            args.add(proofPath);\n            args.add(threaded);\n        }\n    }\n\n    public MixerTool(AuditContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    @Override\n    public boolean run(MixerArgs args) throws Exception {\n        console.println();\n        console.println(Msg.m_shuffle_proof_loading, args.protPath.value(), args.proofPath.value());\n        ShuffleConsole sc = new ShuffleConsole(console);\n        ShuffleProof proof = new ShuffleProof(args.protPath.value(), args.proofPath.value(), sc);\n        Verifier ver;\n        if (args.threaded.value()) {\n            ver = new ThreadedVerifier(sc, proof, ctx.args.threads.value());\n        } else {\n            ver = new Verifier(sc, proof);\n        }\n        boolean res = false;\n        try {\n            res = ver.verify_all();\n        } catch (ShuffleException e) {\n            console.println(Msg.m_shuffle_proof_failed_reason, e);\n            res = false;\n        }\n        if (res) {\n            console.println(Msg.m_shuffle_proof_succeeded);\n        } else {\n            console.println(Msg.m_shuffle_proof_failed);\n        }\n        return res;\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/util/DiscardedBallot.java",
    "content": "package ee.ivxv.audit.util;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.List;\n\npublic class DiscardedBallot {\n    private final String election;\n\n    private final List<VoteJson> discarded;\n\n    @JsonCreator\n    public DiscardedBallot(\n            @JsonProperty(\"election\") String election,\n            @JsonProperty(\"invalid\") List<VoteJson> invalid) {\n        this.election = election;\n        this.discarded = invalid;\n    }\n\n    public List<VoteJson> getDiscarded() {\n        return discarded;\n    }\n\n    @JsonIgnore\n    public int getCount() {\n        return discarded.size();\n    }\n\n    public static class VoteJson {\n        private final String district;\n        private final String station;\n        private final String question;\n        private final byte[] vote;\n\n        @JsonCreator\n        private VoteJson(\n                @JsonProperty(\"district\") String district,\n                @JsonProperty(\"station\") String station,\n                @JsonProperty(\"question\") String question,\n                @JsonProperty(\"vote\") byte[] vote) {\n            this.district = district;\n            this.station = station;\n            this.question = question;\n            this.vote = vote;\n        }\n\n        public byte[] getVote() {\n            return vote;\n        }\n    }\n\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/util/InvalidDecProofs.java",
    "content": "package ee.ivxv.audit.util;\n\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.common.model.Proof;\nimport ee.ivxv.common.util.*;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.LinkedBlockingQueue;\n\n/**\n * Class for handling ciphertexts with invalid proofs.\n */\npublic class InvalidDecProofs {\n    private static final Path FAILED_PROOF_VALID_PATH = Paths.get(\"failed-validity\");\n    private static final Path FAILED_PROOF_INVALID_PATH = Paths.get(\"failed-invalidity\");\n    private final BlockingQueue<Object> in = new LinkedBlockingQueue<>();\n    private final Proof invalidProofs;\n\n    /**\n     * Initialize using the election identifier.\n     *\n     * @param electionName\n     */\n    public InvalidDecProofs(String electionName) {\n        invalidProofs = new Proof(electionName);\n    }\n\n    /**\n     * Get the worker for parsing all added proofs.\n     *\n     * @return\n     */\n    public InvalidProofHandler getResultWorker() {\n        return new InvalidProofHandler();\n    }\n\n    /**\n     * Set the terminator after last added invalid proof.\n     */\n    public void setEot() {\n        in.add(ee.ivxv.common.util.Util.EOT);\n    }\n\n    /**\n     * Add invalid proof.\n     *\n     * @param proof\n     */\n    public void addInvalidProof(ElGamalDecryptionProof proof) {\n        in.add(proof);\n    }\n\n    /**\n     * Get the total number of invalid proofs.\n     *\n     * @return\n     */\n    public int getCount() {\n        return invalidProofs.getCount();\n    }\n\n    /**\n     * Serialize the structure to directory.\n     *\n     * @param outDir\n     * @throws Exception\n     */\n    public void outputFailedProofsOfValidity(Path outDir) throws Exception {\n        Json.write(invalidProofs, outDir.resolve(FAILED_PROOF_VALID_PATH));\n    }\n\n    /**\n     * Serialize the structure to directory.\n     *\n     * @param outDir\n     * @throws Exception\n     */\n    public void outputFailedProofsOfInvalidity(Path outDir) throws Exception {\n        Json.write(invalidProofs, outDir.resolve(FAILED_PROOF_INVALID_PATH));\n    }\n\n    private class InvalidProofHandler implements Callable<Void> {\n\n        @Override\n        public Void call() throws Exception {\n            Object obj;\n            while ((obj = in.take()) != ee.ivxv.common.util.Util.EOT) {\n                if (obj instanceof ElGamalDecryptionProof) {\n                    invalidProofs.addProof((ElGamalDecryptionProof) obj);\n                } else {\n                    throw new IllegalArgumentException(\n                            \"Unexpected decryption result type: \" + obj.getClass());\n                }\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "auditor/src/main/java/ee/ivxv/audit/util/RawBallotWithDigest.java",
    "content": "package ee.ivxv.audit.util;\n\nimport ee.ivxv.common.crypto.hash.HashType;\n\n\npublic class RawBallotWithDigest {\n    private final String id;\n\n    private final byte[] ballot;\n\n    private final byte[] rawDigest;\n\n    public RawBallotWithDigest(byte[] ballot, String id) {\n        this.id = id;\n        this.ballot = ballot;\n        this.rawDigest = HashType.SHA256.getFunction().digest(ballot);\n    }\n\n    public String getId() {\n        return this.id;\n    }\n\n    public byte[] getBallot() {\n        return this.ballot;\n    }\n\n    public byte[] getRawDigest() {\n        return this.rawDigest;\n    }\n}\n"
  },
  {
    "path": "choices/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "choices/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "choices/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------\n Choices service\n-----------------\n\n<Description of choices service.>\n"
  },
  {
    "path": "choices/cmd/choiceimp/log_desc.go",
    "content": "package main\n\nconst (\n\t_CHOICES_VERSION_FROM_DB           = \"Cannot fetch choices list version from a database\"\n\t_CHOICES_LIST_BDOC_OPEN            = \"Failed to open choices list BDOC container\"\n\t_CHOICES_LIST_BDOC_NO_SIG          = \"No signatures found in choices list BDOC container\"\n\t_CHOICES_LIST_BDOC_SIG_INFO        = \"Choices list BDOC container signature info\"\n\t_CHOICES_LIST_BDOC_SIG_TO_JSON     = \"Failed to JSON marshal choices list BDOC container signature\"\n\t_CHOICES_LIST_BDOC_HAS_MANY_FILES  = \"Choices list BDOC container should only have 1 file\"\n\t_CHOICES_LIST_READ                 = \"Reading choices list\"\n\t_CHOICES_LIST_TO_DB_UPLOAD         = \"Failed to upload choices list to database\"\n\t_CHOICES_LIST_INVALID_JSON         = \"Choices list has an invalid JSON format\"\n\t_CHOICES_LIST_ID_MISMATCH          = \"Choices list ID doesn't match the one used in election configuration file\"\n\t_CHOICES_LIST_PREPARE_UPLOAD_TO_DB = \"Choices list is ready to uploaded into database\"\n\t_CHOICES_LIST_UPLOAD_TO_DB_FAIL    = \"Unable to upload choices list to database\"\n)\n"
  },
  {
    "path": "choices/cmd/choiceimp/main.go",
    "content": "/*\nThe choiceimp application is used for loading choice lists into the storage\nservice.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `choiceimp loads choice lists into the collector's storage service for use by\nother services.\n\nThe choice list container must have an extension corresponding to the container\ntype it is, e.g., choicelist.bdoc.`\n\nvar (\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call choiceimpmain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(choiceimpmain())\n}\n\nfunc choiceimpmain() (code int) {\n\tc := command.New(\"ivxv-choiceimp\", usage, \"choice list container\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\t// Only check the version if it was the specific check requested.\n\tif c.Until == command.CheckVersion {\n\t\tversion1, err := c.Storage.GetChoicesVersion(c.Ctx)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) == nil {\n\t\t\t\treturn c.Error(exit.Unavailable, CheckVersionError{Err: err,\n\t\t\t\t\tDescription: _CHOICES_VERSION_FROM_DB},\n\t\t\t\t\t\"failed to check imported list version:\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tfmt.Println(version1)\n\t\t}\n\t}\n\n\tif c.Until < command.CheckInput {\n\t\treturn exit.OK\n\t}\n\tpath := c.Args[0]\n\n\t// Open the choice list container file.\n\tcnt, err := c.Conf.Container.OpenFile(path)\n\tif err != nil {\n\t\tcode = exit.DataErr\n\t\tif perr := errors.CausedBy(err, new(os.PathError)); perr != nil {\n\t\t\tif os.IsNotExist(perr) {\n\t\t\t\tcode = exit.NoInput\n\t\t\t}\n\t\t}\n\t\treturn c.Error(code, OpenContainerError{Container: path, Err: err,\n\t\t\tDescription: _CHOICES_LIST_BDOC_OPEN},\n\t\t\t\"failed to open choice list container:\", err)\n\t}\n\tdefer cnt.Close()\n\n\t// Ensure that the container is signed and log the signatures.\n\tsignatures := cnt.Signatures()\n\tif len(signatures) == 0 {\n\t\treturn c.Error(exit.DataErr, UnsignedContainerError{Container: path,\n\t\t\tDescription: _CHOICES_LIST_BDOC_NO_SIG},\n\t\t\t\"unsigned choice list container\")\n\t}\n\tfor _, s := range signatures {\n\t\tlog.Log(c.Ctx, ContainerSignature{Signer: s.Signer, SigningTime: s.SigningTime,\n\t\t\tDescription: _CHOICES_LIST_BDOC_SIG_INFO})\n\t}\n\n\t// Get the version string of the container.\n\tversion1, err := version.Container(cnt)\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, ContainerVersionError{Container: path, Err: err,\n\t\t\tDescription: _CHOICES_LIST_BDOC_SIG_TO_JSON},\n\t\t\t\"failed to format container version string:\", err)\n\t}\n\n\t// Check that the container only has a single file.\n\tdata := cnt.Data()\n\tif len(data) != 1 {\n\t\treturn c.Error(exit.DataErr, DataCountError{Count: len(data),\n\t\t\tDescription: _CHOICES_LIST_BDOC_HAS_MANY_FILES},\n\t\t\t\"choice list container has\", len(data), \"files, expected 1\")\n\t}\n\n\t// Process the choice list. We do not know the key, so do a single cycle loop.\n\tfor key, list := range data {\n\t\tlog.Log(c.Ctx, ProcessingList{List: key,\n\t\t\tDescription: _CHOICES_LIST_READ})\n\t\tif err := choiceimp(c.Ctx, c.Until, c.Conf, c.Storage, version1, list); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ImportChoicesError{Err: err,\n\t\t\t\tDescription: _CHOICES_LIST_TO_DB_UPLOAD},\n\t\t\t\t\"failed to import choice list\", key+\":\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n\ntype choicelist struct {\n\tElection string\n\tChoices  map[string]json.RawMessage\n}\n\n// choiceimp parses the list of choices and uploads them to the storage\n// service.\nfunc choiceimp(ctx context.Context, until int, c *conf.C, s *storage.Client,\n\tversion string, list []byte) error {\n\n\tvar l choicelist\n\tif err := json.Unmarshal(list, &l); err != nil {\n\t\treturn JSONUnmarshalError{Err: err, Description: _CHOICES_LIST_INVALID_JSON}\n\t}\n\n\t// Ensure that the election identifier matches the configured one.\n\tif l.Election != c.Election.Identifier {\n\t\treturn ElectionIDMismatchError{Conf: c.Election.Identifier, List: l.Election,\n\t\t\tDescription: _CHOICES_LIST_ID_MISMATCH}\n\t}\n\n\tif until >= command.Execute {\n\t\t// Convert map[string]json.RawMessage to map[string][]byte.\n\t\tchoices := make(map[string][]byte)\n\t\tfor id, list := range l.Choices {\n\t\t\tchoices[id] = list\n\t\t}\n\n\t\tlog.Log(ctx, ImportingChoices{Count: len(choices), Description: _CHOICES_LIST_PREPARE_UPLOAD_TO_DB})\n\t\tprogress.Static(fmt.Sprintf(\"Importing %d choices:\", len(choices)))\n\t\taddprogress := progress.Percent(uint64(len(choices)), true)\n\t\tprogress.Redraw()\n\t\tdefer progress.Keep()\n\n\t\tif err := s.PutChoices(ctx, version, choices, addprogress); err != nil {\n\t\t\treturn PutChoicesError{Err: err, Description: _CHOICES_LIST_UPLOAD_TO_DB_FAIL}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "choices/cmd/districtimp/district_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/storage\"\n)\n\nfunc testDistrictsLookupFormatError(t *testing.T, id string) {\n\tt.Helper()\n\tlist := districtlist{Districts: map[string]district{id: {}}}\n\n\t_, err := districtsLookup(list)\n\tif errors.CausedBy(err, new(DistrictIDFormatError)) == nil {\n\t\tt.Error(\"unexpected success with malformed district code\", id)\n\t}\n}\n\nfunc TestDistrictsLookupFormatError(t *testing.T) {\n\ttestDistrictsLookupFormatError(t, \"0000\")\n\ttestDistrictsLookupFormatError(t, \"0000.\")\n\ttestDistrictsLookupFormatError(t, \".0000\")\n\ttestDistrictsLookupFormatError(t, \"0000.0.0\")\n}\n\nfunc TestDistrictsLookupMultipleCodes(t *testing.T) {\n\tlist := districtlist{\n\t\tDistricts: map[string]district{\n\t\t\t\"0001.1\": {\n\t\t\t\tParish: []string{\"1111\"},\n\t\t\t},\n\t\t\t\"0002.2\": {\n\t\t\t\tParish: []string{\"1111\"},\n\t\t\t},\n\t\t},\n\t}\n\n\t_, err := districtsLookup(list)\n\tif errors.CausedBy(err, new(ParishWithMultipleDistrictCodesError)) == nil {\n\t\tt.Error(\"unexpected success with multiple district codes\")\n\t}\n}\n\nfunc assertDistricts(t *testing.T, districts map[string][]byte,\n\tadminCode, districtNr, districtID string) {\n\n\tt.Helper()\n\tencoded := storage.EncodeAdminDistrict(adminCode, districtNr)\n\tid, ok := districts[string(encoded)]\n\tif !ok {\n\t\tt.Error(\"unexpected error: administrative unit code\", adminCode,\n\t\t\t\"and district number\", districtNr, \"not in districts\")\n\t}\n\tif string(id) != districtID {\n\t\tt.Errorf(\"unexpected district identifier for %s and %s: got %s, want %s\",\n\t\t\tadminCode, districtNr, id, districtID)\n\t}\n}\n\nfunc TestDistrictsLookup(t *testing.T) {\n\tlist := districtlist{\n\t\tDistricts: map[string]district{\n\t\t\t// One-to-one match.\n\t\t\t\"0001.1\": {\n\t\t\t\tParish: []string{\"1111\"},\n\t\t\t},\n\n\t\t\t// District with multiple parishes.\n\t\t\t\"0002.1\": {\n\t\t\t\tParish: []string{\"2222\", \"3333\"},\n\t\t\t},\n\n\t\t\t// Parish with multiple districts.\n\t\t\t\"0004.1\": {\n\t\t\t\tParish: []string{\"4444\"},\n\t\t\t},\n\t\t\t\"0004.2\": {\n\t\t\t\tParish: []string{\"4444\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tdistricts, err := districtsLookup(list)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassertDistricts(t, districts, \"1111\", \"1\", \"0001.1\")\n\tassertDistricts(t, districts, \"2222\", \"1\", \"0002.1\")\n\tassertDistricts(t, districts, \"3333\", \"1\", \"0002.1\")\n\tassertDistricts(t, districts, \"4444\", \"1\", \"0004.1\")\n\tassertDistricts(t, districts, \"4444\", \"2\", \"0004.2\")\n}\n"
  },
  {
    "path": "choices/cmd/districtimp/log_desc.go",
    "content": "package main\n\nconst (\n\t_DISTRICTS_VERSION_FROM_DB                          = \"Cannot fetch districts list version from a database\"\n\t_DISTRICTS_LIST_BDOC_OPEN                           = \"Failed to open district list BDOC container\"\n\t_DISTRICTS_LIST_BDOC_NO_SIG                         = \"No signatures found in districts list BDOC container\"\n\t_DISTRICTS_LIST_BDOC_SIG_INFO                       = \"Districts list BDOC container signature info\"\n\t_DISTRICTS_LIST_BDOC_SIG_TO_JSON                    = \"Failed to JSON marshal districts list BDOC container signature\"\n\t_DISTRICTS_LIST_BDOC_HAS_MANY_FILES                 = \"Districts list BDOC container should only have 1 file\"\n\t_DISTRICTS_LIST_READ                                = \"Reading districts list\"\n\t_DISTRICTS_LIST_TO_DB_UPLOAD                        = \"Failed to upload districts list to database\"\n\t_DISTRICTS_LIST_INVALID_JSON                        = \"Choices list has an invalid JSON format\"\n\t_DISTRICTS_LIST_ID_MISMATCH                         = \"Choices list ID doesn't match the one used in election configuration file\"\n\t_DISTRICTS_LIST_INVALID_COUNTIES_JSON               = \"District list has an invalid 'counties' JSON\"\n\t_DISTRICTS_LIST_PREPARE_UPLOAD_TO_DB                = \"Choices list is ready to uploaded into database\"\n\t_DISTRICTS_LIST_UPLOAD_TO_DB_FAIL                   = \"Unable to upload districts list to database\"\n\t_DISTRICTS_LIST_INVALID_DISTRICT_ID                 = \"District ID has an invalid format\"\n\t_DISTRICTS_LIST_PARISH_CONTAINS_SAME_DISTRICT_CODES = \"Only 1 district code is allowed per parish\"\n)\n"
  },
  {
    "path": "choices/cmd/districtimp/main.go",
    "content": "/*\nThe districtimp application is used for loading district lists into the storage\nservice.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `districtimp loads district lists into the collector's storage service for use\nby other services.\n\nThe district list container must have an extension corresponding to the\ncontainer type it is, e.g., choicelist.bdoc.`\n\nvar (\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call districtimpmain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(districtimpmain())\n}\n\nfunc districtimpmain() (code int) {\n\tc := command.New(\"ivxv-districtimp\", usage, \"district list container\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\t// Only check the version if it was the specific check requested.\n\tif c.Until == command.CheckVersion {\n\t\tversion1, err := c.Storage.GetDistrictsVersion(c.Ctx)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) == nil {\n\t\t\t\treturn c.Error(exit.Unavailable, CheckVersionError{Err: err,\n\t\t\t\t\tDescription: _DISTRICTS_VERSION_FROM_DB,\n\t\t\t\t},\n\t\t\t\t\t\"failed to check imported list version:\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tfmt.Println(version1)\n\t\t}\n\t}\n\n\tif c.Until < command.CheckInput {\n\t\treturn exit.OK\n\t}\n\tpath := c.Args[0]\n\n\t// Open the district list container file.\n\tcnt, err := c.Conf.Container.OpenFile(path)\n\tif err != nil {\n\t\tcode = exit.DataErr\n\t\tif perr := errors.CausedBy(err, new(os.PathError)); perr != nil {\n\t\t\tif os.IsNotExist(perr) {\n\t\t\t\tcode = exit.NoInput\n\t\t\t}\n\t\t}\n\t\treturn c.Error(code, OpenContainerError{Container: path, Err: err,\n\t\t\tDescription: _DISTRICTS_LIST_BDOC_OPEN},\n\t\t\t\"failed to open district list container:\", err)\n\t}\n\tdefer cnt.Close()\n\n\t// Ensure that the container is signed and log the signatures.\n\tsignatures := cnt.Signatures()\n\tif len(signatures) == 0 {\n\t\treturn c.Error(exit.DataErr, UnsignedContainerError{Container: path,\n\t\t\tDescription: _DISTRICTS_LIST_BDOC_NO_SIG},\n\t\t\t\"unsigned districts list container\")\n\t}\n\tfor _, s := range signatures {\n\t\tlog.Log(c.Ctx, ContainerSignature{Signer: s.Signer, SigningTime: s.SigningTime,\n\t\t\tDescription: _DISTRICTS_LIST_BDOC_SIG_INFO})\n\t}\n\n\t// Get the version string of the container.\n\tversion1, err := version.Container(cnt)\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, ContainerVersionError{Container: path, Err: err,\n\t\t\tDescription: _DISTRICTS_LIST_BDOC_SIG_TO_JSON},\n\t\t\t\"failed to format container version string:\", err)\n\t}\n\n\t// Check that the container only has a single file.\n\tdata := cnt.Data()\n\tif len(data) != 1 {\n\t\treturn c.Error(exit.DataErr, DataCountError{Count: len(data),\n\t\t\tDescription: _DISTRICTS_LIST_BDOC_HAS_MANY_FILES},\n\t\t\t\"districts list container has\", len(data), \"files, expected 1\")\n\t}\n\n\t// Process the district list. We do not know the key, so do a single cycle loop.\n\tfor key, list := range data {\n\t\tlog.Log(c.Ctx, ProcessingList{List: key, Description: _DISTRICTS_LIST_READ})\n\t\tif err := districtimp(c.Ctx, c.Until, c.Conf, c.Storage, version1, list); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ImportDistrictsError{Err: err,\n\t\t\t\tDescription: _DISTRICTS_LIST_TO_DB_UPLOAD},\n\t\t\t\t\"failed to import district list\", key+\":\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n\ntype districtlist struct {\n\tElection  string\n\tDistricts map[string]district\n\tCounties  json.RawMessage\n}\n\ntype district struct {\n\tName   string\n\tParish []string\n}\n\n// districtimp parses the districts list and uploads data from it to the\n// storage service.\nfunc districtimp(ctx context.Context, until int, c *conf.C, s *storage.Client,\n\tversion string, list []byte) error {\n\n\tvar l districtlist\n\tif err := json.Unmarshal(list, &l); err != nil {\n\t\treturn JSONUnmarshalError{Err: err, Description: _DISTRICTS_LIST_INVALID_JSON}\n\t}\n\n\t// Ensure that the election identifier matches the configured one.\n\tif l.Election != c.Election.Identifier {\n\t\treturn ElectionIDMismatchError{Conf: c.Election.Identifier, List: l.Election,\n\t\t\tDescription: _DISTRICTS_LIST_ID_MISMATCH}\n\t}\n\n\t// Convert districts to lookup table where a administrative unit code\n\t// and district number map to the district identifier.\n\tdistricts, err := districtsLookup(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Although Counties is stored as-is, ensure that it is of expected\n\t// type, i.e., a JSON object with string array values.\n\tvar counties map[string][]string\n\tif err := json.Unmarshal(l.Counties, &counties); err != nil {\n\t\treturn JSONUnmarshalCountiesError{Err: err, Description: _DISTRICTS_LIST_INVALID_COUNTIES_JSON}\n\t}\n\n\tif until >= command.Execute {\n\t\tlog.Log(ctx, ImportingDistricts{Count: len(l.Districts),\n\t\t\tDescription: _DISTRICTS_LIST_PREPARE_UPLOAD_TO_DB})\n\t\tprogress.Static(fmt.Sprintf(\"Importing %d districts:\", len(l.Districts)))\n\t\taddprogress := progress.Percent(uint64(len(districts)), true)\n\t\tprogress.Redraw()\n\t\tdefer progress.Keep()\n\n\t\terr := s.PutDistricts(ctx, version, districts, l.Counties, addprogress)\n\t\tif err != nil {\n\t\t\treturn PutDistrictsError{Err: err, Description: _DISTRICTS_LIST_UPLOAD_TO_DB_FAIL}\n\t\t}\n\t}\n\treturn nil\n}\n\n// districtsLookup converts the districts from a district list to a lookup\n// table from an administrative unit code and district number to a district\n// identifier.\nfunc districtsLookup(list districtlist) (map[string][]byte, error) {\n\tdistricts := make(map[string][]byte)\n\tparishCodes := make(map[string]string)\n\tfor id, district := range list.Districts {\n\t\tsplits := strings.Split(id, \".\")\n\t\tif len(splits) != 2 || len(splits[0]) == 0 || len(splits[1]) == 0 {\n\t\t\t// Must contain a single period, but not first or last.\n\t\t\treturn nil, DistrictIDFormatError{District: id,\n\t\t\t\tDescription: _DISTRICTS_LIST_INVALID_DISTRICT_ID}\n\t\t}\n\t\tcode, nr := splits[0], splits[1]\n\n\t\tfor _, parish := range district.Parish {\n\t\t\tif parishCode, ok := parishCodes[parish]; ok && code != parishCode {\n\t\t\t\treturn nil, ParishWithMultipleDistrictCodesError{First: parishCode, Second: code,\n\t\t\t\t\tDescription: _DISTRICTS_LIST_PARISH_CONTAINS_SAME_DISTRICT_CODES,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdistricts[string(storage.EncodeAdminDistrict(parish, nr))] = []byte(id)\n\t\t\tparishCodes[parish] = code\n\t\t}\n\t}\n\treturn districts, nil\n}\n"
  },
  {
    "path": "choices/cmd/voterimp/container.go",
    "content": "package main\n\nimport (\n\t\"archive/zip\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"regexp\"\n\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/errors\"\n)\n\n// openContainer attemps to open path as a trusted signed container using\n// opener, but if this fails because it is an unknown type, then it opens it as\n// an untrusted ZIP-archive mimicing the container interface.\nfunc openContainer(opener container.Opener, path string) (\n\tcnt container.Container, trusted bool, err error) {\n\n\tcnt, err = opener.OpenFile(path)\n\tif errors.CausedBy(err, new(container.UnconfiguredTypeError)) == nil {\n\t\treturn cnt, true, err\n\t}\n\n\t// OpenFile failed because it was an unknown type: check if it is ZIP.\n\tif filepath.Ext(path) != \".zip\" {\n\t\treturn nil, false, err // Return the UnconfiguredTypeError.\n\t}\n\n\tarchive, err := zip.OpenReader(path)\n\tif err != nil {\n\t\treturn nil, false, OpenZIPContainerError{Err: err, Description: _VOTERS_LIST_OPEN_ZIP_CONTAINER}\n\t}\n\tdefer archive.Close()\n\n\tdata := make(map[string][]byte, len(archive.File))\n\tfor _, file := range archive.File {\n\t\trc, err := file.Open()\n\t\tif err != nil {\n\t\t\treturn nil, false, OpenZIPFileError{File: file.Name, Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_OPEN_ZIPPED_FILE}\n\t\t}\n\t\tdefer rc.Close()\n\n\t\tbytes, err := io.ReadAll(rc)\n\t\tif err != nil {\n\t\t\treturn nil, false, ReadZIPFileError{File: file.Name, Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_READ_ZIPPED_FILE}\n\t\t}\n\t\tdata[file.Name] = bytes\n\t}\n\n\treturn zipContainer{\n\t\tcomment: archive.Comment,\n\t\tdata:    data,\n\t}, false, nil\n}\n\ntype zipContainer struct {\n\tcomment string\n\tdata    map[string][]byte\n}\n\n// Close does nothing, because there are no resources to manually free.\nfunc (z zipContainer) Close() error {\n\treturn nil\n}\n\n// Signatures returns an empty slice since the container is not signed.\nfunc (z zipContainer) Signatures() []container.Signature {\n\treturn nil\n}\n\n// Data returns the container data from the ZIP-archive.\nfunc (z zipContainer) Data() map[string][]byte {\n\treturn z.data\n}\n\nvar containerVersionRE = regexp.MustCompile(\"(?m)^Version: +(.+)$\")\n\n// containerVersion returns a container's version string. If the container is a\n// trusted signed container, then it calls version.Container, otherwise it\n// parses version metadata from the ZIP-archive comments.\nfunc containerVersion(cnt container.Container) (string, error) {\n\tz, ok := cnt.(zipContainer)\n\tif !ok {\n\t\treturn version.Container(cnt)\n\t}\n\n\tmatches := containerVersionRE.FindAllStringSubmatch(z.comment, -1)\n\tif len(matches) == 0 {\n\t\treturn \"\", MissingZIPVersionError{Description: _VOTERS_LIST_UNABLE_TO_EXTRACT_ZIP_VERSION}\n\t}\n\tversions := make([]string, len(matches))\n\tfor i, match := range matches {\n\t\tversions[i] = match[1] // The first submatch is the version string.\n\t}\n\tversion, err := json.Marshal(versions)\n\treturn string(version), err\n}\n"
  },
  {
    "path": "choices/cmd/voterimp/log_desc.go",
    "content": "package main\n\nconst (\n\t_VOTERS_LIST_OPEN_ZIP_CONTAINER            = \"Failed to open .zip container with .utf and .sig files\"\n\t_VOTERS_LIST_OPEN_ZIPPED_FILE              = \"Cannot open a file from .zip container\"\n\t_VOTERS_LIST_READ_ZIPPED_FILE              = \"Failed to read a file from .zip container\"\n\t_VOTERS_LIST_UNABLE_TO_EXTRACT_ZIP_VERSION = \".zip container doesn't have a version\"\n\t_VOTERS_LIST_VERSION_FROM_DB               = \"Cannot fetch voters list version from a database\"\n\t_VOTERS_LIST_VERSION_TO_JSON               = \"Cannot JSON marshal voters list version\"\n\t_VOTERS_LIST_BDOC_OPEN                     = \"Failed to open voters list BDOC container\"\n\t_VOTERS_LIST_BDOC_NO_SIG                   = \"No signatures found in voters list BDOC container\"\n\t_VOTERS_LIST_BDOC_SIG_INFO                 = \"Districts list BDOC container signature info\"\n\t_VOTERS_LIST_BDOC_SIG_TO_JSON              = \"Failed to JSON marshal voters list BDOC container signature\"\n\t_VOTERS_LIST_BDOC_INVALID_DATA             = \"Voters list BDOC container has invalid data\"\n\t_VOTERS_LIST_UTF_AND_SIG_VERIFY            = \".sig signature doesn't correspond to .utf\"\n\t_VOTERS_LIST_CURRENT_VERSION               = \"Fetched from a database current voters list version\"\n\t_VOTERS_LIST_CREATE_MAP_OF_ALL_CHANGES     = \"Failed to construct a map of all voters list being used so far\"\n\t_VOTERS_LIST_TO_DB_UPLOAD                  = \"Uploading voters list to database\"\n\t_VOTERS_LIST_TO_DB_UPLOAD_FAIL             = \"Failed to upload voters list to database\"\n\t_VOTERS_LIST_BDOC_HAS_MORE_THAN_2_FILES    = \"Voters list BDOC container doesn't have exactly 2 files (.utf and .sig)\"\n\t_VOTERS_LIST_NO_UTF_FILE                   = \"Voters list BDOC container doesn't have a .utf file\"\n\t_VOTERS_LIST_NO_SIG_FILE                   = \"Voters list BDOC container doesn't have a .sig file\"\n\t_VOTERS_LIST_PUB_KEY_INVALID_PEM           = \"Voters list public key is invalid PEM\"\n\t_VOTERS_LIST_PUB_KEY_INVALID_PKIX          = \"Voters list public key, after PEM decoding, is invalid PKIX\"\n\t_VOTERS_LIST_PUB_KEY_INVALID_ECDSA         = \"Voters list public key algorithm is not ECDSA\"\n\t_VOTERS_LIST_ASN1_SIG_ASN1_UNMARSHAL       = \"Cannot ASN.1 unmarshal .sig content\"\n\t_VOTERS_LIST_SIG_VERIFY_FAIL               = \"Failed to verify .sig content with voters list public key\"\n\t_VOTERS_LIST_READ                          = \"Reading voters list\"\n\t_VOTERS_LIST_READ_CTX_CANCEL               = \"Context has been cancelled during voters list reading\"\n\t_VOTERS_LIST_UTF_INVALID_COLUMNS_COUNT     = \".utf has invalid columns count\"\n\t_VOTERS_LIST_UTF_UNKNOWN_COLUMN            = \".utf contains unknown column\"\n\t_VOTERS_LIST_READING_ERRORS                = \"Report of all errors occurred during voters file reading\"\n\t_VOTERS_LIST_READING_ERRORS_COUNT          = \"Report errors count during voters file reading\"\n\t_VOTERS_LIST_READ_UTF_FORMAT_VERSION       = \"Failed to read .utf format version\"\n\t_VOTERS_LIST_UTF_FORMAT_VERSION            = \"Invalid .utf format version\"\n\t_VOTERS_LIST_READ_UTF_ELECTION_ID          = \"Failed to read .utf election ID\"\n\t_VOTERS_LIST_UTF_ELECTION_ID               = \".utf election ID doesn't match the one in election configuration file\"\n\t_VOTERS_LIST_READ_UTF_VOTERS_LIST_VERSION  = \"Failed to read .utf voters list version\"\n\t_VOTERS_LIST_UTF_VOTERS_LIST_VERSION       = \".utf voters list version is not a base-10 integer\"\n\t_VOTERS_LIST_UTF_VOTERS_LIST_VERSION_0     = \"Current .utf voters list to be uploaded has a version of 0, however such version already exists\"\n\t_VOTERS_LIST_INITIAL_NOT_UPLOADED_YET      = \"Voters list with version 0 (initial) hasn't been yet uploaded\"\n\t_VOTERS_LIST_UPLOAD_OLD_VERSION            = \"Trying to upload old voters list version\"\n\t_VOTERS_LIST_READ_ELECTION_FROM_PERIOD     = \"Failed to read .utf election 'from' period\"\n\t_VOTERS_LIST_ELECTION_FROM_PERIOD          = \".utf election 'from' value is not a RFC3339 timestamp\"\n\t_VOTERS_LIST_READ_ELECTION_TO_PERIOD       = \"Failed to read .utf election 'to' period\"\n\t_VOTERS_LIST_ELECTION_TO_PERIOD            = \".utf election 'to' value is not a RFC3339 timestamp\"\n\t_VOTERS_LIST_UTF_VOTER_ID_EMPTY            = \".utf voters list contains empty voter ID\"\n\t_VOTERS_LIST_UTF_INITIAL_ACTION            = \".utf voters list initial action should be 'lisamine'\"\n\t_VOTERS_LIST_UTF_ADMIN_CODE_EMPTY          = \".utf voters list contains empty admin code\"\n\t_VOTERS_LIST_UTF_DISTRICT_EMPTY            = \".utf voters list contains empty district\"\n\t_VOTERS_LIST_UTF_ADD_SAME_VOTER            = \".utf voters list attempts to add ('lisamine' action) same voters multiple time\"\n\t_VOTERS_LIST_MODIFY_WITH_PREVIOUS_ERRORS   = \"Trying to process next line of voters list, but previous line was rejected with errors\"\n\t_VOTERS_LIST_GET_VOTER_FROM_DB             = \"Cannot fetch voter ID from database\"\n\t_VOTERS_LIST_NON_EXISTING_VOTER            = \"Voter doesn't exist in a database\"\n\t_VOTERS_LIST_DELETE_JUST_ADDED_VOTER       = \"Voters list has 'lisamine' and then 'kustutamine' action for the same voter\"\n\t_VOTERS_LIST_UNKNOWN_ACTION                = \"Voters list contains unknown action type\"\n\t_VOTERS_LIST_READ_FILE_PROGRESS            = \"Reading voters file progress info\"\n\t_VOTERS_LIST_READ_LINE_ERROR               = \"Cannot read voters file line\"\n\t_VOTERS_LIST_UTF_COLUMNS_COUNT             = \"Voters file has invalid columns count for a given row\"\n)\n"
  },
  {
    "path": "choices/cmd/voterimp/main.go",
    "content": "/*\nThe voterimp application is used for loading voter lists into the storage\nservice.\n\nThe voter list is generated and signed by the Estonian Election Information\nSystem and contains Estonian strings, so the format is very specific to\nEstonia. voterimp checks the raw ECDSA signature with SHA-256 digest, parses\nthe contents of the voter list, and adds a new version of voter information\ninto the storage service.\n*/\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `voterimp loads voter lists into the collector's storage service for use by\nother services.\n\nThe voter list container must contain exactly two files: the voter list and the\nvoter list signature. The key for the voter list can be anything, but the key\nfor the signature must be the list key with a \".signature\" suffix. E.g.,\n\"voter.list\" and \"voter.list.signature\".\n\nThe voter list container must have an extension corresponding to the container\ntype it is, e.g., voterlist.bdoc. voterimp additionally supports unsigned ZIP\ncontainers with metadata stored in the archive comment.`\n\nvar (\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call voterimpmain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(voterimpmain())\n}\n\nfunc voterimpmain() (code int) {\n\tc := command.New(\"ivxv-voterimp\", usage, \"voter list container\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\t// Only check the version if it was the specific check requested.\n\tif c.Until == command.CheckVersion {\n\t\tversion, err := c.Storage.GetVotersContainerVersions(c.Ctx)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) == nil {\n\t\t\t\treturn c.Error(exit.Unavailable, CheckVersionError{Err: err,\n\t\t\t\t\tDescription: _VOTERS_LIST_VERSION_FROM_DB},\n\t\t\t\t\t\"failed to check imported container versions:\", err)\n\t\t\t}\n\t\t} else {\n\t\t\t// Convert from new-line separation to JSON array.\n\t\t\tvar vers []json.RawMessage\n\t\t\tfor _, ver := range strings.Split(strings.TrimSpace(version), \"\\n\") {\n\t\t\t\tvers = append(vers, json.RawMessage(ver))\n\t\t\t}\n\t\t\tif err := json.NewEncoder(os.Stdout).Encode(vers); err != nil {\n\t\t\t\treturn c.Error(exit.Unavailable, ConvertVersionError{Err: err,\n\t\t\t\t\tDescription: _VOTERS_LIST_VERSION_TO_JSON},\n\t\t\t\t\t\"failed to encode container versions:\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.Until < command.CheckInput {\n\t\treturn exit.OK\n\t}\n\tpath := c.Args[0]\n\n\t// Open the voter list container file.\n\tcnt, trusted, err := openContainer(c.Conf.Container, path)\n\tif err != nil {\n\t\tcode = exit.DataErr\n\t\tif perr := errors.CausedBy(err, new(os.PathError)); perr != nil {\n\t\t\tif os.IsNotExist(perr) {\n\t\t\t\tcode = exit.NoInput\n\t\t\t}\n\t\t}\n\t\treturn c.Error(code, OpenContainerError{Container: path, Err: err,\n\t\t\tDescription: _VOTERS_LIST_BDOC_OPEN},\n\t\t\t\"failed to open voter list container:\", err)\n\t}\n\tdefer cnt.Close()\n\n\t// Ensure that a trusted container is signed and log the signatures.\n\tsignatures := cnt.Signatures()\n\tif trusted && len(signatures) == 0 {\n\t\treturn c.Error(exit.DataErr, UnsignedContainerError{Container: path,\n\t\t\tDescription: _VOTERS_LIST_BDOC_NO_SIG},\n\t\t\t\"unsigned voter list container\")\n\t}\n\tfor _, s := range signatures {\n\t\tlog.Log(c.Ctx, ContainerSignature{\n\t\t\tSigner:      s.Signer,\n\t\t\tSigningTime: s.SigningTime,\n\t\t\tDescription: _VOTERS_LIST_BDOC_SIG_INFO,\n\t\t})\n\t}\n\n\t// Get the version string of the container.\n\tcversion, err := containerVersion(cnt)\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, ContainerVersionError{Container: path,\n\t\t\tErr: err, Description: _VOTERS_LIST_BDOC_SIG_TO_JSON},\n\t\t\t\"failed to format container version string:\", err)\n\t}\n\n\t// Get the contents of the container.\n\tlist, sig, err := containerData(cnt.Data())\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, ContainerDataError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_BDOC_INVALID_DATA},\n\t\t\t\"failed to find expected data from container:\", err)\n\t}\n\n\t// Check the signature.\n\tif err = verifyECDSA(c.Conf.Election.VoterList.Key, list, sig); err != nil {\n\t\treturn c.Error(exit.DataErr, VerifySignatureError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_UTF_AND_SIG_VERIFY},\n\t\t\t\"failed to verify voter list signature:\", err)\n\t}\n\n\t// Get the current voter list version.\n\toldver, err := c.Storage.GetVotersListVersion(c.Ctx)\n\tswitch {\n\tcase err == nil:\n\t\tlog.Log(c.Ctx, CurrentVoterListVersion{Version: oldver,\n\t\t\tDescription: _VOTERS_LIST_CURRENT_VERSION})\n\tcase errors.CausedBy(err, new(storage.NotExistError)) != nil:\n\t\toldver = \"\" // Do not rely on storage returning zero value on error.\n\tdefault:\n\t\treturn c.Error(exit.Unavailable, CheckListVersionError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_VERSION_FROM_DB},\n\t\t\t\"failed to check current list version:\", err)\n\t}\n\n\t// Parse the voter list and preprocess into a map of changes.\n\tvoters, newver, err := preprocess(c.Ctx, list, oldver, c.Conf.Election.Identifier, c.Storage)\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, PreprocessVotersError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_CREATE_MAP_OF_ALL_CHANGES},\n\t\t\t\"failed to preprocess voter list:\", err)\n\t}\n\n\t// Store the new list.\n\tif c.Until >= command.Execute {\n\t\tlog.Log(c.Ctx, ImportingVoters{Version: newver, Count: len(voters),\n\t\t\tDescription: _VOTERS_LIST_TO_DB_UPLOAD})\n\t\tprogress.Static(fmt.Sprintf(\"Importing %d voters:\", len(voters)))\n\t\taddprogress := progress.Percent(uint64(len(voters)), true)\n\t\tprogress.Redraw()\n\t\tdefer progress.Keep()\n\n\t\tif err := c.Storage.PutVoters(c.Ctx, cversion, voters,\n\t\t\toldver, newver, addprogress); err != nil {\n\n\t\t\treturn c.Error(exit.Unavailable, PutVotersError{Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_TO_DB_UPLOAD_FAIL},\n\t\t\t\t\"failed to import voter list:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n\n// containerData returns voter list and signature contents from container data.\nfunc containerData(data map[string][]byte) (list, signature []byte, err error) {\n\t// The container data should have exactly two keys: *.utf and *.sig.\n\t// Although the files are usually named more specifically\n\t//\n\t//   <election_identifier>-voters-<changeset>.{utf,sig},\n\t//\n\t// do not enforce this.\n\tconst utf = \".utf\"\n\tconst sig = \".sig\"\n\n\tif len(data) != 2 {\n\t\treturn nil, nil, KeyCountError{Count: len(data),\n\t\t\tDescription: _VOTERS_LIST_BDOC_HAS_MORE_THAN_2_FILES}\n\t}\n\tvar utfKey, sigKey string\n\tfor key, content := range data {\n\t\tif strings.HasSuffix(key, utf) {\n\t\t\tutfKey = key\n\t\t\tlist = content\n\t\t\tsigKey = key[:len(key)-len(utf)] + sig\n\t\t\tbreak\n\t\t}\n\t}\n\tif len(utfKey) == 0 {\n\t\treturn nil, nil, MissingUTFKeyError{\n\t\t\tDescription: _VOTERS_LIST_NO_UTF_FILE,\n\t\t}\n\t}\n\tsignature, ok := data[sigKey]\n\tif !ok {\n\t\treturn nil, nil, MissingSigKeyError{Expected: sigKey,\n\t\t\tDescription: _VOTERS_LIST_NO_SIG_FILE}\n\t}\n\n\treturn list, signature, nil\n}\n\n// verifyECDSA parses an ECDSA public key from a PEM-encoded X.509 structure\n// and verifies the signature sig on data.\nfunc verifyECDSA(pub string, data, sig []byte) error {\n\tder, err := cryptoutil.PEMDecode(pub, \"PUBLIC KEY\")\n\tif err != nil {\n\t\treturn ECDSAPEMDecodeError{Err: err, Description: _VOTERS_LIST_PUB_KEY_INVALID_PEM}\n\t}\n\tparsed, err := x509.ParsePKIXPublicKey(der)\n\tif err != nil {\n\t\treturn ECDSAParsePKIXError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_PUB_KEY_INVALID_PKIX}\n\t}\n\tkey, ok := parsed.(*ecdsa.PublicKey)\n\tif !ok {\n\t\treturn ECDSAPublicKeyNotECDSAError{Type: fmt.Sprintf(\"%T\", parsed),\n\t\t\tDescription: _VOTERS_LIST_PUB_KEY_INVALID_ECDSA}\n\t}\n\n\thashed := sha256.Sum256(data) // Hardcoded regardless of key parameters.\n\tr, s, err := cryptoutil.ParseECDSAASN1Signature(sig)\n\tif err != nil {\n\t\treturn ECDSAParseSignatureError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_ASN1_SIG_ASN1_UNMARSHAL}\n\t}\n\tif !ecdsa.Verify(key, hashed[:], r, s) {\n\t\treturn ECDSASignatureVerificationError{Description: _VOTERS_LIST_SIG_VERIFY_FAIL}\n\t}\n\treturn nil\n}\n\nconst (\n\tdelim = '\\n'\n\tsep   = '\\t'\n)\n\n// linefunc is the type of functions used to process voter lines. Given a\n// voter, action, administrative unit code and district number, it reports any\n// problems or if there were none, adds an entry for voter into voters.\n// previousErrors indicates if there were previous lines with errors for this\n// voter. version is the currently applied voter list version.\ntype linefunc func(ctx context.Context, voter, action, adminCode, district string,\n\tvoters map[string][]byte, previousErrors bool, version string, s *storage.Client) (\n\terrs []error)\n\n// preprocess parses the list and preprocesses the changes for storage.\nfunc preprocess(ctx context.Context, list []byte, version, election string, s *storage.Client) (\n\tvoters map[string][]byte, newver string, err error) {\n\n\tb := bytes.NewBuffer(list)\n\n\t// Parse the header to determine list version and type.\n\tnewver, lf, err := header(b, election, version)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// Loop over all list entries, calling lf for each. Report progress of\n\t// this process.\n\tlog.Log(ctx, PreprocessingVoterList{Version: newver,\n\t\tDescription: _VOTERS_LIST_READ})\n\tprogress.Static(\"Preprocessing voter list:\")\n\taddcount := progress.Count(0, false) // Do not redraw every time.\n\tconst countstep = 10000              // Redraw after each countstep.\n\tprogress.Redraw()\n\tdefer progress.Keep()\n\n\tvoters = make(map[string][]byte)\n\twithErrors := make(map[string]struct{}) // Voters with previous errors.\n\tvar errcount int\n\n\tline := 3 // Voter entries start after the third line.\nloop:\n\tfor ; ; stepadd(ctx, addcount, 1, countstep) {\n\t\t// Check if preprocessing was cancelled.\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, \"\", PreprocessVoterListCanceled{\n\t\t\t\tErr: ctx.Err(), Description: _VOTERS_LIST_READ_CTX_CANCEL}\n\t\tdefault:\n\t\t}\n\n\t\t// Read the next line.\n\t\tline++\n\t\taction, voter, adminCode, district, err := next(b)\n\t\tswitch {\n\t\tcase err == nil:\n\t\tcase err == io.EOF:\n\t\t\tbreak loop\n\t\tcase errors.CausedBy(err, new(FieldCountError)) != nil:\n\t\t\treport(ctx, VoterEntryFormatError{Line: line, Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_UTF_INVALID_COLUMNS_COUNT})\n\t\t\terrcount++\n\t\t\tcontinue loop\n\t\tdefault:\n\t\t\treport(ctx, ReadVoterEntryError{Line: line, Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_UTF_UNKNOWN_COLUMN})\n\t\t\terrcount++\n\t\t\tbreak loop\n\t\t}\n\n\t\t// Call lf for the line.\n\t\t_, previous := withErrors[voter]\n\t\terrs := lf(ctx, voter, action, adminCode, district, voters, previous, version, s)\n\t\tif len(errs) > 0 {\n\t\t\twithErrors[voter] = struct{}{}\n\t\t}\n\t\tfor _, err = range errs {\n\t\t\treport(ctx, PreprocessVoterEntryError{Line: line, Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_READING_ERRORS})\n\t\t\terrcount++\n\t\t}\n\t}\n\t// Report final count if not already done by stepadd.\n\tif addcount(0)%countstep > 0 {\n\t\tstepadd(ctx, addcount, 0, 0)\n\t}\n\tif errcount > 0 {\n\t\treturn nil, \"\", PreprocessVoterListError{\n\t\t\tErrorCount: errcount, Description: _VOTERS_LIST_READING_ERRORS_COUNT}\n\t}\n\treturn voters, newver, nil\n}\n\nfunc header(b *bytes.Buffer, election, oldver string) (newver string, lf linefunc, err error) {\n\t// First line is the format version number, which must be 2.\n\tfver, err := readString(b, delim)\n\tif err != nil {\n\t\treturn \"\", nil, ReadFileVersionError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_UTF_FORMAT_VERSION}\n\t}\n\tif fver != \"2\" {\n\t\treturn \"\", nil, FileVersionError{Version: fver,\n\t\t\tDescription: _VOTERS_LIST_READ_UTF_FORMAT_VERSION}\n\t}\n\n\t// Second is the election identifier. Must match the one in the election\n\t// configuration.\n\telid, err := readString(b, delim)\n\tif err != nil {\n\t\treturn \"\", nil, ReadElectionIDError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_READ_UTF_ELECTION_ID}\n\t}\n\tif elid != election {\n\t\treturn \"\", nil, ElectionIDMismatchError{Conf: election,\n\t\t\tList: elid, Description: _VOTERS_LIST_UTF_ELECTION_ID}\n\t}\n\n\t// Third is the list version number.\n\tnewver, err = readString(b, delim)\n\tif err != nil {\n\t\treturn \"\", nil, ReadListVersionError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_READ_UTF_VOTERS_LIST_VERSION}\n\t}\n\tnewver64, err := strconv.ParseUint(newver, 10, 64)\n\tif err != nil {\n\t\treturn \"\", nil, ParseListVersionError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_UTF_VOTERS_LIST_VERSION}\n\t}\n\tif newver64 == 0 {\n\t\tif len(oldver) > 0 {\n\t\t\treturn \"\", nil, VoterListExistsError{Version: oldver,\n\t\t\t\tDescription: _VOTERS_LIST_UTF_VOTERS_LIST_VERSION_0}\n\t\t}\n\t\tlf = initial\n\t} else {\n\t\tvar oldver64 uint64\n\t\tif len(oldver) == 0 {\n\t\t\treturn \"\", nil, NoExistingVoterListError{\n\t\t\t\tDescription: _VOTERS_LIST_INITIAL_NOT_UPLOADED_YET}\n\t\t} else if oldver64, err = strconv.ParseUint(oldver, 10, 64); err != nil {\n\t\t\treturn \"\", nil, ParseCurrentListVersionError{Err: err,\n\t\t\t\tDescription: _VOTERS_LIST_UTF_VOTERS_LIST_VERSION}\n\t\t} else if oldver64 >= newver64 {\n\t\t\treturn \"\", nil, UnexpectedListVersionError{\n\t\t\t\tCurrent:     oldver64,\n\t\t\t\tList:        newver64,\n\t\t\t\tDescription: _VOTERS_LIST_UPLOAD_OLD_VERSION,\n\t\t\t}\n\t\t}\n\t\tlf = changes\n\t}\n\n\t// Fourth are two timestamps representing the period. Not used by the\n\t// collector, but make sure that the format is valid.\n\tfromstr, err := readString(b, sep)\n\tif err != nil {\n\t\treturn \"\", nil, ReadPeriodFromError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_READ_ELECTION_FROM_PERIOD}\n\t}\n\tif _, err = time.Parse(time.RFC3339, fromstr); err != nil {\n\t\treturn \"\", nil, ParsePeriodFromError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_ELECTION_FROM_PERIOD}\n\t}\n\ttostr, err := readString(b, delim)\n\tif err != nil {\n\t\treturn \"\", nil, ReadPeriodToError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_READ_ELECTION_TO_PERIOD}\n\t}\n\tif _, err = time.Parse(time.RFC3339, tostr); err != nil {\n\t\treturn \"\", nil, ParsePeriodToError{Err: err,\n\t\t\tDescription: _VOTERS_LIST_ELECTION_TO_PERIOD}\n\t}\n\n\treturn newver, lf, nil\n}\n\nfunc initial(_ context.Context, voter, action, adminCode, district string,\n\tvoters map[string][]byte, _ bool, _ string, _ *storage.Client) (\n\terrs []error) {\n\n\t// Skip duplicate checking if there are relevant errors. We want to\n\t// check duplicates even if the voter had previous errors, so use a\n\t// flag separate from previousErrors.\n\tvar skip bool\n\n\tif len(voter) == 0 {\n\t\terrs = append(errs, InitialEmptyVoterError{Description: _VOTERS_LIST_UTF_VOTER_ID_EMPTY})\n\t\tskip = true\n\t}\n\tif action != \"lisamine\" {\n\t\terrs = append(errs, InitialNonAddActionError{Action: action,\n\t\t\tDescription: _VOTERS_LIST_UTF_INITIAL_ACTION})\n\t\tskip = true\n\t}\n\tif len(adminCode) == 0 {\n\t\terrs = append(errs, InitialEmptyAdminUnitCodeError{\n\t\t\tDescription: _VOTERS_LIST_UTF_ADMIN_CODE_EMPTY,\n\t\t})\n\t}\n\tif len(district) == 0 {\n\t\terrs = append(errs, InitialEmptyDistrictNumberError{\n\t\t\tDescription: _VOTERS_LIST_UTF_DISTRICT_EMPTY,\n\t\t})\n\t}\n\tif !skip {\n\t\tif _, ok := voters[voter]; ok {\n\t\t\terrs = append(errs, InitialAddDuplicateVoterError{Voter: voter,\n\t\t\t\tDescription: _VOTERS_LIST_UTF_ADD_SAME_VOTER})\n\t\t}\n\t\t// It is OK to add invalid entries (skip was not set for\n\t\t// adminCode or district errors) since then we will error\n\t\t// anyway and the map values are not used during preprocessing.\n\t\tvoters[voter] = storage.EncodeAdminDistrict(adminCode, district)\n\t}\n\treturn\n}\n\nfunc changes(ctx context.Context, voter, action, adminCode, district string,\n\tvoters map[string][]byte, previousErrors bool, version string, s *storage.Client) (\n\terrs []error) {\n\n\tif len(voter) == 0 {\n\t\terrs = append(errs, ChangesEmptyVoterError{\n\t\t\tDescription: _VOTERS_LIST_UTF_VOTER_ID_EMPTY,\n\t\t})\n\t\tpreviousErrors = true\n\t} else if previousErrors {\n\t\terrs = append(errs, ChangesVoterWithPreviousErrorsError{Voter: voter,\n\t\t\tDescription: _VOTERS_LIST_MODIFY_WITH_PREVIOUS_ERRORS})\n\t}\n\n\t// Only perform consistency checks if there are no previous errors.\n\tvar voterExists bool    // Is this voter on the current voter list?\n\tvar voterProcessed bool // Has this voter already been processed?\n\tif !previousErrors {\n\t\t// Check the current entry for the voter. Look at the in-memory\n\t\t// map first and only then storage.\n\t\tif entry, ok := voters[voter]; ok {\n\t\t\tvoterExists = entry != nil\n\t\t\tvoterProcessed = true\n\t\t} else {\n\t\t\tvar err error\n\t\t\t_, _, err = s.GetVoter(ctx, version, voter)\n\t\t\tswitch {\n\t\t\tcase err == nil:\n\t\t\t\tvoterExists = true\n\t\t\tcase errors.CausedBy(err, new(storage.NotExistError)) != nil:\n\t\t\tdefault:\n\t\t\t\terrs = append(errs, GetOldVoterError{Voter: voter, Err: err,\n\t\t\t\t\tDescription: _VOTERS_LIST_GET_VOTER_FROM_DB})\n\t\t\t\tpreviousErrors = true\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch action {\n\tcase \"lisamine\":\n\t\tif len(adminCode) == 0 {\n\t\t\terrs = append(errs, ChangesEmptyAdminUnitCodeError{\n\t\t\t\tDescription: _VOTERS_LIST_UTF_ADMIN_CODE_EMPTY,\n\t\t\t})\n\t\t\tpreviousErrors = true\n\t\t}\n\t\tif len(district) == 0 {\n\t\t\terrs = append(errs, ChangesEmptyDistrictNumberError{\n\t\t\t\tDescription: _VOTERS_LIST_UTF_DISTRICT_EMPTY,\n\t\t\t})\n\t\t\tpreviousErrors = true\n\t\t}\n\t\tif !previousErrors {\n\t\t\tif voterExists {\n\t\t\t\terrs = append(errs, ChangesAddDuplicateVoterError{Voter: voter,\n\t\t\t\t\tDescription: _VOTERS_LIST_UTF_ADD_SAME_VOTER})\n\t\t\t}\n\t\t\tvoters[voter] = storage.EncodeAdminDistrict(adminCode, district)\n\t\t}\n\n\tcase \"kustutamine\":\n\t\tif !previousErrors {\n\t\t\tswitch {\n\t\t\tcase !voterExists:\n\t\t\t\terrs = append(errs, ChangesRemoveNotExistingVoterError{\n\t\t\t\t\tVoter: voter, Description: _VOTERS_LIST_NON_EXISTING_VOTER})\n\t\t\tcase voterProcessed:\n\t\t\t\terrs = append(errs, ChangesRemoveAddedVoterError{Voter: voter,\n\t\t\t\t\tDescription: _VOTERS_LIST_DELETE_JUST_ADDED_VOTER})\n\t\t\t}\n\t\t\tvoters[voter] = nil // nil to distinguish from unchanged voters.\n\t\t}\n\n\tdefault:\n\t\terrs = append(errs, ChangesUnsupportedActionError{Action: action,\n\t\t\tDescription: _VOTERS_LIST_UNKNOWN_ACTION})\n\t}\n\treturn\n}\n\n// stepadd calls add with count and, if step is zero or the new total modulo\n// step is zero, logs and redraws progress.\nfunc stepadd(ctx context.Context, add status.Add, count, step uint64) {\n\tif new1 := add(count); step == 0 || new1%step == 0 {\n\t\tlog.Log(ctx, PreprocessProgress{Count: new1,\n\t\t\tDescription: _VOTERS_LIST_READ_FILE_PROGRESS})\n\t\tprogress.Redraw()\n\t}\n}\n\n// next reads and parses the next voter line from b.\nfunc next(b *bytes.Buffer) (action, voter, adminCode, district string, err error) {\n\tline, err := readString(b, delim)\n\tif err != nil {\n\t\tif err != io.EOF || len(line) > 0 {\n\t\t\terr = ReadNextError{Line: line, Err: err, Description: _VOTERS_LIST_READ_LINE_ERROR}\n\t\t}\n\t\treturn\n\t}\n\n\t// Split on tabs and expect five fields or two fields when 'kustutamine':\n\t//\n\t//\t0. action,\n\t//\t1. voter ID,\n\t//\t2. voter name (ignored),\n\t//\t3. administrative unit code, and\n\t//\t4. district number.\n\t//\n\t//\t0. action,\n\t//\t1. voter ID,\n\t//\n\t// The choices identifier is formed from the administrative unit code\n\t// and district number.\n\tfields := strings.Split(line, string(sep))\n\tswitch len(fields) {\n\tcase 2:\n\t\treturn fields[0], fields[1], \"\", \"\", nil\n\tcase 5:\n\t\treturn fields[0], fields[1], fields[3], fields[4], nil\n\tdefault:\n\t\terr = FieldCountError{Fields: len(fields), Description: _VOTERS_LIST_UTF_COLUMNS_COUNT}\n\t\treturn\n\t}\n}\n\n// report reports an error to both the log and standard output.\nfunc report(ctx context.Context, entry log.ErrorEntry) {\n\tlog.Error(ctx, entry)\n\tprogress.Hide()\n\tdefer progress.Show()\n\tfmt.Fprintln(os.Stderr, \"error:\", entry)\n}\n\n// readString returns the result of b.ReadString(delim) with delim trimmed.\nfunc readString(b *bytes.Buffer, delim byte) (string, error) {\n\tline, err := b.ReadString(delim)\n\tif err == nil {\n\t\tline = line[:len(line)-1] // Trim the delimiter.\n\t}\n\treturn line, err\n}\n"
  },
  {
    "path": "choices/cmd/voterimp/voterimp_test.go",
    "content": "package main\n\nimport \"testing\"\n\nfunc TestZIPVersion(t *testing.T) {\n\tconst comment = `Version: start of text\n\nSome commentary about the archive.\n\nVersion: mid-text\n\nSome more commentary about the archive.\n\nVersion:    left-trim and Unicode ✔️\nversion: lowercase\nVersion:no space\nVersion: end of text`\n\tconst expected = `[\"start of text\",\"mid-text\",\"left-trim and Unicode ✔️\",\"end of text\"]`\n\n\tversion, err := containerVersion(zipContainer{comment: comment})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif version != expected {\n\t\tt.Errorf(\"unexpected ZIP version: got %s, want %s\", version, expected)\n\t}\n}\n"
  },
  {
    "path": "choices/go.mod",
    "content": "module ivxv.ee/choices\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "choices/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "choices/internal/client/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value for ID card\n\tEmpty = \"\"\n\n\t// This should be a StatusReadResp.Caller value for Mobile-ID/Smart-ID\n\tAuthenticateStatus = \"RPC.AuthenticateStatus\"\n\n\t// This should be a StatusReadResp.Caller value for Web eID\n\tToken = \"RPC.Token\"\n\n\t// This should be a StatusUpdateReq.Caller value for ID card/Mobile-ID/Smart-ID/Web eID\n\tVoterChoices = \"RPC.VoterChoices\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tchoiceTTL int64\n\tclient    client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != exitCodeOK {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:    tlsDialer,\n\t\tchoiceTTL: c.Conf.Technical.Status.Session.ChoiceTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\t// Send request to session status server and verify response\n\tok, err := r.verifyAndUpdateSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndUpdateSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndUpdateSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will update h.Header.SessionID record in the underlying storage\n// by marking session status Caller as serviceMethod.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndUpdateSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Extract authentication method from a header.Ctx\n\tauthFilter, err := server.AuthMethod(h.Ctx)\n\tif err != nil {\n\t\treturn false, AuthMethodFromCtxError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_AUTHMETHOD_FROM_CTX}\n\t}\n\tif authFilter == \"\" {\n\t\treturn false, AuthMethodIsEmptyError{AuthFilter: authFilter,\n\t\t\tDescription: _SESSIONSTATUS_AUTHMETHOD_EMPTY}\n\t}\n\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\tttl := strconv.FormatInt(r.choiceTTL, 10)\n\t// Reset the LeaseID\n\trespRead.Lease = \"\"\n\n\t// NB! Most important part, that prevents any attack on SessionID\n\tok, err := verifyStatusReadResp(&respRead, voterChoicesHandler)\n\tif err != nil || !ok {\n\t\treturn false, VerifyStatusReadRespError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\t// SessionID is valid, however there is one more possibility to tamper\n\t// a SessionID == to wait until SessionID TTL is expired and perform\n\t// RPC.VoterChoices query. This is the only edge case that could happen\n\t// since RPC.VoterChoices is the only endpoint which allows\n\t// empty Auth and Caller fields.\n\t//\n\t// This behaviour can be prevented if we know, how user exactly authenticated\n\t// in an authentication filter. For Mobile-ID/Smart-ID/Web eID it is auth.Ticket,\n\t// however for ID card it is auth.TLS\n\tif respRead.Auth == client.NoAuth && auth.Type(authFilter) != auth.TLS {\n\t\t// SessionID is attempted to tamper\n\t\treturn false, EmptyAuthAndCallerForNonIDCardUserError{\n\t\t\tMethod:      VoterChoices,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tCaller:      respRead.Caller,\n\t\t\tAuthMethod:  authFilter,\n\t\t\tDescription: _SESSIONSTATUS_IDCARD,\n\t\t}\n\t}\n\n\t// Here is 100% clear, that user is ID card\n\tif respRead.Auth == client.NoAuth {\n\t\trespRead.Auth = client.IDcardAuth\n\t}\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp,\n\th func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// voterChoicesHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.VoterChoices request.\nfunc voterChoicesHandler(r *api.StatusReadResp) (bool, error) {\n\t// When authenticating with ID card, i.e. first interaction with IVXV\n\tidCardAuth := r.Caller == Empty && r.Auth == client.NoAuth\n\n\t// When authenticating with Mobile-ID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.AuthenticateStatus method\n\tmidAuth := r.Caller == AuthenticateStatus && r.Auth == client.MobileIDAuth\n\n\t// When authenticating with Smart-ID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.AuthenticateStatus method\n\tsidAuth := r.Caller == AuthenticateStatus && r.Auth == client.SmartIDAuth\n\n\t// When authenticating with Web eID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.Token method\n\twidAuth := r.Caller == Token && r.Auth == client.WebeIDAuth\n\n\t// All conditions must satisfy simultaneously!\n\tif !(idCardAuth) && !(midAuth) && !(sidAuth) && !(widAuth) {\n\t\treturn false, VoterChoicesInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      VoterChoices,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID,\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "choices/internal/client/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ          = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT  = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_AUTHMETHOD_FROM_CTX            = \"Unable to parse server.AuthMethod from context\"\n\t_SESSIONSTATUS_AUTHMETHOD_EMPTY               = \"server.AuthMethod parsed from context is empty\"\n\t_SESSIONSTATUS_TLS_DIAL                       = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                    = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_IDCARD                         = \"Empty server.AuthMethod is only allowed for ID card users\"\n\t_SESSIONSTATUS_UPDATE_FAIL                    = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID           = \"SessionID has been attempted to tamper\"\n)\n"
  },
  {
    "path": "choices/service/choices/log_desc.go",
    "content": "package main\n\nconst (\n\t_CHOICES_CHOICESREQ              = \"RPC.ChoicesReq\"\n\t_CHOICES_ADMIN_CODE              = \"Unable to fetch admin code from database for this voter ID\"\n\t_CHOICES_NO_FOR_VOTER            = \"No choices for this voter ID\"\n\t_CHOICES_FROM_DB                 = \"Unable to fetch choices for this voter ID from a database\"\n\t_CHOICES_CHOICESRESP             = \"RPC.ChoicesResp\"\n\t_CHOICES_VOTERCHOICESREQ         = \"RPC.VoterChoicesReq\"\n\t_CHOICES_VOTER_NO_AUTH           = \"Voter has not been authenticated\"\n\t_CHOICES_SESSION_ID              = \"Malformed SessionID\"\n\t_CHOICES_SESSION_ID_EXPIRED      = \"SessionID has been expired\"\n\t_CHOICES_NO_VOTER_IN_VOTERS_LIST = \"No such voter ID in a voters list\"\n\t_CHOICES_DB                      = \"Successfully got choices for this voter ID from a database\"\n\t_CHOICES_CHECK_VOTED             = \"Unable to get a response from database whether this voter ID has already voted or not\"\n\t_CHOICES_VOTERCHOICESRESP        = \"RPC.VoterChoicesResp\"\n\t_CHOICES_START                   = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_CHOICES_STOP                    = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_CHOICES_AUTH                    = \"Failed to parse authentication configuration for choices service\"\n\t_CHOICES_SERVER                  = \"Failed to parse server configuration for choices service\"\n\t_CHOICES_SERVER_SERVE            = \"Failed to serve choices service to clients\"\n)\n"
  },
  {
    "path": "choices/service/choices/main.go",
    "content": "/*\nThe choices service serves choice lists for voting and verification.\n*/\npackage main\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tinternal \"ivxv.ee/choices/internal/client/sessionstatus/rpc\"\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/auth\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\n// RPC is the handler for choices service calls.\ntype RPC struct {\n\tstatus      client.Verifier\n\tstorage     *storage.Client\n\tforceList   string // If set, VoterChoices always returns this list.\n\tforeignCode string // Administrative unit code for foreign voters.\n}\n\n// ChoicesArgs are the arguments provided to a call of RPC.Choices.\ntype ChoicesArgs struct {\n\tserver.Header\n\tChoices string `size:\"10\"` // Identifier of the requested choice list.\n}\n\n// VoterArgs are the arguments provided to a call of RPC.VoterChoices. There\n// are none, because the identity of the voter will be extracted from the\n// authentication info.\ntype VoterArgs struct {\n\tserver.Header\n}\n\n// Response is the response returned by RPC.Choices and RPC.VoterChoices.\ntype Response struct {\n\tserver.Header\n\tChoices string // Identifier of the requested choices.\n\tList    []byte // The requested choices.\n\tVoted   bool   `json:\",omitempty\"` // Has the voter voted already?\n}\n\n// Deprecated: Choices RPC endpoint is not used by IVXV backend anymore.\n// Choices is the remote procedure call performed by verification clients to\n// retrieve a specific choices list.\nfunc (r *RPC) Choices(args ChoicesArgs, resp *Response) (err error) {\n\tlog.Log(args.Ctx, ChoicesReq{Choices: args.Choices,\n\t\tDescription: _CHOICES_CHOICESREQ})\n\tresp.Choices = args.Choices\n\n\tif resp.List, err = r.storage.GetChoices(args.Ctx, args.Choices); err != nil {\n\t\tif errors.CausedBy(err, new(storage.NotExistError)) != nil {\n\t\t\tlog.Error(args.Ctx, BadChoicesError{Err: err,\n\t\t\t\tDescription: _CHOICES_NO_FOR_VOTER})\n\t\t\treturn server.ErrBadRequest\n\t\t}\n\t\tlog.Error(args.Ctx, GetChoicesError{Err: log.Alert(err),\n\t\t\tDescription: _CHOICES_FROM_DB})\n\t\treturn server.ErrInternal\n\t}\n\n\t// The choices are not actually sensitive, but just really large.\n\tlog.Log(args.Ctx, ChoicesResp{List: log.Sensitive(resp.List),\n\t\tDescription: _CHOICES_CHOICESRESP})\n\treturn\n}\n\n// VoterChoices is the remote procedure call performed by voting clients to\n// retrieve the choices list for a voter.\nfunc (r *RPC) VoterChoices(args VoterArgs, resp *Response) (err error) {\n\tlog.Log(args.Ctx, VoterChoicesReq{Description: _CHOICES_VOTERCHOICESREQ})\n\n\t// Get the voter identifier associated with the RPC call. If empty, then the request\n\t// was not properly authenticated and error shall be logged\n\tvoter := server.VoterIdentity(args.Ctx)\n\tif len(voter) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedVoterChoicesError{Description: _CHOICES_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.VoterChoices).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, VoterChoicesVerifySessionIDError{Err: err,\n\t\t\tDescription: _CHOICES_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, VoterChoicesUpdateSessionIDError{Description: _CHOICES_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tif len(r.forceList) > 0 {\n\t\tresp.Choices = r.forceList\n\t} else {\n\t\t_, resp.Choices, err = r.storage.VoterChoices(args.Ctx, voter, r.foreignCode)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) != nil {\n\t\t\t\t// Voter successfully authenticated to the backend but\n\t\t\t\t// was not found in the current voterlist\n\t\t\t\tlog.Error(args.Ctx, IneligibleVoterError{Err: err,\n\t\t\t\t\tDescription: _CHOICES_NO_VOTER_IN_VOTERS_LIST})\n\t\t\t\treturn server.ErrIneligible\n\t\t\t}\n\t\t\t// Backend cannot fetch choices list from storage, database may be unreachable\n\t\t\tlog.Error(args.Ctx, VoterChoicesError{Err: log.Alert(err),\n\t\t\t\tDescription: _CHOICES_ADMIN_CODE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\t}\n\tlog.Log(args.Ctx, VoterChoices{Choices: resp.Choices, Description: _CHOICES_DB})\n\n\tif resp.List, err = r.storage.GetChoices(args.Ctx, resp.Choices); err != nil {\n\t\t// Backend cannot fetch choices list from storage, database may be unreachable\n\t\tlog.Error(args.Ctx, GetVoterChoicesError{Err: log.Alert(err),\n\t\t\tDescription: _CHOICES_FROM_DB})\n\t\treturn server.ErrInternal\n\t}\n\n\tif resp.Voted, err = r.storage.CheckVoted(args.Ctx, voter); err != nil {\n\t\t// Backend cannot check whether voter has already voted or not,\n\t\t// database may be unreachable\n\t\tlog.Error(args.Ctx, CheckVotedError{Err: log.Alert(err),\n\t\t\tDescription: _CHOICES_CHECK_VOTED})\n\t\treturn server.ErrInternal\n\t}\n\n\t// The choices are not actually sensitive, but just really large.\n\tlog.Log(args.Ctx, VoterChoicesResp{List: log.Sensitive(resp.List),\n\t\tDescription: _CHOICES_VOTERCHOICESRESP})\n\treturn\n}\n\nfunc main() {\n\t// Call choicesmain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(choicesmain())\n}\n\nfunc choicesmain() (code int) {\n\tc := command.New(\"ivxv-choices\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := internal.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Create new RPC instance with storage and status clients\n\trpc := &RPC{storage: c.Storage, status: statusClient}\n\n\tvar start, stop time.Time\n\tvar authConf server.AuthConf\n\tvar err error\n\n\tif elec := c.Conf.Election; elec != nil {\n\t\t// Check election configuration time values.\n\t\tif start, err = elec.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err,\n\t\t\t\tDescription: _CHOICES_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\tif stop, err = elec.ElectionStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StopTimeError{Err: err,\n\t\t\t\tDescription: _CHOICES_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\n\t\t// Parse client-authentication configuration.\n\t\tif authConf, err = server.NewAuthConf(\n\t\t\telec.Auth, elec.Identity, &elec.Age); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ServerAuthConfError{Err: err,\n\t\t\t\tDescription: _CHOICES_AUTH},\n\t\t\t\t\"failed to configure client authentication:\", err)\n\t\t}\n\n\t\trpc.forceList = strings.TrimSpace(elec.IgnoreVoterList)\n\t\trpc.foreignCode = strings.TrimSpace(elec.VoterForeignEHAKDefault())\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err,\n\t\t\t\tDescription: _CHOICES_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.WithAuth(authConf).ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err,\n\t\t\t\tDescription: _CHOICES_SERVER_SERVE},\n\t\t\t\t\"failed to serve choices service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "collector-admin/Makefile",
    "content": "check:\n\tenv PYTHONPATH=../common/external/schematics \\\n\t\tpylint3 --rcfile=../common/tools/pylintrc ivxv_admin\n"
  },
  {
    "path": "collector-admin/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------------------------\n Collector administration interface\n-----------------------------------\n\n<Description of Collector administration interface.>\n"
  },
  {
    "path": "collector-admin/config/ivxv-admin-ui.conf",
    "content": "# IVXV Internet voting framework\n\n# Apache site configuration for Collector Management Service\n# /etc/apache2/sites-available/ivxv-admin-ui.conf\n\n# When Apache server responds to client, then 'Header:Server:' will be 'Apache', and not 'Apache/2.4.52 (Ubuntu)'\nServerTokens Prod\n\nServerName ivxv-admin\nAddDefaultCharset utf-8\n\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n# Nobody can browse URLs that are outside the <Location> tags listed below in this configuration.\n# E.g. URLs like 'https://myhost.ee/ui/js' will be HTTP 403 Forbidden\n<Directory /var/www/>\n        Options FollowSymLinks\n        AllowOverride None\n        Require all granted\n</Directory>\n\n<VirtualHost *:80>\n    # Redirect all HTTP requests to HTTPS site\n    RewriteEngine on\n    RewriteCond %{HTTPS} !=on\n    RewriteRule ^.* https://%{HTTP_HOST}/ [R=301,L]\n\n    # Access log\n    CustomLog ${APACHE_LOG_DIR}/access-http.log combined\n</VirtualHost>\n\n<VirtualHost *:443>\n    # The ServerName directive sets the request scheme, hostname and port that\n    # the server uses to identify itself. This is used when creating\n    # redirection URLs. In the context of virtual hosts, the ServerName\n    # specifies what hostname must appear in the request's Host: header to\n    # match this virtual host. For the default virtual host (this file) this\n    # value is not decisive as it is used as a last resort host regardless.\n    # However, you must set it for any further virtual host explicitly.\n    ServerAdmin webmaster@localhost\n    DocumentRoot /var/www/collector-admin/\n\n    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n    # error, crit, alert, emerg.\n    # It is also possible to configure the loglevel for particular\n    # modules, e.g.\n    #LogLevel info ssl:warn\n\n    # Access log\n    LogFormat \"%a %l %u %t \\\"%r\\\" %>s %b %T \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" \\\"%q\\\" \\\"%{SSL_CLIENT_S_DN_CN}e\\\"\" ivxv-admin-https\n    CustomLog ${APACHE_LOG_DIR}/access.log ivxv-admin-https\n\n    # SSL {{{\n    # SSL Engine Switch\n    SSLEngine on\n\n    # Create the standard set of SSL related CGI/SSI environment variables\n    SSLOptions StdEnvVars\n\n    # A self-signed certificate can be created by installing\n    # the ssl-cert package.\n    # If both key and certificate are stored in the same file, only the\n    # SSLCertificateFile directive is needed.\n    SSLCertificateFile    \"/etc/ssl/certs/ivxv-admin-default.crt\"\n    SSLCertificateKeyFile \"/etc/ssl/private/ivxv-admin-default.key\"\n\n    # Server Certificate Chain:\n    # Point SSLCertificateChainFile at a file containing the\n    # concatenation of PEM encoded CA certificates which form the\n    # certificate chain for the server certificate. Alternatively\n    # the referenced file can be the same as SSLCertificateFile\n    # when the CA certificates are directly appended to the server\n    # certificate for convinience.\n    #SSLCertificateChainFile /etc/ssl/certs/KLASS3-SK_2010_EECCRCA_SHA384.pem.crt\n\n    # Certificate Authority (CA):\n    # Set the CA certificate verification path where to find CA\n    # certificates for client authentication or alternatively one\n    # huge file containing all of them (file must be PEM encoded)\n    # Note: Inside SSLCACertificatePath you need hash symlinks\n    #       to point to the certificate files. Use the provided\n    #       Makefile to update the hash symlinks after changes.\n    #SSLCACertificatePath /etc/ssl/certs/\n    SSLCACertificateFile /etc/ssl/certs/sk-juur.crt\n\n    # Certificate Revocation Lists (CRL):\n    # Set the CA revocation path where to find CA CRLs for client\n    # authentication or alternatively one huge file containing all\n    # of them (file must be PEM encoded)\n    # Note: Inside SSLCARevocationPath you need hash symlinks\n    #       to point to the certificate files. Use the provided\n    #       Makefile to update the hash symlinks after changes.\n    #SSLCARevocationPath /etc/apache2/ssl.crl/\n    #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl\n\n    # Client Authentication (Type):\n    # Client certificate verification type and depth.  Types are\n    # none, optional, require and optional_no_ca.  Depth is a\n    # number which specifies how deeply to verify the certificate\n    # issuer chain before deciding the certificate is not valid.\n    SSLVerifyClient require\n    SSLVerifyDepth  2\n\n    # Ref: https://www.id.ee/wp-content/uploads/2021/12/21.11-ubuntu-apache2-webserver-ssl-configuration.pdf\n    # Accept only TLSv1.3 protocol in new connections.\n    SSLProtocol -all +TLSv1.3\n\n    # Use the server's preference when choosing a cipher\n    # during an TLSv1.3 handshake.\n    SSLHonorCipherOrder on\n\n    # Uncomment in case you want to check client certificate against OCSP\n    # responder, parsed from certificate's 'Authority Information Access:' field\n    # SSLOCSPEnable on\n\n    # Ensures Perfect Forward Secrecy is not compromised if the server is not restarted regularly.\n    SSLSessionTickets off\n\n    # Specify Diffie-Hellman group (strong, 2048 bit)\n    SSLOpenSSLConfCmd DHParameters \"/etc/ssl/dhparams.pem\"\n\n    # SSL Protocol Adjustments:\n    # The safe and default but still SSL/TLS standard compliant shutdown\n    # approach is that mod_ssl sends the close notify alert but doesn't wait for\n    # the close notify alert from client. When you need a different shutdown\n    # approach you can use one of the following variables:\n    # o ssl-unclean-shutdown:\n    #    This forces an unclean shutdown when the connection is closed, i.e. no\n    #    SSL close notify alert is send or allowed to received.  This violates\n    #    the SSL/TLS standard but is needed for some brain-dead browsers. Use\n    #    this when you receive I/O errors because of the standard approach where\n    #    mod_ssl sends the close notify alert.\n    # o ssl-accurate-shutdown:\n    #    This forces an accurate shutdown when the connection is closed, i.e. a\n    #    SSL close notify alert is send and mod_ssl waits for the close notify\n    #    alert of the client. This is 100% SSL/TLS standard compliant, but in\n    #    practice often causes hanging connections with brain-dead browsers. Use\n    #    this only for browsers where you know that their SSL implementation\n    #    works correctly.\n    # Notice: Most problems of broken clients are also related to the HTTP\n    # keep-alive facility, so you usually additionally want to disable\n    # keep-alive for those clients, too. Use variable \"nokeepalive\" for this.\n    # Similarly, one has to force some clients to use HTTP/1.0 to workaround\n    # their broken HTTP/1.1 implementation. Use variables \"downgrade-1.0\" and\n    # \"force-response-1.0\" for this.\n    BrowserMatch \"MSIE [2-6]\" \\\n                 nokeepalive ssl-unclean-shutdown \\\n                 downgrade-1.0 force-response-1.0\n    # MSIE 7 and newer should be able to use keepalive\n    BrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n    # }}}\n\n    # /ivxv/data - data files for UI {{{\n\n    # Aliases for data files in file system\n    # Access to files is defined in <Directory /var/lib/ivxv/admin-ui-data>\n    Alias \"/ivxv/data/ballot-box\" \"/var/lib/ivxv/ballot-box/\"\n    Alias \"/ivxv/data/commands\"   \"/var/lib/ivxv/commands/\"\n    Alias \"/ivxv/data/\"           \"/var/lib/ivxv/admin-ui-data/\"\n\n    # }}}\n    # /ivxv/cgi - CGI application for UI {{{\n\n    # Define CGI application alias\n    WSGIScriptAlias \"/ivxv/cgi\" \"/var/www/cgi/ivxv-admin.wsgi\"\n\n    # }}}\n</VirtualHost>\n\n\n# Main site\n<Directory /var/www/collector-admin>\n    Require all denied\n\n    ExpiresActive on\n    ExpiresDefault \"access plus 1 minute\"\n\n    # Allow all roles to access main site\n    <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-admin'\">\n        Require all granted\n    </If>\n    <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-election-conf-manager'\">\n        Require all granted\n    </If>\n    <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-viewer'\">\n        Require all granted\n    </If>\n</Directory>\n\n# Exported votes downloadable through user interface\n<Directory /var/lib/ivxv/ballot-box>\n    Require all denied\n\n    ExpiresActive on\n    ExpiresDefault \"access plus 1 second\"\n\n    <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-admin'\">\n        Require all granted\n    </If>\n    <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-download-ballot-box'\">\n        Require all granted\n    </If>\n</Directory>\n\n# Command files with command status files\n<Directory /var/lib/ivxv/commands>\n    Require all denied\n\n    ExpiresActive on\n    ExpiresDefault \"access plus 1 second\"\n\n    # All config command files (bdoc, zip) and command status files (json) for admin\n    <FilesMatch \".+\\.(bdoc|json|zip)\">\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-admin'\">\n            Require all granted\n        </If>\n    </FilesMatch>\n\n    # Voting list command files for election config manager\n    <FilesMatch \"(choices|districts|election|voters)-.+\\.bdoc\">\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-election-conf-manager'\">\n            Require all granted\n        </If>\n    </FilesMatch>\n\n    # Voter list update files for election config manager\n    <FilesMatch \"voters-.+\\.zip\">\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-election-conf-manager'\">\n            Require all granted\n        </If>\n    </FilesMatch>\n\n</Directory>\n\n# JSON data for user interface\n<Directory /var/lib/ivxv/admin-ui-data>\n    AllowOverride None\n    Require all denied\n\n    ExpiresActive on\n    ExpiresDefault \"access plus 1 second\"\n\n    # Districts list, stats and status for every role\n    <FilesMatch \"(districts|stats|status)\\.json\">\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-admin'\">\n            Require all granted\n        </If>\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-election-conf-manager'\">\n            Require all granted\n        </If>\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-viewer'\">\n            Require all granted\n        </If>\n    </FilesMatch>\n\n    # Uploading status files for every role\n    <FilesMatch \".{8}-.{4}-.{4}-.{4}-.{12}\\.json$\">\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-admin'\">\n            Require all granted\n        </If>\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-election-conf-manager'\">\n            Require all granted\n        </If>\n        <If \"-f '/var/lib/ivxv/admin-ui-permissions/%{SSL_CLIENT_S_DN_CN}-viewer'\">\n            Require all granted\n        </If>\n    </FilesMatch>\n</Directory>\n\n# vim:ft=apache foldmethod=marker:\n"
  },
  {
    "path": "collector-admin/config/rsyslog.conf",
    "content": "# IVXV Internet voting framework\n\n# Logging configuration\n# /etc/rsyslog.d/90-ivxv-admin.conf\n\n# Write audit log in JSON format\nif ($programname == 'ivxv_audit_log') then\naction(\n    type=\"omfile\"\n    file=\"/var/log/ivxv/ivxv-audit.log\"\n    # template is defined by ivxv-common package\n    template=\"ivxv-json\"\n)\n"
  },
  {
    "path": "collector-admin/ivxv-collector-admin.conf",
    "content": "# IVXV Internet voting framework\n# Config file for collector admin utilities\n# This file should normally installed at /etc/ivxv-admin-utils.conf\n\n[DEFAULT]\n# base directory for management service data files\n#ivxv_admin_data_path = /var/lib/ivxv\n# directory for admin UI static data files (used directly by web UI)\n#admin_ui_data_path = %(ivxv_admin_data_path)s/admin-ui-data\n# directory for admin UI permissions\n#permissions_path = %(ivxv_admin_data_path)s/admin-ui-permissions\n# directory for applied command files\n#command_files_path = %(ivxv_admin_data_path)s/commands\n# directory for collector config files\n#active_config_files_path = /etc/ivxv\n# directory for uploaded files\n#file_upload_path = %(ivxv_admin_data_path)s/upload\n# directory for exported votes\n#exported_votes_path = %(ivxv_admin_data_path)s/ballot-box\n# directory for ivxv debian packages\n#deb_pkg_path = /etc/ivxv/debs\n# directory for VIS related data files (voter lists)\n#vis_path = %(ivxv_admin_data_path)s/vis\n# directory for service specific files\n#service_files_path = %(ivxv_admin_data_path)s/service\n\n# management database directory\n#ivxv_db_path = %(ivxv_admin_data_path)s/db\n# management database file path\n#ivxv_db_file_path = %(ivxv_db_path)s/ivxv-management.db\n\n# ADMIN UTILS LOGGING\n# https://docs.python.org/3/howto/logging.html\n[loggers]\nkeys=root,\n    ivxv_admin.admin_util,\n    ivxv_admin.command_file,\n    ivxv_admin.db,\n    ivxv_admin.http_daemon,\n    ivxv_admin.lib,\n    ivxv_admin.service\n\n[handlers]\nkeys=null,\n    console,\n    stdout,\n    stdout_service,\n    syslog\n\n[formatters]\nkeys=console,\n    stdout,\n    stdout_service,\n    syslog\n\n\n# LOGGERS\n\n# root logger\n[logger_root]\nlevel=DEBUG\n# Send all log to syslog\nhandlers=syslog\n\n# Admin utility logger\n[logger_ivxv_admin.admin_util]\nlevel=DEBUG\nqualname=ivxv_admin.admin_util\nhandlers=stdout\n\n# Command file handling module logger\n[logger_ivxv_admin.command_file]\nlevel=DEBUG\nqualname=ivxv_admin.command_file\nhandlers=console\n\n# Database layer logger\n[logger_ivxv_admin.db]\nlevel=INFO\nqualname=ivxv_admin.db\nhandlers=console\n\n# Management daemon logger\n[logger_ivxv_admin.http_daemon]\nlevel=INFO\nqualname=ivxv_admin.http_daemon\nhandlers=null\n\n# Lib module logger\n[logger_ivxv_admin.lib]\nlevel=DEBUG\nqualname=ivxv_admin.lib\nhandlers=console\n\n# Service configurator logger\n[logger_ivxv_admin.service]\nlevel=DEBUG\nqualname=ivxv_admin.service\nhandlers=stdout_service\n\n\n# LOG HANDLERS\n\n# Console handler - output log to stderr\n[handler_console]\nclass=StreamHandler\nlevel=INFO\nformatter=console\nargs=(sys.stderr,)\n\n# Stdout handler - output log to stdout\n[handler_stdout]\nclass=StreamHandler\nlevel=INFO\nformatter=stdout\nargs=(sys.stdout,)\n\n# Stdout handler for service module - output log to stdout\n[handler_stdout_service]\nclass=StreamHandler\nlevel=INFO\nformatter=stdout_service\nargs=(sys.stdout,)\n\n# Null handler - drop log messages\n[handler_null]\nclass=NullHandler\nargs=()\n\n# Syslog handler\n[handler_syslog]\nclass=handlers.SysLogHandler\nlevel=DEBUG\nformatter=syslog\nargs=('/dev/log', handlers.SysLogHandler.LOG_LOCAL4)\n\n\n# LOG FORMATTERS\n\n# console formatter\n[formatter_console]\nformat=%(module)s:%(levelname)s: %(message)s\n\n# stdout formatter\n[formatter_stdout]\nformat=%(levelname)s: %(message)s\n\n# stdout formatter for service module\n[formatter_stdout_service]\nformat=%(message)s\n\n# syslog formatter\n[formatter_syslog]\nformat=%(name)s %(message)s\n\n# vim:ft=dosini:\n"
  },
  {
    "path": "collector-admin/ivxv_admin/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Collector Management Service.\"\"\"\n\n__version__ = '1.10.3'\nDEB_PKG_VERSION = '1.10.3'\n\n#: Management daemon data\nMANAGEMENT_DAEMON_PORT = 8080\nMANAGEMENT_DAEMON_URL = f\"http://localhost:{MANAGEMENT_DAEMON_PORT}/\"\n\n#: RFC3339 date format\nRFC3339_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'\n#: RFC3339 date format (without second fractions)\nRFC3339_DATE_FORMAT_WO_FRACT = '%Y-%m-%dT%H:%M:%SZ'\n\n#: User permissions\nPERMISSION_BALLOT_BOX_DOWNLOAD = 'download-ballot-box'\nPERMISSION_ELECTION_CONF = 'election-conf-admin'\nPERMISSION_LOG_VIEW = 'log-view'\nPERMISSION_STATS_VIEW = 'stats-view'\nPERMISSION_TECH_CONF = 'tech-conf-admin'\nPERMISSION_USERS_ADMIN = 'user-admin'\n#: User roles\nUSER_ROLES = {\n    'admin': {\n        'description': 'Administrator',\n        'permissions': (\n            PERMISSION_BALLOT_BOX_DOWNLOAD,\n            PERMISSION_ELECTION_CONF,\n            PERMISSION_LOG_VIEW,\n            PERMISSION_STATS_VIEW,\n            PERMISSION_TECH_CONF,\n            PERMISSION_USERS_ADMIN,\n        ),\n    },\n    'election-conf-manager': {\n        'description': 'Election config manager',\n        'permissions': (\n            PERMISSION_BALLOT_BOX_DOWNLOAD,\n            PERMISSION_ELECTION_CONF,\n            PERMISSION_STATS_VIEW\n        ),\n    },\n    'viewer': {\n        'description': 'Viewer',\n        'permissions': (\n            PERMISSION_STATS_VIEW,\n        ),\n    },\n    'none': {\n        'description': 'No permissions',\n        'permissions': tuple(),\n    },\n}\n\n#: Config types\nCFG_TYPES = {\n    'trust': 'trust root configuration',\n    'election': 'elections configuration',\n    'technical': 'collectors technical configuration',\n}\n#: Voting list types\nVOTING_LIST_TYPES = {\n    'choices': 'choices list',\n    'districts': 'districts list',\n    'voters': 'voters list',\n}\n#: Command types\nCMD_TYPES = list(CFG_TYPES) + list(VOTING_LIST_TYPES) + ['user']\n#: Command descriptions\nCMD_DESCR = {'user': 'user permissions configuration'}\nCMD_DESCR.update(CFG_TYPES)\nCMD_DESCR.update(VOTING_LIST_TYPES)\n\n\n#: Collector states\nCOLLECTOR_STATE_NOT_INSTALLED = 'NOT INSTALLED'\nCOLLECTOR_STATE_INSTALLED = 'INSTALLED'\nCOLLECTOR_STATE_CONFIGURED = 'CONFIGURED'\nCOLLECTOR_STATE_FAILURE = 'FAILURE'\nCOLLECTOR_STATE_PARTIAL_FAILURE = 'PARTIAL FAILURE'\nCOLLECTOR_STATES = [\n    COLLECTOR_STATE_NOT_INSTALLED,\n    COLLECTOR_STATE_INSTALLED,\n    COLLECTOR_STATE_CONFIGURED,\n    COLLECTOR_STATE_FAILURE,\n    COLLECTOR_STATE_PARTIAL_FAILURE,\n]\n\n#: Service states\nSERVICE_STATE_NOT_INSTALLED = 'NOT INSTALLED'\nSERVICE_STATE_INSTALLED = 'INSTALLED'\nSERVICE_STATE_CONFIGURED = 'CONFIGURED'\nSERVICE_STATE_FAILURE = 'FAILURE'\nSERVICE_STATE_REMOVED = 'REMOVED'\nSERVICE_STATES = [\n    SERVICE_STATE_NOT_INSTALLED,\n    SERVICE_STATE_INSTALLED,\n    SERVICE_STATE_CONFIGURED,\n    SERVICE_STATE_FAILURE,\n    SERVICE_STATE_REMOVED,\n]\n\n#: Service states included to status monitoring\nSERVICE_MONITORING_STATES = [SERVICE_STATE_CONFIGURED, SERVICE_STATE_FAILURE]\n\n#: Service type parameters.\n#: ``main_service`` - is service required to collect votes\n#: (*False* = support service);\n#: **require_config** - does service require election config package for\n#: operation;\n#: ``require_tls`` - does service require TLS certificate and key to\n#: ``tspreg`` - can communicate with TSP registration service\n#: ``mobile_id`` - can communicate with Mobile ID service\n#: communicate with other services;\nSERVICE_TYPE_PARAMS = {\n    'backup': {\n        'main_service': False,\n        'require_config': False,\n        'require_tls': False,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'choices': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': True,\n    },\n    'log': {\n        'main_service': False,\n        'require_config': False,\n        'require_tls': False,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'mid': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': True,\n    },\n    'votesorder': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'proxy': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': False,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'smartid': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': True,\n    },\n    'webeid': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': True,\n    },\n    'storage': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'verification': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n    'voting': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': True,\n        'mobile_id': True,\n    },\n    'sessionstatus': {\n        'main_service': True,\n        'require_config': True,\n        'require_tls': True,\n        'tspreg': False,\n        'mobile_id': False,\n    },\n}\n\n#: Service secret types\nSERVICE_SECRET_TYPES = {\n    'tls-cert': {\n        'description': 'Service TLS certificate',\n        'db-key': 'tls-cert',\n        'target-path': '/var/lib/ivxv/service/{service_id}/tls.pem',\n        'shared': False,\n    },\n    'tls-key': {\n        'description': 'Service TLS key',\n        'db-key': 'tls-key',\n        'target-path': '/var/lib/ivxv/service/{service_id}/tls.key',\n        'shared': False,\n    },\n    'mid-token-key': {\n        'description': 'Mobile-ID/Smart-ID/Web eID identity token',\n        'db-key': 'mid-token-key',\n        'target-path': '/var/lib/ivxv/service/ticket.key',\n        'shared': True,\n    },\n    'tsp-regkey': {\n        'description': 'PKIX TSP registration key',\n        'db-key': 'tspreg-key',\n        'target-path': '/var/lib/ivxv/service/{service_id}/tspreg.key',\n        'shared': False,\n    },\n}\n\n#: Filenames of collector deb packages\nCOLLECTOR_PKG_FILENAMES = {\n    'ivxv-admin': f'ivxv-admin_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-backup': f'ivxv-backup_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-choices': f'ivxv-choices_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-common': f'ivxv-common_{DEB_PKG_VERSION}_all.deb',\n    'ivxv-log': f'ivxv-log_{DEB_PKG_VERSION}_all.deb',\n    'ivxv-mid': f'ivxv-mid_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-votesorder': f'ivxv-votesorder_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-smartid': f'ivxv-smartid_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-webeid': f'ivxv-webeid_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-proxy': f'ivxv-proxy_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-storage': f'ivxv-storage_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-verification': f'ivxv-verification_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-voting': f'ivxv-voting_{DEB_PKG_VERSION}_amd64.deb',\n    'ivxv-sessionstatus': f'ivxv-sessionstatus_{DEB_PKG_VERSION}_amd64.deb',\n}\n\n#: Event log filename\nEVENT_LOG_FILENAME = 'ivxv-management-events.log'\n\n#: Event descriptions\nEVENTS = {\n    # collector state events\n    \"COLLECTOR_INIT\": \"Initialize Collector\",\n    \"COLLECTOR_RESET\":\n    f\"Reset Collector (state: {COLLECTOR_STATE_NOT_INSTALLED!r})\",\n    \"COLLECTOR_STATE_CHANGE\":\n    \"Collector state changed from {last_state!r} to {state!r}\",\n    # command loading events\n    \"CMD_LOAD\": \"Load command {cmd_type!r} version {version!r}\",\n    \"CMD_LOADED\": \"Command {cmd_type!r} is loaded, version {version!r}\",\n    \"CMD_REMOVED\": \"Command {cmd_type!r} is removed, version {version!r}\",\n    # voter list downloading events\n    \"VOTER_LIST_DOWNLOADED\": \"Downloaded voter list changeset #{changeset_no}\",\n    \"VOTER_LIST_DOWNLOAD_FAILED\":\n    \"Failed to download voter list changeset #{changeset_no}\",\n    # user permission management events\n    \"PERMISSION_SET\": \"Add permission {permission!r} to user {user_cn!r}\",\n    \"PERMISSION_RESET\": \"Reset user {user_cn!r} permissions\",\n    # election start/stop times registering\n    \"SET_ELECTION_TIME\": \"Election {period!r} timestamp set to {timestamp}\",\n    # service management events\n    \"SERVICE_REGISTER\":\n    f\"Add {{service_type}} service (state: {SERVICE_STATE_NOT_INSTALLED!r})\",\n    \"SERVICE_CONFIG_APPLY\": 'Applied {cfg_descr} version {cfg_version!r}',\n    \"SERVICE_STATE_CHANGE\": 'Service state changed from {last_state!r} to {state!r}',\n    \"SECRET_INSTALL\": \"{secret_descr} loaded to service\",\n}\n"
  },
  {
    "path": "collector-admin/ivxv_admin/agent_daemon.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nAgent daemon for collector management service.\n\nThis daemon is managed by systemd.\n\n* Systemd service file:\n  :file:`/lib/systemd/system/ivxv-admin.service`\n\n* Query daemon status:\n  :command:`systemctl status ivxv-admin`\n\n* Start daemon:\n  :command:`systemctl stop ivxv-admin`\n\n* Stop daemon:\n  :command:`systemctl status ivxv-admin`\n\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\nimport re\nimport subprocess\nimport time\nimport typing\n\nimport dateutil.parser\n\nfrom . import (CMD_DESCR, RFC3339_DATE_FORMAT, SERVICE_MONITORING_STATES,\n               SERVICE_STATE_CONFIGURED, SERVICE_STATE_FAILURE, lib)\nfrom .cli_utils import init_cli_util\nfrom .config import cfg_path\nfrom .db import DB_FILE_PATH, IVXVManagerDb\nfrom .event_log import register_service_event\nfrom .lib.lockfile import PidLocker\nfrom .service.service import Service\n\n# create logger\nlog = logging.getLogger(__name__)\n\n#: Service ping interval in seconds.\nPING_INTERVAL = 60\n#: State file path.\nSTATE_FILEPATH = cfg_path(\"admin_ui_data_path\", \"status.json\")\n#: Stats file path.\nSTATS_FILEPATH = cfg_path('admin_ui_data_path', 'stats.json')\n#: Maximum count of automatic attempts to apply config\nMAX_AUTO_ATTEMPTS = 3\n#: Unix epoch timestamp\nUNIX_EPOCH_TIMESTAMP = '1970-01-01T00:00:00'\n\n\ndef main_loop():\n    \"\"\"Agent daemon main loop.\"\"\"\n    args = init_cli_util(\"\"\"\n        IVXV Collector Management Service agent daemon.\n\n        Usage: ivxv-agent-daemon [--get-stats] [--register-status]\n\n        Options:\n            --get-stats         Copy statistics from Log Monitor to\n                                Management Service without daemonizing.\n            --register-status   Register collector state (if not registered).\n    \"\"\")\n\n    # copy stats and finish process\n    if args['--get-stats'] and args[\"--register-status\"]:\n        log.error('Conflicting options: --get-stats and --register-status')\n        return 1\n    if args['--get-stats']:\n        return generate_stats_data(force=True)\n    if args[\"--register-status\"]:\n        with IVXVManagerDb(for_update=True) as db:\n            register_collector_state(db)\n        return 0\n\n    # daemon process\n    log.info('Starting Collector Management Service agent daemon')\n    check_management_db()\n\n    jobs: typing.Set[subprocess.Popen] = set()\n\n    while True:\n        loop_start_time = datetime.datetime.now()\n\n        # generate stats/state data\n        state = None\n        try:\n            PidLocker.rm_stale_pidfile('ivxv-config-apply.pid')\n            state = generate_state_data()\n            generate_stats_data()\n        except OSError as err:\n            log.error(err)\n        except Exception as err:  # pylint: disable=broad-except\n            log.error(\n                'Unknown error while generating stats/state data: %s', err)\n        finally:\n            for job in jobs:\n                if job.poll() is not None:  # job is not yet terminated\n                    if job.stdin is not None:\n                        job.stdin.close()\n                    if job.stdout is not None:\n                        job.stdout.close()\n                    if job.stderr is not None:\n                        job.stderr.close()\n                    job.kill()  # send SIGKILL to terminated process, just in case\n                    jobs.remove(job)\n\n        # start config applying if required\n        if state and len(jobs) == 0:\n            jobs = apply_cfg(state)\n\n        # pause after loop\n        duration = datetime.datetime.now() - loop_start_time\n        if duration.seconds < 5:\n            log.debug('Sleeping for 5 seconds')\n            time.sleep(5)\n        else:\n            time.sleep(1)\n\n\ndef generate_state_data():\n    \"\"\"\n    Ping services, generate state data and write it to :file:`status.json`.\n    \"\"\"\n    # fetch services data\n    services = get_collector_data()\n    if services:\n        # sort services list in order of last data\n        services_sorted = sorted([\n            [service['last-data'], service_id]\n            for service_id, service in services.items()])\n\n        # check services\n        for last_data_timestamp, service_id in services_sorted:\n            service_data = services[service_id]\n            if service_data['state'] not in SERVICE_MONITORING_STATES:\n                # log.debug('Service %s state is %s, skipping check',\n                #           service_id, service_data['state'])\n                continue\n            next_check_timestamp = (\n                dateutil.parser.parse(last_data_timestamp or\n                                      UNIX_EPOCH_TIMESTAMP) +\n                datetime.timedelta(seconds=PING_INTERVAL))\n            if next_check_timestamp > datetime.datetime.now():\n                log.debug('Service %s next check is in the future, '\n                          'skipping check', service_id)\n                continue\n\n            ping_service(service_id, service_data)\n            time.sleep(1)\n\n    # generate state data\n    with IVXVManagerDb(for_update=True) as db:\n        state = register_collector_state(db)\n\n    # generate config applying state\n    state['config-apply'] = {}\n    for cfg_key in ['trust', 'technical', 'election', 'choices', 'districts']:\n        get_cfg_applying_state(cfg_key, state)\n    for changeset_no in range(10_000):\n        if not get_cfg_applying_state(f\"voters{changeset_no:04}\", state):\n            break\n\n    # write state data to file\n    state['meta'] = get_agent_metadata()\n    with open(STATE_FILEPATH, \"w\") as fp:\n        fp.write(json.dumps(state, indent=4, sort_keys=True))\n\n    return state\n\n\ndef get_cfg_applying_state(cfg_key, state):\n    \"\"\"Get config applying state.\n\n    :return: True if config file exist and state data is generated,\n             False if config file does not exist.\n    \"\"\"\n    cfg_filepath = lib.get_loaded_cfg_file_path(cfg_key)\n    if cfg_filepath is None:\n        return False\n    state_file_filepath = cfg_filepath.replace(\n        os.path.splitext(cfg_filepath)[1], \".json\"\n    )\n\n    try:\n        with open(state_file_filepath) as fp:\n            apply_state = json.load(fp)\n        state['config-apply'][cfg_key] = {\n            'version': apply_state['config_version'],\n            'attempts': apply_state['attempts'],\n            'completed': apply_state['completed'],\n            'state_file': os.path.basename(state_file_filepath),\n        }\n    except FileNotFoundError:\n        state['config-apply'][cfg_key] = {}\n    state['config-apply'][cfg_key]['cmd_file'] = os.path.basename(cfg_filepath)\n\n    return True\n\n\ndef get_agent_metadata():\n    \"\"\"Generate agent metadata block.\"\"\"\n    timestamp = datetime.datetime.utcnow()\n    return dict(generator='IVXV Management Service Agent Daemon',\n                time_generated=timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'))\n\n\ndef apply_cfg(state) -> typing.Set[subprocess.Popen]:\n    \"\"\"Apply config files.\"\"\"\n    jobs: typing.Set[subprocess.Popen] = set()\n\n    job = apply_cfg_for_services(\"technical\", \"technical\", state)\n    if job is not None and job is not False:\n        jobs.add(job)\n\n    if state['config']['technical']:\n        for cfg_key in [\"election\", \"choices\", \"districts\"]:\n            job = apply_cfg_for_services(cfg_key, cfg_key, state)\n            if job is not None and job is not False:\n                jobs.add(job)\n    for changeset_no in range(10_000):\n        job = apply_cfg_for_services(\"voters\", f\"voters{changeset_no:04}\", state)\n        if job is None:  # voter list doesn't exist or currently being loaded\n            break\n        if job is False:  # voter list has already been loaded\n            continue\n        jobs.add(job)\n\n    return jobs\n\n\ndef apply_cfg_for_services(cfg_type, cfg_key, state) -> subprocess.Popen | bool | None:\n    \"\"\"Apply config for services.\n\n    :return: None if config file does not exist,\n             True if config applying is started,\n             False if config applying is not started.\n    \"\"\"\n    cfg_filepath = lib.get_loaded_cfg_file_path(cfg_key)\n    if cfg_filepath is None:\n        return None\n\n    state_file_filepath = cfg_filepath.replace(\n        os.path.splitext(cfg_filepath)[1], \".json\"\n    )\n    with open(state_file_filepath) as fp:\n        state = json.load(fp)\n\n    # check preconditions\n    if (not state['autoapply'] or\n            state['completed'] or\n            state['attempts'] >= MAX_AUTO_ATTEMPTS):\n        return False\n\n    if PidLocker.pidfile_exists('ivxv-config-apply.pid'):\n        log.info('Can\\'t start automatic applying of %s, pidfile exists',\n                 CMD_DESCR[cfg_type])\n        return None\n\n    # execute config applying command\n    log.info('Automatically apply %s, attempt #%d',\n             CMD_DESCR[cfg_type], state['attempts'] + 1)\n\n    return subprocess.Popen(\n        args=['ivxv-config-apply', f'--type={cfg_type}'],\n        stdin=subprocess.DEVNULL,\n        stdout=subprocess.DEVNULL,\n        stderr=subprocess.DEVNULL,\n    )\n\n\ndef ping_service(service_id, service_data):\n    \"\"\"Ping service.\"\"\"\n    with Service(service_id, service_data) as service:\n        service_ok = service.ping()\n\n        if not is_db_accessible(service.get_db_key(\"last-data\")):\n            return False\n\n        # register result in database\n        with IVXVManagerDb(for_update=True) as db:\n            db.set_value(\n                service.get_db_key('last-data'),\n                datetime.datetime.now().strftime(RFC3339_DATE_FORMAT))\n\n            ping_errors_key = service.get_db_key('ping-errors')\n            ping_errors_old = db.get_value(ping_errors_key)\n\n            service_state_key = service.get_db_key('state')\n            service_state_old = db.get_value(service_state_key)\n\n            if service_state_old not in SERVICE_MONITORING_STATES:\n                log.warning('Service has removed from monitoring')\n                return False\n\n            service_state_new = service_state_old\n            if service_ok:\n                log.debug('Service %s is alive', service_id)\n                ping_errors_new = '0'\n                service_state_new = SERVICE_STATE_CONFIGURED\n                if ping_errors_old != '0':\n                    db.set_value(ping_errors_key, '0')\n            else:\n                ping_errors_new = str(int(ping_errors_old) + 1)\n                if (int(ping_errors_new) >= 3 and\n                        service_state_old != SERVICE_STATE_FAILURE):\n                    log.warning('Status check failed three times, '\n                                'setting service state from %s to FAILURE',\n                                service_state_old)\n                    service_state_new = SERVICE_STATE_FAILURE\n                else:\n                    log.warning('Status check for service %s failed (%s times). '\n                                'Service state is %s',\n                                service_id, ping_errors_new, service_state_old)\n\n            if ping_errors_old != ping_errors_new:\n                db.set_value(ping_errors_key, ping_errors_new)\n            if service_state_old != service_state_new:\n                db.set_value(service_state_key, service_state_new)\n                register_collector_state(db)\n\n    return service_ok\n\n\ndef generate_stats_data(force=False):\n    \"\"\"Generate stats data and write it to :file:`stats.json`.\n\n    :param force: Force check even the next check timestamp is in the future.\n    :type force: bool\n    \"\"\"\n    # read existing stats file\n    stats = {}\n    try:\n        with open(STATS_FILEPATH) as fp:\n            stats = json.load(fp)\n    except json.decoder.JSONDecodeError as err:\n        log.error(\"Invalid JSON in existing stats file %r: %s\", STATS_FILEPATH, err)\n    except OSError as err:\n        log.error(\"Cannot load existing stats JSON file %r: %s\", STATS_FILEPATH, err)\n    stats.setdefault('data', {})\n\n    # import stats file from Log Monitor\n    logmon_address, last_data_timestamp = get_logmon_data()\n    if logmon_address:\n        next_check_timestamp = (\n            dateutil.parser.parse(last_data_timestamp) +\n            datetime.timedelta(seconds=PING_INTERVAL))\n        if not force and next_check_timestamp > datetime.datetime.now():\n            log.debug('Log Monitor service next check is in the future, '\n                      'skipping check')\n        else:\n            stats_data = query_logmon_stats(logmon_address)\n            if 'error' in stats_data:\n                stats['error'] = stats_data['error']\n            else:\n                normalize_stats(stats_data)\n                stats['data'] = stats_data\n                try:\n                    del stats['error']\n                except KeyError:\n                    pass\n    else:\n        stats = {'error': 'Log Monitor address is not defined'}\n        log.error(stats['error'])\n\n    # update metadata\n    stats['meta'] = get_agent_metadata()\n\n    # write stats data to file\n    with open(STATS_FILEPATH, 'w') as fp:\n        fp.write(json.dumps(stats, indent=4, sort_keys=True))\n\n\ndef normalize_stats(stats_data):\n    \"\"\"Normalize Log Monitor stats.\n\n    Convert dictionaries to sorted lists.\n    \"\"\"\n    for district_id, district_stats in sorted(stats_data.items()):\n        if district_id == 'time':\n            continue\n        for stats_key, stats_val in district_stats.items():\n            if isinstance(stats_val, dict):\n                stats_data[district_id][stats_key] = sorted(\n                    [[item[0], item[1]] for item in stats_val.items()],\n                    reverse=True\n                )[:10 if stats_key in [\n                    'voting-operating-systems',\n                    'verify-operating-system'] else 100]\n\n    # generate empty values for missing districts.\n    # Log Monitor does not have district list and cannot generate stats for\n    # districts that have no voter data. Generate empty data for such\n    # districts.\n    districts = []\n    districts_filename = cfg_path('admin_ui_data_path', 'districts.json')\n    try:\n        with open(districts_filename) as fp:\n            districts = json.load(fp)\n    except FileNotFoundError:\n        log.debug('Missing districts list. '\n                  'Will not generate empty blocks for missing districts')\n    empty_stats = dict(\n        [key, (list() if isinstance(val, list) else 0)]\n        for key, val in stats_data['TOTAL'].items())\n    for district_id, _ in districts:\n        if district_id not in stats_data:\n            stats_data[district_id] = empty_stats\n\n\ndef query_logmon_stats(address):\n    \"\"\"Query stats file from Log Monitor service.\n\n    Query stats file from Log Monitor service and register last query timestamp\n    in the management database.\n\n    :return: Stats data. Existing stats data is returned with error message in\n             ``error`` value if query fails.\n    :rtype: dict\n    \"\"\"\n    ssh_cmd = [\n        'ssh', '-T', '-o', 'PreferredAuthentications=publickey',\n        f\"logmon@{address}\", \"cat\", \"/var/lib/ivxv/stats.json\"\n    ]\n\n    log.debug('Querying stats from Log Monitor')\n    proc = subprocess.run(\n        ssh_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False\n    )\n\n    stats = {}\n    if proc.returncode:\n        log.error(\n            'Querying stats from Log Monitor finished with error code %d',\n            proc.returncode)\n        stats['error'] = (\n            'Error while transporting stats data from Log Monitor over SSH:'\n            f\"\\n {proc.stderr.decode('utf-8')}\"\n        )\n    else:\n        try:\n            stats = json.loads(proc.stdout.decode('utf-8'))\n        except json.JSONDecodeError as err:\n            errmsg = f\"{err.msg}: line {err.lineno} column {err.colno} (char {err.pos})\"\n            stats['error'] = f'Invalid JSON data from Log Monitor: {errmsg}'\n            log.error('Error while parsing JSON stats from Log Monitor: %s',\n                      errmsg)\n\n    if is_db_accessible(\"logmonitor/last-data\"):\n        with IVXVManagerDb(for_update=True) as db:\n            db.set_value('logmonitor/last-data',\n                         datetime.datetime.now().strftime(RFC3339_DATE_FORMAT))\n\n    return stats\n\n\ndef check_management_db():\n    \"\"\"Check management database file.\n\n    Wait if database file does not exist.\n    \"\"\"\n    # wait if database does not exist\n    check_interval = 1\n    check_notif_interval = 60\n    check_counter = 0\n    while not os.path.exists(DB_FILE_PATH):\n        if check_counter % check_notif_interval == 0:\n            log.warning('Collector management database does not exist, '\n                        'waiting')\n        check_counter += 1\n        time.sleep(check_interval)\n\n    log.info('Collector management database is available')\n\n\ndef get_collector_data():\n    \"\"\"Read collector data from management database.\n\n    :return: service data or None if election config is not loaded.\n    :rtype: dict\n    \"\"\"\n    with IVXVManagerDb() as db:\n        election_cfg_ver = db.get_value('config/election')\n        if not election_cfg_ver:\n            return None\n\n        services = {}\n        for key in db.keys():\n            if re.match(r'service/', key):\n                service_id, field_name = key.split('/')[1:]\n                services[service_id] = services.get(service_id, {})\n                services[service_id][field_name] = db.get_value(key)\n\n    return services\n\n\ndef get_logmon_data():\n    \"\"\"Read logmonitor address from management database.\n\n    :return: Log Monitor data: [address, last_data_timestamp].\n    :rtype: list\n    \"\"\"\n    with IVXVManagerDb() as db:\n        address = db.get_value('logmonitor/address')\n        last_data_timestamp = db.get_value('logmonitor/last-data')\n\n    if not last_data_timestamp:\n        last_data_timestamp = UNIX_EPOCH_TIMESTAMP\n\n    return [address, last_data_timestamp]\n\n\ndef register_collector_state(db):\n    \"\"\"Detect collector state and register state change in database.\"\"\"\n    state = lib.generate_collector_state(db)\n    last_state = state['collector']['state']\n    if state['collector_state'] != last_state:\n        log.info('Registering new state for collector: %s',\n                 state['collector_state'])\n        db.set_value('collector/state', state['collector_state'])\n        register_service_event(\n            'COLLECTOR_STATE_CHANGE',\n            params={\n                'state': state['collector_state'],\n                'last_state': last_state\n            })\n\n    return state\n\n\ndef is_db_accessible(check_value):\n    \"\"\"Try to read field value from database before writing it.\n\n    If this fails, then something is happened with database (e.g. database is\n    recreated during service reset) and writing to database must be cancelled.\n    \"\"\"\n    with IVXVManagerDb() as db:\n        try:\n            db.get_value(check_value)\n        except KeyError:\n            log.warning(\"Cannot read value %r from database\", check_value)\n            return False\n\n    return True\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities.\"\"\"\n\nimport logging\nimport os\nimport sys\nimport textwrap\n\nfrom docopt import docopt\n\nfrom .. import __version__\nfrom ..config import CONFIG\n\nassert CONFIG  # logging is configured by config module\nlog = logging.getLogger('ivxv_admin.admin_util')\n\n\ndef init_cli_util(docstr, allow_root=False):\n    \"\"\"\n    Initialize command line utility.\n\n    1. Validate command line arguments\n    2. Set up logging\n\n    Config file :file:`ivxv-collector-admin.conf` is searched from the\n    following locations:\n\n    * current directory\n\n    * :file:`/etc/ivxv`\n\n    * directory specified by environment variable :envvar:`IVXV_ADMIN_CONF`\n\n    * file specified by environment variable :envvar:`IVXV_ADMIN_CONF`\n    \"\"\"\n    # validate CLI arguments\n    cli_args = docopt(textwrap.dedent(docstr), version=__version__)\n\n    # set log level\n    log.setLevel(cli_args.get('--log-level', 'INFO'))\n\n    # check user rights\n    if not allow_root and not os.getuid():\n        log.error('IVXV collector admin utils cannot be run as root')\n        sys.exit(1)\n\n    return cli_args\n\n\ndef ask_user_confirmation(question):\n    \"\"\"Ask user confirmation.\n\n    :return: True if user answers \"Yes\" or False if not.\n    \"\"\"\n    while True:\n        answer = input(question).upper()\n        if answer in 'YN':\n            return answer == 'Y'\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/admin_storage_utils.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for management service data storage.\"\"\"\n\nimport os\nimport re\nimport shutil\nimport sys\n\nfrom .service_utils import remove_ivxv_admin_crontab\nfrom ..config import CONFIG, cfg_path\nfrom ..db import IVXVManagerDb, check_db_dir\nfrom ..event_log import init_event_log\nfrom ..lib import IvxvError, clean_dir\nfrom . import ask_user_confirmation, init_cli_util, log\n\n#: Config value names for Management Service data directories\nMANAGEMENT_PATH_PARAM_NAMES = [\n    'ivxv_admin_data_path',\n    'admin_ui_data_path',\n    'permissions_path',\n    'command_files_path',\n    'active_config_files_path',\n    'file_upload_path',\n    'exported_votes_path',\n    'deb_pkg_path',\n    'ivxv_db_path',\n    'vis_path',\n]\n\n\ndef ivxv_create_data_dirs_util():\n    \"\"\"Create management service data directories.\"\"\"\n    init_cli_util(\n        \"\"\"\n        Create IVXV Collector Management Service data directories.\n\n        NOTE: Directory owners and permissions are not set by this utility!\n\n        Usage: ivxv-create-data-dirs\n    \"\"\",\n        allow_root=True)\n\n    # config parameter names for directories\n    for cfg_var in MANAGEMENT_PATH_PARAM_NAMES:\n        dirname = CONFIG[cfg_var]\n        if os.path.exists(dirname):\n            log.info(\"Path %r already exist\", dirname)\n        else:\n            log.info(\"Creating data directory %r\", dirname)\n            os.mkdir(dirname)\n        if not os.path.isdir(dirname):\n            log.error(\"Path %r is not a directory\", dirname)\n            return 1\n\n    return 0\n\n\ndef ivxv_collector_init_util():\n    \"\"\"Initialize IVXV Collector.\"\"\"\n    args = init_cli_util(\"\"\"\n    Initialize IVXV Collector.\n\n    Usage: ivxv-collector-init [--force]\n\n    Options:\n        --force     Don't ask user confirmation\n    \"\"\")\n\n    # ask confirmation\n    if not args['--force']:\n        if not ask_user_confirmation(\n                'Do You want to initialize IVXV Collector (Y=yes) ?'):\n            return 1\n\n    # remove ivxv-admin crontab if exist\n    remove_ivxv_admin_crontab()\n\n    # initialize data directories\n    try:\n        init_data_directories()\n    except IvxvError as err:\n        log.error(err)\n        return 1\n\n    # initialize management database\n    init_management_database()\n\n    init_event_log()\n\n    return 0\n\n\ndef database_util():\n    \"\"\"Management service database utility.\"\"\"\n    args = init_cli_util(\"\"\"\n    Add, remove or modify key/value pairs in\n    IVXV Collector Management Service database.\n\n    WARNING!\n        Use this utility only for testing purposes!\n        Never change database in production systems!\n\n    Usage: ivxv-db <key> <value>\n        ivxv-db --del <key>\n    \"\"\")\n\n    # check database directory\n    if not check_db_dir():\n        return 1\n\n    # process record\n    with IVXVManagerDb(for_update=True) as db:\n        if args['--del']:\n            db.rm_value(args['<key>'])\n            log.info(\"Database value %r successfully removed\", args[\"<key>\"])\n        else:\n            db.set_value(args['<key>'], args['<value>'])\n            log.info(\"Database value %r set to %r\", args[\"<key>\"], args[\"<value>\"])\n\n    return 0\n\n\ndef database_dump_util():\n    \"\"\"Dump management service database.\"\"\"\n    args = init_cli_util(\"\"\"\n        Dump IVXV Collector Management Service database.\n\n        Usage: ivxv-db-dump [<key>] ...\n    \"\"\")\n\n    log.info('Dumping IVXV management database')\n\n    # check database directory\n    if not check_db_dir():\n        return 1\n\n    with IVXVManagerDb() as db:\n        db.dump(args['<key>'])\n\n    return 0\n\n\ndef database_reset_util():\n    \"\"\"Reset management service database.\"\"\"\n    args = init_cli_util(\"\"\"\n        Reset IVXV Collector Management Service database.\n\n        Usage: ivxv-db-reset [--force]\n\n        Options:\n            --force     Don't ask user confirmation\n    \"\"\")\n\n    # check database directory\n    db_file_path = check_db_dir()\n    if not db_file_path:\n        return 1\n\n    # ask confirmation\n    if not args['--force']:\n        if not ask_user_confirmation(\n                'Do You want to reset IVXV management database (Y=yes) ?'):\n            return 1\n\n    # initialize database\n    init_management_database()\n\n    # initialize data files\n    init_management_datafiles()\n\n    # initialize data directories\n    clean_dir(CONFIG['file_upload_path'])\n\n    return 0\n\n\ndef init_data_directories():\n    \"\"\"Initialize data directories.\"\"\"\n    for cfg_var in MANAGEMENT_PATH_PARAM_NAMES:\n        dirpath = CONFIG[cfg_var]\n        if not os.path.exists(dirpath):\n            log.info(\"Creating directory %r\", dirpath)\n            os.mkdir(dirpath)\n        elif not os.path.isdir(dirpath):\n            raise IvxvError(f\"Path {dirpath!r} is not a directory\")\n        if cfg_var not in [\n            \"ivxv_admin_data_path\",\n            \"deb_pkg_path\",\n            \"active_config_files_path\",\n        ]:\n            clean_dir(dirpath)\n        patterns = (\n            r\"(choices|districts|election|technical|trust|voters0000)\\.bdoc$\",\n            r\"voters[0-9]{2}\\.zip$\",\n        )\n        for filename in os.listdir(CONFIG[\"active_config_files_path\"]):\n            if any(re.match(pattern, filename) for pattern in patterns):\n                os.unlink(cfg_path(\"active_config_files_path\", filename))\n\n    init_management_datafiles()\n\n\ndef init_management_database():\n    \"\"\"Initialize management database.\"\"\"\n    log.debug('Initializing IVXV management database')\n\n    IVXVManagerDb.reset()\n\n    log.info('New management database is created with default values')\n\n\ndef init_management_datafiles():\n    \"\"\"Initialize management data files.\"\"\"\n    # install empty stats.json to admin UI data path\n    module_path = os.path.dirname(sys.modules['ivxv_admin'].__file__)\n    file_path = os.path.join(module_path, 'templates/stats.json')\n    shutil.copy(file_path, CONFIG['admin_ui_data_path'])\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/backup_utils.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for backup service.\"\"\"\n\nimport datetime\nimport os\nimport random\nimport subprocess\nimport sys\nimport time\n\nfrom jinja2 import Environment, PackageLoader\n\nfrom .. import SERVICE_STATE_CONFIGURED\nfrom ..config import CONFIG\nfrom ..lib import get_services\nfrom ..service.service import Service\nfrom . import init_cli_util, log\n\n\ndef insert_backup_crontab(data: str, crontab: str) -> str:\n    \"\"\"Wrap crontab in an ivxv_backup_crontab block and insert it to a data.\n\n    :param data: any data\n    :type data: str\n    :param crontab: rendered ivxv_backup_crontab Jinja2 crontab template file\n    :type crontab: str\n    :return: data with an ivxv_backup_crontab block\n    :rtype: str\n    \"\"\"\n    header = '### block ivxv_backup_crontab ###'\n    tail = '### endblock ivxv_backup_crontab ###'\n    return data + \"\\n\\n\" + header + \"\\n\" + crontab + \"\\n\" + tail + \"\\n\"\n\n\ndef __get_backup_crontab_block(data: str) -> (str, bool):\n    \"\"\"Get ivxv_backup_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: ivxv_backup_crontab block and True on success,\n    otherwise data and False\n    :rtype: str, bool\n    :raise ValueError: if ivxv_backup_crontab block's header or tail is malformed,\n    however though, if both header and tail are malformed then function assumes that\n    ivxv_backup_crontab block doesn't exist in a data\n    \"\"\"\n    header = '### block ivxv_backup_crontab ###'\n    tail = '### endblock ivxv_backup_crontab ###'\n\n    # remove leading and trailing whitespaces/newlines\n    data_stripped = data.strip()\n\n    # get start index of a header\n    header_start_index = data_stripped.find(header)\n\n    # get start index of a tail\n    tail_start_index = data_stripped.find(tail)\n\n    # both header and tail aren't present in a data_stripped\n    if header_start_index < 0 and tail_start_index < 0:\n        return data, False\n    # both header and tail present in a data_stripped\n    elif header_start_index >= 0 and tail_start_index >= 0:\n        # get end index of a tail\n        tail_end_index = tail_start_index + len(tail)\n        # extract ivxv_backup_crontab block from a data_stripped\n        return data_stripped[header_start_index:tail_end_index], True\n    else:\n        if header_start_index < 0:\n            raise ValueError(\"malformed ### block ivxv_backup_crontab ###\")\n        else:\n            raise ValueError(\"malformed ### endblock ivxv_backup_crontab ###\")\n\n\ndef remove_backup_crontab(data: str) -> str:\n    \"\"\"Remove ivxv_backup_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: data without ivxv_backup_crontab block\n    :rtype: str\n    \"\"\"\n    block, found = __get_backup_crontab_block(data=data)\n    if not found:\n        return data\n    return data.replace(block, '').strip()\n\n\ndef backup_crontab_generator_util():\n    \"\"\"Generate crontab for backup automation.\"\"\"\n    args = init_cli_util(\"\"\"\n    Generate crontab for IVXV backup automation.\n\n    This utility must be called as editor by crontab utility:\n\n        $ env VISUAL=ivxv-backup-crontab crontab -e\n\n    Usage: ivxv-backup-crontab <filename>\n    \"\"\")\n    filepath = args['<filename>']\n\n    crontab_tmp_file_content: str\n\n    # check input file\n    try:\n        with open(filepath) as fp:\n            crontab_tmp_file_content = fp.read()\n    except OSError as err:\n        log.error(\"Can't read file %r: %s\", filepath, err.strerror)\n        return 1\n\n    # load crontab template\n    tmpl_env = Environment(loader=PackageLoader('ivxv_admin', 'templates'))\n    template = tmpl_env.get_template('ivxv_backup_crontab.jinja')\n\n    # detect service states\n    template_params = {'backup_times': []}\n    backup_services = get_services(\n        include_types=['backup'], service_state=[SERVICE_STATE_CONFIGURED])\n    for service_val in backup_services.values():\n        if service_val['backup-times']:\n            template_params['backup_times'] = sorted(\n                [[int(timeval.split(':')[0]), int(timeval.split(':')[1])]\n                 for timeval in service_val['backup-times'].split(' ')])\n    template_params['configured_backup_services'] = backup_services\n    template_params['configured_voting_services'] = sorted(\n        get_services(\n            include_types=['voting'],\n            service_state=[SERVICE_STATE_CONFIGURED]))\n    template_params['configured_log_collectors'] = sorted(\n        get_services(\n            include_types=['log'],\n            service_state=[SERVICE_STATE_CONFIGURED],\n        ))\n\n    # render crontab\n    backup_crontab = template.render(\n        time_generated=datetime.datetime.now().strftime('%d.%M.%Y %H:%M:%S'),\n        **template_params,\n    )\n\n    # remove old block:\n    # ### block ivxv_backup_crontab ###\n    # ...\n    # ### endblock ivxv_backup_crontab ###\n    # from a crontab temporary file if any exists\n    try:\n        without_backup_crontab = remove_backup_crontab(\n            data=crontab_tmp_file_content)\n    except ValueError as err:\n        msg = \"Can't remove ivxv_backup_crontab block from a temporary file %r: %s\"\n        log.error(msg, filepath, err.__str__())\n        return 1\n\n    # add new block to a crontab temporary file\n    crontab = insert_backup_crontab(\n        data=without_backup_crontab, crontab=backup_crontab)\n\n    # Pause for 1 second. Crontab checks mtime to detect file modifications. It\n    # seems that crontab can't detect mtime change if changes happens too\n    # quickly (tested in Ubuntu Xenial).\n    time.sleep(1)\n\n    # override crontab temporary file with a new content\n    try:\n        with open(filepath, 'w') as fp:\n            fp.write(crontab)\n    except OSError as err:\n        log.error(\"Can't write file %r: %s\", filepath, err.strerror)\n        return 1\n\n    return 0\n\n\ndef backup_util():\n    \"\"\"Backup collector data.\"\"\"\n    args = init_cli_util(\"\"\"\n    Backup IVXV collector data.\n\n    Usage: ivxv-backup management-conf\n           ivxv-backup ballot-box [<voting_service_id>]\n           ivxv-backup log\n    \"\"\")\n\n    # execute ballot box and log backup command with ssh-agent wrapper\n    if ((args['ballot-box'] or args['log'])\n            and not os.environ.get('SSH_AUTH_SOCK')\n            and not os.environ.get('SSH_AGENT_PID')):\n        log.debug('Starting command with ssh-agent wrapper')\n        os.execvp('ssh-agent', ['ssh-agent'] + sys.argv)\n\n    services = get_services(include_types=['backup'],\n                            service_state=[SERVICE_STATE_CONFIGURED])\n    if not services:\n        log.error('Backup service is not defined')\n        return 1\n\n    # Only 1 'CONFIGURED' backup service is allowed at the same time\n    assert len(services) == 1\n\n    with Service(*list(services.items())[0]) as backup_service:\n        log.debug('Backup service: %s', backup_service.service_id)\n        if backup_service.data['state'] != SERVICE_STATE_CONFIGURED:\n            log.error(\"Backup service state is %r (expected state is %r)\",\n                      backup_service.data['state'], SERVICE_STATE_CONFIGURED)\n            return 1\n\n        if args['management-conf']:\n            return backup_management_cfg(backup_service)\n\n        # copy list of known SSH hosts to backup server\n        backup_service.scp(\n            os.path.expanduser('~/.ssh/known_hosts'), '~/.ssh/',\n            'list of known SSH hosts')\n        backup_timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M')\n\n        if args['ballot-box']:\n            backup_target = datetime.datetime.now().strftime(\n                f'ballot-box-{backup_timestamp}.zip')\n            services = get_services(\n                include_types=['voting'],\n                service_state=[SERVICE_STATE_CONFIGURED],\n            )\n            voting_service_id = (\n                args['<voting_service_id>'] or random.choice(list(services)))\n            if args['<voting_service_id>'] and voting_service_id not in services:\n                log.error('Unknown voting service ID: %s', voting_service_id)\n                return 1\n\n            with Service(voting_service_id, services[voting_service_id]) as service:\n                proc = backup_service.ssh(\n                    [\n                        'ivxv-admin-sudo',\n                        'backup-ballot-box',\n                        service.hostname,\n                        voting_service_id,\n                        backup_target,\n                    ],\n                    fwd_auth_agent=True,\n                )\n\n        else:\n            assert args['log']\n            services = get_services(\n                include_types=['log'], service_state=[SERVICE_STATE_CONFIGURED])\n            for log_collector_id in services:\n                with Service(log_collector_id, services[log_collector_id]) as service:\n                    proc = backup_service.ssh(\n                        [\n                            'ivxv-admin-sudo',\n                            'backup-log',\n                            service.hostname,\n                            backup_timestamp,\n                        ],\n                        fwd_auth_agent=True,\n                    )\n                    if proc.returncode:\n                        break\n\n        if proc.returncode:\n            log.error('Command execution failed with error code %d',\n                      proc.returncode)\n            return 1\n\n    return 0\n\n\ndef exec_backup_service_cmd(backup_service, *cmd):\n    \"\"\"Execute command in backup service.\n\n    :raises OSError: if command fails\n    \"\"\"\n    proc = backup_service.ssh(list(cmd))\n    if proc.returncode:\n        raise OSError(\n            f\"Command {' '.join(cmd)} failed in backup service \"\n            f\"with exit code {proc.returncode}\"\n        )\n\n\ndef backup_management_cfg(backup_service):\n    \"\"\"Creating management config backup.\"\"\"\n    log.info('Creating management config backup')\n    backup_target = datetime.datetime.now().strftime('%Y%m%d_%H%M')\n    backup_basedir = '/var/backups/ivxv/management-conf'\n    backup_tmpdir = os.path.join(backup_basedir, f'tmp-{backup_target}')\n    backup_tgt_path = os.path.join(backup_basedir, backup_target)\n\n    dirs_to_backup = [\n        ['config', '/etc/ivxv', 'etc'],\n        [\n            'admin UI permissions', CONFIG['permissions_path'],\n            'admin-ui-permissions'\n        ],\n        ['command history', CONFIG['command_files_path'], 'commands'],\n    ]\n    try:\n        exec_backup_service_cmd(backup_service, \"rm\", \"-rfv\", backup_tmpdir)\n        exec_backup_service_cmd(backup_service, \"mkdir\", \"-v\", backup_tmpdir)\n        for description, src_dir, tgt_dir in dirs_to_backup:\n            log.info(\"Backing up %s directory %r\", description, src_dir)\n            subprocess.run(\n                [\n                    'rsync',\n                    '-av',\n                    '--del',\n                    f\"{src_dir}/\",\n                    f'{backup_service.hostname}:{backup_tmpdir}/{tgt_dir}/',\n                ],\n                check=True,\n            )\n        exec_backup_service_cmd(backup_service, \"rm\", \"-rfv\", backup_tgt_path)\n        exec_backup_service_cmd(\n            backup_service, \"mv\", \"-v\", backup_tmpdir, backup_tgt_path\n        )\n    except (OSError, subprocess.CalledProcessError) as err:\n        log.error(err)\n        return 1\n\n    return 0\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for management service configuration.\"\"\"\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/command_load.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for command loading.\"\"\"\n\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\n\nfrom ...cli_utils.service_utils import (\n    create_voter_list_download_crontab,\n    remove_ivxv_admin_crontab,\n    generate_detail_stats_crontab,\n    generate_voting_facts_crontab,\n    install_detail_stats_crontab,\n    install_voting_facts_crontab\n)\nfrom ... import CFG_TYPES, CMD_DESCR, CMD_TYPES, VOTING_LIST_TYPES\nfrom ...command_file import (\n    check_cmd_signature,\n    load_cfg_file_content,\n    load_collector_cmd_file,\n)\nfrom ...config import cfg_path\nfrom ...db import IVXVManagerDb\nfrom ...event_log import register_service_event\nfrom ...lib import (\n    IvxvError,\n    get_current_voter_list_changeset_no,\n    manage_db_mobileid_fields,\n    manage_db_tsp_fields,\n    populate_user_permissions,\n    register_tech_cfg_items,\n)\nfrom .. import init_cli_util, log\n\n\ndef main():\n    \"\"\"Load command to IVXV Collector Management Service.\"\"\"\n    args = init_cli_util(\"\"\"\n    Load command to IVXV Collector Management Service.\n\n    Usage: ivxv-cmd-load [--autoapply] [--show-version] <type> FILE\n\n    Options:\n        <type>              Command type. Possible values are:\n                            - election: election config\n                            - technical: collector technical config\n                            - trust: trust root config\n                            - choices: choices list\n                            - districts: districts list\n                            - voters: voters list or voters list skipping\n                            - user: user account and role(s)\n        --autoapply         Apply command file automatically (by Agent Daemon).\n        --show-version      Output config file version and exit.\n    \"\"\")\n\n    # validate CLI arguments\n    cmd_type = args['<type>'].lower()\n    if cmd_type not in CMD_TYPES:\n        log.error(\"Invalid command type %r. Possible values are: %s\",\n                  cmd_type, ', '.join(CMD_TYPES))\n        return 1\n    cfg_filename = args['FILE']\n\n    # check command file signature and loading state\n    try:\n        check_cmd_loading_state(cmd_type)\n        cfg_timestamp, cfg_version = check_signer_permissions(\n            cmd_type, filename=args['FILE'])\n    except IvxvError as err:\n        log.error(str(err))\n        return 1\n\n    # output config version\n    log.info(\"Config file version is %r\", cfg_version)\n    if args['--show-version']:\n        return 0\n\n    # raise error if reloading current version\n    if cmd_type in CFG_TYPES:\n        with IVXVManagerDb() as db:\n            if db.get_value(f'config/{cmd_type}') == cfg_version:\n                log.error(\"%s version %r is already loaded\",\n                          CFG_TYPES[cmd_type].capitalize(), cfg_version)\n                return 1\n\n    # load config (includes config validation)\n    log.info(\"Loading command %r from file %r\", CMD_DESCR[cmd_type], cfg_filename)\n    cfg_data = load_collector_cmd_file(cmd_type, args['FILE'])\n    if cfg_data is None:\n        return 1\n\n    # validate voting lists consistency\n    if cmd_type in VOTING_LIST_TYPES:\n        if not validate_lists_consistency(cmd_type, args['FILE']):\n            return 1\n\n    register_service_event(\n        'CMD_LOAD', params={'cmd_type': cmd_type, 'version': cfg_version})\n\n    # reset database and remove crontab on trust root config loading\n    if cmd_type == 'trust':\n        log.info('Resetting collector management database')\n        db = IVXVManagerDb()\n        db.reset()\n\n        remove_ivxv_admin_crontab()\n        register_service_event('COLLECTOR_RESET')\n\n    # register new and removed services on technical config loading\n    elif cmd_type == 'technical':\n        register_tech_cfg_items(cfg_data, cfg_version)\n\n    # write districts JSON to web server data directory\n    elif cmd_type == 'districts':\n        districts_filename = cfg_path('admin_ui_data_path', 'districts.json')\n        districts_list = sorted(\n            [[dist_id, f'{dist_id} - {val[\"name\"]}']\n             for dist_id, val in cfg_data['districts'].items()])\n        log.info(\"Writing simplified district list to %r\", districts_filename)\n        with open(districts_filename, 'w') as fp:\n            json.dump(districts_list, fp)\n\n    # generate detail stats crontab\n    elif cmd_type == 'election':\n        create_voter_list_download_crontab(cfg_data)\n        generate_detail_stats_crontab(cfg_data)\n        install_detail_stats_crontab()\n        generate_voting_facts_crontab(cfg_data)\n        install_voting_facts_crontab()\n\n    # register loaded config\n    cfg_state = None\n    if cmd_type == \"voters\":\n        cfg_state = \"SKIPPED\" if \"skip_voter_list\" in cfg_data else \"PENDING\"\n    register_cfg(\n        cmd_type,\n        cfg_data,\n        cfg_filename,\n        cfg_timestamp,\n        cfg_version,\n        args[\"--autoapply\"],\n        state=cfg_state,\n    )\n    register_service_event(\n        'CMD_LOADED', params={'cmd_type': cmd_type, 'version': cfg_version})\n\n    return 0\n\n\ndef register_cfg(\n    cmd_type, cfg_data, cfg_filename, cfg_timestamp, cfg_version, autoapply, state\n):\n    \"\"\"Register config version in database and file system.\"\"\"\n    db_key = 'config' if cmd_type in CFG_TYPES else 'list'\n    cfg_file_ext = \"bdoc\"\n\n    # connect to management service database\n    with IVXVManagerDb(for_update=True) as db:\n        # detect order number for voters list\n        if cmd_type == 'voters':\n            if state == \"SKIPPED\":\n                changeset_no = cfg_data[\"changeset\"]\n            else:\n                changeset_no = get_current_voter_list_changeset_no(db) + 1\n                if changeset_no:\n                    cfg_file_ext = \"zip\"\n            active_cfg_filename = f\"voters{changeset_no:04}.{cfg_file_ext}\"\n            db_key += f\"/voters{changeset_no:04}\"\n        else:\n            active_cfg_filename = f'{cmd_type}.bdoc'\n            db_key += f\"/{cmd_type}\"\n\n        # write config file to admin ui data path\n        cmd_filepath = cfg_path(\n            \"command_files_path\", f\"{cmd_type}-{cfg_timestamp}.{cfg_file_ext}\"\n        )\n        log.debug(\"Copying file %r to %r\", cfg_filename, cmd_filepath)\n        shutil.copy(cfg_filename, cmd_filepath)\n        log.info('%s file loaded successfully',\n                 CMD_DESCR[cmd_type].capitalize())\n\n        # register config file version\n        if cmd_type in CFG_TYPES or cmd_type in VOTING_LIST_TYPES:\n            db.set_value(db_key, cfg_version)\n            if cmd_type == 'voters':\n                db.set_value(f\"{db_key}-state\", state)\n\n        # register user permissions\n        if cmd_type == 'trust':  # initial permissions\n            log.info('Resetting user permissions')\n            for user_cn in db.get_all_values('user'):\n                db.rm_value(f\"user/{user_cn}\")\n            for user_cn in cfg_data['authorizations']:\n                register_user_permissions(db, user_cn, [\"admin\"])\n\n        elif cmd_type == 'user':  # permissions from user management command\n            user_cn = cfg_data['cn']\n            log.info(\"Resetting user %r permissions\", cfg_data['cn'])\n            register_service_event(\n                'PERMISSION_RESET', params={'user_cn': user_cn})\n            for existing_user_cn in db.get_all_values('user'):\n                if existing_user_cn == user_cn:\n                    db.rm_value(f\"user/{user_cn}\")\n            register_user_permissions(db, user_cn, cfg_data[\"roles\"])\n\n        elif cmd_type == 'election':  # register election params\n            cfg_data = load_cfg_file_content(\n                cmd_type,\n                re.compile(r'(.+\\.)?{}.yaml'.format(cmd_type)), cfg_filename)\n            db.set_value('election/election-id', cfg_data['identifier'])\n\n            # start/stop times\n            for key in [\n                    'servicestart', 'electionstart', 'electionstop',\n                    'servicestop', 'verificationstop'\n            ]:\n                db.set_value(f\"election/{key}\", cfg_data[\"period\"][key])\n                register_service_event(\n                    'SET_ELECTION_TIME',\n                    params={\n                        'period': key, 'timestamp': cfg_data['period'][key]})\n\n            # authentication methods\n            auth_in_db = set(db.get_all_values('election').get('auth', []))\n            auth_in_cfg = set(cfg_data.get('auth', []).keys())\n            for key in auth_in_db.difference(auth_in_cfg):\n                db.rm_value(f'election/auth/{key}')\n            for key in auth_in_cfg.difference(auth_in_db):\n                db.set_value(f'election/auth/{key}', 'TRUE')\n\n            # TSP qualification protocol\n            for key in cfg_data.get('qualification', []):\n                if key.get('protocol') == 'tspreg':\n                    db.set_value('election/tsp-qualification', 'TRUE')\n                    break\n            else:\n                try:\n                    db.rm_value('election/tsp-qualification')\n                except KeyError:\n                    pass\n\n        if cmd_type in ['technical', 'election']:\n            manage_db_mobileid_fields(db)\n            manage_db_tsp_fields(db)\n\n    register_cfg_in_fs(\n        cmd_type, cmd_filepath, active_cfg_filename, cfg_version, autoapply)\n\n\ndef register_cfg_in_fs(cmd_type,\n                       src_path,\n                       tgt_filename,\n                       cfg_version,\n                       autoapply):\n    \"\"\"Register config in file system.\n\n    - Create symlink to active config directory\n      (e.g. /var/lib/ivxv/commands/<cfg-version>.bdoc -> /etc/ivxv/cmd.bdoc)\n    - Create state file for some command types\n      (/var/lib/ivxv/commands/<cfg-version>.json)\n    \"\"\"\n    if cmd_type not in CFG_TYPES and cmd_type not in VOTING_LIST_TYPES:\n        return\n\n    tgt_path = cfg_path('active_config_files_path', tgt_filename)\n    state_path = os.path.splitext(src_path)[0] + '.json'\n\n    # create symlink to active config directory\n    try:\n        os.remove(tgt_path)\n    except FileNotFoundError:\n        pass\n    log.debug(\"Creating symlink for config file %r -> %r\", src_path, tgt_path)\n    os.symlink(src_path, tgt_path)\n\n    # create state file for loaded command\n    if cmd_type in [\"technical\", \"election\", \"choices\", \"districts\", \"voters\"]:\n        try:\n            os.remove(state_path)\n        except FileNotFoundError:\n            pass\n        log.debug(\"Generating config state file %r\", state_path)\n        default_state_data = {\n            # config data\n            'config_type': CMD_DESCR[cmd_type],\n            'config_file': os.path.basename(tgt_path),\n            'config_version': cfg_version,\n            # applying data\n            'autoapply': autoapply,\n            'completed': False,\n            'attempts': 0,\n            'log': [],\n        }\n        with open(state_path, 'w') as fp:\n            json.dump(default_state_data, fp, indent=4, sort_keys=True)\n\n    log.info('%s file is registered in management service',\n             CMD_DESCR[cmd_type].capitalize())\n\n\ndef check_cmd_loading_state(cmd_type):\n    \"\"\"Check command loading state.\n\n    :raises IvxvError: on any known error\n    \"\"\"\n    # don't allow to load choices list more than once\n    if cmd_type == 'choices':\n        with IVXVManagerDb() as db:\n            choices_list_version = db.get_value('list/choices')\n        if choices_list_version:\n            raise IvxvError(\n                f\"Choices list is already loaded (version: {choices_list_version})\"\n            )\n\n    # don't allow to load technical config if trust root config is not loaded\n    elif cmd_type == 'technical':\n        with IVXVManagerDb() as db:\n            trust_cfg = db.get_value('config/trust')\n        if not trust_cfg:\n            raise IvxvError(\n                'Trust root must be loaded before technical configuration')\n\n    # don't allow to load voters list if districts list is not loaded\n    elif cmd_type == \"voters\":\n        with IVXVManagerDb() as db:\n            districts_cfg = db.get_value(\"list/districts\")\n        if not districts_cfg:\n            raise IvxvError(\"Districts list must be loaded before voters list\")\n\n\ndef check_signer_permissions(cmd_type, filename):\n    \"\"\"Check command file signer permissions.\n\n    Detect config file timestamp and version based on signature(s).\n\n    :return: config timestamp, config version\n    :rtype: list\n\n    :raises IvxvError: on any known error\n    \"\"\"\n    # check config permissions\n    try:\n        cfg_signatures, all_signatures = check_cmd_signature(\n            cmd_type, filename)\n    except LookupError as err:\n        raise IvxvError(f'Failed to verify config file signatures: {err}')\n    except (OSError, subprocess.SubprocessError) as err:\n        raise IvxvError(str(err))\n\n    for signature in all_signatures:\n        log.info('Config file is signed by: %s', signature[2])\n    if not cfg_signatures:\n        raise IvxvError('No signatures by authorized users')\n    cfg_version, role = cfg_signatures[0]\n    log.info(\"User %s with role %r is authorized to execute %r commands\",\n             cfg_version.split(' ')[0], role, cmd_type)\n    log.info(\"Using signature %r as config file version\", cfg_version)\n    cfg_timestamp = cfg_version.split(' ')[1]\n\n    return cfg_timestamp, cfg_version\n\n\ndef register_user_permissions(db, user_cn, roles):\n    \"\"\"Register user permissions.\"\"\"\n    roles = sorted(roles)\n    register_service_event(\n        'PERMISSION_SET',\n        params={'user_cn': user_cn, 'permission': ','.join(roles)})\n    db.set_value(f\"user/{user_cn}\", \",\".join(roles))\n    populate_user_permissions(db)\n\n\ndef validate_lists_consistency(cmd_type, cmd_filepath):\n    \"\"\"Validate voting lists consistency.\"\"\"\n    # create list of existing voting lists\n    list_files = {}\n    with IVXVManagerDb() as db:\n        for db_key, db_val in db.get_all_values('list').items():\n            if re.match(r\"voters[0-9]{2}\", db_key):\n                if db_key.endswith(\"-state\"):\n                    cli_key = db_key.replace(\"-state\", \"\")\n                    if db_val in [\"PENDING\", \"APPLIED\"]:\n                        list_files[cli_key] = cfg_path(\n                            \"active_config_files_path\",\n                            \"voters0000.bdoc\"\n                            if cli_key == \"voters0000\"\n                            else f\"{cli_key}.zip\",\n                        )\n                    elif db_val == \"SKIPPED\":\n                        list_files[cli_key] = cfg_path(\n                            \"active_config_files_path\",\n                            f\"{cli_key}.bdoc\",\n                        )\n                continue\n            if db_key.endswith(\"-loaded\") or not db_val:\n                continue\n            list_files[db_key] = cfg_path(\n                'active_config_files_path', f'{db_key}.bdoc')\n\n    # add current list\n    if cmd_type != 'voters':\n        list_files[cmd_type] = cmd_filepath\n    else:  # change voters list key for proper ordering\n        for _ in range(10_000):\n            if f'voters{_:04d}' not in list_files:\n                list_files[f'voters{_:04d}'] = cmd_filepath\n                break\n\n    # validate\n    if len(list_files) > 1:\n        election_cfg = cfg_path(\"active_config_files_path\", \"election.bdoc\")\n        cmd = [\"ivxv-config-validate\", f\"--election={election_cfg}\"]\n        for cfg_type, filepath in sorted(list_files.items()):\n            cfg_type = re.sub(r'[0-9]+', '', cfg_type)\n            cmd.append(f'--{cfg_type}={filepath}')\n        log.info(\n            'Some voting lists are already loaded, '\n            'executing consistency checks: %s', ' '.join(cmd))\n        proc = subprocess.run(cmd, check=False)\n        if proc.returncode != 0:\n            log.error('Config validation command raised exception')\n            return False\n\n    return True\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/config_apply.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for config applying.\"\"\"\n\nimport json\nimport os\nimport shutil\nimport sys\n\nfrom ... import (CFG_TYPES, CMD_TYPES, SERVICE_STATE_CONFIGURED,\n                 SERVICE_TYPE_PARAMS, lib)\nfrom ...command_file import load_collector_cmd_file\nfrom ...config import cfg_path\nfrom ...db import IVXVManagerDb\nfrom ...lib.lockfile import PidLocker\nfrom ...service import generate_service_list, get_service_cfg_state\nfrom ...service.service import Service\nfrom .. import init_cli_util, log\n\n#: config types in applying order\nCFG_TYPES_DEFAULT = (\"technical\", \"election\", \"choices\", \"districts\", \"voters\")\n\n\ndef main():\n    \"\"\"Apply loaded config to IVXV services.\"\"\"\n    args = init_cli_util(\"\"\"\n    Apply loaded IVXV Collector config to services.\n\n    Usage: ivxv-config-apply [--type=<type>] ... [<service-id>] ...\n\n    Options:\n        --type=<type>    Config type. Possible values are:\n                         - election: election config file\n                         - technical: collector technical config file\n                         - choices: choices list\n                         - districts: districts list\n                         - voters: voters list\n    \"\"\")\n\n    # validate CLI arguments\n    for cfg_type in args['--type']:\n        if cfg_type not in CMD_TYPES or cfg_type == 'trust':\n            log.error('Invalid config type specified: --type=%s', cfg_type)\n            return 1\n\n    cfg_types = args['--type'] or list(CFG_TYPES_DEFAULT)\n    service_ids = args['<service-id>']\n\n    # load config data\n    with IVXVManagerDb() as db:\n        cfg_data = get_cfg_data(db)\n    try:\n        tech_cfg = load_tech_cfg(\n            cfg_data['technical']['version'],\n            cfg_data['election']['version'],\n            'election' in cfg_types)\n    except lib.IvxvError as err:\n        log.error(err)\n        return 1\n\n    # remove choices/voters list version record from config data\n    # if list is not loaded and also not specified in CLI args\n    if not args['--type']:\n        if not cfg_data.get('choices', {}).get('version'):\n            cfg_types.remove('choices')\n        if not cfg_data.get('districts', {}).get('version'):\n            cfg_types.remove('districts')\n        if not cfg_data.get('voters0000', {}).get('version'):\n            cfg_types.remove('voters')\n\n    # generate service list from tech config\n    services = generate_service_list(tech_cfg['network'], service_ids)\n    if services is None:\n        return 1\n    services = prepare_service_list(services, service_ids)\n\n    # create pidfile to avoid running of multiple instances\n    pidfile_path = cfg_path('ivxv_admin_data_path', 'ivxv-config-apply.pid')\n    log.debug(\"Creating pidfile %r\", pidfile_path)\n    try:\n        PidLocker(pidfile_path, timeout=5)\n    except IOError:\n        log.error(\"Creating pidfile %r failed. Is another %s running?\",\n                  pidfile_path, os.path.basename(sys.argv[0]))\n        sys.exit(1)\n\n    # apply config\n    apply_result = apply_cfg(\n        cfg_types=cfg_types,\n        services=services,\n        tech_cfg=tech_cfg,\n        cfg_data=cfg_data)\n    return int(not apply_result)\n\n\ndef get_cfg_data(db):\n    \"\"\"Load config versions from database.\n\n    :rtype: dict\n    \"\"\"\n    # get config versions\n    cfg_data = {\n        'technical': {\n            'version': db.get_value('config/technical') or None\n        },\n        'election': {\n            'version': db.get_value('config/election') or None\n        },\n        'choices': {\n            'version': db.get_value('list/choices') or None\n        },\n        'districts': {\n            'version': db.get_value('list/districts') or None\n        },\n    }\n    if not cfg_data['choices']['version']:\n        del cfg_data['choices']\n    if not cfg_data['districts']['version']:\n        del cfg_data['districts']\n    try:\n        for changeset_no in range(10_000):\n            cfg_data[f\"voters{changeset_no:04}\"] = {\n                \"version\": db.get_value(f\"list/voters{changeset_no:04}\") or None\n            }\n    except KeyError:\n        pass\n\n    # get config applying report filenames\n    for cmd_type in cfg_data:\n        cfg_filepath = lib.get_loaded_cfg_file_path(cmd_type)\n        cfg_data[cmd_type][\"state_file\"] = (\n            cfg_filepath.replace(os.path.splitext(cfg_filepath)[1], \".json\")\n            if cfg_filepath\n            else None\n        )\n\n    return cfg_data\n\n\ndef load_tech_cfg(tech_cfg_ver, election_cfg_ver, has_election_cfg):\n    \"\"\"Load technical config.\n\n    * Load technical config\n    * Log election config state\n    * Generate database records for undefined services\n\n    :return: technical config\n    :rtype: dicts\n\n    :raises IvxvError: if technical config cannot be loaded\n    \"\"\"\n    # load technical config\n    if not tech_cfg_ver:\n        raise lib.IvxvError(\n            'Technical config is not loaded to management service')\n    log.info('Technical config is signed by %s', tech_cfg_ver)\n    tech_cfg = load_collector_cmd_file(\n        'technical', cfg_path('active_config_files_path', 'technical.bdoc'))\n    if tech_cfg is None:\n        raise lib.IvxvError('Error while loading technical config')\n\n    # log election config state\n    if has_election_cfg:\n        if election_cfg_ver:\n            log.info('Election config is signed by %s', election_cfg_ver)\n        else:\n            log.info('Election config is not loaded to management service')\n\n    return tech_cfg\n\n\ndef prepare_service_list(services, service_ids):\n    \"\"\"Prepare service list for applying config to services.\n\n    Return services that are in service_ids list.\n    \"\"\"\n    services = [[service_type, service_data]\n                for service_type, service_data in services\n                if not service_ids or service_data['id'] in service_ids]\n\n    # reorder services to satisfy dependencies\n    for service_block in services[:]:\n        # move log service record to beginning of list\n        # to configure log collector(s) before other services\n        if service_block[0] == 'log':\n            services.remove(service_block)\n            services.insert(0, service_block)\n\n        # move proxy service records to end of list to ensure proxied services\n        # are started and initial health checks can succeed\n        if service_block[0] == 'proxy':\n            services.remove(service_block)\n            services.append(service_block)\n\n    return services\n\n\ndef apply_cfg(cfg_types, services, tech_cfg, cfg_data):\n    \"\"\"Apply config to services.\"\"\"\n    results = dict([i, {}] for i in cfg_types)\n    for cfg_type in CFG_TYPES_DEFAULT:\n        if cfg_type not in cfg_types:\n            continue\n        # disallow applying choices/districts/voters list\n        # if required services aren't operational\n        if cfg_type in [\"choices\", \"districts\", \"voters\"]:\n            list_services = lib.get_services(\n                include_types=['choices', 'storage'],\n                service_state=[SERVICE_STATE_CONFIGURED])\n            if not list_services:\n                log.error('Choices and/or storage services are not configured')\n                return False\n        result_by_service = apply_cfg_to_services(\n            services=services,\n            cfg_type=cfg_type,\n            tech_cfg=tech_cfg,\n            cfg_data=cfg_data,\n        )\n        results[cfg_type] = result_by_service\n\n        # update config state file\n        if (cfg_type in CFG_TYPES and result_by_service\n                and False not in result_by_service.values()):\n            state_filepath = cfg_data[cfg_type]['state_file']\n            with open(state_filepath) as fp:\n                cfg_state = json.load(fp)\n            cfg_state['completed'] = True\n\n            tmp_filepath = f'{state_filepath}.tmp'\n            with open(tmp_filepath, 'x') as fp:\n                json.dump(cfg_state, fp, indent=4, sort_keys=True)\n            shutil.move(tmp_filepath, state_filepath)\n\n    return aggregate_results(results)\n\n\ndef apply_cfg_to_services(services, cfg_type, tech_cfg, cfg_data):\n    \"\"\"Helper to apply config to services.\n\n    :param services: Service list\n    :type services: list\n    :param cfg_type: Config type to apply\n    :type cfg_type: str\n    :param tech_cfg: Technical configuration from config file\n    :type tech_cfg: dict\n    :param services: List of services, grouped by service type\n    :type services: list\n    :param cfg_data: Loaded config/list files data (version, filenames)\n    :type cfg_data: dict\n\n    :return: dict with results for every service\n    \"\"\"\n    # get list of registered services that require config update\n    with IVXVManagerDb() as db:\n        service_cfg_state = get_service_cfg_state(db, tech_cfg)\n        hostnames = sorted(\n            set(\n                service_data['address'].split(':')[0]\n                for _, service_data in services\n            ))\n        host_state = dict(\n            [hostname, db.get_value(f'host/{hostname}/state')]\n            for hostname in hostnames)\n\n    # apply config to services\n    def report_applying_result():\n        \"\"\"Report process result.\"\"\"\n        cfg_descr = '{} {}'.format(\n            cfg_type, 'config' if cfg_type in CFG_TYPES else 'list')\n        results[service_id] = cfg_success\n        if cfg_success:\n            log.info('Service %s: %s config applied successfully',\n                     service_id, cfg_descr)\n            service.register_event(\n                'SERVICE_CONFIG_APPLY',\n                params={'cfg_descr': cfg_descr, 'cfg_version': cfg_version})\n        else:\n            log.error('Service %s: failed to apply %s', service_id, cfg_descr)\n\n    # apply config\n    results = {}\n    applied_lists = []\n    attempt_no = 0\n    for service_type, service_data in services:\n        service_id = service_data['id']\n        if not service_cfg_state[service_id][cfg_type]:\n            continue\n\n        service_data['service_type'] = service_type  # used by Service()\n        with Service(service_id, service_data) as service:\n            # apply technical config\n            if cfg_type == 'technical':\n                # initialize service host if required\n                if not host_state[service.hostname]:\n                    log.info(\"Initializing service host %r\", service.hostname)\n                    success = service.init_service_host()\n                    if not success:\n                        results[service_id] = False\n                        continue\n                    with IVXVManagerDb(for_update=True) as db:\n                        db.set_value(f'host/{service.hostname}/state',\n                                     'REGISTERED')\n                    host_state[service.hostname] = 'REGISTERED'\n\n                # apply config\n                log.info('Service %s: Applying technical config', service_id)\n                cfg_version = cfg_data[cfg_type]['version']\n                attempt_no = service.load_apply_state(\n                    cfg_data[cfg_type]['state_file'], attempt_no)\n                cfg_success = service.apply_tech_cfg(cfg_version, tech_cfg)\n                report_applying_result()\n                service.update_apply_state()\n\n            # apply election config\n            elif (cfg_type == 'election'\n                  and SERVICE_TYPE_PARAMS[service_type]['require_config']):\n                log.info('Service %s: Applying elections config', service_id)\n                cfg_version = cfg_data[cfg_type]['version']\n                attempt_no = service.load_apply_state(\n                    cfg_data[cfg_type]['state_file'], attempt_no)\n                cfg_success = service.apply_election_cfg(cfg_version)\n                report_applying_result()\n                service.update_apply_state()\n\n            # apply choices/districts list\n            elif (cfg_type in [\"choices\", \"districts\"] and service_type == \"choices\"\n                  and cfg_type not in applied_lists):\n                log.info(\"Service %s: Applying %s list\", service_id, cfg_type)\n                cfg_version = cfg_data[cfg_type]['version']\n                attempt_no = service.load_apply_state(\n                    cfg_data[cfg_type]['state_file'], attempt_no)\n                cfg_success = service.apply_list(cfg_type)\n                report_applying_result()\n                service.update_apply_state(completed=True)\n                applied_lists.append(cfg_type)\n\n            # apply voters lists\n            elif (cfg_type == 'voters' and service_type == 'voting'\n                  and 'voters' not in applied_lists):\n                for changeset_no in service_cfg_state[service_id][\"voters\"]:\n                    log.info(\n                        \"Service %s: Applying voter list changeset #%d\",\n                        service_id,\n                        changeset_no,\n                    )\n                    voters_list_id = f\"voters{changeset_no:04}\"\n                    cfg_version = cfg_data[voters_list_id]['version']\n                    attempt_no = service.load_apply_state(\n                        cfg_data[voters_list_id]['state_file'])\n                    cfg_success = service.apply_list(cfg_type, changeset_no)\n                    report_applying_result()\n                    service.update_apply_state(completed=True)\n                    applied_lists.append('voters')\n\n    return results\n\n\ndef aggregate_results(results):\n    \"\"\"Aggregate and output collector config applying results for services.\n\n    :rtype: bool\n    :return: True if no error detected\n    \"\"\"\n    results_summary = []\n    for cfg_type in CFG_TYPES_DEFAULT:\n        if results.get(cfg_type):\n            log.info('Results for %s:', lib.cfg_type_verbose(cfg_type))\n            for service_id, result in sorted(results[cfg_type].items()):\n                log.info('  Service %s: %s',\n                         service_id, 'success' if result else 'FAILED')\n                results_summary.append(result)\n\n    log.info('%d configuration packages successfully applied',\n             results_summary.count(True))\n\n    if False in results_summary:\n        log.error('Failed to apply %d configuration packages',\n                  results_summary.count(False))\n\n    return False not in results_summary\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/config_validate.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for collector config validation.\"\"\"\n\nimport json\n\nfrom ... import CMD_DESCR, CMD_TYPES\nfrom ...command_file import load_collector_cmd_file\nfrom ...lib import IvxvError\nfrom .. import init_cli_util, log\n\n\ndef main():\n    \"\"\"Validate IVXV collector config.\"\"\"\n    args = init_cli_util(\"\"\"\n    Validate IVXV collector config files.\n\n    Validate single config files. Also validate voting lists consistency if\n    multiple lists are provided.\n\n    Usage:\n        ivxv-config-validate [--plain] [--trust=<trust-file>]\n            [--technical=<technical-file>] [--election=<election-file>]\n            [--choices=<choices-file>] [--districts=<districts-file>]\n            [--voters=<voters-file> ...]\n\n    Options:\n        --plain     Validate plain config file (Default: BDOC container)\n    \"\"\")\n    plain = args['--plain']\n\n    # validate CLI arguments\n    cfg_files = []\n    for cfg_type in CMD_TYPES:\n        files = args.get(f'--{cfg_type}')\n        if isinstance(files, list):\n            cfg_files += [[cfg_type, filepath] for filepath in files]\n        elif files:\n            cfg_files.append([cfg_type, files])\n    if not cfg_files:\n        log.error('Config file is not specified')\n        return 1\n\n    # validate single files\n    try:\n        validated_cfg = validate_cfg_files(cfg_files, plain)\n    except IvxvError:\n        return 1\n    log.info('Config files are valid')\n\n    if validate_cfg_consistency(\n        validated_cfg,\n        election=args[\"--election\"],\n        choices=args[\"--choices\"],\n        districts=args[\"--districts\"],\n        voters=args[\"--voters\"],\n    ):\n        return 0\n    return 1\n\n\ndef validate_cfg_consistency(\n    validated_cfg,\n    election=None,\n    choices=None,\n    districts=None,\n    voters=None,\n):\n    \"\"\"Perform consistency checks for config files.\"\"\"\n    # validate election ID consistency\n    if not validate_cfg_election_id(validated_cfg):\n        log.info(\"Election ID consistency check failed\")\n        return False\n    log.info(\"Election ID consistency check succeeded\")\n\n    # validate multiple voters lists consistency\n    if len(voters) > 1:\n        if not validate_voters_lists_consistency(validated_cfg):\n            log.info('Voters lists consistency check failed')\n            return False\n\n        log.info('Voters lists consistency check succeeded')\n\n    # determine voterforeignehak, there are no consistency checks\n    voterforeignehak = \"0000\"\n    if election:\n        for cfg_type, cfg in validated_cfg:\n            if cfg_type == \"election\":\n                voterforeignehak = cfg.get(\"voterforeignehak\", voterforeignehak)\n                break\n\n    # validate districts and choices/voters lists consistency\n    if districts and (choices or voters):\n        if not validate_lists_consistency(validated_cfg, voterforeignehak):\n            log.info('Voting lists consistency check failed')\n            return False\n\n        log.info('Voting lists consistency check succeeded')\n\n    return True\n\n\ndef validate_cfg_election_id(validated_cfg):\n    \"\"\"Validate election ID consistency in config files.\"\"\"\n    # try to detect election ID\n    election_id = None\n    for cfg_type, cfg in validated_cfg:\n        if cfg_type == \"election\":\n            election_id = cfg[\"identifier\"]\n            log.info(\"Detected election ID %r from election config\", election_id)\n            break\n\n    # validate election ID consistency\n    for cfg_type, cfg in validated_cfg:\n        if cfg_type in [\"election\", \"technical\", \"trust\"]:\n            continue\n        cfg_election_id = cfg[\"election\"]\n        if not election_id:\n            election_id = cfg_election_id\n            log.debug(\"Detected election ID %r from %s config\", election_id, cfg_type)\n        else:\n            if election_id != cfg_election_id:\n                log.error(\n                    \"Election ID %r in %s config does not match with %r\",\n                    cfg_election_id,\n                    cfg_type,\n                    election_id,\n                )\n                return False\n\n    return True\n\n\ndef validate_cfg_files(cfg_files, plain):\n    \"\"\"Load and validate config files.\"\"\"\n    cfg_array = []\n    for cfg_type, filepath in cfg_files:\n        log.info(\"Validating %s file %r\", CMD_DESCR[cfg_type], filepath)\n        try:\n            cfg = load_collector_cmd_file(cfg_type, filepath, plain)\n        except json.decoder.JSONDecodeError:\n            cfg = None\n        if cfg is None:\n            raise IvxvError\n        cfg_array.append([cfg_type, cfg])\n\n    return cfg_array\n\n\ndef validate_voters_lists_consistency(cfg_objects):\n    \"\"\"Validate voters lists consistency.\"\"\"\n    check_failed = False\n    voters_reg = None\n    changeset = 0\n    errors = []\n    for cfg_type, cfg in cfg_objects:\n        if cfg_type != 'voters':\n            continue\n\n        if changeset == 0:\n            if int(cfg['changeset']) != 0:\n                errors.append(\n                    f'Invalid changeset {cfg[\"changeset\"]} for initial voter list'\n                )\n                break\n            voters_reg = dict(\n                [voter[1], voter[3:5]] for voter in cfg['voters'])\n            changeset = 1\n            continue\n\n        if int(cfg['changeset']) < changeset:\n            errors.append(\n                f'Invalid changeset {cfg[\"changeset\"]}, '\n                f'expected {changeset} or greater')\n            break\n\n        changeset = int(cfg['changeset'])\n\n        # voter list skipping command acts as an empty changeset\n        if cfg.get('skip_voter_list'):\n            continue\n\n        log.info(\"Checking voters list changeset #%d consistency\", changeset)\n        voters_in_patch = set()\n        for rec_no, voter in enumerate(cfg['voters']):\n            voter_id = voter[1]\n\n            if voter[0] == 'lisamine':\n                if (voter_id not in voters_reg\n                        and voter_id not in voters_in_patch):\n                    voters_reg[voter_id] = voter[3:5]\n                else:\n                    errors.append(\n                        f'Record #{rec_no}: Adding voter ID {voter_id} '\n                        'that is already in voters list')\n                voters_in_patch.add(voter_id)\n            else:\n                if voter_id in voters_in_patch:\n                    errors.append(\n                        f'Record #{rec_no}: Removing ID {voter_id} '\n                        'that is added with this patch')\n                try:\n                    # just to test that voter id is valid before removing it\n                    voters_reg.pop(voter_id)\n                except KeyError:\n                    errors.append(\n                        f'Record #{rec_no}: Removing voter ID {voter_id} '\n                        'that is not in voters list')\n\n        if errors:\n            break\n\n    for error in errors:\n        log.error(error)\n\n    check_failed = check_failed or bool(errors)\n\n    return not check_failed\n\n\ndef validate_lists_consistency(cfg_objects, voterforeignehak):\n    \"\"\"Validate voting lists consistency.\"\"\"\n    districts_cfg = ([\n        cfg\n        for cfg_type, cfg in cfg_objects\n        if cfg_type == 'districts'\n    ][0])\n    districts = set(districts_cfg['districts'])\n\n    check_failed = False\n    changeset_no = 0\n    for cfg_type, cfg in cfg_objects:\n        errors = []\n        if cfg_type == 'choices':\n            log.info('Checking districts and choices lists consistency')\n            errors = validate_choices_consistency(districts, cfg)\n            check_failed = check_failed or bool(errors)\n        elif cfg_type == 'voters':\n            log.info(\n                \"Checking districts and voter list changeset #%d consistency\",\n                changeset_no,\n            )\n            errors = validate_voters_consistency(districts_cfg, districts, cfg,\n                                                 voterforeignehak)\n            check_failed = check_failed or bool(errors)\n            changeset_no += 1\n        for error in errors:\n            log.error(error)\n\n    return not check_failed\n\n\ndef validate_choices_consistency(districts, choices_cfg):\n    \"\"\"Validate districts and choices lists consistency.\"\"\"\n    choices = set(choices_cfg['choices'])\n    errors = []\n\n    # Each choice is in an existing district\n    choices_wo_district = choices.difference(districts)\n    if choices_wo_district:\n        errors.append(\n            'The following choices have no matching district: {}'\n            .format(','.join(sorted(choices_wo_district))))\n\n    # In each district there must be at least one choice\n    districts_wo_choices = districts.difference(choices)\n    if districts_wo_choices:\n        errors.append(\n            'The following districts have no choice defined: {}'\n            .format(','.join(sorted(districts_wo_choices))))\n\n    return errors\n\n\ndef validate_voters_consistency(\n    districts_cfg, districts, voters_cfg, voterforeignehak\n):\n    \"\"\"Validate voting lists consistency.\"\"\"\n    errors = []\n\n    if \"skip_voter_list\" in voters_cfg:\n        return errors\n\n    parish_to_district = {}\n\n    for dist in districts_cfg[\"districts\"]:\n        for parish in districts_cfg[\"districts\"][dist][\"parish\"]:\n            if parish not in parish_to_district:\n                parish_to_district[parish] = []\n\n            parish_to_district[parish].append(dist)\n\n    # check consistency\n    for voter in voters_cfg['voters']:\n        # if action is \"kustutamine\" then don't check districts/parish\n        if voter[0] == \"kustutamine\":\n            continue\n        # Voterlist contains voter EHAK and district no which must be\n        # translated into full district identifier\n        voter_district = None\n        voter_id = voter[1]\n        voter_ehak = voter[3]\n        voter_district_no = voter[4]\n\n        # Voterlist uses parish FOREIGN, other configs may use some other code\n        if voter_ehak == \"FOREIGN\":\n            voter_ehak = voterforeignehak\n\n        # Search district by parish, single parish may be in several districts\n        # in this case the district no's must be different and match with voter\n        if voter_ehak in parish_to_district:\n            for dist in parish_to_district[voter_ehak]:\n                if dist.split(\".\")[1] == voter_district_no:\n                    voter_district = dist\n                    break\n        else:\n            errors.append(f'Parish {voter_ehak} not found in districts')\n\n        # Voter must belong to an existing district\n        if voter_district is None:\n            errors.append(\n                f'District for voter {voter_id} in EHAK {voter_ehak} / '\n                f'{voter_district_no} cannot be determined')\n        elif voter_district not in districts:\n            errors.append(\n                f'District {voter_district} for voter {voter_id} not found')\n\n    return errors\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/load_secret_data_file.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilites for sercet loading.\"\"\"\n\nimport hashlib\n\nimport OpenSSL\n\nfrom ... import (COLLECTOR_STATE_CONFIGURED, COLLECTOR_STATE_FAILURE,\n                 COLLECTOR_STATE_INSTALLED, COLLECTOR_STATE_PARTIAL_FAILURE,\n                 SERVICE_SECRET_TYPES, SERVICE_STATE_CONFIGURED,\n                 SERVICE_STATE_FAILURE, SERVICE_STATE_INSTALLED,\n                 SERVICE_TYPE_PARAMS, lib)\nfrom ...event_log import register_service_event\nfrom ...lib import IvxvError\nfrom ...service.service import Service\nfrom .. import init_cli_util, log\n\n\ndef main():\n    \"\"\"Load secret data to IVXV services.\"\"\"\n    # validate CLI arguments\n    args = init_cli_util(\"\"\"\n    Load secret data to IVXV services.\n\n    This utility loads file that contains secret data to services.\n\n    Supported secret types are:\n\n        tls-cert - TLS certificate for service.\n\n            Certificate (and key) is used for securing\n            communication between services and service instances.\n\n        tls-key - TLS key for service.\n\n            Key is used together with service certificate.\n\n        tsp-regkey - PKIX TSP registration key for voting services.\n\n            Key is used for signing Time Stamp Protocol requests.\n\n            Key file must be in PEM format and must be not password protected.\n\n        mid-token-key - Mobile-ID/Smart-ID/Web eID identity token for\n                        choices, mobile-id and voting services.\n\n            Key file must be 32 bytes long.\n\n    Usage: ivxv-secret-load [--service=<service-id>] <secret-type> <keyfile>\n    \"\"\")\n    secret_type = args['<secret-type>'].lower()\n    filepath = args['<keyfile>']\n    if secret_type not in SERVICE_SECRET_TYPES:\n        log.error(\"Invalid secret type %r\", secret_type)\n        log.info('Supported secret types are: %s',\n                 ', '.join(SERVICE_SECRET_TYPES))\n        return 1\n    secret_descr = SERVICE_SECRET_TYPES[secret_type]['description']\n\n    # load file\n    log.debug('Loading %s file', secret_descr)\n    try:\n        with open(filepath, 'rb') as fp:\n            file_content = fp.read()\n    except (FileNotFoundError, PermissionError) as err:\n        log.error(\"Unable to load file %r: %s\", filepath, err.strerror)\n        return 1\n\n    # validate file\n    try:\n        validate_secret_file(secret_type, file_content, args[\"--service\"])\n    except IvxvError as err:\n        log.error('Error while validating %s: %s', secret_descr, err)\n        return 1\n\n    # calculate file checksum\n    file_checksum = hashlib.sha256(file_content).hexdigest()\n\n    # generate list of services that are in required state\n    key_param = {\n        'tls-cert': 'require_tls',\n        'tls-key': 'require_tls',\n        'tsp-regkey': 'tspreg',\n        'mid-token-key': 'mobile_id',\n    }[secret_type]\n    service_types_affected = sorted(\n        set(\n            service_type\n            for service_type, service_params in SERVICE_TYPE_PARAMS.items()\n            if service_params[key_param]\n        )\n    )\n\n    services = lib.get_services(\n        include_types=service_types_affected,\n        require_collector_state=[\n            COLLECTOR_STATE_INSTALLED, COLLECTOR_STATE_CONFIGURED,\n            COLLECTOR_STATE_FAILURE, COLLECTOR_STATE_PARTIAL_FAILURE\n        ],\n        service_state=[\n            SERVICE_STATE_INSTALLED, SERVICE_STATE_CONFIGURED,\n            SERVICE_STATE_FAILURE\n        ])\n    if not services:\n        return 1\n\n    # copy key to service hosts\n    # FIXME avoid multiple copying of shared secret to single host\n    services_updated = []\n    for service_id, service_data in sorted(services.items()):\n        if args['--service'] and service_id != args['--service']:\n            continue\n        if (service_data.get(SERVICE_SECRET_TYPES[secret_type]['db-key']) ==\n                file_checksum):\n            log.info('Service %s already contains specified %s',\n                     service_id, secret_descr)\n            continue\n\n        with Service(service_id, service_data) as service:\n            if not service.load_secret_file(secret_type, filepath, file_checksum):\n                return 1\n        register_service_event(\n            'SECRET_INSTALL',\n            service=service_id,\n            params={\n                'secret_descr': secret_descr,\n            })\n        services_updated.append(service_id)\n\n    if args['--service'] and args['--service'] not in services_updated:\n        log.error('%s was not loaded to service %s',\n                  secret_descr, args['--service'])\n        return 1\n\n    if args['--service']:\n        log.info('%s is loaded to service %s', secret_descr, args['--service'])\n    else:\n        log.info('%s is loaded to services', secret_descr)\n\n    return 0\n\n\ndef validate_secret_file(secret_type, file_content, service_id):\n    \"\"\"Validate secret data file.\n\n    :raises IvxvError:\n    \"\"\"\n    if secret_type in ('tls-cert', 'tls-key') and not service_id:\n        raise IvxvError(\"Service ID is not specified\")\n    if secret_type == 'mid-token-key' and len(file_content) != 32:\n        raise IvxvError(\n            f'File is not 32 bytes long '\n            f'(actual size: {len(file_content)} bytes)')\n    if secret_type == 'tsp-regkey':\n        try:\n            privkey = OpenSSL.crypto.load_privatekey(\n                OpenSSL.crypto.FILETYPE_PEM, file_content\n            )\n        except OpenSSL.crypto.Error as err:\n            err_lib, err_func, err_reason = err.args[0][0]\n            raise IvxvError(\n                f'Error in {err_lib} library {err_func} '\n                f'function: {err_reason}')\n        with open(\"/var/lib/ivxv/service/tspreg-pubkey.pem\", \"wb\") as fd:\n            fd.write(\n                OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_PEM, privkey)\n            )\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/config_utils/voter_list_download.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utility to download voter list from VIS.\"\"\"\n\nimport datetime\nimport json\nimport os\nimport shutil\nimport sys\nimport zipfile\n\nimport requests\n\nfrom ... import command_file\nfrom ...config import CONFIG, cfg_path\nfrom ...db import IVXVManagerDb\nfrom ...event_log import register_service_event\nfrom ...lib import get_current_voter_list_changeset_no\nfrom ...lib.lockfile import PidLocker\nfrom .. import init_cli_util, log\nfrom .command_load import register_cfg, validate_lists_consistency\n\n\ndef main():\n    \"\"\"Download voter list from VIS to IVXV Collector Management Service.\"\"\"\n    args = init_cli_util(\n        \"\"\"\n        Download next available voter list changeset from VIS to IVXV Collector\n        Management Service.\n\n        Usage:\n            ivxv-voter-list-download [--output-report=<filepath>] [--log-level=<level>]\n\n        Options:\n            --output-report=<filepath>  Write JSON report about HTTP request to VIS.\n            --log-level=<level>         Logging level [Default: INFO].\n    \"\"\"\n    )\n\n    # validate CLI arguments\n    output_report_filepath = args[\"--output-report\"]\n\n    # suppress error if executed from crontab before loading election config\n    if not os.path.exists(\"/etc/ivxv/election.bdoc\") and not sys.stdout.isatty():\n        return 0\n\n    # check process lock to start voterlist download without interruption, otherwise\n    # skip downloading until next time\n    if PidLocker.pidfile_exists('ivxv-voter-list-download.pid'):\n        log.info('Cannot start voterlist download, pidfile exists')\n        return 0\n\n    # create process lock and start voterlist downloading\n    pidfile_path = cfg_path('ivxv_admin_data_path',\n                            'ivxv-voter-list-download.pid')\n    log.debug(\"Creating pidfile %r\", pidfile_path)\n    try:\n        PidLocker(pidfile_path, timeout=5)\n    except IOError:\n        log.error(\"Creating pidfile %r failed\", pidfile_path)\n        return 1\n\n    retries: int = 0\n\n    try:\n        while True:\n            retries += 1\n\n            # detect state of the last changeset\n            with IVXVManagerDb() as db:\n                changeset_no = get_current_voter_list_changeset_no(db)\n                changeset_state = db.get_value(f\"list/voters{changeset_no:04d}-state\")\n\n            # stop on INVALID changeset\n            if changeset_state == \"INVALID\":\n                log.info(\n                    \"State of the last registered voter list changeset #%d is %s\",\n                    changeset_no,\n                    changeset_state,\n                )\n                log.info(\"Skipping download of the next voter list changeset\")\n                if output_report_filepath:\n                    with open(output_report_filepath, \"w\") as fd:\n                        json.dump(\n                            {\"voter-list-changeset\": {\"status\": \"Skipped\"}}, fd,\n                            indent=True\n                        )\n                return 0\n            changeset_no += 1\n\n            # get params from election config\n            command_file.log.setLevel(args[\"--log-level\"])\n            cfg = command_file.load_collector_cmd_file(\n                \"election\", \"/etc/ivxv/election.bdoc\")\n            if not cfg:\n                return 1\n            election_id = cfg[\"identifier\"]\n            vis_base_url_template = f\"{cfg['vis']['url']}{{endpoint}}\"\n            ca_certs_filepath = None\n            if cfg[\"vis\"].get(\"ca\"):\n                ca_certs_filepath = cfg_path(\"vis_path\", \"ca.pem\")\n                with open(ca_certs_filepath, \"w\") as fd:\n                    fd.write(\"\\n\".join(cfg[\"vis\"][\"ca\"]))\n\n            # download voter list\n            output_filepath = f\"{CONFIG.get('vis_path')}/voters{changeset_no:04d}.zip\"\n            url = vis_base_url_template.format(endpoint=\"ehs-election-voters-changeset\")\n            log.info(\"Query voter list update #%d from %r\", changeset_no, url)\n            request_params = {\"electionCode\": election_id, \"changeset\": changeset_no}\n            resp = requests.get(\n                url,\n                verify=ca_certs_filepath,\n                cert=(\n                    \"/etc/ssl/certs/ivxv-admin-client.crt\",\n                    \"/etc/ssl/private/ivxv-admin-client.key\",\n                ),\n                params=request_params,\n            )\n\n            log.info(\n                \"VIS responded to voter list update #%d: %r %s\",\n                changeset_no,\n                resp.status_code,\n                resp.reason,\n            )\n            timestamp = datetime.datetime.utcnow().strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n            version = f\"{url} {timestamp}\"\n\n            # write query report\n            if output_report_filepath:\n                report = {\n                    \"voter-list-changeset\": {\n                        \"operation\": f\"Download voter list update #{changeset_no} from \"\n                                     + \"VIS\",\n                        \"request\": {\"url\": resp.url, \"method\": \"GET\",\n                                    \"params\": request_params},\n                        \"response\": {\n                            \"status\": \"OK\" if resp.ok else \"ERROR\",\n                            \"status_code\": resp.status_code,\n                            \"reason\": resp.reason,\n                            \"time_elapsed\": str(resp.elapsed),\n                            \"retries\": retries,\n                            \"headers\": dict([key, val] for key, val in\n                                            resp.headers.items()),\n                            \"content\": resp.text,\n                        },\n                    },\n                }\n                with open(output_report_filepath, \"w\") as fd:\n                    json.dump(report, fd, indent=True)\n\n            # register download event / handle download errors\n            if resp.status_code == 404:\n                log.info(\"Voter list changeset #%d is not available in VIS\",\n                         changeset_no)\n                return 0\n            if resp.status_code != 200:\n                log.error(\"Server responded with status %d - %r\", resp.status_code,\n                          resp.reason)\n                register_service_event(\n                    \"VOTER_LIST_DOWNLOAD_FAILED\",\n                    level=\"INFO\",\n                    service=\"admin\",\n                    params={\"changeset_no\": changeset_no},\n                )\n                return 1\n            register_service_event(\n                \"VOTER_LIST_DOWNLOADED\",\n                level=\"INFO\",\n                service=\"admin\",\n                params={\"changeset_no\": changeset_no},\n            )\n\n            # write downloaded content to file\n            write_voter_list_zip(resp.content, version, output_filepath)\n\n            # register voter list\n            if changeset_no:\n                register_voter_list(output_filepath, timestamp, version)\n    finally:\n        try:\n            log.debug(\"Releasing pidfile %r\", pidfile_path)\n            PidLocker.rm_stale_pidfile('ivxv-voter-list-download.pid')\n        except FileNotFoundError:  # pid file doesn't exist\n            pass\n\n\ndef write_voter_list_zip(content, version, output_filepath):\n    \"\"\"Write voter list ZIP file.\"\"\"\n    # write list to temporary ZIP file\n    tmp_filepath = f\"{output_filepath}.tmp\"\n    with open(tmp_filepath, \"bw\") as fd:\n        tmp_filepath = fd.name\n        log.info(\"Writing VIS response to temporary file %r\", tmp_filepath)\n        fd.write(content)\n\n    # add version to ZIP\n    comment = f\"Version: {version}\"\n    log.info(\"Adding comment %r to %r\", comment, tmp_filepath)\n    with zipfile.ZipFile(tmp_filepath, \"a\") as zip_file:\n        zip_file.comment = bytes(comment, \"ASCII\")\n\n    # move ZIP file to final place\n    log.info(\"Moving temporary file %r to %r\", tmp_filepath, output_filepath)\n    shutil.move(tmp_filepath, output_filepath)\n\n\ndef register_voter_list(filepath, timestamp, version):\n    \"\"\"Register voter list in management service.\"\"\"\n    cfg_data = command_file.load_collector_cmd_file(\"voters\", filepath)\n    is_cfg_valid = cfg_data is not None\n    if is_cfg_valid:\n        is_cfg_valid = validate_lists_consistency(\"voters\", filepath)\n\n    register_service_event(\n        \"CMD_LOAD\", params={\"cmd_type\": \"voters\", \"version\": version}\n    )\n    register_cfg(\n        cmd_type=\"voters\",\n        cfg_data=cfg_data,\n        cfg_filename=filepath,\n        cfg_timestamp=timestamp,\n        cfg_version=version,\n        autoapply=is_cfg_valid,\n        state=\"PENDING\" if is_cfg_valid else \"INVALID\",\n    )\n    register_service_event(\n        \"CMD_LOADED\", params={\"cmd_type\": \"voters\", \"version\": version}\n    )\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/service_utils.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for service management.\"\"\"\n\nimport datetime\nimport os\nimport shutil\nimport json\nimport logging\nimport random\nimport subprocess\nimport sys\nimport tempfile\nimport time\nimport zipfile\nfrom collections import OrderedDict\nfrom crontab import CronTab\nfrom jinja2 import Environment, PackageLoader\n\nimport fasteners\nimport yaml\nimport requests\n\nfrom .. import (\n    COLLECTOR_STATE_CONFIGURED,\n    COLLECTOR_STATE_FAILURE,\n    COLLECTOR_STATE_INSTALLED,\n    COLLECTOR_STATE_NOT_INSTALLED,\n    COLLECTOR_STATE_PARTIAL_FAILURE,\n    SERVICE_STATE_CONFIGURED,\n    SERVICE_STATE_FAILURE,\n    SERVICE_STATE_INSTALLED,\n    SERVICE_STATE_REMOVED,\n    SERVICE_TYPE_PARAMS,\n    __version__,\n    command_file,\n)\nfrom ..agent_daemon import get_collector_data, ping_service\nfrom ..config import cfg_path\nfrom ..db import IVXVManagerDb\nfrom ..lib import IvxvError, get_services\nfrom ..service import logging as service_logging\nfrom ..service.service import Service, exec_remote_cmd\nfrom . import init_cli_util, log\n\n#: JSON formatting options\nJSON_DUMP_ARGS = dict(indent=2, sort_keys=True)\n\nIVXV_ADMIN_CRON_USER = 'ivxv-admin'\nVOTER_LIST_DOWNLOAD_CRON_JOB = 'if [ -x /usr/bin/ivxv-voter-list-download ]; then /usr/bin/ivxv-voter-list-download --log-level=WARNING; fi'  # noqa: E501\n\n\ndef export_votes_util():\n    \"\"\"Export collected votes.\"\"\"\n    args = init_cli_util(\"\"\"\n    Export collected votes.\n\n    This utility copies current ballot box from voting service to backup\n    service and outputs ballot box content.\n\n    Usage: ivxv-export-votes [--consolidate] <output-file>\n\n    Options:\n        --consolidate   Consolidate all collected votes\n    \"\"\")\n\n    try:\n        export_votes(args[\"--consolidate\"], args[\"<output-file>\"])\n    except IvxvError as err:\n        log.error(err)\n        return 1\n\n    return 0\n\n\ndef export_votes(must_consolidate, output_filename):\n    \"\"\"Export votes.\"\"\"\n    # fail if output file already exist\n    if os.path.exists(output_filename):\n        raise IvxvError(f\"Output file {output_filename!r} already exist\")\n\n    # create backup of current ballot box\n    log.info('Creating backup copy from current ballot box')\n    try:\n        subprocess.run(['ivxv-backup', 'ballot-box'], check=True)\n    except OSError as err:\n        raise IvxvError(f\"Creating ballot box backup failed: {err.strerror}\")\n    except subprocess.CalledProcessError as err:\n        raise IvxvError(f\"Creating ballot box backup failed: {err}\")\n\n    # create handler for backup service\n    services = get_services(include_types=['backup'])\n    if not services:\n        raise IvxvError('Backup service is not defined')\n    assert len(services) == 1\n\n    with Service(*list(services.items())[0]) as backup_service:\n        log.debug('Backup service: %s', backup_service.service_id)\n        ballot_box_filepath = datetime.datetime.now().strftime(\n            '/var/lib/ivxv/ballot-box-consolidated-%Y%m%d_%H%M.zip')\n\n        # run consolidation in backup service\n        if must_consolidate:\n            proc = backup_service.ssh([\n                'ivxv-voteunion', ballot_box_filepath,\n                '/var/backups/ivxv/ballot-box/ballot-box-????????_????.zip'\n            ])\n            if proc.returncode:\n                raise IvxvError('Consolidation command failed in backup service')\n        else:  # export without consolidation\n            # detect last backup filename\n            cmd = [\n                'ls', '/var/backups/ivxv/ballot-box/ballot-box-????????_????.zip'\n            ]\n            proc = backup_service.ssh(cmd, stdout=subprocess.PIPE, check=True)\n            ballot_box_filepath = proc.stdout.decode('UTF-8').strip().split(\n                '\\n')[-1]\n\n        # copy consolidated ballot box from backup\n        log.info('Copying ballot box to management service')\n        if not backup_service.scp(\n                output_filename,\n                ballot_box_filepath,\n                \"consolidated ballot box\" if must_consolidate else \"ballot box\",\n                to_remote=False):\n            raise IvxvError('Failed to copy ballot box to management service')\n        if must_consolidate:\n            log.info('Removing consolidated ballot box from backup service')\n            backup_service.ssh(['rm', '-v', ballot_box_filepath])\n\n    log.info(\"Collected votes archive is written to %r\", output_filename)\n\n\ndef generate_processor_input_util():\n    \"\"\"Generate input for processor application.\"\"\"\n    args = init_cli_util(\n        \"\"\"\n    Generate input for processor application.\n\n    This utility generates ZIP container with data files\n    for processor application to validate ballot box:\n\n        1. District list;\n        2. Voter lists;\n        3. Validation key for vote registration requests;\n        4. Configuration for processor application.\n\n    Usage: ivxv-generate-processor-input <output-file>\n    \"\"\"\n    )\n\n    try:\n        election_id, cfg = generate_processor_check_cfg()\n        generate_processor_input(election_id, cfg, args[\"<output-file>\"])\n    except IvxvError as err:\n        log.error(err)\n        return 1\n\n    return 0\n\n\ndef generate_processor_check_cfg():\n    \"\"\"Generate config file for processor application.\"\"\"\n    log.info(\"Generating processor application config\")\n\n    processor_check_cfg = OrderedDict(\n        districts=None,\n        registrationlist=\"registrationlist.zip\",\n        registrationlist_checksum=\"registrationlist.zip.sha256sum.asice\",\n        vlkey=None,\n        tskey=\"ts.key\",\n        voterlists=[],\n        election_start=None,\n        voterforeignehak=None,\n        out=None,\n    )\n\n    # read collector config from management database\n    with IVXVManagerDb() as db:\n        election_id = db.get_value(\"election/election-id\")\n        processor_check_cfg[\"election_start\"] = db.get_value(\"election/electionstart\")\n        districts_version = db.get_value(\"list/districts\")\n        voter_list_states = []\n        for changeset_no in range(10_000):\n            try:\n                voter_list_state = db.get_value(f\"list/voters{changeset_no:04d}-state\")\n            except KeyError:\n                break\n            if voter_list_state not in [\"APPLIED\", \"SKIPPED\"]:\n                raise IvxvError(\n                    f\"Voter list changeset #{changeset_no} \"\n                    f\"has unexpected state {voter_list_state!r}\"\n                )\n            voter_list_states.append(voter_list_state)\n    if not election_id:\n        raise IvxvError(\"Election is not configured\")\n    if not districts_version:\n        raise IvxvError(\"District list is not loaded\")\n    if not voter_list_states:\n        raise IvxvError(\"Initial voter list is not loaded\")\n\n    processor_check_cfg.update(\n        {\n            \"districts\": f\"{election_id}.districts.json.bdoc\",\n            \"out\": f\"{election_id}-out-1\",\n        }\n    )\n\n    # read election config from command file\n    try:\n        election_cfg = command_file.load_cfg_file_content(\n            \"election\", f\"{election_id}.election.yaml\", \"/etc/ivxv/election.bdoc\"\n        )\n    except (IvxvError, OSError) as err:\n        raise IvxvError(f\"Error while loading config file: {err}\")\n    except UnicodeDecodeError as err:\n        raise IvxvError(f\"Error while decoding config file: {err}\")\n\n    # copy election config values\n    if \"voterforeignehak\" in election_cfg:\n        processor_check_cfg[\"voterforeignehak\"] = election_cfg[\"voterforeignehak\"]\n    else:\n        processor_check_cfg.pop(\"voterforeignehak\")\n    processor_check_cfg[\"vlkey\"] = election_cfg[\"voterlist\"][\"key\"]\n\n    # generate voter list filenames\n    for changeset_no, voter_list_state in enumerate(voter_list_states):\n        voter_list_record = dict(\n            path=f\"{changeset_no:02d}.{election_id}.voters.utf\",\n            signature=f\"{changeset_no:02d}.{election_id}.voters.sig\",\n        )\n        if voter_list_state == \"SKIPPED\":\n            voter_list_record[\n                \"skip_cmd\"\n            ] = f\"{changeset_no:02d}.{election_id}.voters-skip.yaml.bdoc\"\n        processor_check_cfg[\"voterlists\"].append(voter_list_record)\n\n    return election_id, processor_check_cfg\n\n\ndef generate_processor_input(election_id, processor_check_cfg, output_filename):\n    \"\"\"Generate input file for processor application.\"\"\"\n    # fail if output file already exist\n    if os.path.exists(output_filename):\n        raise IvxvError(f\"Output file {output_filename!r} already exist\")\n\n    log.info(\"Creating input file for processor application\")\n\n    with tempfile.TemporaryDirectory() as output_dir:\n        log.info(\"Preparing container structure in directory %r\", output_dir)\n\n        # district list\n        filename = processor_check_cfg[\"districts\"]\n        log.info(\"Copying district list %r\", filename)\n        shutil.copy(\n            cfg_path(\"active_config_files_path\", \"districts.bdoc\"),\n            f\"{output_dir}/{filename}\",\n        )\n\n        # key to verify voter list signer\n        filename = \"voterfile.pub.key\"\n        log.info(\"Copying voter list signing key %r\", filename)\n        with open(f\"{output_dir}/{filename}\", \"w\") as fd:\n            fd.write(processor_check_cfg[\"vlkey\"])\n        processor_check_cfg[\"vlkey\"] = filename\n\n        # voter lists\n        for changeset_no, voter_list_files in enumerate(\n            processor_check_cfg[\"voterlists\"]\n        ):\n            input_filepath = cfg_path(\n                \"active_config_files_path\",\n                f\"voters{changeset_no:04d}.zip\" if changeset_no else \"voters0000.bdoc\",\n            )\n            with zipfile.ZipFile(input_filepath) as input_zip:\n                for description, key in [\n                    [\"content\", \"path\"],\n                    [\"signature\", \"signature\"],\n                    [\"skipping command\", \"skip_cmd\"],\n                ]:\n                    try:\n                        filename = voter_list_files[key]\n                    except KeyError:\n                        continue\n                    file_ext = os.path.splitext(filename)[-1]\n                    log.info(\n                        \"Copying voter list #%d %s %r\",\n                        changeset_no,\n                        description,\n                        filename,\n                    )\n                    if key == \"skip_cmd\":\n                        shutil.copy(\n                            cfg_path(\n                                \"active_config_files_path\",\n                                f\"voters{changeset_no:04d}.bdoc\",\n                            ),\n                            f\"{output_dir}/\"\n                            f\"{changeset_no:02d}.{election_id}.voters-skip.yaml.bdoc\",\n                        )\n                    else:\n                        with input_zip.open(\n                            f\"{election_id}-voters-{changeset_no}{file_ext}\"\n                        ) as zip_fd:\n                            with open(f\"{output_dir}/{filename}\", \"wb\") as fd:\n                                fd.write(zip_fd.read())\n\n        # key to verify registration requests\n        filename = processor_check_cfg[\"tskey\"]\n        log.info(\"Copying registration requests verification key %r\", filename)\n        shutil.copy(\n            \"/var/lib/ivxv/service/tspreg-pubkey.pem\",\n            f\"{output_dir}/{filename}\",\n        )\n\n        # setup YAML module to write OrderedDict\n        def represent_dictionary_order(self, dict_data):\n            return self.represent_mapping(\"tag:yaml.org,2002:map\", dict_data.items())\n\n        def setup_yaml():\n            yaml.add_representer(OrderedDict, represent_dictionary_order)\n\n        setup_yaml()\n\n        # config file for processor application\n        filename = f\"{election_id}.processor.yaml\"\n        log.info(\"Writing processor application config %r\", filename)\n        with open(f\"{output_dir}/{filename}\", \"w\") as fd:\n            fd.write(\n                \"# Automaatselt genereeritud fail \"\n                f\"{datetime.datetime.now():%d.%m.%Y %R}\\n\\n\"\n                f\"# Generaator: {__file__}\\n\\n\"\n            )\n            yaml.dump(dict(check=processor_check_cfg), fd, default_flow_style=False)\n            fd.write('\\n')\n            fd.write('  # BALLOT BOX\\n')\n            fd.write('  #ballotbox: votes.zip\\n')\n            fd.write('  #ballotbox_checksum: votes.zip.sha256sum\\n')\n\n        # generate ZIP container\n        filenames = sorted(os.listdir(output_dir))\n        output_filename_tmp = f\"{output_filename}.tmp\"\n        log.info(\n            \"Generating ZIP container %r with %d files\",\n            output_filename_tmp,\n            len(filenames),\n        )\n        with zipfile.ZipFile(output_filename_tmp, \"w\", allowZip64=True) as zip_file:\n            for filename in filenames:\n                log.info(\"Adding %r to ZIP container\", filename)\n                zip_file.write(f\"{output_dir}/{filename}\", filename)\n\n    shutil.move(output_filename_tmp, output_filename)\n\n    log.info(\"Processor input is written to %r\", output_filename)\n\n\ndef copy_logs_to_logmon_util():\n    \"\"\"Copy IVXV log files from service hosts to Log Monitor.\"\"\"\n    # validate CLI arguments\n    args = init_cli_util(\"\"\"\n    Copy IVXV log files from service hosts to Log Monitor.\n\n    This utility transports collected IVXV log files from IVXV services\n    (including Log Collector Service) to Log Monitor.\n\n    Usage: ivxv-copy-log-to-logmon [--log-level=<level>] [<hostname> ...]\n\n    Options:\n        <hostname>              Service host name.\n        --log-level=<level>     Logging level [Default: INFO].\n    \"\"\")\n\n    # check collector state\n    with IVXVManagerDb() as db:\n        collector_state = db.get_value('collector/state')\n        logmonitor_address = db.get_value('logmonitor/address')\n        services = db.get_all_values('service')\n    if collector_state == COLLECTOR_STATE_NOT_INSTALLED:\n        if not sys.stdout.isatty():  # suppress warning if executed from crontab\n            return 0\n        log.warning(\"Collector is not installed\")\n        return 1\n\n    # create service host list\n    hostnames = args['<hostname>']\n    if not hostnames:\n        hostnames = []\n        for service in services.values():\n            is_main_service = (\n                SERVICE_TYPE_PARAMS[service['service-type']]['main_service'])\n            if ((is_main_service\n                    or service['service-type'] == 'log')\n                    and service['state'] != SERVICE_STATE_REMOVED):\n                hostnames.append(service['ip-address'].split(':')[0])\n        hostnames = list(set(hostnames))\n\n    # checking access to Log Monitor account\n    if not logmonitor_address:\n        log.error('Log monitor is not defined')\n        return 1\n    log.info(\"Using address %r for log monitor\", logmonitor_address)\n    logmon_account = f'logmon@{logmonitor_address}'\n    log.info('Checking SSH access to Log Monitor account %s', logmon_account)\n    proc = exec_remote_cmd(['ssh', logmon_account, 'true'])\n    if proc.returncode:\n        log.error('Cannot access to Log Monitor (%s)', logmon_account)\n        return 1\n\n    # copy log file from service hosts to Log Monitor\n    exit_code = 0\n    for hostname in hostnames:\n        remote_account = f'ivxv-admin@{hostname}'\n\n        # check if service host have Log Monitor host key\n        log.info(\"Checking if %r have Log Monitor host key\", remote_account)\n        proc = exec_remote_cmd(\n            ['ssh', hostname, 'ssh-keygen', '-F', logmonitor_address],\n            stdout=subprocess.PIPE)\n        if proc.returncode:\n            log.error(\n                'Failed to check Log Monitor host key for %s', remote_account)\n            log.info('Installing Log Monitor host key for %s', remote_account)\n            proc = subprocess.run(\n                ['sh', '-c',\n                 f'ssh-keygen -F {logmonitor_address} | '\n                 'grep -v ^# | '\n                 f'ssh {hostname} tee --append .ssh/known_hosts'],\n                check=False,\n            )\n            if proc.returncode:\n                log.error('Failed to install Log Monitor host key for %s',\n                          remote_account)\n                continue\n\n        if not copy_logs_from_host_to_logmon(hostname, logmon_account):\n            exit_code = 1\n\n    return exit_code\n\n\ndef copy_logs_from_host_to_logmon(hostname, logmon_account):\n    \"\"\"Copy log files from service host to Log Monitor.\"\"\"\n    # acquire process lock\n    lockfile_path = cfg_path(\n        'ivxv_admin_data_path',\n        f'service/copy-log-from-{hostname}-to-logmon.lock')\n    lock = fasteners.InterProcessLock(lockfile_path)\n    if not lock.acquire(blocking=False):\n        log.warning('Lock exists for process \"copy logs from host %s '\n                    'to Log Monitor\"', hostname)\n        return False\n\n    # execute rsync command in host to copy log file to Log Monitor\n    proc = None\n    try:\n        log.info(\n            \"Copying IVXV service log files from host %r to Log Monitor\",\n            hostname)\n        transfer_cmd = [\n            'ivxv-admin-helper', 'copy-logs-to-logmon', hostname,\n            logmon_account\n        ]\n        cmd = ['ssh-agent', 'ssh', '-A', hostname] + transfer_cmd\n\n        proc = subprocess.run(cmd, check=False)\n    finally:\n        lock.release()\n\n    if not proc.returncode:\n        return True\n\n    log.error(\"Failed to copy log file from host %r to Log Monitor\",\n              hostname)\n    return False\n\n\ndef update_software_pkg_util():\n    \"\"\"Update service packages in service hosts.\"\"\"\n    # validate CLI arguments\n    args = init_cli_util(\"\"\"\n    Update service packages in IVXV service hosts.\n\n    This utility checks versions of software packages in service hosts\n    and installs new versions if required.\n\n    Usage: ivxv-update-packages [--force] [--service=<type>] [--package=<package>]\n\n    Options:\n        --force                 Update even package version does not require update\n        --service=<type>        Update only specific service package (e.g. ivxv-voting)\n        --package=<package>     Path to the service package\n    \"\"\")\n\n    if (not args['--service'] and args['--package']\n            or args['--service'] and not args['--package']):\n        log.error('--service and --package should be set at the same time')\n        return 1\n\n    # generate list of voting services that are in required state\n    services = get_services(\n        require_collector_state=[\n            COLLECTOR_STATE_INSTALLED,\n            COLLECTOR_STATE_CONFIGURED,\n            COLLECTOR_STATE_FAILURE,\n            COLLECTOR_STATE_PARTIAL_FAILURE,\n        ],\n        service_state=[\n            SERVICE_STATE_INSTALLED,\n            SERVICE_STATE_CONFIGURED,\n            SERVICE_STATE_FAILURE,\n        ],\n        include_types=[args['--service'] or []]\n    )\n    if not services:\n        return 1\n\n    host_versions = dict(\n        [services[service_id]['ip-address'].split(':')[0], '']\n        for service_id in sorted(services))\n    update_res = dict(failure=[], install=[], skip=[])\n\n    def get_installed_pkg_ver():\n        \"\"\"Get version string if installed package in service host.\"\"\"\n        service.log.info('Detect %s package version', pkg_name)\n        proc = service.ssh(\n            f'dpkg --status {pkg_name} | grep ^Version: | cut -d: -f2',\n            stdout=subprocess.PIPE,\n            account='ivxv-admin')\n        return proc.stdout.decode('UTF-8').strip()\n\n    for service_id, service_data in sorted(services.items()):\n        with Service(service_id, service_data) as service:\n            # check ivxv-common version\n            host_version = host_versions.get(service.hostname)\n            pkg_name = 'ivxv-common'\n            # if specific service is requested to update, then skip\n            install_pkg = bool(args['--force']) or bool(args['--service'])\n            if not install_pkg and host_version != __version__:\n                host_versions[service.hostname] = get_installed_pkg_ver()\n                install_pkg = host_versions[service.hostname] != __version__\n\n            # install ivxv-common if required, don't install if specific service\n            # is requested\n            if install_pkg and not bool(args['--service']):\n                if not service.update_ivxv_common_pkg():\n                    update_res['failure'].append([service_id, pkg_name])\n                    continue\n                update_res['install'].append([service_id, pkg_name])\n                host_versions[service.hostname] = __version__\n            else:\n                update_res['skip'].append([service_id, pkg_name])\n\n            # check service package version and upgrade if required,\n            # package will be upgraded if specific service requested\n            pkg_name = service.deb_pkg_name\n            install_pkg = (args['--force'] or get_installed_pkg_ver() != __version__\n                           or args['--service'])\n\n            # install package if required\n            if install_pkg:\n                if not service.install_service_pkg(is_update=True,\n                                                   package=args['--package']):\n                    update_res['failure'].append([service_id, pkg_name])\n                    continue\n                update_res['install'].append([service_id, pkg_name])\n            else:\n                update_res['skip'].append([service_id, pkg_name])\n\n    # output result\n    for service_id, pkg_name in update_res['install']:\n        log.info('Successfully installed service %s package %s',\n                 service_id, pkg_name)\n    for service_id, pkg_name in update_res['failure']:\n        log.error('Failed to install service %s package %s',\n                  service_id, pkg_name)\n    log.info('Service update stats: %d packages installed, '\n             '%d package installations failed, %s packages skipped',\n             len(update_res['install']),\n             len(update_res['failure']),\n             len(update_res['skip']))\n\n    return 1 if update_res['failure'] else None\n\n\ndef manage_service():\n    \"\"\"Manage IVXV services.\"\"\"\n    # validate CLI arguments\n    args = init_cli_util(\"\"\"\n    Manage IVXV services.\n\n    Usage: ivxv-service <action> <service-id> ...\n\n    Options:\n        <action>    Management action: start, stop, restart, ping\n    \"\"\")\n    action = args['<action>']\n    if action not in ['start', 'stop', 'restart', 'ping']:\n        log.error('Invalid action: %s', action)\n        return 1\n\n    services = get_collector_data()\n    if services is None:\n        log.error('Election data is not loaded')\n        return 1\n\n    exit_code = 0\n    for service_id in args['<service-id>']:\n        if service_id not in services:\n            log.error('Unknown service %s', service_id)\n            exit_code = 1\n            continue\n        if action == 'ping':\n            log.info('Pinging service %s', service_id)\n            if ping_service(service_id, services[service_id]):\n                log.info('Service %s is alive', service_id)\n            else:\n                log.error('Failed to query service %s status', service_id)\n                exit_code = 1\n            continue\n\n        with Service(service_id, services[service_id]) as service:\n            if action == 'stop':\n                log.info('Stopping service %s', service_id)\n                if service.stop_service():\n                    log.info('Service %s stopped', service_id)\n                else:\n                    log.error('Failed to stop service %s', service_id)\n                    exit_code = 1\n                continue\n\n        # start, restart\n        log.info('%sing service %s', action.capitalize(), service_id)\n        with Service(service_id, services[service_id]) as service:\n            if service.restart_service():\n                log.info('Service %s %sed', service_id, action)\n            else:\n                log.error('Failed to %s service %s', action, service_id)\n                exit_code = 1\n\n    return exit_code\n\n\ndef voterstats_util():\n    \"\"\"Import voter stats from voting service and export to VIS.\"\"\"\n    args = init_cli_util(\n        \"\"\"\n        Import voter stats from voting service and export common stats to VIS.\n\n        Usage: ivxv-voterstats <TYPE> [--action=<action>] [--file=<file>]\n                    [--service-id=<service_id>] [--log-level=<level>]\n\n        Options:\n            <TYPE>                      Stats type \"common\" or \"detail\".\n            --action=<action>           Limit actions for \"common\" stats type.\n                                        Possible values are \"import\" and \"export\".\n                                        [Default: all]\n            --file=<file>               Path to stats file.\n            --service-id=<service_id>   Voting service ID [Default: random].\n            --log-level=<level>         Logging level [Default: INFO].\n        \"\"\"\n    )\n    stats_type = args[\"<TYPE>\"]\n    action = args[\"--action\"]\n    filepath = args[\"--file\"]\n    if not filepath:\n        filepath = f\"/var/lib/ivxv/admin-ui-data/voterstats-{stats_type}.json\"\n    service_id = args[\"--service-id\"]\n\n    # validate CLI args\n    if stats_type not in [\"common\", \"detail\"]:\n        log.error(\"Unexpected stats type %r\", stats_type)\n        return 1\n\n    # check collector config state\n    try:\n        with IVXVManagerDb() as db:\n            if not db.get_value(\"list/districts\"):\n                raise IvxvError(\"District list is not loaded\")\n            services = get_services(\n                db=db, service_state=[\"CONFIGURED\"], include_types=[\"voting\"]\n            )\n            if not services:\n                raise IvxvError(\"No configured voting service found\")\n    except IvxvError as err:\n        if not sys.stdout.isatty():  # suppress warning if executed from crontab\n            return 0\n        log.warning(err)\n        return 1\n\n    # choose voting service\n    if service_id == \"random\":\n        service_id = random.choice(list(services.keys()))\n    if service_id not in services:\n        services = get_services(include_types=[\"voting\"])\n        if service_id in services:\n            log.error(\n                \"Voting service %r is not configured (current state: %s)\",\n                service_id,\n                services[service_id][\"state\"],\n            )\n        else:\n            log.error(\"No voting service %r found\", service_id)\n        return 1\n    service_logging.log.setLevel(args[\"--log-level\"])\n    with Service(service_id, services[service_id]) as service:\n        # import stats\n        try:\n            if action in [\"all\", \"import\"]:\n                import_voterstats(stats_type, service, filepath, args[\"--log-level\"])\n        except IvxvError:\n            log.error(\n                \"Failed to import %s stats from service %r\", stats_type, service_id)\n            return 1\n\n    # export stats to VIS only if requested\n    command_file.log.setLevel(args[\"--log-level\"])\n    try:\n        if action in [\"all\", \"export\"]:\n            export_voterstats(stats_type, filepath)\n    except IvxvError:\n        log.error(\"Failed to export %s stats to VIS\", stats_type)\n        return 1\n\n    return 0\n\n\ndef import_voterstats(stats_type, service, filepath, log_level):\n    \"\"\"Import voter stats from voting service.\"\"\"\n    log.info(\"Generating %s stats in voting service %r\", stats_type, service.service_id)\n    cmd = [\"ivxv-voterstats\", \"-instance\", service.service_id]\n    if stats_type == \"detail\":\n        cmd.append(\"-detailed\")\n    if logging.getLevelName(log_level) >= 30:\n        cmd.append(\"-q\")\n    remote_filepath = (\n        f\"/var/lib/ivxv/user/ivxv-voting/ivxv-voterstats-{os.getpid()}.json\"\n    )\n    cmd.append(remote_filepath)\n    proc = service.ssh(cmd)\n    if proc.returncode:\n        raise IvxvError(\n            f\"Failed to generate {stats_type} stats \"\n            f\"in voting service {service.service_id}\"\n        )\n\n    log.info(\n        \"Importing %s stats from voting service %r\", stats_type, service.service_id\n    )\n    if not service.scp(\n        filepath, remote_filepath, f\"{stats_type} stats\", to_remote=False\n    ):\n        raise IvxvError(f\"Failed to {stats_type} stats to management service\")\n\n\ndef export_voterstats(stats_type, filepath):\n    \"\"\"Export voter stats to VIS.\"\"\"\n    log.info(\"Exporting %s stats to VIS\", stats_type)\n    with open(filepath) as fd:\n        stats = json.load(fd)\n\n    # get params from election config\n    cfg = command_file.load_collector_cmd_file(\"election\", \"/etc/ivxv/election.bdoc\")\n    if not cfg:\n        raise IvxvError(\"Election config not found\")\n    ca_certs_filepath = None\n    if cfg[\"vis\"].get(\"ca\"):\n        ca_certs_filepath = cfg_path(\"vis_path\", \"ca.pem\")\n        with open(ca_certs_filepath, \"w\") as fd:\n            fd.write(\"\\n\".join(cfg[\"vis\"][\"ca\"]))\n\n    # upload voter stats\n    url = f\"{cfg['vis']['url']}voters-by-county\"\n    log.info(\"Upload voter stats to %r\", url)\n    resp = requests.post(\n        url,\n        verify=ca_certs_filepath,\n        cert=(\n            \"/etc/ssl/certs/ivxv-admin-client.crt\",\n            \"/etc/ssl/private/ivxv-admin-client.key\",\n        ),\n        json=stats,\n    )\n    log.info(\n        \"VIS responded to voter stats upload: %r %s\", resp.status_code, resp.reason\n    )\n\n    if resp.status_code != 204:\n        raise IvxvError(\n            f\"Server responded with status {resp.status_code} - {resp.reason}\"\n        )\n\n\ndef voting_sessions_util():\n    \"\"\"Utility to import voting sessions from Log Monitor.\"\"\"\n    args = init_cli_util(\n        \"\"\"\n        Import list of voting sessions from Log Monitor.\n\n        Session data is in CSV format.\n\n        Usage: ivxv-voting-sessions (vote | verify) <output_file> [--anonymize] [--uniq]\n                    [--log-level=<level>]\n\n        Options:\n            <output_file>               Write sessions to file.\n            --anonymize                 Anonymize session data\n                                        (IP addresses and ID codes).\n            --uniq                      Consolidate session data.\n            --log-level=<level>         Logging level [Default: INFO].\n        \"\"\"\n    )\n\n    # check collector state\n    with IVXVManagerDb() as db:\n        collector_state = db.get_value(\"collector/state\")\n        logmonitor_address = db.get_value(\"logmonitor/address\")\n    if collector_state == COLLECTOR_STATE_NOT_INSTALLED:\n        # emit warning only if attached to console\n        # (suppress if executed from crontab)\n        if sys.stdout.isatty():\n            log.warning(\"Collector is not installed\")\n            return 1\n        return 0\n\n    # checking access to Log Monitor account\n    if not logmonitor_address:\n        log.error(\"Log monitor is not defined\")\n        return 1\n    log.info(\"Using address %r for log monitor\", logmonitor_address)\n    logmon_account = f\"logmon@{logmonitor_address}\"\n    log.info(\"Checking SSH access to Log Monitor account %s\", logmon_account)\n    proc = exec_remote_cmd([\"ssh\", logmon_account, \"true\"])\n    if proc.returncode:\n        log.error(\"Cannot access to Log Monitor (%s)\", logmon_account)\n        return 1\n\n    try:\n        import_voting_sessions(\n            logmon_account,\n            session_type=\"vote\" if args[\"vote\"] else \"verify\",\n            anonymize=args[\"--anonymize\"],\n            uniq=args[\"--uniq\"],\n            output_filepath=args[\"<output_file>\"],\n            log_level=args[\"--log-level\"],\n        )\n    except IvxvError as err:\n        log.error(err)\n        return 1\n\n    return 0\n\n\ndef import_voting_sessions(\n    logmon_account, session_type, anonymize, uniq, output_filepath, log_level\n):\n    \"\"\"Import voting sessions from Log Monitor.\"\"\"\n    # generate CSV\n    log.info(\"Generating voting sessions file in Log Monitor\")\n    remote_outfile = f\"~logmon/voting-sessions-{session_type}-{os.getpid()}.json\"\n    remote_cmd = [\"ivxv-export-voting-sessions\", f\"--log-level={log_level}\"]\n    if anonymize:\n        remote_cmd.append(\"--anonymize\")\n    if uniq:\n        remote_cmd.append(\"--uniq\")\n    remote_cmd += [session_type, remote_outfile]\n    cmd = [\"ssh\", logmon_account] + remote_cmd\n    try:\n        subprocess.run(cmd, stdout=subprocess.PIPE, check=True)\n    except subprocess.CalledProcessError as err:\n        raise IvxvError(\"Failed to generate voting sessions\") from err\n    log.info(\"Voting sessions file successfully generated in Log Monitor\")\n\n    # import CSV\n    log.info(\"Importing voting sessions file from Log Monitor\")\n    cmd = [\"scp\", f\"{logmon_account}:{remote_outfile}\", output_filepath]\n    try:\n        subprocess.run(cmd, stdout=subprocess.PIPE, check=True)\n    except subprocess.CalledProcessError as err:\n        raise IvxvError(\"Failed to import voting sessions\") from err\n    log.info(\"Voting sessions file successfully imported from Log Monitor\")\n\n\ndef remove_ivxv_admin_crontab():\n    \"\"\"Remove ivxv-admin user crontab if exists.\"\"\"\n    log.info('Removing ivxv-admin user crontab (if exists)')\n    proc = subprocess.run([\"crontab\", \"-r\"], check=False)\n    assert proc.returncode in [0, 1], 'Unexpected exit code for crontab command'\n\n\ndef generate_detail_stats_crontab(cfg: dict):\n    \"\"\"Generate crontab for detailed statistics exchange with VIS.\n    Generated file will be stored to db.\n\n    :param cfg: content of configuration file\n    :type cfg: dict\n    \"\"\"\n\n    # load Jinja2 crontab template for detailed statistics\n    template_dir = Environment(loader=PackageLoader('ivxv_admin', 'templates'))\n    template = template_dir.get_template('ivxv_detail_stats_crontab.jinja')\n\n    # read `stats:` section from a configuration file,\n    # use default values if configuration file doesn't have `stats:` section\n    cron_cfg = cfg.get('stats', {}).get('detail', {}).get('scheduler', {})\n    crontab_params = {\n        'minute': cron_cfg.get('cron', {}).get('min', '*/15') or '*/15',\n        'hour': cron_cfg.get('cron', {}).get('hour', '*') or '*',\n        'day': cron_cfg.get('cron', {}).get('day', '*') or '*',\n        'month': cron_cfg.get('cron', {}).get('month', '*') or '*',\n        'weekday': cron_cfg.get('cron', {}).get('weekday', '*') or '*',\n    }\n\n    # render Jinja2 template\n    rendered_template = template.render(\n        time_generated=datetime.datetime.now().strftime('%d.%M.%Y %H:%M:%S'),\n        **crontab_params,\n    )\n\n    # override crontab rendered template in a db\n    with IVXVManagerDb(for_update=True) as db:\n        db.set_value('stats/detail/scheduler/cron', rendered_template)\n\n\ndef detail_stats_crontab_editor() -> int:\n    args = init_cli_util(\"\"\"\n    Generate crontab for IVXV detail statistics export to VIS automation.\n\n    This utility must be called as editor by crontab utility:\n\n        $ env VISUAL=ivxv-detail-stats-crontab crontab -e\n\n    Usage: ivxv-detail-stats-crontab <filename>\n    \"\"\")\n    filepath = args['<filename>']\n\n    # when you run `crontab -e`, cron generates temporary file at '/tmp/XYZ/crontab',\n    # and that temporary file is passed here as args['<filename>'], i.e 'filepath'\n    crontab_tmp_file_content: str\n\n    # read crontab temporary file\n    with open(filepath, 'r') as fp:\n        try:\n            crontab_tmp_file_content = fp.read()\n        except Exception as err:\n            msg = \"Can't read crontab temporary file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n\n    # get rendered Jinja2 crontab template for detail stats from a db\n    with IVXVManagerDb() as db:\n        # get from db\n        detail_stats_crontab = db.get_value('stats/detail/scheduler/cron')\n        # remove old block:\n        # ### block ivxv_detail_stats_crontab ###\n        # ...\n        # ### endblock ivxv_detail_stats_crontab ###\n        # from a crontab temporary file if any exists\n        try:\n            without_detail_stats_crontab = remove_detail_stats_crontab(\n                data=crontab_tmp_file_content)\n        except ValueError as err:\n            msg = \"Can't remove ivxv_detail_stats block from a temporary file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n        # add new block to a crontab temporary file\n        crontab = insert_detail_stats_crontab(\n            data=without_detail_stats_crontab, crontab=detail_stats_crontab)\n\n    # Pause for 1 second. Crontab checks mtime to detect file modifications. It\n    # seems that crontab can't detect mtime change if changes happens too\n    # quickly (tested in Ubuntu Xenial).\n    time.sleep(1)\n\n    # override crontab temporary file with a new content\n    with open(filepath, \"w\") as fp:\n        try:\n            fp.write(crontab)\n        except Exception as err:\n            msg = \"Can't write to a crontab temporary file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n\n\ndef remove_detail_stats_crontab(data: str) -> str:\n    \"\"\"Remove ivxv_detail_stats_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: data without ivxv_detail_stats_crontab block\n    :rtype: str\n    \"\"\"\n    block, found = __get_detail_stats_crontab_block(data=data)\n    if not found:\n        return data\n    return data.replace(block, '').strip()\n\n\ndef insert_detail_stats_crontab(data: str, crontab: str) -> str:\n    \"\"\"Wrap crontab in an ivxv_detail_stats_crontab block and insert it to a data.\n\n    :param data: any data\n    :type data: str\n    :param crontab: rendered ivxv_detail_stats Jinja2 crontab template file\n    :type crontab: str\n    :return: data with an ivxv_detail_stats_crontab block\n    :rtype: str\n    \"\"\"\n    header = '### block ivxv_detail_stats_crontab ###'\n    tail = '### endblock ivxv_detail_stats_crontab ###'\n    return data + \"\\n\\n\" + header + \"\\n\" + crontab + \"\\n\" + tail + \"\\n\"\n\n\ndef __get_detail_stats_crontab_block(data: str) -> (str, bool):\n    \"\"\"Get ivxv_detail_stats_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: ivxv_detail_stats_crontab block and True on success,\n    otherwise data and False\n    :rtype: str, bool\n    :raise ValueError: if ivxv_detail_stats_crontab block's header/tail is malformed,\n    however though, if both header and tail are malformed then function assumes that\n    ivxv_detail_stats_crontab block doesn't exist in a data\n    \"\"\"\n    header = '### block ivxv_detail_stats_crontab ###'\n    tail = '### endblock ivxv_detail_stats_crontab ###'\n\n    # remove leading and trailing whitespaces/newlines\n    data_stripped = data.strip()\n\n    # get start index of a header\n    header_start_index = data_stripped.find(header)\n\n    # get start index of a tail\n    tail_start_index = data_stripped.find(tail)\n\n    # both header and tail aren't present in a data_stripped\n    if header_start_index < 0 and tail_start_index < 0:\n        return data, False\n    # both header and tail present in a data_stripped\n    elif header_start_index >= 0 and tail_start_index >= 0:\n        # get end index of a tail\n        tail_end_index = tail_start_index + len(tail)\n        # extract ivxv_detail_stats_crontab block from a data_stripped\n        return data_stripped[header_start_index:tail_end_index], True\n    else:\n        if header_start_index < 0:\n            raise ValueError(\"malformed ### block ivxv_detail_stats_crontab ###\")\n        else:\n            raise ValueError(\"malformed ### endblock ivxv_detail_stats_crontab ###\")\n\n\ndef install_detail_stats_crontab():\n    \"\"\"Install crontab for detail stats export with VIS automation.\"\"\"\n    subprocess.run(\n        [\"env\", \"VISUAL=ivxv-detail-stats-crontab\", \"crontab\", \"-e\"], check=True)\n\n\ndef generate_voting_facts_crontab(cfg: dict):\n    \"\"\"Generate crontab for ivxv-storageorder automation.\n    Generated file will be stored to db.\n\n    :param cfg: content of configuration file\n    :type cfg: dict\n    \"\"\"\n\n    # load Jinja2 crontab template for voting facts\n    template_dir = Environment(loader=PackageLoader('ivxv_admin', 'templates'))\n    template = template_dir.get_template('ivxv_voting_facts_crontab.jinja')\n\n    # read `stats:` section from a configuration file,\n    # use default values if configuration file doesn't have `stats:` section\n    cron_cfg = cfg.get('stats', {}).get('voting_facts', {}).get('scheduler', {})\n    crontab_params = {\n        'minute': cron_cfg.get('cron', {}).get('min', '*/15') or '*/15',\n        'hour': cron_cfg.get('cron', {}).get('hour', '*') or '*',\n        'day': cron_cfg.get('cron', {}).get('day', '*') or '*',\n        'month': cron_cfg.get('cron', {}).get('month', '*') or '*',\n        'weekday': cron_cfg.get('cron', {}).get('weekday', '*') or '*',\n    }\n\n    # render Jinja2 template\n    rendered_template = template.render(\n        time_generated=datetime.datetime.now().strftime('%d.%M.%Y %H:%M:%S'),\n        **crontab_params,\n    )\n\n    # override crontab rendered template in a db\n    with IVXVManagerDb(for_update=True) as db:\n        db.set_value('stats/voting_facts/scheduler/cron', rendered_template)\n\n\ndef voting_facts_crontab_editor() -> int:\n    args = init_cli_util(\"\"\"\n    Generate crontab for ivxv-storageorder automation.\n\n    This utility must be called as editor by crontab utility:\n\n        $ env VISUAL=ivxv-voting-facts-crontab crontab -e\n\n    Usage: ivxv-voting-facts-crontab <filename>\n    \"\"\")\n    filepath = args['<filename>']\n\n    # when you run `crontab -e`, cron generates temporary file at '/tmp/XYZ/crontab',\n    # and that temporary file is passed here as args['<filename>'], i.e 'filepath'\n    crontab_tmp_file_content: str\n\n    # read crontab temporary file\n    with open(filepath, 'r') as fp:\n        try:\n            crontab_tmp_file_content = fp.read()\n        except Exception as err:\n            msg = \"Can't read crontab temporary file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n\n    # get rendered Jinja2 crontab template for voting facts from a db\n    with IVXVManagerDb() as db:\n        # get from db\n        voting_facts_crontab = db.get_value('stats/voting_facts/scheduler/cron')\n        # remove old block:\n        # ### block ivxv_voting_facts_crontab ###\n        # ...\n        # ### endblock ivxv_voting_facts_crontab ###\n        # from a crontab temporary file if any exists\n        try:\n            without_voting_facts_crontab = remove_voting_facts_crontab(\n                data=crontab_tmp_file_content)\n        except ValueError as err:\n            msg = \"Can't remove ivxv_voting_facts_crontab block from a tmp file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n        # add new block to a crontab temporary file\n        crontab = insert_voting_facts_crontab(\n            data=without_voting_facts_crontab, crontab=voting_facts_crontab)\n\n    # Pause for 1 second. Crontab checks mtime to detect file modifications. It\n    # seems that crontab can't detect mtime change if changes happens too\n    # quickly (tested in Ubuntu Xenial).\n    time.sleep(1)\n\n    # override crontab temporary file with a new content\n    with open(filepath, \"w\") as fp:\n        try:\n            fp.write(crontab)\n        except Exception as err:\n            msg = \"Can't write to a crontab temporary file %r: %s\"\n            log.error(msg, filepath, err.__str__())\n            return 1\n\n\ndef remove_voting_facts_crontab(data: str) -> str:\n    \"\"\"Remove ivxv_voting_facts_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: data without ivxv_voting_facts_crontab block\n    :rtype: str\n    \"\"\"\n    block, found = __get_voting_facts_crontab_block(data=data)\n    if not found:\n        return data\n    return data.replace(block, '').strip()\n\n\ndef insert_voting_facts_crontab(data: str, crontab: str) -> str:\n    \"\"\"Wrap crontab in an ivxv_voting_facts_crontab block and insert it to a data.\n\n    :param data: any data\n    :type data: str\n    :param crontab: rendered ivxv_voting_facts_crontab Jinja2 crontab template file\n    :type crontab: str\n    :return: data with an ivxv_voting_facts_crontab block\n    :rtype: str\n    \"\"\"\n    header = '### block ivxv_voting_facts_crontab ###'\n    tail = '### endblock ivxv_voting_facts_crontab ###'\n    return data + \"\\n\\n\" + header + \"\\n\" + crontab + \"\\n\" + tail + \"\\n\"\n\n\ndef __get_voting_facts_crontab_block(data: str) -> (str, bool):\n    \"\"\"Get ivxv_voting_facts_crontab block from a data.\n\n    :param data: any data\n    :type data: str\n    :return: ivxv_voting_facts_crontab block and True on success,\n    otherwise data and False\n    :rtype: str, bool\n    :raise ValueError: if ivxv_voting_facts_crontab block's header/tail is malformed,\n    however though, if both header and tail are malformed then function assumes that\n    ivxv_voting_facts_crontab block doesn't exist in a data\n    \"\"\"\n    header = '### block ivxv_voting_facts_crontab ###'\n    tail = '### endblock ivxv_voting_facts_crontab ###'\n\n    # remove leading and trailing whitespaces/newlines\n    data_stripped = data.strip()\n\n    # get start index of a header\n    header_start_index = data_stripped.find(header)\n\n    # get start index of a tail\n    tail_start_index = data_stripped.find(tail)\n\n    # both header and tail aren't present in a data_stripped\n    if header_start_index < 0 and tail_start_index < 0:\n        return data, False\n    # both header and tail present in a data_stripped\n    elif header_start_index >= 0 and tail_start_index >= 0:\n        # get end index of a tail\n        tail_end_index = tail_start_index + len(tail)\n        # extract ivxv_voting_facts_crontab block from a data_stripped\n        return data_stripped[header_start_index:tail_end_index], True\n    else:\n        if header_start_index < 0:\n            raise ValueError(\"malformed ### block ivxv_voting_facts_crontab ###\")\n        else:\n            raise ValueError(\"malformed ### endblock ivxv_voting_facts_crontab ###\")\n\n\ndef install_voting_facts_crontab():\n    \"\"\"Install crontab for ivxv-storageorder automation.\"\"\"\n    subprocess.run(\n        [\"env\", \"VISUAL=ivxv-voting-facts-crontab\", \"crontab\", \"-e\"], check=True)\n\n\ndef voting_facts_util():\n    \"\"\"Launch log analyzer on IVXV Logmonitor to search for missing voting facts, then\n    export these facts as .csv file and run 'ivxv-storageorder' on a first Collector\n    host, where operation succeeds, in case of unsuccessful operation it will choose\n    another Collector host, until succeeds, or returns an error.\"\"\"\n    init_cli_util(\n        \"\"\"\n        Launch log analyzer on IVXV Logmonitor to search for missing voting facts, then\n        export these facts as .csv file and then run 'ivxv-storageorder' on a first\n        Collector host, where operation succeeds, in case of unsuccessful operation it\n        will choose another Collector host, until succeeds, or returns an error.\n\n        Usage: ivxv-voting-facts\n        \"\"\"\n    )\n\n    filename = f\"votesorder-{time.time_ns()}.csv\"\n\n    with IVXVManagerDb() as db:\n        collector_state = db.get_value(\"collector/state\")\n        services = db.get_all_values(\"service\")\n        logmon_host = db.get_value('logmonitor/address')\n        if not logmon_host:\n            log.error(\"Log monitor is not defined\")\n            return 1\n    if collector_state != COLLECTOR_STATE_CONFIGURED:\n        if not sys.stdout.isatty():  # suppress warning if executed from crontab\n            return 0\n        log.warning(\"Collector is not configured\")\n        return 1\n\n    votesorder_services = {}\n\n    # Find all votesorder service host addresses\n    for service_id, service in services.items():\n        service_type = service[\"service-type\"]\n        is_votesorder_service = service_type == \"votesorder\"\n        if is_votesorder_service and service[\"state\"] != SERVICE_STATE_REMOVED:\n            votesorder_services[service_id] = service[\"ip-address\"].split(\":\")[0]\n\n    if len(votesorder_services.keys()) < 1:\n        log.error(\"No votesorder services found\")\n        return 1\n\n    # Prepare missing voting facts .csv file on first succeeded Logmonitor host\n    logmon_account = f\"logmon@{logmon_host}\"\n    proc = exec_remote_cmd(['ssh', logmon_account,\n                            \"ivxv-storageorder-prepare-csv.sh\",\n                            \"--filename\", filename])\n    if proc.returncode:\n        logmon_account = \"\"\n        if proc.returncode == 2:\n            log.info(\"No missing voting facts found\")\n            return 0\n        log.error(\n            f\"Error while preparing .csv: {proc}\")\n\n    if logmon_account == \"\":\n        log.error(f\"Logmonitor host is not active {proc}\")\n        return 1\n\n    # Copy .csv file from succeeded Log host to first active Collector host\n    collector_account = \"\"\n    for service_host in votesorder_services.values():\n        collector_account = f\"ivxv-votesorder@{service_host}\"\n        proc = exec_remote_cmd(\n            [\"scp\", f\"{logmon_account}:{filename}\", f\"{collector_account}:{filename}\"])\n        if proc.returncode:\n            collector_account = \"\"\n            log.error(\n                f\"Cannot copy .csv file from {logmon_account} to {collector_account} \"\n                f\"due to an error: {proc}\")\n            continue\n\n    if collector_account == \"\":\n        log.error(f\"No active Collector hosts among {votesorder_services.values()}\")\n        return 1\n\n    # Add missing voting facts using ivxv-storageorder\n    for service_id, service_host in votesorder_services.items():\n        collector_account = f\"ivxv-votesorder@{service_host}\"\n        proc = exec_remote_cmd(\n            [\"ssh\", f\"{collector_account}\",\n             \"ivxv-storageorder\", \"-file\", f\"{filename}\",\n             \"-instance\", f\"{service_id}\"])\n        if proc.returncode:\n            collector_account = \"\"\n            log.error(f\"Running ivxv-storageorder failed with error: {proc}\")\n            continue\n\n    if collector_account == \"\":\n        log.error(f\"ivxv-storageorder failed on {votesorder_services.values()} hosts\")\n        return 1\n\n    return 0\n\n\ndef create_voter_list_download_crontab(cfg: dict):\n    \"\"\"\n    Creates a cron job for running CLI ivxv-voter-list-download every X minute as\n    defined in configuration file vis:min: section, X defaults to 15 minutes.\n    :param cfg: configuration file\n    \"\"\"\n    # read `vis:` section from a configuration file and parse 'min:' int value out\n    minutes = cfg.get('vis', {}).get('min', 15)\n\n    with CronTab(user=IVXV_ADMIN_CRON_USER) as cron:\n        # remove old ivxv-voter-list-download job\n        for job in cron:\n            if job.command == VOTER_LIST_DOWNLOAD_CRON_JOB:\n                cron.remove(job)\n\n        # install new one\n        cron.new(command=VOTER_LIST_DOWNLOAD_CRON_JOB).minute.every(minutes)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/cli_utils/status_utils.py",
    "content": "# IVXV Internet voting framework\n\"\"\"CLI utilities for management service state.\"\"\"\n\nimport json\nimport re\n\nfrom jinja2 import Environment, PackageLoader\n\nfrom .. import lib\nfrom ..db import IVXVManagerDb\nfrom . import init_cli_util, log\n\n\ndef users_list_util():\n    \"\"\"User listing utility for collector administrator interface.\"\"\"\n    init_cli_util(\"\"\"\n    List IVXV Collector Management Service registered users.\n\n    Usage: ivxv-users-list\n    \"\"\")\n\n    # read users from database\n    with IVXVManagerDb() as db:\n        users = db.get_all_values('user')\n\n    # output users\n    user_no = 0\n    for user_cn, permissions in sorted(users.items()):\n        user_no += 1\n        print(f\"{user_no}. {user_cn}   Permissions: {permissions}\")\n\n    if not user_no:\n        print('No users defined')\n\n\ndef status_util():\n    \"\"\"Output collector state.\"\"\"\n    args = init_cli_util(\"\"\"\n    Output IVXV Collector state.\n\n    Usage: ivxv-status [--json] [--service=<service-id> ...] [<filter> ...]\n\n    Options:\n        --json                  Output full data in JSON format.\n                                Note: filters have no effect in JSON output.\n        --service=<service-id>  Filter output by service ID.\n                                Note: This filter conflicts other section\n                                filters than \"smart\" or \"service\".\n        <filter>                Filter output by section. Possible values are:\n                                  * collector - collector state;\n                                  * election - election data;\n                                  * config - versions of loaded config;\n                                  * list - versions of loades lists;\n                                  * service - service information;\n                                  * ext - external service information;\n                                  * storage - storage information;\n                                  * smart - output only relevant data;\n                                  * all - output all data;\n                                [Default: smart].\n    \"\"\")\n    filters = args['<filter>'] or ['smart']\n\n    # validate CLI args\n    data_sections = [\n        'collector', 'election', 'config', 'list', 'service', 'ext', 'storage']\n    for arg in filters:\n        if arg not in data_sections + ['all', 'smart']:\n            log.error('Invalid section: %s', arg)\n            return 1\n    if 'all' in filters:\n        filters = data_sections\n    if args['--service']:\n        if filters not in (['smart'], ['service']):\n            log.error('Service ID can only used with '\n                      '\"smart\" or \"service\" section filters')\n            return 1\n        filters = ['service']\n    if 'smart' in filters:\n        filters = ['smart', 'collector', 'storage']\n\n    # generate state data\n    with IVXVManagerDb() as db:\n        state = lib.generate_collector_state(db)\n\n    # output JSON\n    if args['--json']:\n        print(json.dumps(state, indent=4, sort_keys=True))\n        return 0\n\n    # apply smart filtering\n    if 'smart' in filters:\n        cfg_state_filters(filters, state)\n\n    # apply service ID filters\n    if args['--service']:\n        for service_id in args['--service']:\n            if service_id not in state['service'].keys():\n                log.error(\"Unknown service ID %r\", service_id)\n                return 1\n        for network_id in list(state['network']):\n            for service_id in list(state['network'][network_id]):\n                if service_id not in args['--service']:\n                    del state['network'][network_id][service_id]\n            if not state['network'][network_id]:\n                del state['network'][network_id]\n\n    # output plain text\n    tmpl_env = Environment(loader=PackageLoader('ivxv_admin', 'templates'))\n    template = tmpl_env.get_template('ivxv_status.jinja')\n    output = template.render(state, sections=filters)\n    output = re.sub(r'\\n\\n+', '\\n\\n', output)\n    print(output.strip())\n\n    return 0\n\n\ndef cfg_state_filters(filters, state):\n    \"\"\"Reconfigure list of state filters depending on actual state.\"\"\"\n    if (state['config']['trust'] or\n            state['config']['technical'] or\n            state['config']['election']):\n        filters += ['config']\n    if state['config']['election']:\n        filters += ['election']\n    if (state['list']['choices-loaded'] or\n            state['list']['choices'] or\n            state['list']['districts-loaded'] or\n            state['list']['districts'] or\n            state['list'].get('voters0000') or\n            state['list'].get('voters0000-loaded')):\n        filters += ['list']\n    if state.get('service'):\n        filters += ['service']\n"
  },
  {
    "path": "collector-admin/ivxv_admin/collector_state.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCollector state for collector management service.\n\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\n\nimport dateutil.parser\n\nfrom . import (COLLECTOR_PKG_FILENAMES, COLLECTOR_STATE_CONFIGURED,\n               COLLECTOR_STATE_FAILURE, COLLECTOR_STATE_INSTALLED,\n               COLLECTOR_STATE_NOT_INSTALLED, COLLECTOR_STATE_PARTIAL_FAILURE,\n               SERVICE_STATE_CONFIGURED, SERVICE_STATE_FAILURE,\n               SERVICE_STATE_INSTALLED, SERVICE_STATE_NOT_INSTALLED,\n               SERVICE_TYPE_PARAMS)\nfrom .config import CONFIG, cfg_path\nfrom .service import generate_service_hints\n\n\n# create logger\nlog = logging.getLogger(__name__)\n\n\ndef generate_collector_state(db):\n    \"\"\"Generate collector state data.\"\"\"\n    state = db.get_all_values()\n\n    # generate service data\n    if state.get('service'):\n        generate_service_hints(state['service'])\n\n        # group services by network\n        state['network'] = dict()\n        for service_id, service_rec in state.get('service').items():\n            nw_name = service_rec.pop('network')\n            state['network'].setdefault(nw_name, dict())\n            state['network'][nw_name][service_id] = service_rec\n\n    # check package files\n    assert 'storage' not in state\n    state[\"storage\"] = {\n        \"debs_exists\": [],\n        \"debs_missing\": [],\n        \"command_files_active\": [],\n        \"command_files_inactive\": [],\n    }\n    for pkg_filename in COLLECTOR_PKG_FILENAMES.values():\n        pkg_filepath = cfg_path('deb_pkg_path', pkg_filename)\n        key = 'debs_exists' if os.path.exists(pkg_filepath) else 'debs_missing'\n        state['storage'][key].append(pkg_filepath)\n\n    # collect command files state\n    command_filepaths = [\n        cfg_path(\"command_files_path\", filename)\n        for filename in os.listdir(CONFIG[\"command_files_path\"])\n        if not filename.endswith(\".json\")\n    ]\n    for filename in os.listdir(CONFIG[\"active_config_files_path\"]):\n        filepath = os.path.join(CONFIG[\"active_config_files_path\"], filename)\n        if os.path.islink(filepath):\n            try:\n                command_filepaths.remove(os.readlink(filepath))\n            except ValueError:\n                log.warning(\n                    \"Unresolved config file link %r -> %r\",\n                    filepath,\n                    os.readlink(filepath),\n                )\n            state[\"storage\"][\"command_files_active\"].append(\n                os.path.basename(os.readlink(filepath))\n            )\n    state[\"storage\"][\"command_files_active\"].sort()\n    state[\"storage\"][\"command_files_inactive\"] = sorted(\n        os.path.basename(filename) for filename in command_filepaths\n    )\n\n    # detect collector state\n    state['collector_state'] = detect_collector_state(state)\n\n    generate_voter_list_state(state)\n    generate_election_state(state)\n\n    return state\n\n\ndef detect_collector_state(state):\n    \"\"\"Detect collector state.\n\n    :param state: Collector state data. Probably values from\n                  Collector Management Database.\n    :type state: dict\n    \"\"\"\n    service_states = [\n        val['state'] for val in state.get('service', {}).values()\n    ]\n\n    # NOT INSTALLED\n    # Stay in NOT INSTALLED state while:\n    # - some service is not installed\n    if (state['collector']['state'] == COLLECTOR_STATE_NOT_INSTALLED and\n            (not state['config']['technical'] or\n             not service_states or\n             SERVICE_STATE_NOT_INSTALLED in service_states)):\n        return COLLECTOR_STATE_NOT_INSTALLED\n\n    # INSTALLED\n    # Stay in INSTALLED state until technical config is applied to all services\n    if (state['collector']['state'] == COLLECTOR_STATE_INSTALLED\n            and SERVICE_STATE_INSTALLED in service_states):\n        return COLLECTOR_STATE_INSTALLED\n\n    # CONFIGURED\n    if SERVICE_STATE_FAILURE not in service_states:\n        return COLLECTOR_STATE_CONFIGURED\n\n    # collect service states by type\n    service_state_by_type = {}\n    for _, service_data in sorted(state['service'].items()):\n        service_state_by_type[service_data['service-type']] = (\n            service_state_by_type.get(service_data['service-type'], []))\n        service_state_by_type[service_data['service-type']].append(\n            service_data['state'])\n\n    # FAILURE\n    for service_type, service_params in SERVICE_TYPE_PARAMS.items():\n        if not service_params['main_service']:\n            continue\n        if (service_type in service_state_by_type\n                and SERVICE_STATE_CONFIGURED not in\n                service_state_by_type[service_type]):\n            return COLLECTOR_STATE_FAILURE\n\n    # PARTIAL FAILURE\n    return COLLECTOR_STATE_PARTIAL_FAILURE\n\n\ndef generate_voter_list_state(state):\n    \"\"\"Generate voter list block for collector state data.\"\"\"\n    state[\"list\"].update(\n        {\n            \"voters-list-total\": 0,\n            \"voters-list-available\": 0,\n            \"voters-list-pending\": 0,\n            \"voters-list-applied\": 0,\n            \"voters-list-invalid\": 0,\n            \"voters-list-skipped\": 0,\n        }\n    )\n    for changeset_no in range(10_000):\n        key = f\"voters{changeset_no:04d}-state\"\n        if key not in state['list']:\n            break\n        list_state = state[\"list\"][key].lower()\n        state[\"list\"][f\"voters-list-{list_state}\"] += 1\n        state[\"list\"][\"voters-list-total\"] += 1\n\n    # compare with VIS changeset data\n    vis_changesets_filepath = f\"{CONFIG.get('vis_path')}/voters-changesets.json\"\n    if state[\"list\"][\"voters-list-total\"] and os.path.exists(vis_changesets_filepath):\n        with open(vis_changesets_filepath) as fd:\n            changesets = json.load(fd)\n\n        for changeset_no, changeset in enumerate(changesets[\"changesets\"]):\n            key = f\"voters{changeset_no:04d}\"\n            if key not in state[\"list\"]:\n                state[\"list\"][\"voters-list-available\"] += 1\n                state[\"list\"].update(\n                    {key: \"[version not registered]\", f\"{key}-state\": \"AVAILABLE\"}\n                )\n\n\ndef generate_election_state(state):\n    \"\"\"Generate election block for collector state data.\"\"\"\n    state['election']['phase'] = None\n    state['election']['phase-start'] = None\n    state['election']['phase-end'] = None\n\n    if not state['election']:\n        return\n\n    def get_ts(name):\n        \"\"\"\n        Get timestamp value as datetime.datetime object\n        or None if value is not set.\n        \"\"\"\n        value = state['election'][name]\n        return dateutil.parser.parse(value) if value else None\n\n    electionstart = get_ts('electionstart')\n    electionstop = get_ts('electionstop')\n    servicestart = get_ts('servicestart')\n    servicestop = get_ts('servicestop')\n    verificationstop = get_ts('verificationstop')\n    ts_format = '%Y-%m-%dT%H:%M %Z'\n\n    phases = [\n        [not electionstart, 'PREPARING', None, None],\n        [\n            servicestart\n            and datetime.datetime.now(servicestart.tzinfo) < servicestart,\n            'WAITING FOR SERVICE START', None, servicestart\n        ],\n        [\n            electionstart\n            and datetime.datetime.now(electionstart.tzinfo) < electionstart,\n            'WAITING FOR ELECTION START', servicestart, electionstart\n        ],\n        [\n            electionstop\n            and datetime.datetime.now(electionstop.tzinfo) < electionstop,\n            'ELECTION', electionstart, electionstop\n        ],\n        [\n            servicestop\n            and datetime.datetime.now(servicestop.tzinfo) < servicestop,\n            'WAITING FOR SERVICE STOP', electionstop, servicestop\n        ],\n        [\n            verificationstop\n            and datetime.datetime.now(verificationstop.tzinfo) < verificationstop,\n            'WAITING FOR VERIFICATION SERVICE STOP', servicestop, verificationstop\n        ],\n        [True, 'FINISHED', verificationstop, None],\n    ]\n    for phase_active, name, start_time, stop_time in phases:\n        if phase_active:\n            state['election']['phase'] = name\n            state['election']['phase-start'] = (\n                start_time.strftime(ts_format) if start_time else '-')\n            state['election']['phase-end'] = (\n                stop_time.strftime(ts_format) if stop_time else '-')\n            break\n"
  },
  {
    "path": "collector-admin/ivxv_admin/command_file.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCommand file loading module collector management service.\n\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\nimport re\nimport subprocess\nimport tempfile\nimport zipfile\n\nimport schematics.exceptions\nimport yaml\n\nfrom . import (CFG_TYPES, CMD_DESCR, CMD_TYPES, PERMISSION_ELECTION_CONF,\n               PERMISSION_TECH_CONF, PERMISSION_USERS_ADMIN,\n               RFC3339_DATE_FORMAT_WO_FRACT, USER_ROLES, VOTING_LIST_TYPES)\nfrom .config import CONFIG\nfrom .config_validator.validator_util import validate_cfg\nfrom .db import IVXVManagerDb\nfrom .lib import IvxvError\n\n# create logger\nlog = logging.getLogger(__name__)\n\n\ndef load_collector_cmd_file(cmd_type, filename, plain=False):\n    \"\"\"Load and validate collector command file.\n\n    :param plain: Load plain file or BDOC container.\n    :type plain: bool\n    :return: Config as data structure or None on error\n    :rtype: dict\n    \"\"\"\n    assert cmd_type in CMD_TYPES, f\"Invalid command type {cmd_type!r}\"\n\n    file_descr = CMD_DESCR[cmd_type]\n    if plain:\n        cmd_filename = filename\n        filename = None\n    elif cmd_type in CFG_TYPES:\n        cmd_filename = re.compile(rf\"(.+\\.)?{cmd_type}.yaml$\")\n    elif cmd_type in VOTING_LIST_TYPES:\n        cmd_filename = None\n    else:\n        assert cmd_type == 'user'\n        cmd_filename = 'user.json'\n\n    # load content from file\n    cfg = None\n    try:\n        cfg = load_cfg_file_content(cmd_type, cmd_filename, filename)\n    except (IvxvError, OSError) as err:\n        log.error('Error while loading config file: %s', err)\n    except UnicodeDecodeError as err:\n        log.error('Error while decoding config file: %s', err)\n    except json.decoder.JSONDecodeError as err:\n        log.error('JSON error in config file: %s', err)\n    if cfg is None:\n        return None\n\n    # validate config file\n    log.info('Validating %s', file_descr)\n    try:\n        cfg = validate_cfg(cfg, cmd_type)\n    except ValueError as err:\n        log.error(err)\n        return None\n    except schematics.exceptions.DataError as data_error:\n        log_cfg_validation_errors(data_error.errors)\n        return None\n\n    # validate election ID in config\n    if cmd_type not in ['technical', 'trust', 'user']:\n        with IVXVManagerDb() as db:\n            election_id = db.get_value('election/election-id')\n            if election_id:\n                if cmd_type == 'election':\n                    cfg_election_id = cfg.get('identifier')\n                elif cmd_type in ['choices', 'districts', 'voters']:\n                    cfg_election_id = cfg.get('election')\n                else:\n                    raise NotImplementedError(f\"Unknown config type {cmd_type}\")\n                if election_id != cfg_election_id:\n                    log.error(\n                        \"Election ID %r in config file does not match \"\n                        \"with current election ID %r\",\n                        cfg_election_id, election_id)\n                    return None\n\n    if plain:\n        log.info('%s is valid', file_descr.capitalize())\n    else:\n        log.info('Files in %s package are valid', file_descr)\n\n    return cfg\n\n\ndef load_cfg_file_content(cmd_type, cfg_filename, cmd_filename):\n    \"\"\"Load config file content from command file container.\n\n    :param cfg_filename: Config file name\n    :type cfg_filename: str or regexp pattern\n    :param cmd_filename: Container file name.\n                          Plain config file is used if None.\n    :type cmd_filename: str\n    :return: Config as data structure; string for voter list; or None on error\n    :rtype: dict\n\n    :raises UnicodeDecodeError: if config file is not in UTF-8\n    :raises OSError: if some file operation fails\n    :raises json.decoder.JSONDecodeError: if JSON decoding fails\n    :raises IvxvError: if some internal check fails\n    \"\"\"\n    file_descr = CMD_DESCR[cmd_type]\n\n    # load plain config file\n    if cmd_filename is None:\n        return load_plain_cfg_file(\n            cmd_type, os.path.dirname(cfg_filename),\n            os.path.basename(cfg_filename))\n\n    # load BDOC command file\n    log.info('Loading command file %r (%s)', cmd_filename, file_descr)\n    try:\n        cmd_file = zipfile.ZipFile(cmd_filename)\n    except FileNotFoundError:\n        raise IvxvError(f\"File {cmd_filename!r} not found\")\n    except zipfile.BadZipFile:\n        raise IvxvError(f\"File {cmd_filename!r} is not a valid ZIP container\")\n    log.debug('Command file loaded')\n    log.debug(\"Files in command file container: %r\", cmd_file.namelist())\n\n    # parse contents of command file\n    filenames_in_bdoc = cmd_file.namelist()\n    for filename in filenames_in_bdoc[:]:\n        if filename in [\n                'META-INF/manifest.xml', 'META-INF/signatures0.xml',\n                'mimetype'\n        ]:\n            filenames_in_bdoc.remove(filename)\n    if cfg_filename is None:\n        cfg_filename = get_command_filename(cmd_type, filenames_in_bdoc)\n        if cfg_filename is None:\n            raise IvxvError(\"No command filename detected\")\n        log.debug(\"Detected command filename %r\", cfg_filename)\n\n    if isinstance(cfg_filename, str) and cfg_filename not in filenames_in_bdoc:\n        raise IvxvError(\n            f'Command file does not contain expected file {cfg_filename} '\n            f'for {file_descr}')\n    if isinstance(cfg_filename, type(re.compile(''))):\n        for filename in filenames_in_bdoc:\n            if cfg_filename.match(filename):\n                cfg_filename = filename\n                break\n        else:\n            raise IvxvError(\n                f'Command file does not contain expected '\n                f'file {cfg_filename.pattern} for {file_descr}')\n\n    # create temporary directory to extract container\n    with tempfile.TemporaryDirectory() as dirpath:\n        log.debug(\"Extracting command file to directory %r\", dirpath)\n        cmd_file.extractall(dirpath)\n        log.debug('Command file successfully extracted')\n        cfg = load_plain_cfg_file(cmd_type, dirpath, cfg_filename)\n\n    return cfg\n\n\ndef ck_json_dict_key_uniqueness(args):\n    \"\"\"Json loader hook to check dict key uniqueness.\n\n    Add strictness for json loader to detect JSON that violates dict key\n    uniqueness.\n    \"\"\"\n    keys = [_[0] for _ in args]\n    for key in keys:\n        if keys.count(key) > 1:\n            raise IvxvError(f\"Duplicate key {key!r} in JSON object\")\n    return dict(args)\n\n\ndef load_plain_cfg_file(cmd_type, dirpath, filename):\n    \"\"\"Load content from plain config file.\"\"\"\n    log.debug(\"Reading file %r\", os.path.join(dirpath, filename))\n    cwd = os.getcwd()\n    if dirpath:\n        os.chdir(dirpath)\n    try:\n        with open(filename, 'rb') as fp:\n            # possible UnicodeDecodeError must be handled by caller\n            file_content = fp.read().decode('utf-8')\n            if cmd_type in CFG_TYPES:\n                cfg = load_yaml_file(file_content)\n            elif cmd_type in ('choices', 'districts', 'user'):\n                cfg = json.loads(\n                    file_content, object_pairs_hook=ck_json_dict_key_uniqueness)\n            else:\n                assert cmd_type == 'voters'\n                if filename.endswith(\".skip.yaml\"):\n                    cfg = load_yaml_file(file_content)\n                else:\n                    cfg = file_content\n    finally:\n        if dirpath:\n            os.chdir(cwd)\n\n    return cfg\n\n\ndef get_command_filename(cmd_type, filenames_in_bdoc):\n    \"\"\"Get command file name from list of filenames.\n\n    Get command file for choice list, district list,\n    voter list or voter list changeset skip command.\n\n    :return: Command filename or None on error\n    :rtype: str\n    \"\"\"\n    filename = None\n\n    if cmd_type in ('choices', 'districts'):\n        if len(filenames_in_bdoc) > 1:\n            log.error(\n                \"Too many files in %s file: %r\",\n                CMD_DESCR[cmd_type], filenames_in_bdoc)\n        elif not filenames_in_bdoc:\n            log.error('Missing %s list in %s file',\n                      cmd_type, CMD_DESCR[cmd_type])\n        else:\n            filename = filenames_in_bdoc[0]\n\n    elif cmd_type == 'voters':\n        if len(filenames_in_bdoc) > 2:\n            log.error(\"Too many files in %s file: %r\",\n                      CMD_DESCR[cmd_type], filenames_in_bdoc)\n        elif (\n            len(filenames_in_bdoc) == 1 and filenames_in_bdoc[0].endswith(\".skip.yaml\")\n        ):\n            filename = filenames_in_bdoc[0]\n        elif len(filenames_in_bdoc) < 2:\n            log.error('Missing voters list or signature in %s file',\n                      CMD_DESCR[cmd_type])\n        else:\n            sig_filename, utf_filename = sorted(filenames_in_bdoc)\n            if (utf_filename.endswith('.utf')\n                    and sig_filename == utf_filename[:-4] + '.sig'):\n                filename = utf_filename\n            else:\n                log.error('Voters list and signature file names do not '\n                          'match (filenames: %s)',\n                          ', '.join([utf_filename, sig_filename]))\n                log.error('List file must have \".utf\" extension and '\n                          'signature file the same base with \".sig\" extension')\n    else:\n        raise NotImplementedError\n\n    return filename\n\n\ndef check_cmd_signature(cmd_type, filename):\n    \"\"\"\n    Check collector command file signature.\n\n    :return: Two lists, first one contains signatures by authorized users,\n             second one contains all signatures.\n    :rtype: tuple\n\n    :raises OSError:\n        if reading command file fails or\n        if :command:`ivxv-verify-container` command not found\n    :raises subprocess.SubprocessError:\n        on :command:`ivxv-verify-container` error\n    :raises LookupError: on invalid signature line\n    :raises IvxvError: on trust root config validation error\n    \"\"\"\n    log.debug(\"Checking command file %r (%s) signature\", filename, cmd_type)\n\n    # detect trust root file\n    trust_container_filepath = (\n        filename if cmd_type == 'trust'\n        else os.path.join(CONFIG['active_config_files_path'], 'trust.bdoc'))\n\n    # execute verifier command\n    proc = exec_container_verifier(trust_container_filepath, filename)\n\n    # parse command output and create signatures list\n    all_signatures = []\n    for line in proc.stdout.strip().split('\\n'):\n        if not re.match(r'.+,.+,[0-9]{11} ', line):\n            raise LookupError(f\"Invalid signature line: {line}\")\n        signer, timestamp_str = line.split(' ')\n        timestamp = datetime.datetime.strptime(\n            timestamp_str, RFC3339_DATE_FORMAT_WO_FRACT).timestamp()\n        all_signatures.append([timestamp, signer, line])\n    all_signatures.sort()\n\n    # check signers authorization for trust root config\n    if cmd_type == 'trust':\n        log.debug('Check signers authorization against trust root config')\n        cfg = load_collector_cmd_file(cmd_type, filename)\n        if cfg is None:\n            raise IvxvError('Trust root file is not valid')\n        trusted_signers = cfg.get('authorizations', [])\n        authorized_signatures = [\n            [signature, 'admin']\n            for timestamp, signer, signature in all_signatures\n            if signer in trusted_signers]\n        return authorized_signatures, all_signatures\n\n    # detect permission for command type\n    if cmd_type == 'technical':\n        permission = PERMISSION_TECH_CONF\n    elif cmd_type in CFG_TYPES or cmd_type in VOTING_LIST_TYPES:\n        permission = PERMISSION_ELECTION_CONF\n    else:\n        assert cmd_type == 'user'\n        permission = PERMISSION_USERS_ADMIN\n\n    # check signers authorization for other config files\n    log.debug(\n        'Check signers authorization against collector management database')\n    authorized_signatures = []\n    with IVXVManagerDb() as db:\n        for timestamp, signer, signature in all_signatures:\n            try:\n                roles = db.get_value(f'user/{signer}')\n            except KeyError:\n                log.debug('No database record for signer %s', signer)\n                continue\n            authorized_signatures += [\n                [signature, role] for role in roles.split(',')\n                if permission in USER_ROLES[role]['permissions']\n            ]\n\n    return authorized_signatures, all_signatures\n\n\ndef exec_container_verifier(trust_container_filepath, filepath):\n    \"\"\"Execute container verifier command.\"\"\"\n    cmd = [\n        'ivxv-verify-container', '-trust', trust_container_filepath, filepath\n    ]\n    log.debug(\"Executing command %r\", \" \".join(cmd))\n    try:\n        proc = subprocess.run(\n            cmd,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            check=False,\n            universal_newlines=True,\n        )\n    except OSError as err:\n        err.strerror = (\n            f\"Error while executing verifier command {' '.join(cmd)!r}: {err.strerror}\"\n        )\n        raise err\n\n    if proc.returncode == 0:\n        return proc\n\n    verifier_errors = {\n        64: 'Command was used incorrectly',\n        65: 'Failed to open container',\n        66: 'Input file did not exist or was not readable',\n        74: 'Failed read trust root',\n    }\n    err_msg = verifier_errors.get(proc.returncode, 'Unhandled error')\n    print('Container verifier output:')\n    print(proc.stdout)\n    print(proc.stderr)\n    raise subprocess.SubprocessError(\n        f'Failed to execute container verifier: {err_msg}')\n\n\ndef log_cfg_validation_errors(items, prefix=\"/\"):\n    \"\"\"Log validation errors.\"\"\"\n    for field_name, val in items.items():\n        if isinstance(val, dict):\n            log_cfg_validation_errors(val, f\"{prefix}{field_name}/\")\n        else:\n            log.error(\"Validation error for field %r: %s\", prefix + field_name, val)\n\n\ndef load_yaml_file(fp):\n    \"\"\"Load YAML file.\n\n    :return: Content of loaded file\n    :rtype: dict\n\n    :raises OSError: if some file operation fails\n    \"\"\"\n\n    def container_constructor_handler(loader, node):\n        \"\"\"Handler for !container constructor.\"\"\"\n        filename = loader.construct_scalar(node)\n        if os.path.dirname(filename):\n            raise OSError(\n                f\"Referenced file {filename!r} must be in the same \"\n                \"directory with YAML file.\"\n            )\n        with open(filename) as fp:\n            content = (yaml.load(fp, yaml.Loader) if filename[-5:] == '.yaml'\n                       else fp.read(-1))\n        return content\n\n    def timestamp_constructor(loader, node):\n        \"\"\"\n        Handler for timestamp fields.\n\n        Returns string field instead of datetime.\n        \"\"\"\n        assert loader  # unused variable\n        return node.value\n\n    yaml.add_constructor('tag:yaml.org,2002:timestamp', timestamp_constructor)\n    yaml.add_constructor('!container', container_constructor_handler)\n\n    return yaml.load(fp, yaml.Loader)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nConfig file loader for collectors management service.\n\"\"\"\n\nimport configparser\nimport logging\nimport logging.config\nimport os\nimport sys\n\n#: Config file name.\nCFG_FILE_NAME = 'ivxv-collector-admin.conf'\n#: Default values for config.\nCFG_DEFAULTS = {\n    # base directory for management service data files\n    'ivxv_admin_data_path': os.environ.get('IVXV_ADMIN_DATA_PATH',\n                                           '/var/lib/ivxv'),\n    # directory for admin UI static data files (used directly by web UI)\n    'admin_ui_data_path': '%(ivxv_admin_data_path)s/admin-ui-data',\n    # directory for admin UI permissions\n    'permissions_path': '%(ivxv_admin_data_path)s/admin-ui-permissions',\n    # directory for applied command files\n    'command_files_path': '%(ivxv_admin_data_path)s/commands',\n    # directory for collector config files\n    'active_config_files_path': '/etc/ivxv',\n    # directory for uploaded files\n    'file_upload_path': '%(ivxv_admin_data_path)s/upload',\n    # directory for exported votes\n    'exported_votes_path': '%(ivxv_admin_data_path)s/ballot-box',\n    # directory for ivxv debian packages\n    'deb_pkg_path': '/etc/ivxv/debs',\n    # management database directory\n    'ivxv_db_path': '%(ivxv_admin_data_path)s/db',\n    # management database file path\n    'ivxv_db_file_path': '%(ivxv_db_path)s/ivxv-management.db',\n    # directory for other service related data files\n    \"service_path\": \"%(ivxv_admin_data_path)s/service\",\n    # directory for VIS related data files (used to download voter list\n    # changesets)\n    \"vis_path\": \"%(ivxv_admin_data_path)s/vis\",\n}\nCFG_PARSER = configparser.ConfigParser(defaults=CFG_DEFAULTS)\nCFG_FILES_USED = []\n#: Config file paths.\nCFG_PATHS = [\n    os.path.join(os.curdir, CFG_FILE_NAME),\n    os.path.join('/etc/ivxv', CFG_FILE_NAME)\n]\nif os.environ.get('IVXV_ADMIN_CONF'):\n    CFG_PATHS += [\n        os.environ.get('IVXV_ADMIN_CONF'),\n        os.path.join(os.environ.get('IVXV_ADMIN_CONF'), CFG_FILE_NAME)\n    ]\n\nfor FILE_PATH in CFG_PATHS:\n    if os.path.isfile(FILE_PATH):\n        CFG_FILES_USED.append(FILE_PATH)\n        logging.config.fileConfig(FILE_PATH)\n        CFG_PARSER.read(FILE_PATH)\n\n# check config files read\nif not CFG_FILES_USED:\n    log = logging.getLogger(__name__)\n    log.error(\n        \"IVXV collector admin utils config file %r not found in the search paths %s\",\n        CFG_FILE_NAME,\n        CFG_PATHS,\n    )\n\nCONFIG = CFG_PARSER['DEFAULT']\n\n\ndef cfg_path(cfg_path_name, filename):\n    \"\"\"Generate full path for specified file.\"\"\"\n    return os.path.join(CONFIG[cfg_path_name], filename)\n\n\nif __name__ == '__main__':\n    log = logging.getLogger(__name__)\n    log.info('Loading config file(s) %s succeeded',\n             ', '.join((CFG_FILES_USED)))\n    sys.exit()\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nValidator for collector config files and voting lists.\n\"\"\"\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/choices_list.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nChoices list validator.\n\"\"\"\n\nfrom schematics.exceptions import DataError\nfrom schematics.models import Model\nfrom schematics.types import DictType, StringType\n\nfrom .fields import ElectionIdType\n\n\nclass ChoicesListSchema(Model):\n    \"\"\"Validating schema for choices list.\"\"\"\n    election = ElectionIdType(required=True)\n    choices = DictType(DictType(DictType(StringType)))\n\n    def validate(self, partial=False, convert=True, app_data=None, **kwargs):\n        \"\"\"Validate model.\"\"\"\n        super().validate(partial, convert, app_data, **kwargs)\n\n        choices = []\n        for district_choices in self.choices.values():\n            for choice in district_choices.values():\n                for choice_id in choice.keys():\n                    if choice_id in choices:\n                        raise DataError(\n                            {'choices': f'Duplicate choice ID: {choice_id}'})\n                    choices.append(choice_id)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/districts_list.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nDistricts list validator.\n\"\"\"\n\nfrom schematics.models import Model\nfrom schematics.types import DictType, ListType, ModelType, StringType\n\nfrom .fields import ElectionIdType\n\n\nclass DistrictsListSchema(Model):\n    \"\"\"Validating schema for districts list.\"\"\"\n\n    class DistrictSchema(Model):\n        \"\"\"Validating schema for district record config.\"\"\"\n        name = StringType(required=True)\n        parish = ListType(StringType, required=True)\n\n    class RegionSchema(Model):\n        \"\"\"Validating schema for region record config.\"\"\"\n        state = StringType()\n        county = StringType()\n        parish = StringType()\n\n    election = ElectionIdType(required=True)\n    districts = DictType(ModelType(DistrictSchema))\n    regions = DictType(ModelType(RegionSchema))\n    counties = DictType(ListType(StringType))\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/election_conf.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nElection config validator.\n\"\"\"\n\n# pylint: disable=no-self-use\n\nfrom schematics.exceptions import ValidationError\nfrom schematics.models import Model\nfrom schematics.types import (\n    BooleanType,\n    DateTimeType,\n    IntType,\n    ListType,\n    ModelType,\n    StringType,\n    URLType,\n    DictType,\n)\n\nfrom .fields import CertificateType, ElectionIdType, PublicKeyType\nfrom .schemas import ContainerSchema, OCSPSchema, TSPSchema, protocol_cfg\n\n\nclass ElectionConfigSchema(Model):\n    \"\"\"Validating schema for election config.\"\"\"\n    identifier = ElectionIdType(required=True)\n    questions = ListType(ElectionIdType, min_size=1, required=True)\n\n    class ElectionVerificationSchema(Model):\n        \"\"\"Validating schema for election verification config.\"\"\"\n        count = IntType(required=True, min_value=0)\n        minutes = IntType(required=True, min_value=0)\n        latestonly = BooleanType(default=False)\n\n    verification = ModelType(ElectionVerificationSchema, required=True)\n\n    class ElectionVotingSchema(Model):\n        \"\"\"Validating schema for election voting config.\"\"\"\n        ratelimitstart = IntType(default=0, min_value=0)\n        ratelimitminutes = IntType(default=0, min_value=0)\n\n        def validate_ratelimitminutes(self, data, value):\n            \"\"\"Validate rate limit.\"\"\"\n            try:\n                if (data['ratelimitstart'] > 0\n                        and data['ratelimitminutes'] == 0):\n                    raise ValidationError(\n                        'ratelimitstart set, but rate limiting disabled')\n            except KeyError:\n                pass  # error in data structure is catched later\n            return value\n\n    voting = ModelType(ElectionVotingSchema)\n\n    class ElectionPeriodSchema(Model):\n        \"\"\"Validating schema for election period config.\"\"\"\n        servicestart = DateTimeType(required=True)\n        electionstart = DateTimeType(required=True)\n        electionstop = DateTimeType(required=True)\n        servicestop = DateTimeType(required=True)\n        verificationstop = DateTimeType(required=True)\n\n    period = ModelType(ElectionPeriodSchema, required=True)\n    voterforeignehak = StringType(regex=r'^[0-9]{1,10}$')\n    ignorevoterlist = StringType()\n\n    class VoterListSchema(Model):\n        \"\"\"Validating schema for voter list updating service config.\"\"\"\n        key = PublicKeyType(required=True)\n\n    voterlist = ModelType(VoterListSchema, required=True)\n\n    class VisSchema(Model):\n        \"\"\"Validating schema for VIS service config.\"\"\"\n\n        url = URLType(required=True)\n        ca = ListType(CertificateType)\n        min = IntType(default=15)\n\n    vis = ModelType(VisSchema, required=True)\n\n    class XroadSchema(Model):\n        \"\"\"Validating schema for VIS service config.\"\"\"\n\n        ca = CertificateType(required=True)\n\n    xroad = ModelType(XroadSchema, required=True)\n\n    class BallotSchema(Model):\n        \"\"\"Validating schema for voter ballot.\"\"\"\n        encpkeygroup = StringType(required=True)  # See available types in docs\n        # PublicKeyType (OpenSSL library under the hood) no support for ElGamal\n        encpkey = StringType(required=False)\n\n    ballot = ModelType(BallotSchema, required=True)\n\n    class AuthSchema(Model):\n        \"\"\"Validating schema for voter authentication config.\"\"\"\n\n        # FIXME: If service.mid exists, auth.ticket field must exist\n        class TicketAuthSchema(Model):\n            \"\"\"Validating schema for ticket authentication config.\"\"\"\n\n        ticket = ModelType(TicketAuthSchema)\n\n        class TLSAuthSchema(Model):\n            \"\"\"Validating schema for TLS authentication config.\"\"\"\n            roots = ListType(CertificateType, required=True)\n            intermediates = ListType(CertificateType)\n            ocsp = ModelType(OCSPSchema)\n\n        tls = ModelType(TLSAuthSchema)\n\n    auth = ModelType(AuthSchema, required=True)\n\n    identity = StringType(\n        required=True, choices=['commonname', 'serialnumber', 'pnoee'])\n\n    class AgeSchema(Model):\n        \"\"\"Validating schema for voters age check config.\"\"\"\n        method = StringType(required=True, choices=['estpic'])\n        timezone = StringType(required=True)\n        limit = IntType(required=True, min_value=14)\n\n    age = ModelType(AgeSchema)\n\n    vote = ModelType(ContainerSchema, required=True)\n\n    class MIDSchema(Model):\n        \"\"\"Validating schema for Mobile ID config.\"\"\"\n        url = URLType(required=True)\n        relyingpartyuuid = StringType(required=True)\n        relyingpartyname = StringType(required=True)\n        language = StringType(\n            required=True, choices=['EST', 'ENG', 'RUS', 'LIT'])\n        authmessage = StringType(required=True, max_length=40)\n        signmessage = StringType(required=True, max_length=40)\n        messageformat = StringType(default='GSM-7', choices=['GSM-7', 'UCS-2'])\n        authchallengesize = IntType(default=32, choices=[32, 48, 64])\n        statustimeoutms = IntType()\n        roots = ListType(CertificateType, required=True)\n        intermediates = ListType(CertificateType)\n        ocsp = ModelType(OCSPSchema)\n\n        # pylint: disable=no-self-use\n        def validate_phonerequired(self, data, value):\n            \"\"\"Validate phone/idcode required field.\"\"\"\n            try:\n                if not data['idcoderequired'] and not data['phonerequired']:\n                    raise ValidationError('Either idcoderequired or '\n                                          'phonerequired must be true')\n            except KeyError:\n                pass  # error in data structure is catched later\n            return value\n\n    mid = ModelType(MIDSchema)\n\n    class SmartIDSchema(Model):\n        \"\"\"Validating schema for Smart ID config.\"\"\"\n        url = URLType(required=True)\n        relyingpartyuuid = StringType(required=True)\n        relyingpartyname = StringType(required=True)\n        certificatelevel = StringType(\n            required=True, choices=[\"QUALIFIED\", \"ADVANCED\", \"QSCD\"]\n        )\n        authinteractionsorder = ListType(DictType(StringType), required=True)\n        signinteractionsorder = ListType(DictType(StringType), required=True)\n        authchallengesize = IntType()\n        statustimeoutms = IntType()\n        roots = ListType(CertificateType, required=True)\n        intermediates = ListType(CertificateType)\n        ocsp = ModelType(OCSPSchema)\n\n    smartid = ModelType(SmartIDSchema)\n\n    qualification = ListType(\n        protocol_cfg({\n            \"ocsp\": OCSPSchema,\n            \"ocsptm\": OCSPSchema,\n            \"tsp\": TSPSchema,\n            \"tspreg\": TSPSchema,\n        }))\n\n    class StatsSchema(Model):\n        \"\"\"Validating schema for stats config.\"\"\"\n        class DetailStatsSchema(Model):\n            class SchedulerSchema(Model):\n                class CronSchema(Model):\n                    min = StringType(required=False)\n                    hour = StringType(required=False)\n                    day = StringType(required=False)\n                    month = StringType(required=False)\n                    weekday = StringType(required=False)\n\n                cron = ModelType(CronSchema, required=False, default={})\n            scheduler = ModelType(SchedulerSchema, required=False, default={})\n        detail = ModelType(DetailStatsSchema, required=False, default={})\n\n        class VotingFactsSchema(Model):\n            class SchedulerSchema(Model):\n                class CronSchema(Model):\n                    min = StringType(required=False)\n                    hour = StringType(required=False)\n                    day = StringType(required=False)\n                    month = StringType(required=False)\n                    weekday = StringType(required=False)\n\n                cron = ModelType(CronSchema, required=False, default={})\n            scheduler = ModelType(SchedulerSchema, required=False, default={})\n        voting_facts = ModelType(VotingFactsSchema, required=False, default={})\n    stats = ModelType(StatsSchema, required=False, default={})\n\n    # pylint: disable=unused-argument\n    def validate_questions(self, data, value):\n        \"\"\"Validate question field.\"\"\"\n        if value and len(value) > len(set(value)):\n            raise ValidationError('Election questions must be unique')\n        return value\n\n    def validate_period(self, data, value):\n        \"\"\"Validate election period.\"\"\"\n        try:\n            if data['period']['servicestart'] >= data['period']['electionstart']:\n                raise ValidationError(\"servicestart is >= than electionstart\")\n            if data['period']['electionstart'] >= data['period']['electionstop']:\n                raise ValidationError(\"electionstart is >= than electionstop\")\n            if data['period']['electionstop'] > data['period']['servicestop']:\n                raise ValidationError(\"electionstop is > than servicestop\")\n            if data['period']['servicestop'] > data['period']['verificationstop']:\n                raise ValidationError(\"servicestop is > than verificationstop\")\n        except KeyError:\n            pass  # error in data structure is catched later\n        return value\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/fields.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCommon field validator classes.\n\"\"\"\n\n# pylint: disable=too-few-public-methods\n\nimport string\n\nimport OpenSSL\nfrom schematics.exceptions import ValidationError\nfrom schematics.types import StringType\n\n\nclass ElectionIdType(StringType):\n    \"\"\"Election ID field validator.\"\"\"\n\n    def validate(self, value, context=None):\n        \"\"\"Validate field.\"\"\"\n        min_length = 1\n        max_length = 28\n\n        if len(value) < min_length or len(value) > max_length:\n            raise ValidationError(\n                f\"Election ID length must be between {min_length} and {max_length}\"\n            )\n        if any(ws in value for ws in list(string.whitespace)):\n            raise ValidationError('Election ID contains whitespace')\n\n        return super().validate(value, context)\n\n\nclass CertificateType(StringType):\n    \"\"\"A field that validates input as a PEM-encoded certificate.\"\"\"\n\n    def validate(self, value, context=None):\n        \"\"\"Validate field.\"\"\"\n        try:\n            OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, value)\n        except OpenSSL.crypto.Error as err:\n            err_lib, err_func, err_reason = err.args[0][0]\n            raise ValidationError(\n                f\"Error in {err_lib} library {err_func} function: {err_reason}\"\n            )\n\n        return super().validate(value, context)\n\n\nclass PublicKeyType(StringType):\n    \"\"\"A field that validates input as a PEM-encoded public key.\"\"\"\n\n    def validate(self, value, context=None):\n        \"\"\"Validate field.\"\"\"\n        try:\n            OpenSSL.crypto.load_publickey(OpenSSL.crypto.FILETYPE_PEM, value)\n        except OpenSSL.crypto.Error as err:\n            err_lib, err_func, err_reason = err.args[0][0]\n            raise ValidationError(\n                f\"Error in {err_lib} library {err_func} function: {err_reason}\"\n            )\n\n        return super().validate(value, context)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/schemas.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCommon schema validator classes.\n\"\"\"\n\nfrom schematics.exceptions import ValidationError\nfrom schematics.models import Model\nfrom schematics.types import (IntType, ListType, ModelType, PolyModelType,\n                              StringType, URLType)\n\nfrom .fields import CertificateType\n\n\n# FIXME: constrainments on schemas passed to wrapper aren't enforced.\n# fields can  be missing from the conf, type constrainments are ignored,\n# without raising any errors (e.g missing storage.conf.ca,\n# invalid URL in qualification.*.url)\ndef protocol_cfg(mapping, **kwargs):\n    \"\"\"\n    Return an alternative protocol configuration type.\n\n    Alternative protocol configurations allow only one from a selection of\n    alternatives to be used. The model looks like this::\n\n        protocol: <protocol name>\n        conf: <configuration for the chosen protocol>\n\n    schematics has PolyModelType which allows \"conf\" to be different models,\n    but it must select the model to use based solely on the contents of \"conf\",\n    which cannot be done with this construction. So create a new wrapper model\n    for each protocol configuration model which also includes the \"protocol\"\n    field and let PolyModelType choose from those.\n    \"\"\"\n\n    models = []\n    for protocol, model in mapping.items():\n        wrapper = type(\n            f\"{model.__name__}Wrapper\", (Model, ), {\n                \"protocol\": StringType(required=True, choices=[protocol]),\n                \"conf\": ModelType(model, required=True),\n                \"ordertimeout\": IntType(required=False, min_value=1)\n            })\n        mapping[protocol] = wrapper\n        models.append(wrapper)\n\n    def _claim(_, data):\n        return mapping.get(data.get(\"protocol\"))\n\n    return PolyModelType(models, claim_function=_claim, **kwargs)\n\n\nclass DummySchema(Model):\n    \"\"\"Validating schema for Dummy container config.\"\"\"\n    trusted = ListType(StringType)\n\n\nclass OCSPSchema(Model):\n    \"\"\"Validating schema for OCSP config.\"\"\"\n    url = URLType(required=True)\n    responders = ListType(CertificateType)\n    retry = IntType(default=0, min_value=0)\n    maxSkew = IntType(default=300, min_value=0)  # 300 milliseconds\n    maxAge = IntType(default=1, min_value=0)  # 1 minute\n    maxAge = IntType(default=1, min_value=0)  # 1 minute\n\n\nclass OCSPSchemaNoURL(Model):\n    \"\"\"Validating schema for OCSP config.\"\"\"\n    responders = ListType(CertificateType)\n    maxSkew = IntType(default=300, min_value=0)  # 300 milliseconds\n    maxAge = IntType(default=1, min_value=0)  # 1 minute\n\n\nclass TSPSchema(Model):\n    \"\"\"Validating schema for timestamp protocol config.\"\"\"\n    url = URLType(required=True)\n    signers = ListType(CertificateType, required=True)\n    delaytime = IntType(required=True, min_value=0)\n    retry = IntType(default=0, min_value=0)\n    maxSkew = IntType(default=2, min_value=0)  # 2 seconds\n    maxAge = IntType(default=1, min_value=0)  # 1 minute\n\n\nclass TSPSchemaNoURL(Model):\n    \"\"\"Validating schema for timestamp protocol config.\"\"\"\n    signers = ListType(CertificateType, required=True)\n    delaytime = IntType(required=True, min_value=0)\n    maxSkew = IntType(default=2, min_value=0)  # 2 seconds\n    maxAge = IntType(default=1, min_value=0)  # 1 minute\n\n\nclass BDocSchema(Model):\n    \"\"\"Validating schema for BDoc config.\"\"\"\n    filecount = IntType(required=True, min_value=1)\n    bdocsize = IntType(required=True, min_value=1)\n    filesize = IntType(required=True, min_value=1)\n    roots = ListType(CertificateType, required=True)\n    intermediates = ListType(CertificateType)\n    profile = StringType(choices=['BES', 'TM', 'TS'], required=True)\n    ocsp = ModelType(OCSPSchemaNoURL)\n    tsp = ModelType(TSPSchemaNoURL)\n    tsdelaytime = IntType(default=0, min_value=0)\n\n    def validate_tsp(self, data, value):\n        \"\"\"Check that tsp exists if profile is TS.\"\"\"\n        try:\n            if (data['profile'] == 'TS' and not data['tsp']):\n                raise ValidationError('TS profile requires a tsp block')\n        except KeyError:\n            pass  # error in data structure is catched later\n        return value\n\n\nclass ContainerSchema(Model):\n    \"\"\"Validating schema for signed container config.\"\"\"\n    bdoc = ModelType(BDocSchema)\n    dummy = ModelType(DummySchema)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/tech_conf.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCollector technical config validator.\n\"\"\"\n\nimport re\n\nfrom schematics.exceptions import ValidationError\nfrom schematics.models import Model\nfrom schematics.types import (BooleanType, IntType, ListType, ModelType,\n                              StringType)\n\nfrom .fields import CertificateType\nfrom .schemas import protocol_cfg\n\n\nclass ServicesSchema(Model):\n    \"\"\"Validating schema for subservices config.\"\"\"\n\n    class BackupServiceSchema(Model):\n        \"\"\"Validating schema for backup subservice config.\"\"\"\n        id = StringType(regex=r'.+@.+', required=True)\n        address = StringType(required=True)\n\n    class ServiceSchema(Model):\n        \"\"\"Validating schema for subservice config.\"\"\"\n        id = StringType(regex=r'.+@.+', required=True)\n        address = StringType(regex=r'.+:[0-9]+', required=True)\n        peeraddress = StringType(regex=r'.+:[0-9]+')\n        origin = StringType(regex=r'.+:[0-9]+')\n\n    proxy = ListType(ModelType(ServiceSchema))\n    mid = ListType(ModelType(ServiceSchema))\n    smartid = ListType(ModelType(ServiceSchema))\n    webeid = ListType(ModelType(ServiceSchema))\n    votesorder = ListType(ModelType(ServiceSchema))\n    voting = ListType(ModelType(ServiceSchema))\n    choices = ListType(ModelType(ServiceSchema))\n    verification = ListType(ModelType(ServiceSchema))\n    sessionstatus = ListType(ModelType(ServiceSchema))\n    storage = ListType(ModelType(ServiceSchema))\n    log = ListType(ModelType(ServiceSchema))\n    backup = ListType(ModelType(BackupServiceSchema), max_size=1)\n\n\nclass CollectorTechnicalConfigSchema(Model):\n    \"\"\"Validating schema for collector technical config.\"\"\"\n    debug = BooleanType(default=False)\n    snidomain = StringType(required=True)\n\n    class FilterSchema(Model):\n        \"\"\"Validating schema for connection filter config.\"\"\"\n\n        class TLSFilterSchema(Model):\n            \"\"\"Validating schema for TLS connection filter config.\"\"\"\n            handshaketimeout = IntType(required=True, min_value=0)\n            ciphersuites = ListType(\n                StringType(choices=[\n                    'TLS_RSA_WITH_AES_128_CBC_SHA',\n                    'TLS_RSA_WITH_AES_256_CBC_SHA',\n                    'TLS_RSA_WITH_AES_128_GCM_SHA256',\n                    'TLS_RSA_WITH_AES_256_GCM_SHA384',\n                    'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',\n                    'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',\n                    'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',\n                    'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',\n                    'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',\n                    'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',\n                    'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',\n                    'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',\n                ]),\n                required=True)\n\n        tls = ModelType(TLSFilterSchema, required=True)\n\n        class CodecFilterSchema(Model):\n            \"\"\"Validating schema for codec connection filter config.\"\"\"\n            rwtimeout = IntType(required=True, min_value=0)\n            requestsize = IntType(min_value=0)\n            logrequests = BooleanType(default=False)\n\n        codec = ModelType(CodecFilterSchema, required=True)\n\n    filter = ModelType(FilterSchema, required=True)\n\n    class SegmentSchema(Model):\n        \"\"\"Validating schema for network segment config.\"\"\"\n        id = StringType(required=True)\n        services = ModelType(ServicesSchema, required=True)\n\n    network = ListType(ModelType(SegmentSchema), required=True)\n\n    class LogServerSchema(Model):\n        \"\"\"Validating schema for log collecting server config.\"\"\"\n        address = StringType(required=True)\n        port = IntType(default=20514)\n\n    logging = ListType(ModelType(LogServerSchema))\n\n    class StatusServerSchema(Model):\n        \"\"\"Validating schema for status servers config.\"\"\"\n        class SessionServiceSchema(Model):\n            name = StringType(required=True)\n            servername = StringType(required=True)\n            authttl = IntType(required=True)\n            choicettl = IntType(required=True)\n            votettl = IntType(required=True)\n            verifyttl = IntType(required=True)\n        session = ModelType(SessionServiceSchema)\n\n    status = ModelType(StatusServerSchema, required=True)\n\n    class FileStorageServiceSchema(Model):\n        \"\"\"Validating schema for file storage service config.\"\"\"\n        wd = StringType(required=True)\n\n    class EtcdStorageServiceSchema(Model):\n        \"\"\"Validating schema for etcd storage service config.\"\"\"\n        ca = CertificateType(required=True)\n        conntimeout = IntType(required=True, min_value=0)\n        optimeout = IntType(required=True, min_value=0)\n        # FIXME: Compare to network.#.services.storage.#.id on first load.\n        bootstrap = ListType(StringType(regex=r'.+@.+'))\n        size = IntType(required=False)\n        snapshotcount = IntType(required=False)\n        heartbeattimeout = IntType(required=False)\n        electiontimeout = IntType(required=False)\n    storage = protocol_cfg(\n        {\n            \"file\": FileStorageServiceSchema,\n            \"etcd\": EtcdStorageServiceSchema,\n        },\n        required=True)\n\n    class BackupTimeType(StringType):\n        \"\"\"Field validator for backup time.\"\"\"\n\n        def validate(self, value, context=None):\n            if not re.match(r'[0-9]{2}:[0-9]{2}', value):\n                raise ValidationError(f\"Value must be in format HH:MM (not {value!r})\")\n            hour, minute = value.split(':')\n            if int(hour) >= 24:\n                raise ValidationError(f\"Hour must be smaller than 24 (not {value!r})\")\n            if int(minute) >= 60:\n                raise ValidationError(f\"Minute must be smaller than 60 (not {value!r})\")\n            return super().validate(value, context)\n\n    backup = ListType(BackupTimeType)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/trust_conf.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCollector trust root config validator.\n\"\"\"\n\nfrom schematics.models import Model\nfrom schematics.types import ListType, ModelType, StringType\n\nfrom .schemas import ContainerSchema\n\n\nclass TrustRootSchema(Model):\n    \"\"\"Validating schema for trust root config.\"\"\"\n    container = ModelType(ContainerSchema, required=True)\n    authorizations = ListType(StringType, required=True)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/user_management.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCollector user management command validator.\n\"\"\"\n\nfrom schematics.exceptions import DataError\nfrom schematics.models import Model\nfrom schematics.types import ListType, StringType\n\nfrom .. import USER_ROLES\n\n\nclass UserManagementCommandSchema(Model):\n    \"\"\"Validating schema for user management command.\"\"\"\n    action = StringType(required=True, regex='user-permissions')\n    cn = StringType(required=True, regex='.+,.+,[0-9]{11}')\n    roles = ListType(StringType(), required=True)\n\n    def validate(self, partial=False, convert=True, app_data=None, **kwargs):\n        \"\"\"Validate model.\"\"\"\n        super().validate(partial, convert, app_data, **kwargs)\n\n        # role list checks\n        if not isinstance(self.roles, list):\n            raise DataError({'roles': 'Value is not a list'})\n        # pylint: disable=not-an-iterable\n        for role in self.roles:\n            if role not in USER_ROLES:\n                raise DataError({\"roles\": f\"Unknown role {role!r}\"})\n        if len(self.roles) != len(set(self.roles)):\n            raise DataError({'roles': 'Duplicate roles'})\n        # pylint: disable=unsupported-membership-test\n        if 'none' in self.roles and len(self.roles) > 1:\n            raise DataError({\n                'roles':\n                'Role \"none\" can\\'t be used with other roles'\n            })\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/validator_util.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Config validator utility for collector config files and voting lists.\"\"\"\n\nimport json\nimport os\n\nimport jsonschema\nimport pkg_resources\n\nfrom .choices_list import ChoicesListSchema\nfrom .districts_list import DistrictsListSchema\nfrom .election_conf import ElectionConfigSchema\nfrom .tech_conf import CollectorTechnicalConfigSchema\nfrom .trust_conf import TrustRootSchema\nfrom .user_management import UserManagementCommandSchema\nfrom .voters_list import VoterListChangesetSkipSchema, parse_voters_list\n\n\ndef validate_cfg(cfg, schema_name):\n    \"\"\"Validate collector config.\n\n    :param cfg: Configuration data structure\n    :type cfg: dict\n\n    :raises ValueError:\n    :raises schematics.exceptions.DataError:\n    \"\"\"\n    # validate choices and districts list with jsonschema\n    if schema_name in ['choices', 'districts']:\n        jsonschema_src = pkg_resources.resource_string(\n            'ivxv_admin',\n            os.path.join('jsonschema', f'ivxv.{schema_name}.schema'))\n        schema = json.loads(jsonschema_src.decode('UTF-8'))\n        try:\n            jsonschema.validate(instance=cfg, schema=schema)\n        except jsonschema.exceptions.ValidationError as err:\n            raise ValueError(err)\n\n    # validate voters list\n    if schema_name == \"voters\" and isinstance(cfg, str):\n        return parse_voters_list(cfg)\n\n    if not isinstance(cfg, dict):\n        raise ValueError('Configuration data is not dictionary')\n\n    # detect config schema\n    schemas = {\n        'trust': TrustRootSchema,\n        'technical': CollectorTechnicalConfigSchema,\n        'election': ElectionConfigSchema,\n        'choices': ChoicesListSchema,\n        'districts': DistrictsListSchema,\n        'user': UserManagementCommandSchema,\n        \"voters\": VoterListChangesetSkipSchema,\n    }\n    validator = schemas[schema_name](cfg.copy())\n    validator.validate()\n\n    return validator.to_primitive()\n"
  },
  {
    "path": "collector-admin/ivxv_admin/config_validator/voters_list.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Voters list validator.\"\"\"\n\nimport re\n\nimport dateutil.parser\nfrom schematics.models import Model\nfrom schematics.types import IntType, StringType\n\nfrom .fields import ElectionIdType\n\nISOPARSER = dateutil.parser.isoparser(\"T\")\n\n\nclass VoterListChangesetSkipSchema(Model):\n    \"\"\"Validating schema for voter list changeset skipping command.\"\"\"\n\n    election = ElectionIdType(required=True)\n    skip_voter_list = StringType(regex=r\"^[^ ]+ [^ ]+$\", required=True)\n    changeset = IntType(required=True)\n\n\ndef parse_voters_list(list_content):\n    \"\"\"Parse voters list.\n\n    :return: Voters list data\n    :rtype: dict\n\n    :raises ValueError:\n    \"\"\"\n    lines = list_content.split('\\n')\n    if len(lines) < 4:\n        raise ValueError(f'Too few lines in voters list ({len(lines)})')\n\n    # parse header\n    data = dict(version=lines[0].rstrip('\\r'))\n    data['election'] = lines[1].rstrip('\\r')\n    try:\n        data['election'].encode('ASCII')\n    except UnicodeEncodeError:\n        raise ValueError('Election ID contains non-ASCII characters')\n    data['changeset'] = lines[2].rstrip('\\r')\n    data['period'] = lines[3].rstrip('\\r')\n    timestamps = data['period'].split('\\t')\n    if len(timestamps) != 2:\n        raise ValueError('Period does not contain two tab-separated fields')\n    data['period_from'] = timestamps[0]\n    data['period_to'] = timestamps[1].rstrip('\\r')\n\n    # validate header\n    # version-no = \"2\"\n    if data[\"version\"] != \"2\":\n        raise ValueError(\n            f\"Invalid voters list version {data['version']!r} in line 1. \"\n            \"Expected value: 2\"\n        )\n    ElectionIdType(required=True).validate(data['election'])\n    # changeset = integer\n    if not data['changeset'].isdigit():\n        raise ValueError(\n            f\"Unknown voters list changeset {data['changeset']!r} in line 3. \"\n            \"Must be an integer\"\n        )\n    # period_from, period_to = RFC 3339 timestamp\n    try:\n        # FIXME: Accepts all ISO 8601 timestamps, not only RFC 3339.\n        ISOPARSER.isoparse(data['period_from'])\n        ISOPARSER.isoparse(data['period_to'])\n    except ValueError as err:\n        raise ValueError(f'Period contains invalid timestamp: {err.args[0]}')\n\n    # parse list\n    is_original_list = data['changeset'] == '0'\n    data['voters'] = []\n    for line_no, line in enumerate(lines[:-1], 1):\n        if '\\r' in line:\n            raise ValueError(f'Line #{line_no}: Invalid character <CR>')\n        if line_no < 5:\n            continue\n        fields = line.split('\\t')\n        try:\n            validate_voter_record(fields, is_original_list)\n        except ValueError as err:\n            raise ValueError(f'Line #{line_no}: {err.args[0]}')\n        data['voters'].append(fields)\n\n    if lines[-1] != '':\n        raise ValueError(f'Line #{len(lines)}: Must end with <LF> character')\n\n    return data\n\n\ndef validate_voter_record(fields, is_original_list):\n    \"\"\"Validate voter record in voters list.\"\"\"\n    # field count\n    if len(fields) == 5:\n        action, voter_personalcode, voter_name, adminunit_code, no_district = fields\n        if not voter_name:\n            raise ValueError(\"voter-name is empty\")\n        if action != \"lisamine\":\n            raise ValueError(\n                f\"Unknown action {action!r}. Must be 'lisamine' or 'kustutamine'\"\n            )\n        # voter-personalcode = 11DIGIT\n        if not re.match(r\"[0-9]{11}$\", voter_personalcode):\n            raise ValueError(f\"Invalid voter-personalcode {voter_personalcode!r}\")\n        if len(voter_name) > 100:\n            raise ValueError(f\"voter-name lenght {len(voter_name)} exceeds 100 chars\")\n        # adminunit-code = 1*4UTF-8-CHAR | \"FOREIGN\"\n        if not adminunit_code:\n            raise ValueError(\"Missing adminunit-code\")\n        if len(adminunit_code) > 4 and adminunit_code != \"FOREIGN\":\n            raise ValueError(\n                f\"adminunit-code {adminunit_code!r} is longer than 4 chars\")\n        # no-district = 1*10DIGIT\n        if not re.match(r\"[0-9]{1,10}$\", no_district):\n            raise ValueError(f\"Invalid no-district {no_district!r}\")\n\n    elif len(fields) == 2:\n        action, voter_personalcode = fields\n        if action != \"kustutamine\":\n            raise ValueError(\n                f\"Unknown action {action!r}. Must be 'lisamine' or 'kustutamine'\"\n            )\n        # voter-personalcode = 11DIGIT\n        if not re.match(r\"[0-9]{11}$\", voter_personalcode):\n            raise ValueError(f\"Invalid voter-personalcode {voter_personalcode!r}\")\n        if is_original_list:\n            raise ValueError(f\"Action {action!r} is not allowed in initial list\")\n\n    else:\n        raise ValueError(f\"Invalid field count {len(fields)}, expected 2 or 5 fields\")\n"
  },
  {
    "path": "collector-admin/ivxv_admin/db.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nDatabase abstraction layer for collector management service.\n\"\"\"\n\nimport datetime\nimport dbm.gnu\nimport logging\nimport os\nimport re\nimport time\n\nimport dateutil.parser\n\nfrom . import (COLLECTOR_STATE_NOT_INSTALLED, COLLECTOR_STATES,\n               RFC3339_DATE_FORMAT, SERVICE_STATE_NOT_INSTALLED,\n               SERVICE_STATES)\nfrom .config import CONFIG\n\n#: Path to database file\nDB_FILE_PATH = CONFIG['ivxv_db_file_path']\n\n#: Database keys with default values\nDB_KEYS = {\n    # collector state\n    'collector/state': COLLECTOR_STATE_NOT_INSTALLED,\n    # election config file version in management service\n    'config/election': '',\n    # technical config file version in management service\n    'config/technical': '',\n    # trust root config file version in management service\n    'config/trust': '',\n    # logmonitor address\n    'logmonitor/address': '',\n    # logmonitor timestamp of last fetch\n    'logmonitor/last-data': '',\n    # choices list file version in management service\n    'list/choices': '',\n    # choices list file version in choices service\n    'list/choices-loaded': '',\n    # districts list file version in management service\n    'list/districts': '',\n    # districts list file version in choices service\n    'list/districts-loaded': '',\n    # election ID\n    'election/election-id': '',\n    # election start time\n    'election/electionstart': '',\n    # election stop time\n    'election/electionstop': '',\n    # collector service start time\n    'election/servicestart': '',\n    # collector service stop time\n    'election/servicestop': '',\n    # verification service stop time\n    'election/verificationstop': '',\n}\n#: Database keys for service hosts with default values\nDB_HOST_SUBKEYS = {\n    # host state\n    'state': '',\n}\n#: Database keys for services with default values\nDB_SERVICE_SUBKEYS = {\n    # service type\n    'service-type': None,\n    # technical config file version in service\n    'technical-conf-version': '',\n    # election config file version in service\n    'election-conf-version': '',\n    # service network ID\n    'network': None,\n    # service state\n    'state': SERVICE_STATE_NOT_INSTALLED,\n    # last data by service PING (timestamp)\n    'last-data': '',\n    # count of ping errors\n    'ping-errors': '0',\n    # service IP address\n    'ip-address': None,\n    # registered background information (usually error message)\n    'bg_info': '',\n}\n#: Database keys for certain service types\nDB_SERVICE_CONDITIONAL_SUBKEYS = {\n    # mobile ID identity token key file checksum (sha256)\n    'mid-token-key': '',\n    # TLS certificate file checksum (sha256)\n    'tls-cert': '',\n    # TLS key file checksum (sha256)\n    'tls-key': '',\n    # TSP registration key file checksum (sha256)\n    'tspreg-key': '',\n    # automatic backup times for backup service\n    'backup-times': '',\n}\n#: Full list of allowed database keys\nALLOWED_SERVICE_KEYS = (\n    list(DB_SERVICE_SUBKEYS) + list(DB_SERVICE_CONDITIONAL_SUBKEYS))\n\n# create logger\nlog = logging.getLogger(__name__)\n\n\nclass IVXVManagerDb:\n    \"\"\"\n    Management service database abstraction class.\n\n    Based on :py:mod:`dbm.gnu` module.\n    \"\"\"\n    db = None  #: Database object.\n    read_only = None  #: Database access mode.\n    _retries = None  #: Retry count for failed database open.\n    _retry_delay = None  #: Delay between retries.\n    _db_mode = None  #: Mode string for :py:func:`dbm.open`.\n\n    def __init__(self, for_update=False, retries=30, retry_delay=0.1):\n        \"\"\"\n        Constructor.\n\n        :param for_update: Open database for update\n        :type for_update: bool\n        :param retries: Retries before giving up\n        :type retries: int\n        :param retry_delay: Pause between retries (in seconds)\n        :type retry_delay: float\n        \"\"\"\n        self.read_only = not for_update\n        self._db_mode = 'ws' if for_update else 'r'\n        self._retries = retries\n        self._retry_delay = retry_delay\n\n    def __enter__(self, mode=None):\n        \"\"\"Enter the runtime context, open database.\"\"\"\n        mode = mode or self._db_mode\n        log.debug(\"Opening management database %r (mode: %s)\", DB_FILE_PATH, mode)\n        retries = self._retries\n        if mode != 'n' and not os.path.exists(DB_FILE_PATH):\n            log.error(\"Database file %r not found\", DB_FILE_PATH)\n            raise FileNotFoundError()\n        db_error = OSError()\n        while retries > 0:\n            try:\n                self.db = dbm.gnu.open(DB_FILE_PATH, mode)\n                break\n            except OSError as err:\n                if err.errno == 11:  # database is locked\n                    log.debug('Database is locked, retrying')\n                else:\n                    log.debug(\"Can't open database, retrying (%s)\", err)\n                db_error = err\n            retries -= 1\n            time.sleep(self._retry_delay)\n        else:\n            log.error('Error while opening management database: %s', db_error)\n            raise db_error\n\n        return self\n\n    def __exit__(self, *args):\n        \"\"\"Exit the runtime context, close database.\"\"\"\n        log.debug('Closing management database')\n        self.db.close()\n\n    def get_value(self, key):\n        \"\"\"Get value from database.\"\"\"\n        return self.db[key].decode('UTF-8')\n\n    def set_value(self, key, value, safe=False):\n        \"\"\"Validate and set database value.\n\n        :param key: Key name\n        :type key: str\n        :param value: Value to set\n        :type value: str\n        :param safe: Is operation safe or not. Safe operation will try to read\n                     old value before writing new one.\n        :type safe: bool\n        \"\"\"\n        assert isinstance(key, str)\n\n        # validate value\n        if isinstance(value, datetime.datetime):\n            value = value.strftime(RFC3339_DATE_FORMAT)\n        assert isinstance(value, str), f\"Invalid value type: {type(value)}\"\n\n        # set value\n        if key in [\n                'election/election-id', 'logmonitor/address',\n                'logmonitor/last-data', 'stats/detail/scheduler/cron',\n                'stats/voting_facts/scheduler/cron',\n        ]:\n            pass\n        elif key == 'collector/state':\n            assert value in COLLECTOR_STATES, f\"Invalid value for {key!r}: {value!r}\"\n        elif re.match('election/(election|service|verification)(start|stop)$', key):\n            assert not value or dateutil.parser.parse(value)\n        elif (re.match('election/auth/.+$', key)\n              or key == 'election/tsp-qualification'):\n            assert value == 'TRUE'\n        elif key in DB_KEYS or key == \"list/voters0000\":\n            if value != '':\n                assert value.count(\" \") == 1, f\"Invalid value {value!r}\"\n                cn, timestamp = value.split(' ')\n                assert re.match(r'^(.+,){2}\\d{11}$', cn)\n                dateutil.parser.parse(timestamp)\n        elif re.match(r\"list/voters[0-9]{4}$\", key):\n            assert value\n            assert value.count(\" \") == 1, f\"Invalid value {value!r}\"\n            url_or_signature, timestamp = value.split(\" \")\n            dateutil.parser.parse(timestamp)\n        elif re.match(r\"list/voters[0-9]{4}-state$\", key):\n            assert value in [\"PENDING\", \"APPLIED\", \"INVALID\", \"SKIPPED\"]\n        elif re.match(r'host/.+/.+$', key):\n            key_type = key.split('/')[2]\n            assert key_type in DB_HOST_SUBKEYS, f\"Invalid host key type {key_type}\"\n        elif re.match(r'service/.+/.+$', key):\n            key_type = key.split('/')[2]\n            assert (\n                key_type in ALLOWED_SERVICE_KEYS\n            ), f\"Invalid service key type {key_type}\"\n            if key_type == 'state':\n                assert value in SERVICE_STATES, f\"Invalid value for {key!r}: {value!r}\"\n            elif key_type == 'backup-times':\n                assert value == \"\" or re.match(\n                    r\"[0-9]{2}:[0-9]{2}( [0-9]{2}:[0-9]{2})*$\", value\n                ), f\"Invalid value for {key!r}: {value!r}\"\n        elif re.match(r'user/.+$', key):\n            assert re.match(r'user/(.+,){2}\\d{11}$', key), (\n                f\"Invalid user CN {key.split('/')[1]!r}\")\n        else:\n            raise KeyError(f\"Invalid database field name {key!r}\")\n\n        # Safe operation reads value before writing it.\n        # This is to avoid unneeded values after database initialization (e.g.\n        # agent daemon may try to update service data in background).\n        if safe:\n            self.db[key]\n\n        log.debug(\"Setting value %r = %r\", key, value)\n        self.db[key] = value\n\n    def rm_value(self, key):\n        \"\"\"Remove database record.\"\"\"\n        assert isinstance(key, str)\n\n        log.debug(\"Removing record %r\", key)\n        del self.db[key]\n\n    def get_all_values(self, section=None):\n        \"\"\"\n        Get all database values as a dictionary.\n\n        :param section: Config section to get\n        :type section: string\n\n        :return: dict\n        \"\"\"\n        values = {}\n        for key in sorted(self.keys()):\n            path = key.split('/')\n            # len(path) == 4 is for stats/detail/scheduler/cron\n            # or stats/voting_facts/scheduler/cron\n            assert len(path) in (2, 3, 4)\n            if path[0] not in values:\n                values[path[0]] = {}\n            if len(path) == 2:\n                values[path[0]][path[1]] = self.get_value(key) or None\n            else:\n                if path[1] not in values[path[0]]:\n                    values[path[0]][path[1]] = {}\n                values[path[0]][path[1]][path[2]] = self.get_value(key) or None\n\n        return values.get(section, {}) if section else values\n\n    @classmethod\n    def reset(cls):\n        \"\"\"Reset database.\"\"\"\n        log.info(\"Initializing management database %r\", DB_FILE_PATH)\n        db = cls()\n        db.__enter__(mode='n')\n\n        # write default values\n        for key, value in sorted(DB_KEYS.items()):\n            db.set_value(key, value)\n\n        db.__exit__()\n\n    def dump(self, filter_keys=None):\n        \"\"\"\n        Dump database to stdout.\n\n        :param filter_keys: Limit dump with specified values\n        :type filter_keys: list\n        \"\"\"\n        for key in self.keys():\n            if not filter_keys or key in filter_keys:\n                print(f'{key}: {self.get_value(key)}')\n\n    def keys(self):\n        \"\"\"Return all database keys in sorted order.\"\"\"\n        return [key.decode('UTF-8') for key in sorted(self.db.keys())]\n\n\ndef check_db_dir():\n    \"\"\"\n    Check database directory exist or not.\n\n    :return: Database file directory path or None if path does not exist.\n    :rtype: str\n    \"\"\"\n    db_file_path = CONFIG['ivxv_db_file_path']\n    db_path = os.path.dirname(db_file_path)\n    if os.path.exists(db_path):\n        return db_file_path\n\n    log.error('Database directory %r does not exist', db_path)\n\n    return None\n"
  },
  {
    "path": "collector-admin/ivxv_admin/event_log.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Event logging for Collector Management service.\"\"\"\n\nimport datetime\nimport json\nimport os\n\nfrom . import EVENT_LOG_FILENAME, EVENTS, RFC3339_DATE_FORMAT\nfrom .cli_utils import init_cli_util\nfrom .config import cfg_path\n\nEVENT_LOG_FILEPATH = cfg_path('ivxv_admin_data_path', EVENT_LOG_FILENAME)\n\n\ndef init_event_log():\n    \"\"\"Initialize Management Service event log.\"\"\"\n    try:\n        os.unlink(EVENT_LOG_FILEPATH)\n    except FileNotFoundError:\n        pass\n    register_service_event('COLLECTOR_INIT')\n\n\ndef register_service_event(event, level='INFO', service=None, params=None):\n    \"\"\"Register Management Service event in event log file.\"\"\"\n    params = params or {}\n    assert level in ['INFO', 'ERROR']\n    log_event = {\n        'event': event,\n        'level': level,\n        'message': EVENTS[event].format(**params),\n        'service': service or 'management',\n        'timestamp': datetime.datetime.now().strftime(RFC3339_DATE_FORMAT),\n    }\n    with open(EVENT_LOG_FILEPATH, 'a') as fp:\n        json.dump(log_event, fp, sort_keys=True)\n        fp.write('\\n')\n\n\ndef event_log_dump_util():\n    \"\"\"Dump collector management event log.\"\"\"\n    init_cli_util(\n        \"\"\"\n        Dump IVXV Collector Management event log in human readable format.\n\n        Usage: ivxv-eventlog-dump\n        \"\"\",\n        allow_root=True)\n\n    with open(EVENT_LOG_FILEPATH) as fp:\n        while True:\n            line = fp.readline()\n            if not line:\n                break\n            event = json.loads(line)\n            print(\n                '{timestamp} {level} {service} {event} {message}'\n                .format(**event))\n"
  },
  {
    "path": "collector-admin/ivxv_admin/http_daemon.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nManagement daemon for collector management service.\n\nThis daemon is based on :py:mod:`bottle` module and listens as web service.\n\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\nimport subprocess\n\nfrom bottle import get, post, request, response, route, run\n\nfrom . import MANAGEMENT_DAEMON_PORT\nfrom .config import cfg_path\n\n# create logger\nlog = logging.getLogger(__name__)\n\n\n@route('/')\ndef index():\n    \"\"\"\n    Index page method.\n\n    :return: Service identification message.\n    \"\"\"\n    return '<b>This is a web server for IVXV Collector Management Service</b>!'\n\n\n@post('/upload-command')\ndef upload_cmd_file():\n    \"\"\"Upload command file.\"\"\"\n    filename = request.forms.get('filename')\n    original_filename = request.forms.get('original_filename')\n    cmd_type = request.forms.get('cmd_type')\n    file_path = cfg_path('file_upload_path', filename)\n    log.info(\"Uploading command file %r (type: %s)\", filename, cmd_type)\n\n    # execute command loading utility\n    cmd = ['ivxv-cmd-load', '--autoapply', cmd_type, file_path]\n    log.info('Executing command: %s', ' '.join(cmd))\n    proc = subprocess.run(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False\n    )\n    log.info('Command finished with exit code %d', proc.returncode)\n\n    # report upload result\n    body = {\n        'success': bool(proc.returncode == 0),\n        'log': proc.stdout.decode('utf-8').split('\\n'),\n    }\n    if proc.returncode:  # error\n        body[\"message\"] = f\"Viga faili {original_filename!r} üleslaadimisel\"\n    else:  # command loaded\n        body[\"message\"] = f\"Fail {original_filename!r} on edukalt üles laaditud\"\n\n    # start response\n    response.content_type = 'application/json'\n    return json.dumps(body)\n\n\n@get('/download-ballot-box')\n@get('/download-consolidated-ballot-box')\ndef download_ballots():\n    \"\"\"Export votes into ballot box folder.\"\"\"\n    timestamp = '{:%Y.%m.%d_%H.%M}'.format(datetime.datetime.now())\n\n    # prepare command\n    cmd = ['ivxv-export-votes']\n    if 'download-consolidated-ballot-box' in request.url:\n        cmd.append('--consolidate')\n        filename = f'exported-votes-consolidated-{timestamp}.zip'\n    else:\n        filename = f'exported-votes-{timestamp}.zip'\n    path = cfg_path('exported_votes_path', filename)\n    cmd.append(path)\n\n    cmd_str = ' '.join(cmd)\n    logfile_path = cfg_path(\n        'exported_votes_path', filename.replace('.zip', '.log'))\n    cmd = [\n        'sh', '-e', '-c',\n        f'( {cmd_str} && chmod 664 {path} ) > {logfile_path}'\n    ]\n    subprocess.Popen(cmd)\n\n    # start response\n    response.content_type = 'application/json'\n    return json.dumps('OK')\n\n\n@get(\"/download-voter-detail-stats\")\ndef download_voter_stats():\n    \"\"\"Download voter stats.\"\"\"\n    # import detail stats from Collector, don't export to VIS\n    cmd = [\"ivxv-voterstats\", \"detail\", \"--action=import\"]\n    log.info(\"Executing command: %s\", \" \".join(cmd))\n    proc = subprocess.run(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False\n    )\n    log.info(\"Command finished with exit code %d\", proc.returncode)\n\n    # report result\n    if proc.returncode:  # error\n        body = {\n            \"success\": True,\n            \"log\": proc.stdout.decode(\"utf-8\").split(\"\\n\"),\n            \"message\": \"Viga statistika hankimisel\",\n        }\n    else:  # success\n        with open(\"/var/lib/ivxv/admin-ui-data/voterstats-detail.json\") as fd:\n            body = json.load(fd)\n\n    # start response\n    response.content_type = 'application/json'\n    return json.dumps(body)\n\n\n@get(\"/download-processor-input\")\ndef download_processor_input():\n    \"\"\"Download processor input.\"\"\"\n    timestamp = \"{:%Y.%m.%d_%H.%M}\".format(datetime.datetime.now())\n    filename = f\"processor-cfg-{timestamp}.zip\"\n    output_filepath = f\"/var/lib/ivxv/admin-ui-data/{filename}\"\n    try:\n        os.unlink(output_filepath)\n    except FileNotFoundError:\n        pass\n    cmd = [\"ivxv-generate-processor-input\", output_filepath]\n    log.info(\"Executing command: %s\", \" \".join(cmd))\n    proc = subprocess.run(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False\n    )\n    log.info(\"Command finished with exit code %d\", proc.returncode)\n\n    # report result\n    if proc.returncode:  # error\n        body = {\n            \"success\": True,\n            \"log\": proc.stdout.decode(\"utf-8\").split(\"\\n\"),\n            \"message\": \"Viga töötlemisrakenduse sisendi aluse genereerimisel\",\n        }\n        response.content_type = 'application/json'\n        return json.dumps(body)\n\n    # success\n    with open(output_filepath, \"rb\") as fd:\n        body = fd.read()\n    response.content_type = \"application/zip\"\n    response.set_header(\"Content-Disposition\", f'attachment; filename=\"{filename}\"')\n    return body\n\n\n@get(\"/download-voting-sessions\")\ndef download_voting_sessions():\n    \"\"\"Download voting sessions.\"\"\"\n    cmd = [\"ivxv-voting-sessions\"]\n    filename = \"voting-sessions\"\n    if request.forms.get(\"verifications\"):\n        cmd.append(\"verify\")\n        filename += \"-with-verifications\"\n    else:\n        cmd.append(\"vote\")\n    if request.forms.get(\"anonymize\"):\n        cmd.append(\"--anonymize\")\n        filename += \"-anonymized\"\n    if request.forms.get(\"uniq\"):\n        cmd.append(\"--uniq\")\n        filename += \"-uniq\"\n    filename += \"-{:%Y.%m.%d_%H.%M}.csv\".format(datetime.datetime.now())  # timestamp\n    filepath = f\"/var/lib/ivxv/admin-ui-data/{filename}\"\n    cmd.append(filepath)\n    log.info(\"Executing command: %s\", \" \".join(cmd))\n    proc = subprocess.run(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False\n    )\n    if proc.returncode:  # error\n        body = {\n            \"success\": False,\n            \"log\": proc.stdout.decode(\"utf-8\").split(\"\\n\"),\n            \"message\": \"Viga hääletamise seansside hankimisel\",\n        }\n        response.content_type = \"application/json\"\n        return json.dumps(body)\n\n    # success\n    log.info(\"Command finished with exit code %d\", proc.returncode)\n\n    response.content_type = \"text/csv\"\n    response.set_header(\"Content-Disposition\", f'attachment; filename=\"{filename}\"')\n    with open(filepath) as fd:\n        return fd.read()\n\n\n@get('/skip-voters-list')\ndef skip_voters_lists():\n    \"\"\"Remove loaded but unapplied voters lists\"\"\"\n    cmd = [\"ivxv-voter-list-skip\"]\n    log.info('Executing command: %s', ' '.join(cmd))\n    proc = subprocess.run(\n        cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False\n    )\n    log.info('Command finished with exit code %d', proc.returncode)\n\n    # report result\n    body = {\n        'success': bool(proc.returncode == 0),\n        'log': proc.stdout.decode('utf-8').split('\\n'),\n    }\n\n    if proc.returncode:  # error\n        body[\"message\"] = \"Viga nimekirja märkimisel vahelejätmiseks\"\n    else:  # command loaded\n        body[\"message\"] = \"Nimekiri on märgitud vahelejätmiseks\"\n\n    # start response\n    response.content_type = 'application/json'\n    return json.dumps(body)\n\n\ndef daemon():\n    \"\"\"Daemon main loop.\"\"\"\n    log.info('Starting Management daemon')\n    run(host='localhost', port=MANAGEMENT_DAEMON_PORT)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/lib/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Library for collector management service.\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\nimport shutil\n\nfrom .. import (\n    CFG_TYPES,\n    RFC3339_DATE_FORMAT_WO_FRACT,\n    SERVICE_STATE_REMOVED,\n    SERVICE_TYPE_PARAMS,\n    VOTING_LIST_TYPES,\n)\nfrom ..collector_state import generate_collector_state\nfrom ..config import CONFIG, cfg_path\nfrom ..db import DB_SERVICE_SUBKEYS, IVXVManagerDb\nfrom ..event_log import register_service_event\nfrom ..service import generate_service_list\nfrom ..service.service import Service\n\n# create logger\nlog = logging.getLogger('lib')\n\n\nclass IvxvError(Exception):\n    \"\"\"IVXV exception class.\"\"\"\n\n\ndef register_tech_cfg_items(cfg, cfg_version):\n    \"\"\"Register technical config items in management database.\n\n    Create database records for service hosts and services if not created and\n    register removed services.\n\n    :param cfg: Technical config\n    :param cfg_version: Config version\n    \"\"\"\n    with IVXVManagerDb(for_update=True) as db:\n        register_removed_services(db, cfg, cfg_version)\n        gen_service_record_defaults(db, cfg)\n        gen_host_record_defaults(db, cfg)\n        gen_logmon_data(db, cfg.get('logging'))\n\n\ndef register_removed_services(db, cfg, cfg_version):\n    \"\"\"Mark removed services in database with REMOVED state.\n\n    Change state of removed services to REMOVED.\n\n    :param db: Database handler\n    :param cfg: Technical config\n    \"\"\"\n    existing_services = get_services(db=db)\n    services_from_cfg = generate_service_list(cfg['network'])\n\n    # find removed services\n    existing_service_ids = set(existing_services.keys())\n    new_service_ids = set(\n        new_service_cfg[1]['id']\n        for new_service_cfg in services_from_cfg)\n    removed_service_ids = existing_service_ids.difference(new_service_ids)\n    if not removed_service_ids:\n        return\n\n    # change loading states for choices, districts and voters lists\n    # if all existing storage services are removed\n    storages_removed = set(\n        service_id\n        for service_id, service_data in existing_services.items()\n        if service_data['service-type'] == 'storage' and\n        service_data['state'] != SERVICE_STATE_REMOVED)\n    storages_added = set(\n        new_service_cfg['id']\n        for service_type, new_service_cfg in services_from_cfg\n        if service_type == 'storage')\n    if not storages_removed.intersection(storages_added):\n        # all storages removed\n        log_msg = (\n            f'All existing storage service instances are removed with '\n            f'technical config {cfg_version}')\n        log.warning(log_msg)\n        reset_list_loading_state(db, \"choices\", \"choices list\", log_msg)\n        reset_list_loading_state(db, \"districts\", \"districts list\", log_msg)\n        for changeset_no in range(get_current_voter_list_changeset_no(db), -1, -1):\n            reset_list_loading_state(\n                db,\n                f\"voters{changeset_no:04d}\",\n                f\"voter list #{changeset_no:04d}\",\n                log_msg,\n            )\n\n    # change removed service states\n    db_values = db.get_all_values()['service']\n    for service_id in sorted(removed_service_ids):\n        log.info('Changing service %s state to REMOVED', service_id)\n        with Service(service_id, db_values[service_id]) as service:\n            service.register_state(\n                db,\n                SERVICE_STATE_REMOVED,\n                bg_info=f'Service removed with technical config: {cfg_version}',\n            )\n\n\ndef reset_list_loading_state(db, list_type, list_descr, log_msg):\n    \"\"\"Reset choices, districts and voters list loading state.\"\"\"\n    # check if list is loaded\n    list_version = db.get_value(f'list/{list_type}')\n    if not list_version:\n        return\n\n    log_msgs = [log_msg]\n    timestamp = datetime.datetime.now().strftime(RFC3339_DATE_FORMAT_WO_FRACT)\n\n    # load list metadata file\n    filepath = get_loaded_cfg_file_path(list_type)\n    assert filepath, f\"Config file for {list_descr} {list_version!r} does not exist\"\n    filepath = filepath.replace(os.path.splitext(filepath)[1], \".json\")\n    with open(filepath) as fp:\n        cfg_state = json.load(fp)\n\n    # set list state in management database\n    log_msg = \"\"\n    if list_type.startswith(\"voters\"):  # APPLIED -> PENDING for voters\n        db_key = f\"list/{list_type}-state\"\n        list_state = db.get_value(db_key)\n        if list_state == \"APPLIED\":\n            log_msg = f\"Setting {list_descr} state from APPLIED to PENDING\"\n            db.set_value(db_key, \"PENDING\")\n    else:  # PENDING for choices/districts\n        db_key = f\"list/{list_type}-loaded\"\n        if db.get_value(db_key):\n            log_msg = f\"Setting {list_descr} state to PENDING\"\n            db.set_value(db_key, \"\")\n    if log_msg:\n        log.info(log_msg)\n        log_msgs.append(log_msg)\n\n    # write config metadata file\n    log_msgs = [f'{timestamp} {log_msg}' for log_msg in log_msgs]\n    cfg_state[\"log\"] += [log_msgs]\n    cfg_state[\"attempts\"] = 0\n    cfg_state[\"completed\"] = False\n    tmp_filepath = f'{filepath}.tmp'\n    with open(tmp_filepath, 'w') as fp:\n        json.dump(cfg_state, fp)\n    shutil.move(tmp_filepath, filepath)\n\n\ndef gen_service_record_defaults(db, cfg):\n    \"\"\"Add database records for services with default values.\n\n    Parse the list of services from technical config and create\n    missing database records for every defined service.\n\n    :param db: Database handler\n    :param cfg: Technical config\n    \"\"\"\n    assert 'network' in cfg\n\n    # generate list of default values\n    service_values = {}\n    for network in cfg['network']:\n        for service_type, services in sorted(network['services'].items()):\n            for service in services or []:\n                service_id = service['id']\n                service_values[service_id] = DB_SERVICE_SUBKEYS.copy()\n                service_values[service_id].update({\n                    'service-type': service_type,\n                    'network': network['id'],\n                    'ip-address': service['address'],\n                })\n\n    # set service default values\n    for service_id, service_defaults in sorted(service_values.items()):\n        db_key_prefix = f'service/{service_id}'\n        try:\n            db.get_value(f\"{db_key_prefix}/service-type\")\n            continue\n        except KeyError:\n            log.info('Registering new service %s in management service',\n                     service_id)\n            register_service_event(\n                'SERVICE_REGISTER',\n                service=service_id,\n                params={'service_type': service_defaults['service-type']})\n\n        for key, val in service_defaults.items():\n            db.set_value(f\"{db_key_prefix}/{key}\", val)\n\n    set_tech_cfg_service_cond_values(db, cfg)\n\n\ndef set_tech_cfg_service_cond_values(db, cfg):\n    \"\"\"Set/remove service conditional values related to technical config.\"\"\"\n    for network in cfg['network']:\n        for service_type, services in sorted(network['services'].items()):\n            for service in services or []:\n                # create 'tls-key' and 'tls-cert' for\n                # choices/mid/storage/verification/voting services\n                if SERVICE_TYPE_PARAMS[service_type]['require_tls']:\n                    manage_db_cond_value(db, service[\"id\"], \"tls-key\", True)\n                    manage_db_cond_value(db, service[\"id\"], \"tls-cert\", True)\n                # create 'backup-times' for backup service\n                elif service_type == 'backup':\n                    manage_db_cond_value(\n                        db, service['id'], 'backup-times',\n                        True, ' '.join(cfg.get('backup') or []))\n\n\ndef manage_db_cond_value(db, service_id, key, set_value, value=None):\n    \"\"\"Manage (create/remove) service conditional database value.\"\"\"\n    key = f'service/{service_id}/{key}'\n    if set_value:  # create key\n        try:\n            db.get_value(key)\n        except KeyError:\n            db.set_value(key, value or '')\n    else:  # remove key\n        try:\n            db.rm_value(key)\n        except KeyError:\n            pass\n\n\ndef manage_db_mobileid_fields(db):\n    \"\"\"Create/remove service Mobile-ID/Smart-ID/Web eID token keys in database.\n\n    Check 'ticket' authentication method in election config and manage\n    \"mid-token-key\" keys in management database for mid, choices and voting\n    services. Keys will created or removed as 'ticket' authentication method is\n    used or not.\n    \"\"\"\n    ticket_auth = 'ticket' in db.get_all_values('election').get('auth', {})\n    for service_id, service_data in db.get_all_values('service').items():\n        if service_data['service-type'] not in [\n                'mid', 'smartid', 'webeid', 'choices', 'voting']:\n            continue\n        key = f'service/{service_id}/mid-token-key'\n        if ticket_auth:\n            if 'mid-token-key' not in service_data:\n                db.set_value(key, '')\n        elif 'mid-token-key' in service_data:\n            db.rm_value(key)\n\n\ndef manage_db_tsp_fields(db):\n    \"\"\"Create/remove service TSP registration keys in database.\n\n    Check 'qualification' protocol in election config and manage \"tspreg-key\"\n    keys in management database for voting services. Keys will created or\n    removed as 'tspreg' protocol is used or not.\n    \"\"\"\n    tspreg = db.get_all_values('election').get('tspreg', '')\n    for service_id, service_data in db.get_all_values('service').items():\n        if service_data['service-type'] not in ['voting']:\n            continue\n        key = f'service/{service_id}/tspreg-key'\n        if tspreg:\n            if 'tspreg-key' not in service_data:\n                db.set_value(key, '')\n        elif 'tspreg-key' in service_data:\n            db.rm_value(key)\n\n\ndef gen_host_record_defaults(db, cfg):\n    \"\"\"Add database records for service hosts with default values.\n\n    Parse the list of services from technical config and create\n    missing database records for service hosts.\n\n    :param db: Database handler\n    :param cfg: Technical config\n    \"\"\"\n    # generate list of default values\n    hostnames = set()\n    for network in cfg['network']:\n        for services in network['services'].values():\n            for service in services or []:\n                hostnames |= {service['address'].split(':')[0]}\n\n    # set host default values\n    for hostname in sorted(hostnames):\n        db_key_prefix = f'host/{hostname}'\n        try:\n            db.get_value(f\"{db_key_prefix}/state\")\n            continue\n        except KeyError:\n            log.info(\n                'Registering new service host %s in management service',\n                hostname)\n        db.set_value(f\"{db_key_prefix}/state\", \"\")\n\n\ndef gen_logmon_data(db, logging_params):\n    \"\"\"Add database record for Log Monitor service.\n\n    :param logging_params: Logging params from technical config.\n    :type logging_params: list of dicts\n    \"\"\"\n    if logging_params:\n        db.set_value('logmonitor/address', logging_params[0]['address'])\n\n\ndef cfg_type_verbose(cfg_type):\n    \"\"\"Get config type as human readable string.\"\"\"\n    try:\n        return CFG_TYPES[cfg_type]\n    except KeyError:\n        return VOTING_LIST_TYPES[cfg_type]\n\n\ndef get_services(db=None,\n                 require_collector_state=None,\n                 service_state=None,\n                 include_types=None,\n                 exclude_types=None):\n    \"\"\"Generate filtered list of services.\n\n    :param require_collector_state: Require specified state for collector\n    :type require_collector_state: list\n    :param include_types: Include specified service types\n    :type include_types: list\n    :param exclude_types: Exclude specified service types\n    :type exclude_types: list\n    :return: dict of services (key is service ID and value\n             is dict with service database values) or\n             None if collector is not in expected state\n    \"\"\"\n    require_collector_state = require_collector_state or []\n    assert isinstance(require_collector_state, list)\n    service_state = service_state or []\n    assert isinstance(service_state, list)\n    include_types = include_types or []\n    exclude_types = exclude_types or []\n    assert isinstance(include_types, list)\n    assert isinstance(exclude_types, list)\n    assert not include_types or not exclude_types\n\n    # get collector state\n    if db:\n        collector_state = generate_collector_state(db)\n    else:\n        with IVXVManagerDb() as _db:\n            collector_state = generate_collector_state(_db)\n\n    # check collector state\n    if (require_collector_state and\n            collector_state['collector_state'] not in require_collector_state):\n        log.info('Collector service state is %s',\n                 collector_state['collector_state'])\n        if len(require_collector_state) == 1:\n            log.error('Collector state must be %s for this operation',\n                      require_collector_state[0])\n        else:\n            log.error('Collector state must be %s or %s for this operation',\n                      ', '.join(require_collector_state[:-1]),\n                      require_collector_state[-1])\n        return None\n\n    # create list of services\n    services = {}\n    for service_id, service_data in collector_state.get('service', {}).items():\n        if service_state and service_data.get('state') not in service_state:\n            continue\n        if include_types:\n            if service_data['service-type'] in include_types:\n                services[service_id] = service_data\n            continue\n        if exclude_types:\n            if service_data['service-type'] not in exclude_types:\n                services[service_id] = service_data\n            continue\n        services[service_id] = service_data\n\n    return services\n\n\ndef clean_dir(path):\n    \"\"\"Remove all files from directory.\"\"\"\n    log.debug(\"Cleaning directory %r\", path)\n    for filename in os.listdir(path):\n        role_filename = os.path.join(path, filename)\n        os.unlink(role_filename)\n\n\ndef populate_user_permissions(db):\n    \"\"\"Populate user permissions for Apache web server.\"\"\"\n    permissions_path = CONFIG['permissions_path']\n    permissions_to_create = []\n\n    for user_cn, permissions in db.get_all_values('user').items():\n        for permission_name in permissions.split(','):\n            permissions_to_create.append(f\"{user_cn}-{permission_name}\")\n\n    # removing permission files\n    for permission_name in os.listdir(permissions_path):\n        if permission_name not in permissions_to_create:\n            filepath = os.path.join(permissions_path, permission_name)\n            log.info(\"Removing Apache Web Server user permission file %r\", filepath)\n            os.remove(filepath)\n\n    # creating permission files\n    for permission_name in permissions_to_create:\n        filepath = os.path.join(permissions_path, permission_name)\n        if not os.path.exists(filepath):\n            log.info(\"Creating Apache Web Server user permission file %r\", filepath)\n            with open(filepath, 'x') as fp:\n                fp.write(f\"Created {datetime.datetime.now().strftime('%c')}\")\n\n\ndef get_current_voter_list_changeset_no(db):\n    \"\"\"Get changeset number from database for last loaded voter list.\n\n    :return: changeset number or -1 if initial voter list is not loaded to database\n    :rtype: int\n    \"\"\"\n    for changeset_no in range(10_000):\n        try:\n            db.get_value(f\"list/voters{changeset_no:04d}\")\n        except KeyError:\n            return changeset_no - 1\n    return 0\n\n\ndef get_loaded_cfg_file_path(cfg_type):\n    \"\"\"Get real file path for loaded config file.\n\n    :return: File path or None if file does not exist\n    :rtype: str\n    \"\"\"\n    ext = (\n        \"zip\" if cfg_type.startswith(\"voters\") and cfg_type != \"voters0000\" else \"bdoc\"\n    )\n    filepath = cfg_path(\"active_config_files_path\", f\"{cfg_type}.{ext}\")\n    if not os.path.exists(filepath):\n        return None\n\n    assert os.path.islink(filepath), (\n        f\"Config file {filepath!r} is not a symbolic link\"\n    )\n\n    return os.path.realpath(filepath)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/lib/lockfile.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Lockfile implementation for collector management service.\"\"\"\n\nimport argparse\nimport atexit\nimport errno\nimport fcntl\nimport logging\nimport os\nimport signal\nimport sys\nimport time\n\nfrom ..config import cfg_path\n\n# create logger\nlog = logging.getLogger(\n    f\"{__package__}.lockfile\" if __name__ == \"__main__\" else __name__\n)\n\n\nclass PidLocker:\n    \"\"\"Pidfile creator and locker.\n\n    Create pidfile and keep it locked until program exit.\n    Raise IOError on locking failure.\n\n    >>> try:\n    ...     PidLocker(\"/path/to/pidfile\", timeout = 5)\n    ... except IOError:\n    ...     print('Locking failed even after waiting 5 seconds')\n    ...     exit(1)\n    \"\"\"\n\n    filepath = None  #: Pidfile name\n    fp = None  #: Pidfile descriptor\n\n    def __init__(self, pidfile_name, timeout=0):\n        \"\"\"Create and lock pidfile.\n\n        :raises OSError: On locking failure\n        \"\"\"\n        self.fp = None\n        self.filepath = pidfile_name\n        atexit.register(self.rm_pid)\n        self.fp = open(pidfile_name, \"ab\")\n        self.acquire_lock_with_timeout(timeout)\n        self.fp.write(bytes(f\"{os.getpid()}\\n\", \"ASCII\"))\n        self.fp.flush()\n\n    def acquire_lock_with_timeout(self, to):\n\n        def handler(signum, frame):\n            raise OSError(errno.EACCES,\n                          f\"Failed to lock {self.filepath} in {to} sec\")\n\n        signal.signal(signal.SIGALRM, handler)\n\n        retry_interval = 0.1\n\n        try:\n            signal.alarm(to)\n            while True:\n                try:\n                    fcntl.flock(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)\n                    # Successfully acquired a lock\n                    break\n                except Exception:  # pylint: disable=broad-except\n                    if to == 0:\n                        raise OSError(errno.EACCES,\n                                      f\"Failed to lock {self.filepath} in {to} sec.\")\n                    # Wait for another process to release the lock\n                    time.sleep(retry_interval)\n\n        finally:\n            signal.alarm(0)\n\n    def rm_pid(self):\n        \"\"\"Remove pidfile.\"\"\"\n        try:\n            os.remove(self.filepath)\n            if self.fp is not None:\n                self.fp.close()\n        except FileNotFoundError:\n            log.warning(\"PID file %r already removed\", self.filepath)\n\n    @classmethod\n    def get_pidfile_path(cls, pidfile_name):\n        \"\"\"Get pidfile path.\"\"\"\n        pidfile_path = cfg_path(\"ivxv_admin_data_path\", pidfile_name)\n        return pidfile_path\n\n    @classmethod\n    def pidfile_exists(cls, pidfile_name):\n        \"\"\"Check if pidfile exist.\"\"\"\n        pidfile_path = cls.get_pidfile_path(pidfile_name)\n        return os.path.exists(pidfile_path)\n\n    @classmethod\n    def rm_stale_pidfile(cls, pidfile_name):\n        \"\"\"Remove stale or invalid pidfile.\"\"\"\n        if not cls.pidfile_exists(pidfile_name):\n            return\n\n        pidfile_path = cls.get_pidfile_path(pidfile_name)\n        with open(pidfile_path) as fp:\n            pidline = fp.readline()\n\n        reason = \"\" if pidline.strip() else \"empty\"\n        if not reason:\n            try:\n                pid = int(pidline)\n            except ValueError:\n                reason = \"invalid\"\n        if not reason and not os.path.exists(f\"/proc/{pid}\"):\n            reason = \"stale\"\n        if reason:\n            log.error(\"Removing %s pidfile %r\", reason, pidfile_path)\n            os.remove(pidfile_path)\n\n\ndef main():\n    \"\"\"Lockfile testing utility.\"\"\"\n    # parse CLI args\n    parser = argparse.ArgumentParser(\n        description=\"\"\"\n        Create lock file and related process to hold it for a specified period.\n        \"\"\"\n    )\n    parser.add_argument(\"--file\", required=True, type=str, help=\"Lock file name\")\n    parser.add_argument(\n        \"--time\", required=True, type=int, help=\"Period to hold lock file (in seconds)\"\n    )\n    parser.add_argument(\n        \"--attempts\",\n        required=False,\n        type=int,\n        default=1,\n        help=\"Attempts to acquire lock file\",\n    )\n    parser.add_argument(\n        \"--background\", action=\"store_true\", help=\"Fork process to background\"\n    )\n\n    args = parser.parse_args()\n    assert args.time >= 0, \"--time can't have negative value\"\n    assert args.attempts > 0, \"--attempts must have positive value\"\n\n    filepath = PidLocker.get_pidfile_path(args.file)\n    attempts_left = args.attempts\n    proc_type = \"background\" if args.background else \"foreground\"\n\n    # fork to background if requested\n    if args.background:\n        log.info(\"Forking to background\")\n        pid = os.fork()\n        if pid:\n            log.info(\"Forked child process %d, exiting foreground process\", pid)\n            return 0\n\n    # acquire lockfile\n    PidLocker.rm_stale_pidfile(args.file)\n    log.info(\"Creating lockfile %r for PID %d in %s\", filepath, os.getpid(), proc_type)\n    while attempts_left:\n        attempts_left -= 1\n        try:\n            PidLocker(filepath)\n        except BlockingIOError as err:\n            if not attempts_left:\n                log.error(\"Failed to create lockfile (%s), no attempts left\", err)\n                return 1\n            log.warning(\n                \"Failed to create lockfile %r, waiting for 1 second (%d attempts left)\",\n                filepath,\n                attempts_left,\n            )\n            time.sleep(1)\n            continue\n        log.info(\"Lockfile %r successfully acquired\", filepath)\n        break\n\n    # sleep\n    log.info(\"Sleeping for %d seconds\", args.time)\n    time.sleep(args.time)\n\n    # cleanup\n    log.info(\"Removing lockfile %r for PID %d in %s\", filepath, os.getpid(), proc_type)\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "collector-admin/ivxv_admin/service/__init__.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Microservice management module.\"\"\"\n\nfrom .. import SERVICE_STATE_REMOVED, SERVICE_TYPE_PARAMS\nfrom .logging import log\n\n#: Path to users SSH public key file.\nIVXV_ADMIN_SSH_PUBKEY_FILE = '~/.ssh/id_ed25519.pub'\n#: Path to rsyslog config filename.\nRSYSLOG_CFG_FILENAME = '/etc/rsyslog.d/ivxv-logging.conf'\n\n\ndef get_service_cfg_state(db, cfg):\n    \"\"\"Get list of services that requires config update.\n\n    Check technical config records against database and generate list of\n    services that require config update.\n\n    :param db: Database handler\n    :param cfg: Technical config\n\n    :return: Service list with config update params:\n\n             .. code-block:: text\n\n                 {\n                     'service-id': {\n                         'technical': bool,\n                         'election': bool,\n                         'choices': bool,\n                         'districts': bool,\n                         'voters': [list-number, ...],\n                     },\n                     ...\n                 }\n\n    :rtype: dict\n\n    \"\"\"\n    assert 'network' in cfg\n\n    # detect config versions\n    tech_cfg_ver = db.get_value('config/technical')\n    election_cfg_ver = db.get_value('config/election')\n\n    # detect need for list updates\n    update_choices_list = (db.get_value('list/choices') !=\n                           db.get_value('list/choices-loaded'))\n    update_districts_list = (db.get_value('list/districts') !=\n                             db.get_value('list/districts-loaded'))\n    update_voters_list = []\n    for changeset_no in range(10_000):\n        try:\n            if db.get_value(f\"list/voters{changeset_no:04d}-state\") == \"PENDING\":\n                update_voters_list.append(changeset_no)\n        except KeyError:\n            break\n\n    # create list of services\n    service_list = {}\n    for network in cfg['network']:\n        for service_type, services in sorted(network['services'].items()):\n            for service in services or []:\n                db_key_prefix = f'service/{service[\"id\"]}'\n\n                service_tech_cfg_ver = db.get_value(\n                    f\"{db_key_prefix}/technical-conf-version\"\n                )\n                service_election_cfg_ver = election_cfg_ver and db.get_value(\n                    f\"{db_key_prefix}/election-conf-version\"\n                )\n\n                service_list[service['id']] = {\n                    'technical': service_tech_cfg_ver != tech_cfg_ver,\n                    'election': service_election_cfg_ver != election_cfg_ver,\n                    'choices':\n                    service_type == 'choices' and update_choices_list,\n                    'districts':\n                    service_type == 'choices' and update_districts_list,\n                    'voters': update_voters_list,\n                }\n                if service_list[service['id']]['technical']:\n                    log.debug('Service %s have no latest version of '\n                              'technical config', service['id'])\n\n                if service_list[service['id']]['election']:\n                    log.debug('Service %s have no latest version of '\n                              'election config', service['id'])\n\n    return service_list\n\n\ndef generate_service_list(service_networks, service_id_filter=None):\n    \"\"\"Generate service list from collector technical config.\n\n    .. code-block:: text\n\n        [\n            [<service_type>, {param: value, ...}],\n            ...\n        ]\n\n    :param service_networks: Service networks from collector technical config\n                             (section 'networks').\n    :type service_networks: dict\n    :param service_id_filter: Service IDs to include.\n                              All services are included if None.\n    :type service_id_filter: list\n\n    :return: List of services or None if some of specified services not found.\n    \"\"\"\n    service_id_filter = service_id_filter or []\n    service_list = []\n    service_ids = service_id_filter[:]\n    for network in service_networks:\n        for service_type, services in sorted(network['services'].items()):\n            for service_data in services or []:\n                service_id = service_data['id']\n                if service_id_filter and service_id not in service_ids:\n                    log.debug('Skipping service %s', service_id)\n                    continue\n                if service_ids:\n                    service_ids.remove(service_id)\n                service_list.append([service_type, service_data])\n\n    if service_ids:\n        log.error('Invalid service ID: %s', ', '.join(service_ids))\n        return None\n\n    return service_list\n\n\ndef generate_service_hints(services):\n    \"\"\"Generate configuration hints for services and inject it to service data.\n\n    :param services: Service data\n    :type services: dict\n    \"\"\"\n    for service_id, params in services.items():\n        if params['state'] == SERVICE_STATE_REMOVED:\n            continue\n        # ordered list of hints\n        hints = [\n            ['Apply technical config', not params['technical-conf-version']],\n        ]\n        service_type_params = SERVICE_TYPE_PARAMS[params['service-type']]\n        if service_type_params['require_tls']:\n            hints += [\n                ['Install service TLS key', not params.get('tls-key', True)],\n                ['Install service TLS certificate',\n                 not params.get('tls-cert', True)],\n            ]\n        if service_type_params['mobile_id']:\n            hints.append(\n                ['Install Mobile-ID/Smart-ID/Web eID identity token key',\n                 not params.get('mid-token-key', True)])\n        if service_type_params['tspreg']:\n            hints.append(\n                ['Install TSP registration key',\n                 not params.get('tspreg-key', True)])\n        if service_type_params['require_config']:\n            hints.append(\n                ['Apply election config', not params['election-conf-version']])\n\n        for hint, is_relevant in hints:\n            if is_relevant:\n                services[service_id]['bg_info'] = hint\n                break\n"
  },
  {
    "path": "collector-admin/ivxv_admin/service/backup_service.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Backup service management helper.\"\"\"\n\nimport subprocess\n\n\ndef install_backup_crontab():\n    \"\"\"Install crontab for backup automation.\"\"\"\n    subprocess.run([\"env\", \"VISUAL=ivxv-backup-crontab\", \"crontab\", \"-e\"], check=True)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/service/logging.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Logging helper for microservice management.\"\"\"\n\nimport logging\n\n# create logger\nlog = logging.getLogger('.'.join(__name__.split('.')[:-1]))\n\n\nclass ServiceLogger(logging.LoggerAdapter):\n    \"\"\"Logger adapter for service object.\n\n    Include service ID for logged messages.\n    Include message level for levels other than INFO.\n    \"\"\"\n\n    def process(self, msg, kwargs):\n        \"\"\"\n        Process the logging message and keyword arguments passed in to\n        a logging call to insert contextual information.\n        \"\"\"\n        return f\"SERVICE {self.extra['service_id']}: {msg}\", kwargs\n\n    def log(self, level, msg, *args, **kwargs):\n        \"\"\"\n        Delegate a log call to the underlying logger, after adding\n        contextual information from this adapter instance.\n        \"\"\"\n        if self.isEnabledFor(level):\n            if level != logging.INFO:\n                msg = f\"{logging.getLevelName(level).upper()}: {msg}\"\n            msg, kwargs = self.process(msg, kwargs)\n            self.logger.log(level, msg, *args, **kwargs)\n"
  },
  {
    "path": "collector-admin/ivxv_admin/service/remote_exec.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Remote execution helper for microservice management.\"\"\"\n\nimport subprocess\n\nfrom .logging import log\n\n\ndef exec_remote_cmd(cmd, **kw):\n    \"\"\"Perform remote operation in service host.\n\n    Execute service management command in service host (over OpenSSH SSH\n    client) or copy file to service host file system (over secure copy).\n\n    :param cmd: Command for subprocess.run()\n    :type cmd: list\n    :param kw: Parameters for subprocess.run()\n    :type kw: dict\n    :return: subprocess.CompletedProcess\n    \"\"\"\n    assert cmd[0] in ('ssh', 'scp')\n\n    # patch command\n    if cmd[0] == 'ssh':\n        cmd.insert(1, '-T')  # disable pseudo-terminal allocation\n    cmd.insert(1, '-o')  # set preferred authentication method\n    cmd.insert(2, 'PreferredAuthentications=publickey')\n\n    # execute command\n    log.debug('Executing command: %s', ' '.join(cmd))\n    try:\n        check = kw.pop('check')\n    except KeyError:\n        check = False\n    proc = subprocess.run(cmd, **kw, check=check)\n    if proc.returncode:\n        log.debug('Command finished with error code %d', proc.returncode)\n    else:\n        log.debug('Command successfully executed')\n\n    return proc\n"
  },
  {
    "path": "collector-admin/ivxv_admin/service/service.py",
    "content": "# IVXV Internet voting framework\n\"\"\"Microservice management helper.\"\"\"\n\nimport datetime\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport tempfile\nfrom logging.handlers import MemoryHandler\n\nfrom jinja2 import Environment, PackageLoader\n\nfrom debian import debfile\n\nfrom .. import (\n    COLLECTOR_PKG_FILENAMES,\n    DEB_PKG_VERSION,\n    RFC3339_DATE_FORMAT_WO_FRACT,\n    SERVICE_SECRET_TYPES,\n    SERVICE_STATE_CONFIGURED,\n    SERVICE_STATE_INSTALLED,\n    SERVICE_STATE_NOT_INSTALLED,\n    SERVICE_TYPE_PARAMS,\n)\nfrom ..config import CONFIG, cfg_path\nfrom ..db import IVXVManagerDb\nfrom ..event_log import register_service_event\nfrom . import IVXV_ADMIN_SSH_PUBKEY_FILE, RSYSLOG_CFG_FILENAME, generate_service_hints\nfrom .backup_service import install_backup_crontab\nfrom .logging import ServiceLogger, log\nfrom .remote_exec import exec_remote_cmd\n\n#: Path to service directory.\nSERVICE_DIR = cfg_path('ivxv_admin_data_path', 'service')\n\n\nclass Service:\n    \"\"\"Service management for collector services.\"\"\"\n    service_id = None  #: Service ID (str)\n    data = None  #: Service data (dict)\n    #: State report file name for currently applied config (str)\n    cfg_state_filepath = None\n    cfg_state = None  #: State report for currently applied config (dict)\n\n    def __init__(self, service_id, service_data):\n        \"\"\"Constructor.\"\"\"\n        self.service_id = service_id\n\n        # Service data from config file and the same data from\n        # management service database are slithgtly different.\n        # Convert service data to database format.\n        if 'service_type' in service_data:\n            service_data = {\n                'service-type': service_data.get('service_type'),\n                'ip-address': service_data.get('address'),\n            }\n        self.data = service_data\n        self.log = ServiceLogger(log, {\"service_id\": service_id})\n        self.memory_log_handler = MemoryHandler(1000)  #: Mem-handler for log buffering\n        log.addHandler(self.memory_log_handler)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def __repr__(self):\n        \"\"\"Printable representation of an object.\"\"\"\n        return f'<Service id={self.service_id} type={self.service_type}>'\n\n    def close(self):\n        self.memory_log_handler.close()\n        log.removeHandler(self.memory_log_handler)\n\n    @property\n    def hostname(self):\n        \"\"\"Hostname (str).\"\"\"\n        return self.data['ip-address'].split(':')[0]\n\n    @property\n    def service_type(self):\n        \"\"\"Service type (str).\"\"\"\n        return self.data['service-type']\n\n    @property\n    def service_account_name(self):\n        \"\"\"Service account name (str).\"\"\"\n        if self.service_type in ['backup', 'log']:\n            return 'ivxv-admin'\n        if self.service_type == 'proxy':\n            return 'haproxy'\n        return f\"ivxv-{self.service_type}\"\n\n    @property\n    def service_systemctl_id(self):\n        \"\"\"Service ID to use with systemctl (str).\"\"\"\n        return f'ivxv-{self.service_type}@{self.service_id}'\n\n    @property\n    def service_data_dir(self):\n        \"\"\"Path to service data directory (str).\"\"\"\n        return os.path.join(SERVICE_DIR, self.service_id)\n\n    @property\n    def deb_pkg_name(self):\n        \"\"\"Debian package name (str).\"\"\"\n        return f'ivxv-{self.service_type}'\n\n    def get_db_key(self, name):\n        \"\"\"Get database key for this service.\"\"\"\n        return f'service/{self.service_id}/{name}'\n\n    def register_event(self, event, params, level='INFO'):\n        \"\"\"Register service event.\"\"\"\n        register_service_event(\n            event, level=level, service=self.service_id, params=params)\n\n    def register_bg_info(self, bg_info):\n        \"\"\"Register service error message in database.\"\"\"\n        if bg_info:\n            self.log.info('Registering background info: %s',\n                          bg_info.split('\\n')[0])\n        with IVXVManagerDb(for_update=True) as db:\n            db.set_value(self.get_db_key('bg_info'), bg_info, safe=True)\n\n    def remove_root_access(self):\n        \"\"\"Remove management service access to service host root account.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # disable ivxv-admin key in authorized_keys file\n        self.log.info(\n            \"Removing management service access to root account in service host %r\",\n            self.hostname,\n        )\n        proc = self.ssh(\n            'sudo ivxv-admin-sudo remove-admin-root-access', account='root')\n\n        # report result\n        if proc.returncode:\n            self.log.info('Failed to remove management service access '\n                          'to service host root account')\n            return False\n\n        self.log.info('Management service access to service host '\n                      'root account removed successfully')\n        return True\n\n    def install_service(self):\n        \"\"\"Install service in service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.log.info(\"Installing service to host %r\", self.hostname)\n\n        # initialize service on the first tech config\n        with IVXVManagerDb() as db:\n            current_cfg_ver = db.get_value(\n                self.get_db_key('technical-conf-version'))\n        if not current_cfg_ver:\n            if not self.init_service():\n                return False\n\n        # install service package\n        error = not self.install_service_pkg()\n\n        # create SSH access to service account\n        if not error and self.service_account_name != 'ivxv-admin':\n            self.log.info(\n                \"Creating access to the service account %r in service host\",\n                self.service_account_name,\n            )\n            proc = self.ssh(\n                'sudo ivxv-admin-sudo create-ssh-access {}'.format(\n                    self.service_account_name),\n                account='ivxv-admin')\n            error = bool(proc.returncode)\n\n            if error:\n                self.log.error('Failed to create access '\n                               'to the service account in service host')\n\n        # report result\n        if error:\n            self.log.error('Failed to install service')\n            return False\n\n        self.log.info('Service installed successfully')\n        return True\n\n    def install_service_pkg(self, is_update=False, package=None):\n        \"\"\"Install service package to service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        if not package:\n            pkg_path = cfg_path(\n                'deb_pkg_path', COLLECTOR_PKG_FILENAMES[self.deb_pkg_name])\n        else:\n            pkg_path = package\n\n        # check package status in service host\n        if not is_update and not package:\n            self.log.info(\n                'Querying state of the service software package %r version %s',\n                self.deb_pkg_name, DEB_PKG_VERSION)\n            proc = self.ssh(\n                f'dpkg --status {self.deb_pkg_name}',\n                account='ivxv-admin',\n                stdout=subprocess.PIPE)\n            if proc.returncode == 0:\n                for line in proc.stdout.decode('UTF-8').split('\\n'):\n                    if line.find('Version:') == 0:\n                        pkg_version = line.split(':')[1].strip()\n                        if DEB_PKG_VERSION == pkg_version:\n                            self.log.info(\n                                \"Package %r is already installed\",\n                                self.deb_pkg_name,\n                            )\n                            return True\n                        self.log.info(\n                            \"Package %r is installed with wrong version %s\",\n                            self.deb_pkg_name,\n                            pkg_version,\n                        )\n                        break\n\n        # copy packages to service host\n        if not self.scp(\n                local_path=pkg_path,\n                remote_path=CONFIG['deb_pkg_path'],\n                description='software package file',\n                account='ivxv-admin'):\n            return False\n\n        # install service package\n        self.log.info(\"Installing package %r\", self.deb_pkg_name)\n        proc = self.ssh(\n            'sudo ivxv-admin-sudo install-pkg {}'.format(\n                pkg_path),\n            account='ivxv-admin')\n        if proc.returncode:\n            self.log.error(\"Failed to install package %r\", self.deb_pkg_name)\n            return False\n        self.log.info(\"Package %r is installed successfully\", self.deb_pkg_name)\n\n        return True\n\n    def update_ivxv_common_pkg(self):\n        \"\"\"Update ivxv-common package in service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # copy package file to service host\n        pkg_name = 'ivxv-common'\n        pkg_path = cfg_path('deb_pkg_path', COLLECTOR_PKG_FILENAMES[pkg_name])\n        if not self.scp(\n                local_path=pkg_path,\n                remote_path=CONFIG['deb_pkg_path'],\n                description='ivxv-common package file',\n                account='ivxv-admin'):\n            return False\n\n        # install ivxv-common package\n        self.log.info('Installing ivxv-common package')\n        proc = self.ssh(\n            'sudo ivxv-admin-sudo install-pkg {}'.format(\n                COLLECTOR_PKG_FILENAMES[pkg_name]),\n            account='ivxv-admin')\n        if proc.returncode:\n            self.log.info('Failed to install {} package', pkg_name)\n            return False\n\n        self.log.info(\"Package %r is installed successfully\", pkg_name)\n        return True\n\n    def init_service(self):\n        \"\"\"Initialize service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # remove service package with all dependent packages\n        if not self.remove_service_pkg():\n            return False\n\n        # initialize service data\n        self.log.info('Initializing service data directory in service host')\n\n        service_id = (\n            'backup' if self.service_type == 'backup' else self.service_id)\n        proc = self.ssh(\n            f'sudo ivxv-admin-sudo init-service {service_id}',\n            account='ivxv-admin')\n\n        if proc.returncode:\n            self.log.error('Failed to initialize service data directory')\n            return False\n\n        self.log.info('Service data directory initialized successfully')\n        return True\n\n    def init_service_host(self):\n        \"\"\"Initialize service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.log.info('Initializing service host')\n\n        # install ivxv-common package in service host if required\n        self.log.info('Detect ivxv-common package status')\n        proc = self.ssh('dpkg --list ivxv-common', account='ivxv-admin')\n        if proc.returncode:\n            self.log.info('Failed to detect ivxv-common package status')\n            # detect ivxv-common package dependencies\n            pkg_path = cfg_path(\n                'deb_pkg_path', COLLECTOR_PKG_FILENAMES['ivxv-common'])\n\n            # copy package file to service host\n            remote_path = os.path.join(\n                '/root', COLLECTOR_PKG_FILENAMES['ivxv-common'])\n            if not self.scp(\n                    local_path=pkg_path,\n                    remote_path=remote_path,\n                    description='ivxv-common package file',\n                    account='root'):\n                return False\n\n            # update package list\n            self.log.info('Updating package list')\n            cmd = [\n                'env', 'TERM=dumb', 'DEBIAN_FRONTEND=noninteractive',\n                'apt-get', 'update'\n            ]\n            proc = self.ssh(cmd, account='root')\n            if proc.returncode:\n                self.log.info('Failed to update package list')\n                return False\n\n            # install ivxv-common package\n            if not self.install_ivxv_common_pkg(pkg_path, remote_path):\n                return False\n\n            # create management service access to\n            # service host ivxv-admin account\n            self.log.info('Creating access to the management account '\n                          '\"ivxv-admin\" in service host')\n            cmd = [\n                'tee', '--append',\n                os.path.join(\n                    os.path.expanduser('~ivxv-admin'), '.ssh',\n                    'authorized_keys')\n            ]\n            with open(os.path.expanduser(IVXV_ADMIN_SSH_PUBKEY_FILE),\n                      'rb') as key_fp:\n                proc = self.ssh(cmd, account='root', stdin=key_fp)\n            if proc.returncode:\n                self.log.error('Failed to remove create SSH access '\n                               'to management account in service host')\n                return False\n\n            # remove management service access to service host root account\n            if not self.remove_root_access():\n                return False\n\n        # initialize service host\n        proc = self.ssh('sudo ivxv-admin-sudo init-host', account='ivxv-admin')\n        if proc.returncode:\n            self.log.error('Failed to initialize service host')\n            return False\n\n        self.log.info('Service host initialized successfully')\n        return True\n\n    def install_ivxv_common_pkg(self, pkg_path, remote_path):\n        \"\"\"Install ivxv-common package with dependent packages.\"\"\"\n        # detect package dependencies\n        deb = debfile.DebFile(pkg_path)\n        deps = []\n        for field in 'Depends', 'Recommends':\n            deps_str = deb.control.debcontrol().get(field)\n            if deps_str is not None:\n                deps += [dep.split(' ')[0] for dep in deps_str.split(', ')]\n\n        # install package dependecies\n        self.log.info('Installing ivxv-common package dependencies')\n        cmd = [\n            'env', 'TERM=dumb', 'DEBIAN_FRONTEND=noninteractive',\n            'apt-get', '--yes', 'install'\n        ] + deps\n        proc = self.ssh(cmd, account='root')\n        if proc.returncode:\n            self.log.info('Failed to install ivxv-common package dependencies')\n            return False\n\n        # install package\n        self.log.info('Installing ivxv-common package')\n        proc = self.ssh(f'dpkg --install {remote_path}', account='root')\n        if proc.returncode:\n            self.log.info('Failed to install ivxv-common package')\n            return False\n\n        return True\n\n    def remove_service_pkg(self):\n        \"\"\"Remove service package with all dependent packages in service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # check package state\n        self.log.info(\"Checking package %r state in service host\", self.deb_pkg_name)\n        cmd = f'dpkg --list {self.deb_pkg_name}'\n        proc = self.ssh(cmd, account='ivxv-admin', stdout=subprocess.PIPE)\n        if proc.returncode:\n            self.log.info(\n                \"Package %r is not installed in service host\",\n                self.deb_pkg_name,\n            )\n            return True\n\n        # detect packages to remove\n        pkg_to_remove = []\n        for line in proc.stdout.decode().split('\\n'):\n            if line[:2] != 'un':\n                for key in COLLECTOR_PKG_FILENAMES:\n                    if f' {key} ' in line:\n                        assert key != 'ivxv-common'\n                        pkg_to_remove.append(key)\n\n        # remove packages\n        self.log.info(\"Removing package %r in service host\", self.deb_pkg_name)\n        cmd = 'apt-get purge --yes {}'.format(' '.join(pkg_to_remove))\n        proc = self.ssh(cmd, account='root', stdout=subprocess.PIPE)\n        if proc.returncode:\n            self.log.info(\n                \"Failed to remove package %r in service host\",\n                self.deb_pkg_name,\n            )\n            if proc.stdout:\n                for line in proc.stdout.decode().split('\\n'):\n                    self.log.info(line)\n            if proc.stderr:\n                for line in proc.stderr.decode().split('\\n'):\n                    self.log.error(line)\n            return False\n\n        self.log.info(\n            \"Package %r in service host removed successfully\",\n            self.deb_pkg_name,\n        )\n        return True\n\n    def restart_service(self):\n        \"\"\"Restart service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # detect service state\n        with IVXVManagerDb() as db:\n            state = db.get_all_values('service')\n        state[self.service_id]['bg_info'] = None\n        generate_service_hints(state)\n\n        # skip restarting of unconfigured service\n        if state[self.service_id]['bg_info']:\n            self.log.info(\n                'Skipping restart of unconfigured service: %s',\n                state[self.service_id]['bg_info'])\n            return True\n\n        # restart service\n        self.log.info('Restarting service')\n\n        cmd = [\n            'ivxv-admin-helper', 'restart-service', self.service_type,\n            self.service_id, self.service_systemctl_id\n        ]\n        proc = self.ssh(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n        error = bool(proc.returncode)\n\n        if error:\n            self.register_bg_info(\n                'Service restart error: {}'\n                .format(proc.stderr.decode('UTF-8').strip()))\n            self.log.error('Failed to restart service')\n            return False\n\n        with IVXVManagerDb(for_update=True) as db:\n            self.register_state(db, SERVICE_STATE_CONFIGURED)\n        self.log.info('Service restarted successfully')\n        return True\n\n    def stop_service(self):\n        \"\"\"Stop service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.log.info('Stopping service')\n\n        cmd = ['systemctl', 'stop', '--user', self.service_systemctl_id]\n        proc = self.ssh(cmd)\n        error = bool(proc.returncode)\n\n        if error:\n            self.log.error('Failed to stop service')\n            return False\n\n        self.log.info('Service stopped successfully')\n        return True\n\n    def configure_logging(self, tech_cfg):\n        \"\"\"Configure rsyslog logging in service host.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.log.info(\"Configuring syslog logging in service host %r\", self.hostname)\n\n        # prepare log collector services data\n        log_collectors = []\n        for network in tech_cfg['network']:\n            for service_type, services in network['services'].items():\n                if service_type == 'log' and services:\n                    for service in services:\n                        log_collectors.append({\n                            'id': service['id'],\n                            'address': service['address'].split(':')[0],\n                            'port': service['address'].split(':')[1],\n                        })\n\n        # prepare external log collectors data (first record is Log Monitor)\n        ext_log_collectors = tech_cfg.get('logging', [])\n\n        # load rsyslog config template\n        tmpl_env = Environment(loader=PackageLoader('ivxv_admin', 'templates'))\n        template = tmpl_env.get_template('ivxv_service_rsyslog_conf.jinja')\n\n        # create rsyslog config content\n        rsyslog_cfg = template.render(\n            cfg_filepath=RSYSLOG_CFG_FILENAME,\n            log_collectors=log_collectors,\n            ext_log_collectors=ext_log_collectors)\n\n        # read existing config file\n        cmd = f\"test -f {RSYSLOG_CFG_FILENAME} && cat {RSYSLOG_CFG_FILENAME}\"\n        proc = self.ssh(cmd, account='ivxv-admin', stdout=subprocess.PIPE)\n        existing_rsyslog_cfg = proc.stdout.decode()\n\n        # don't replace file if not required\n        if rsyslog_cfg == existing_rsyslog_cfg:\n            self.log.info(\n                \"Rsyslog config file %r already exist with required content\",\n                RSYSLOG_CFG_FILENAME,\n            )\n            return True\n\n        # write config file\n        temp_file = tempfile.NamedTemporaryFile(delete=False)\n        temp_file.write(bytes(rsyslog_cfg, 'UTF-8'))\n        temp_file.close()\n        if not self.scp(\n                local_path=[temp_file.name],\n                remote_path='/etc/ivxv/ivxv-logging.conf',\n                description='logging configuration file',\n                account='ivxv-admin'):\n            return False\n        os.unlink(temp_file.name)\n\n        # restart rsyslog service\n        proc = self.ssh(\n            'sudo ivxv-admin-sudo rsyslog-config-apply', account='ivxv-admin')\n\n        if proc.returncode:\n            self.log.info('Failed to restart rsyslog server in service host')\n            return False\n\n        self.log.info('Logging configuration for service created successfully')\n        return True\n\n    def load_apply_state(self, filepath, attempt_no=0):\n        \"\"\"Load config applying state from state file.\n\n        :return: Attempt number\n        :rtype: int\n        \"\"\"\n        self.cfg_state_filepath = filepath\n        with open(filepath) as fp:\n            self.cfg_state = json.load(fp)\n\n        if not attempt_no:\n            self.cfg_state['attempts'] += 1\n            self.cfg_state['log'].append([])\n            attempt_no = self.cfg_state['attempts']\n        assert attempt_no == self.cfg_state['attempts']\n        assert attempt_no <= len(self.cfg_state['log'])\n        self.update_apply_state()\n\n        return attempt_no\n\n    def update_apply_state(self, **kw):\n        \"\"\"Update config applying state file.\"\"\"\n        self.cfg_state.update(kw)\n        self.cfg_state[\"log\"][-1] = list(self.get_log_buffer())\n        tmp_filepath = f\"{self.cfg_state_filepath}.tmp\"\n        with open(tmp_filepath, 'x') as fp:\n            json.dump(self.cfg_state, fp, indent=4, sort_keys=True)\n        shutil.move(tmp_filepath, self.cfg_state_filepath)\n\n    def apply_tech_cfg(self, cfg_ver, tech_cfg):\n        \"\"\"Apply collector technical config to service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.update_apply_state()\n\n        # install service package to service host\n        if not self.install_service():\n            return False\n        self.update_apply_state()\n\n        # configure service logging\n        if self.service_type != 'log' and not self.configure_logging(tech_cfg):\n            return False\n        self.update_apply_state()\n\n        # mark services that does not require config packages as CONFIGURED\n        # after install\n        if not SERVICE_TYPE_PARAMS[self.service_type]['require_config']:\n            self.register_cfg_version(\n                'technical', cfg_ver, SERVICE_STATE_CONFIGURED)\n            install_backup_crontab()\n            return True\n\n        # create data directory for service\n        proc = self.ssh(f'mkdir --verbose --parents {self.service_data_dir}')\n        if proc.returncode:\n            self.log.error('Failed to generate service data directory')\n            return False\n        self.update_apply_state()\n\n        # apply trust root config to service\n        self.log.info('Applying trust root config to service')\n        if not self.copy_cfg_to_service('trust'):\n            self.log.error('Failed to apply trust root config to service')\n            return False\n        self.log.info('Trust root config successfully applied to service')\n        self.update_apply_state()\n\n        # apply technical config to service\n        self.log.info('Applying technical config to service')\n        if not self.copy_cfg_to_service('technical'):\n            self.log.error('Failed to apply technical config to service')\n            return False\n        self.update_apply_state()\n\n        # notify service about config change\n        with IVXVManagerDb() as db:\n            election_cfg_ver = db.get_value(\n                self.get_db_key('election-conf-version'))\n        if election_cfg_ver and not self.restart_service():\n            return False\n        self.log.info('Technical config successfully applied to service')\n        self.update_apply_state()\n\n        # detect service current state\n        with IVXVManagerDb() as db:\n            service_old_state = db.get_value(self.get_db_key('state'))\n\n        # register config version (and new service state for NOT INSTALLED\n        # service) in management database\n        self.register_cfg_version(\n            'technical', cfg_ver,\n            SERVICE_STATE_INSTALLED\n            if service_old_state == SERVICE_STATE_NOT_INSTALLED\n            else None\n        )\n\n        return True\n\n    def apply_election_cfg(self, cfg_ver):\n        \"\"\"Apply elections config to service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # detect service state\n        with IVXVManagerDb() as db:\n            state = db.get_all_values('service')\n\n        # service issues must be fixed before installing first election config\n        if not state[self.service_id]['election-conf-version']:\n            state = {self.service_id: state[self.service_id]}\n            generate_service_hints(state)\n            if state[self.service_id].get('bg_info') not in [\n                    None, 'Apply election config'\n            ]:\n                self.log.error('Cannot apply election config: %s',\n                               state[self.service_id].get('bg_info'))\n                return False\n        del state\n\n        self.log.info('Applying election config to service')\n        if not self.copy_cfg_to_service('election'):\n            return False\n        self.update_apply_state()\n\n        # enable service systemd unit if first config is loaded\n        self.log.info('Enabling systemd unit')\n        cmd = f'systemctl enable --user {self.service_systemctl_id}'\n        proc = self.ssh(cmd)\n        if proc.returncode:\n            self.log.error('Failed to enable systemd unit')\n            return False\n        self.update_apply_state()\n\n        # register config version and service state in management database\n        self.register_cfg_version(\n            'election', cfg_ver, SERVICE_STATE_CONFIGURED)\n\n        # notify service about config change\n        if not self.restart_service():\n            return False\n        self.update_apply_state()\n\n        # install crontab for automatic backups\n        if self.service_type == 'voting':\n            install_backup_crontab()\n\n        return True\n\n    def apply_list(self, list_type, changeset_no=None):\n        \"\"\"Apply choices list to service.\n\n        :param list_type: List type (choices, voters)\n        :type list_type: str\n        :param changeset_no: Changeset number for voter list\n        :type changeset_no: int\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # apply list\n        self.log.info('Applying %s list to service', list_type)\n        if not self.copy_cfg_to_service(list_type, changeset_no):\n            self.log.error('Failed to apply %s list to service', list_type)\n            return False\n        self.update_apply_state()\n\n        # register list version\n        with IVXVManagerDb(for_update=True) as db:\n            if list_type in [\"choices\", \"districts\"]:\n                cfg_ver = db.get_value(f\"list/{list_type}\")\n                self.log.info(\n                    \"Registering applied %s list version %r in management database\",\n                    list_type,\n                    cfg_ver,\n                )\n                db.set_value(f\"list/{list_type}-loaded\", cfg_ver)\n            else:\n                assert list_type == \"voters\"\n                cfg_ver = db.get_value(f\"list/voters{changeset_no:04}\")\n                self.log.info(\n                    \"Registering applied voters list #%d \"\n                    \"version %r in management database\",\n                    changeset_no,\n                    cfg_ver,\n                )\n                db.set_value(f\"list/voters{changeset_no:04}-state\", \"APPLIED\")\n\n        self.log.info('%s list applied successfully to service',\n                      list_type.capitalize())\n        return True\n\n    def load_secret_file(self, secret_type, filepath, checksum):\n        \"\"\"Load secret key file to service and register file checksum.\n\n        :param secret_type: Secret type\n        :type secret_type: str\n        :param filepath: Source file path\n        :type filepath: str\n        :param checksum: File checksum\n        :type checksum: str\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        secret_descr = SERVICE_SECRET_TYPES[secret_type]['description']\n        target_path = (SERVICE_SECRET_TYPES[secret_type]['target-path']\n                       .format(service_id=self.service_id))\n        shared_sercet = SERVICE_SECRET_TYPES[secret_type]['shared']\n        file_mode = '0640' if shared_sercet else '0600'\n        remote_account = ('ivxv-admin' if shared_sercet\n                          else self.service_account_name)\n\n        # copy file to service host\n        self.log.info('Loading %s to service', secret_descr)\n        if not self.scp(\n                local_path=[filepath],\n                remote_path=target_path,\n                account=remote_account,\n                description=secret_descr):\n            return False\n\n        # set file permissions\n        proc = self.ssh(\n            f'chmod --changes {file_mode} {target_path}',\n            account=remote_account)\n        if proc.returncode:\n            self.log.error('Failed to set %s file permissions '\n                           'in service host', secret_descr)\n            return False\n\n        # register key checksum\n        with IVXVManagerDb(for_update=True) as db:\n            self.log.info(\n                'Registering loaded %s checksum in management database',\n                secret_descr)\n            db.set_value(\n                self.get_db_key(SERVICE_SECRET_TYPES[secret_type]['db-key']),\n                checksum)\n\n        self.log.info('%s loaded successfully to service', secret_descr)\n\n        # restart service\n        return self.restart_service()\n\n    def copy_cfg_to_service(self, cfg_type, changeset_no=None):\n        \"\"\"Copy config to service host and reload service if needed.\n\n        :param cfg_type: Config type\n                         (trust, technical, elections, choices, voters)\n        :type cfg_type: str\n        :param changeset_no: Changeset number for voter list\n        :type changeset_no: int\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        # copy config to host\n        cfg_filename_suffix = f\"{changeset_no:04}\" if cfg_type == \"voters\" else \"\"\n        cfg_filename_ext = \"zip\" if changeset_no else \"bdoc\"\n        cfg_filename = f\"{cfg_type}{cfg_filename_suffix}.{cfg_filename_ext}\"\n        cfg_filepath = cfg_path('active_config_files_path', cfg_filename)\n        target_path = f'/etc/ivxv/{cfg_filename}'\n        if not self.scp(\n                local_path=[cfg_filepath],\n                remote_path=target_path,\n                account='ivxv-admin',\n                description=f'{cfg_type} config'):\n            return False\n\n        # set config file permissions\n        self.log.info('Set %s config file permissions in service host',\n                      cfg_type)\n        cmd = ['chmod', '--changes', '0640', target_path]\n        proc = self.ssh(cmd, account='ivxv-admin')\n        if proc.returncode:\n            self.log.error('Failed to set %s config file permissions '\n                           'in service host', cfg_type)\n            return False\n\n        # notify choices service about list change\n        list_change_util = {\n            'choices': 'ivxv-choiceimp',\n            'districts': 'ivxv-districtimp',\n            'voters': 'ivxv-voterimp'\n        }.get(cfg_type)\n        if list_change_util:\n            self.log.info('Notify service about %s list change', cfg_type)\n            cmd = [list_change_util, '-instance', self.service_id, target_path]\n            proc = self.ssh(cmd)\n            if proc.returncode:\n                self.log.info('Failed to notify service about %s list change',\n                              cfg_type)\n                return False\n\n        return True\n\n    def register_cfg_version(self, cfg_type, cfg_ver, service_state):\n        \"\"\"Register config version and service state in database.\"\"\"\n        with IVXVManagerDb(for_update=True) as db:\n            self.log.info(\n                \"Registering %s config version %r in management database\",\n                cfg_type,\n                cfg_ver,\n            )\n            db.set_value(\n                self.get_db_key(f\"{cfg_type}-conf-version\"), cfg_ver)\n\n            if service_state is not None:\n                self.register_state(db, service_state)\n\n    def register_state(self, db, state, bg_info=None):\n        \"\"\"Register service state in database.\"\"\"\n        last_state = db.get_value(self.get_db_key('state'))\n        if state == last_state:\n            self.log.info(\"Service state %r not changed\", state)\n            return\n\n        # set background info\n        if state in SERVICE_STATE_CONFIGURED:\n            bg_info = ''\n        if bg_info is not None:\n            db.set_value(self.get_db_key('bg_info'), bg_info, safe=True)\n\n        # set service state\n        self.log.info(\n            \"Registering service state as %r \"\n            \"in management database (last state: %r)\",\n            state,\n            last_state,\n        )\n        db.set_value(self.get_db_key('state'), state, safe=True)\n        self.register_event(\n            'SERVICE_STATE_CHANGE',\n            params={\n                'state': state,\n                'last_state': last_state\n            })\n\n    def ping(self):\n        \"\"\"Ping service.\n\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        self.log.debug('Pinging service')\n\n        # query service status\n        cfg_check_enabled = False\n        if self.service_type == 'backup':\n            ping_cmd = 'true'\n        elif self.service_type == 'log':\n            ping_cmd = 'systemctl status rsyslog'\n        else:\n            ping_cmd = 'env LC_ALL=C systemctl status --user {}'.format(\n                self.service_systemctl_id)\n            cfg_check_enabled = True\n        ping_proc = self.ssh(\n            ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\n        if not ping_proc.returncode:\n            if cfg_check_enabled:\n                try:\n                    self._verify_cfg_version()\n                except LookupError as err:\n                    self.register_bg_info(str(err))\n                    self.log.error('Failed to detect service status')\n                    return False\n            self.register_bg_info('')\n            self.log.debug('Service is alive')\n            return True\n\n        # return without config check\n        cmd_err_output = ping_proc.stderr.decode('UTF-8').strip()\n        if not cfg_check_enabled or cmd_err_output:\n            self.register_bg_info(\n                'Ping error: {}'.format(\n                    cmd_err_output or\n                    ping_proc.stdout.decode('UTF-8').strip() or\n                    f\"Command {' '.join(ping_cmd)!r} failed\"))\n            self.log.error('Pinging service failed')\n            return False\n\n        # check service config\n        cfg_check_cmd = [\n            'ivxv-admin-helper', 'check-service-config', self.service_type,\n            self.service_id\n        ]\n        cfg_check_proc = self.ssh(\n            cfg_check_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n        if cfg_check_proc.returncode == 0:  # valid config\n            self.register_bg_info(\n                'Ping error: {}'.format(\n                    ping_proc.stdout.decode('UTF-8').strip() or\n                    f\"Command {' '.join(ping_cmd)!r} failed\"))\n            self.log.error('Pinging service failed')\n        else:  # invalid config\n            self.register_bg_info(\n                'Ping error: Invalid config: {}'.format(\n                    cfg_check_proc.stderr.decode('UTF-8').strip() or\n                    cfg_check_proc.stdout.decode('UTF-8').strip()))\n            self.log.error('Pinging service failed (invalid configuration)')\n\n        return False\n\n    def _get_cfg_versions_from_service(self):\n        \"\"\"Get config versions from service.\n\n        Service version data is provided by commands:\n\n        * ``systemctl show`` - trust, technical and election config\n        * ``ivxv-choiceimp`` - choices list\n        * ``ivxv-districtimp`` - districts list\n        * ``ivxv-voterimp`` - voters lists\n\n        :raises LookupError: if some internal check fails\n        \"\"\"\n        cfg_versions = {\n            'service_state': {},\n            'choices_list_versions': [],\n            'districts_list_versions': [],\n            'voters_lists_versions': [],\n        }\n\n        # query status text for trust, technical and election config\n        cmd = ('systemctl --user show --property StatusText '\n               f'{self.service_systemctl_id}')\n        proc = self.ssh(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n        if proc.returncode:\n            raise LookupError(\n                'Error while querying service status text: {}'.format(\n                    proc.stderr.decode('UTF-8')))\n        status_text = proc.stdout.decode('UTF-8')\n\n        # parse status\n        re_pattern = re.compile(r'StatusText=(.+)')\n        if not re_pattern.match(status_text):\n            raise LookupError(f'Invalid service state text: {status_text}')\n        state_json = re_pattern.sub('\\\\1', status_text)\n        try:\n            cfg_versions['service_state'] = json.loads(state_json)\n        except json.decoder.JSONDecodeError as err:\n            raise LookupError(\n                f'Error while decoding JSON data from service state: {err}. '\n                f'JSON: {state_json}')\n        self.log.debug('Service internal status: %s',\n                       cfg_versions['service_state']['Status'])\n\n        # query status text for choices and districts list\n        if self.service_type == 'choices':\n            for cfg_type in [\"choices\", \"districts\"]:\n                utility = f\"ivxv-{cfg_type[:-1]}imp\"\n                cmd = f\"{utility} --check version --instance {self.service_id}\"\n                proc = self.ssh(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n                if proc.returncode:\n                    raise LookupError(\n                        f\"Error while querying {cfg_type} list version: \"\n                        f\"{proc.stderr.decode('UTF-8')}\"\n                    )\n                status_text = proc.stdout.decode(\"UTF-8\")\n                try:\n                    list_versions = json.loads(status_text) if status_text else []\n                    assert isinstance(list_versions, list)\n                except json.decoder.JSONDecodeError as err:\n                    raise LookupError(\n                        \"Error while decoding JSON data from service state: \"\n                        f\"{err}. JSON: {status_text}\"\n                    )\n\n        # query status text for voters lists\n        elif self.service_type == 'voting':\n            cmd = (\n                f'ivxv-voterimp --check version --instance {self.service_id}')\n            proc = self.ssh(\n                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n            if proc.returncode:\n                raise LookupError(\n                    'Error while querying voters list versions: {}'.format(\n                        proc.stderr.decode('UTF-8')))\n            status_text = proc.stdout.decode('UTF-8')\n            try:\n                cfg_versions['voters_lists_versions'] = (\n                    json.loads(status_text) if status_text else [])\n                assert isinstance(cfg_versions['voters_lists_versions'], list)\n            except json.decoder.JSONDecodeError as err:\n                raise LookupError(\n                    'Error while decoding JSON data from service state: '\n                    f'{err}. JSON: {status_text}')\n\n        return cfg_versions\n\n    def _verify_cfg_version(self):\n        \"\"\"Verify config version.\n\n        Compare config version data provided by service and version in\n        management database.\n\n        :return: Message to register in service background info\n        :raises LookupError: if some internal check fails\n        \"\"\"\n        cfg_versions = self._get_cfg_versions_from_service()\n        version_in_db = None\n        with IVXVManagerDb() as db:\n            for cfg_type in 'trust', 'technical', 'election':\n                db_key = (\n                    'config/trust' if cfg_type == 'trust' else\n                    self.get_db_key(f'{cfg_type}-conf-version'))\n                version_in_db = db.get_value(db_key)\n                versions_in_service = cfg_versions['service_state']['Version'][\n                    cfg_type.capitalize()]\n                if version_in_db not in versions_in_service:\n                    raise LookupError(\n                        f\"{cfg_type.capitalize()} config version \"\n                        \"in service status info \"\n                        f\"does not contain version {version_in_db!r} \"\n                        \"registered in management database. \"\n                        f\"Service status info: {versions_in_service}\"\n                    )\n            for list_type in [\"choices\", \"districts\"]:\n                versions_in_service = cfg_versions[f\"{list_type}_list_versions\"]\n                if versions_in_service:\n                    version_in_db = db.get_value(f\"list/{list_type}\")\n                    if version_in_db not in versions_in_service:\n                        raise LookupError(\n                            f\"{list_type.capitalize()} list version \"\n                            \"in service status info \"\n                            f\"does not contain version {version_in_db!r} \"\n                            \"as registered in management database. \"\n                            \"Choices list versions in services: \"\n                            f\"{versions_in_service}\"\n                        )\n\n            version_lists_in_service = cfg_versions['voters_lists_versions']\n            if version_lists_in_service:\n                for changeset_no in range(10_000):\n                    key = f\"list/voters{changeset_no:04d}-loaded\"\n                    try:\n                        version_in_db = db.get_value(key)\n                    except KeyError:\n                        break\n                    try:\n                        versions_in_service = version_lists_in_service[\n                            changeset_no - 1]\n                    except IndexError:\n                        raise LookupError(\n                            \"Service status info does not contain \"\n                            \"version info for voters list \"\n                            f\"#{changeset_no:04d}. \"\n                            \"Version for this list in management database \"\n                            f\"is {version_in_db!r}\"\n                        )\n                    if version_in_db not in versions_in_service:\n                        raise LookupError(\n                            f\"Voters list #{changeset_no:04d} version \"\n                            \"in service status info \"\n                            f\"does not contain version {version_in_db!r} \"\n                            \"as registered in management database. \"\n                            \"Voters list versions in services: \"\n                            f\"{versions_in_service}\"\n                        )\n\n    def scp(self,\n            local_path,\n            remote_path,\n            description,\n            account=None,\n            to_remote=True):\n        \"\"\"Copy file using SCP protocol.\n\n        :param local_path: Local file path(s)\n        :type local_path: str or list of strings\n        :param remote_path: Remote file path\n        :type remote_path: str\n        :param description: Description of files to copy (for logging)\n        :type description: str\n        :param account: Remote account name (None = default service account)\n        :type account: str\n        :param to_remote: Copy to remote host (True) or from remote host\n        :type to_remote: bool\n        :return: True on success, False on error.\n        :rtype: bool\n        \"\"\"\n        if not isinstance(local_path, list):\n            local_path = [local_path]\n        assert to_remote or len(local_path) == 1\n        remote_path = '{}@{}:{}'.format(account or self.service_account_name,\n                                        self.hostname, remote_path)\n        direction_str = 'to' if to_remote else 'from'\n\n        # prepare command\n        cmd = ['scp'] + (local_path + [remote_path]\n                         if to_remote else [remote_path] + local_path)\n\n        # execute command\n        self.log.info('Copying %s %s service host', description, direction_str)\n        proc = exec_remote_cmd(cmd)\n\n        # return results\n        if proc.returncode:\n            self.log.error('Failed to copy %s %s service host',\n                           description, direction_str)\n\n        return proc.returncode == 0\n\n    def ssh(self, cmd, account=None, fwd_auth_agent=False, **kw):\n        \"\"\"Execute command in remote host.\n\n        :param cmd: Command to execute\n        :type cmd: str or list\n        :param account: Remote account name\n        :type account: str\n        :param fwd_auth_agent: Start ssh with authentication agent forwarding\n        :type fwd_auth_agent: bool\n        :param kw: Command arguments\n\n        :return: subprocess.CompletedProcess\n        \"\"\"\n        if not isinstance(cmd, list):\n            cmd = [cmd]\n        ssh_cmd = ['ssh', '-A'] if fwd_auth_agent else ['ssh']\n        ssh_cmd.append(\n            '{}@{}'.format(\n                account or self.service_account_name, self.hostname))\n\n        return exec_remote_cmd(ssh_cmd + cmd, **kw)\n\n    def get_log_buffer(self):\n        \"\"\"Get formatted log messages from log buffer.\"\"\"\n        for rec in self.memory_log_handler.buffer:\n            timestamp = datetime.datetime.fromtimestamp(rec.created).strftime(\n                RFC3339_DATE_FORMAT_WO_FRACT\n            )\n            msg = rec.getMessage()\n            yield f\"{timestamp} {msg}\"\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/ivxv_backup_crontab.jinja",
    "content": "{#\n\nIVXV Internet voting framework\n\nTemplate file for backup automation crontab.\n\n-#}\n# Crontab for IVXV Management Service backup automation\n\n# This file is managed by IVXV Management Service\n# Current version is generated @ {{ time_generated }}\n\n# This file is (re)generated automatically on the following events:\n#\n# - Backup service comes to configured state\n# - Any of voting service instances comes to configured state\n# - Any of log collectors comes to configured state\n#\n# This file is removed automatically on the following events:\n#\n# - Management database is initialized\n\n{% if not configured_backup_services %}\n# Backup service is NOT CONFIGURED\n{% else %}\n# Backup service is configured\n# Configured voting services: {{ configured_voting_services }}\n# Configured log collectors: {{ configured_log_collectors }}\n\n{% for hour, minute in backup_times -%}\n\n# @ {{ \"%02d\"|format(hour) }}:{{ \"%02d\"|format(minute) }}\n    {%- if configured_backup_services %}\n{{ minute }} {{ hour }} * * * ivxv-backup management-conf\n    {%- endif -%}\n    {%- if configured_voting_services %}\n{{ minute }} {{ hour }} * * * ivxv-backup ballot-box\n    {%- endif -%}\n    {%- if configured_log_collectors %}\n{{ minute }} {{ hour }} * * * ivxv-backup log\n    {%- endif %}\n\n{% endfor -%}\n{% endif %}\n{#-\nvim:ft=jinja:\n#}\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/ivxv_detail_stats_crontab.jinja",
    "content": "{#\n\nIVXV Internet voting framework\n\nCrontab template file to automate detail statistics export to VIS.\n\n-#}\n# Crontab for IVXV Management Service detail statistics export to VIS automation\n\n# This file is managed by IVXV Management Service\n# Current version is generated @ {{ time_generated }}\n\n# This file is (re)generated automatically on the following events:\n#\n# - `ivxv-cmd-load election <digitally signed election configuration>`\n#\n# This file is removed automatically on the following events:\n#\n# - `ivxv-collector-init`\n\n# Import detail voting stats from voting service and export it to VIS\n{{ minute }} {{ hour }} {{ day }} {{ month }} {{ weekday }} if [ -x /usr/bin/ivxv-voterstats ]; then /usr/bin/ivxv-voterstats detail --log-level=WARNING; fi\n\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/ivxv_service_rsyslog_conf.jinja",
    "content": "{#\n\nIVXV Internet voting framework\n\nTemplate file for rsyslog config in service host.\n\n-#}\n# IVXV Internet voting framework\n\n# Rsyslog logging configuration file\n# {{ cfg_filepath }}\n\n# This file is automatically generated\n# by collector management service! DO NOT CHANGE!\n\n# load RELP output module\nmodule(load=\"omrelp\")\n\n# increase max message size\nglobal(maxMessageSize=\"32k\")\n\n# LOCAL LOGGING\n\n# write IVXV log to file (up to level INFO)\nif ($programname startswith 'ivxv-') and ($syslogfacility-text == 'local0') and\n   ($syslogseverity <= '6') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_DEFAULT_LOG_FILENAME\"\n    template=\"ivxv-json\"\n)\n\n# write IVXV request log to /var/log/ivxv/ivxv-request-YYYY-MM-DD-HH.log\nif ($programname startswith 'ivxv-') and ($syslogfacility-text == 'local1') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_REQUEST_LOG_FILENAME\"\n)\n\n# write IVXV debug log and log of related\n# services (haproxy, etcd, rsyslog, sshd) to /var/log/ivxv/ivxv-debug-YYYY-MM-DD-HH.log\nif ($programname startswith 'ivxv-') or ($programname startswith 'rsyslog') or\n   ($programname == 'haproxy') or ($programname == 'sshd') or ($programname == 'etcd') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_DEBUG_LOG_FILENAME\"\n)\n\n\n# REMOTE LOGGING - log collector services\n{% for log_collector in log_collectors %}\n# forward log messages to log collector \"{{ log_collector.id }}\" using RELP protocol\naction(\n    type=\"omrelp\"\n    target=\"{{ log_collector.address }}\"\n    port=\"{{ log_collector.port }}\"\n)\n{% else %}\n# --- no log collectors defined ---\n{% endfor %}\n\n# REMOTE LOGGING - Log Monitor and other external log collectors\n{% if ext_log_collectors %}\n{% for log_collector in ext_log_collectors -%}\n{% if loop.index == 1 %}\n# forward log messages to Log Monitor using RELP protocol\nif ($programname startswith 'ivxv-') and ($syslogfacility-text == 'local0') and\n   ($syslogseverity <= '6') then\n{% else %}\n# forward log messages to external log collector using RELP protocol\n{% endif -%}\naction(\n    type=\"omrelp\"\n    target=\"{{ log_collector.address }}\"\n    port=\"{{ log_collector.port }}\"\n)\n{% endfor %}\n{% else %}\n# --- no log montitors defined ---\n{% endif %}\n{#-\nvim:ft=jinja:\n#}\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/ivxv_status.jinja",
    "content": "{#\n\nIVXV Internet voting framework\n\nTemplate file for ivxv-status utility output.\n\n-#}\n{# COLLECTOR #}\n{% if 'collector' in sections %}\nCollector state: {{ collector_state }}\n{% endif %}\n\n{# ELECTION #}\n{% if 'election' in sections %}\nElection ID: {{ election['election-id'] or '-' }}\nVoting period config:\n    Service start: {{ election['servicestart'] or '-' }}\n    Service end: {{ election['servicestop'] or '-' }}\n    Verification end: {{ election['verificationstop'] or '-' }}\n    Election start: {{ election['electionstart'] or '-' }}\n    Election stop: {{ election['electionstop'] or '-' }}\nVoting phase: {{ election['phase'] or '-' }}\n    Phase start: {{ election['phase-start'] or '-' }}\n    Phase end:   {{ election['phase-end'] or '-' }}\n{% endif %}\n\n{# CONFIG #}\n{% if 'config' in sections %}\nConfig state:\n    Trust root config: {{ config.trust or '-' }}\n    Technical config: {{ config.technical or '-' }}\n    Election config: {{ config.election or '-' }}\n{% endif %}\n\n{# LIST #}\n{% if 'list' in sections %}\nLists state:\n    Choices list: {{ list.choices or '-' }}\n{%- if list.choices %}\n    Choices list state: {{ list.choices == list['choices-loaded'] and\n                           'LOADED TO SERVICE' or 'PENDING' }}\n{% endif %}\n    Districts list: {{ list.districts or '-' }}\n{%- if list.districts %}\n    Districts list state: {{ list.districts == list['districts-loaded'] and\n                           'LOADED TO SERVICE' or 'PENDING' }}\n{% endif %}\n    Voters lists: {{ list['voters-list-total']\n        }} [ APPLIED {{ list['voters-list-applied']\n        }} ; PENDING {{ list['voters-list-pending']\n        }} ; INVALID {{ list['voters-list-invalid']\n        }} ; SKIPPED {{ list['voters-list-skipped']\n        }} ]\n{%- if not list['voters0000'] %}\n        [No voter lists loaded]\n{% else %}\n{%- set ns = namespace(changeset_no=0) %}\n{%- for key in list if key.startswith('voters') and key.endswith('-state') %}\n        {% set ns.changeset_no = ns.changeset_no + 1 -%}\n        {% set prefix = key|replace('-state', '') -%}\n        {{ ns.changeset_no }}. {{ \"%-7s\"|format(list[prefix + '-state']) }} {{ list[prefix] }}\n{%- endfor %}\n{%- endif %}\n{% endif %}\n\n{# SERVICE #}\n{% if 'service' in sections %}\nServices: {{ service|count }} services in {{ network|count }} network(s)\n{%- for network_name in network|sort %}\n    Service network: {{ network_name }}, {{\n        network[network_name]|count }} service(s)\n{%- for service_id in network[network_name]|sort %}\n{% set service = network[network_name][service_id] %}\n        Service ID: {{ service_id }}\n          Type: {{ service['service-type'] }}\n          State: {{ service['state'] }}\n          Address: {{ service['ip-address'] }}\n          Technical config: {{ service['technical-conf-version']\n                               or '-' }}\n{%- if service['service-type'] not in ['backup', 'log'] %}\n          Election config: {{ service['election-conf-version']\n                              or '-' }}\n{%- endif %}\n{%- if service['service-type'] == 'backup' %}\n          Automatic backup times: {{ service['backup-times'] or '-' }}\n{%- endif %}\n{%- if 'tls-key' in service %}\n          Service TLS key: {{ service['tls-key'] or '-' }}\n          Service TLS certificate: {{ service['tls-cert'] or '-' }}\n{%- endif %}\n{%- if 'mid-token-key' in service %}\n          Mobile-ID/Smart-ID/Web eID identity token key: {{ service['mid-token-key'] or '-' }}\n{%- endif %}\n{%- if 'tspreg-key' in service %}\n          PKIX TSP registration key: {{ service['tspreg-key'] or '-' }}\n{%- endif %}\n          Last data received: {{ service['last-data'] or '-' }}\n          Background info: {{ service['bg_info'] or '-' }}\n{% endfor %}\n{% endfor %}\n{% endif -%}\n\n{# STORAGE #}\n{% if 'storage' in sections %}\nData storage:\n    Software packages: {{ storage.debs_exists|count }} exists, {{\n                          storage.debs_missing|count }} missing\n{%- if storage.debs_missing %}\n        Missing packages:\n{%- for pkg in storage.debs_missing %}\n            {{ pkg }}\n{%- endfor %}\n{%- endif %}\n    Users count: {{ user|count }}\n    Commands: {{\n        storage.command_files_active|count + storage.command_files_inactive|count }} loaded ({{\n        storage.command_files_active|count }} active, {{\n        storage.command_files_inactive|count }} inactive)\n        Active commands:\n        {%- for command_file in storage.command_files_active %}\n            {{ loop.index }}. {{ command_file }}\n        {%- endfor %}\n        Inactive commands:\n        {%- for command_file in storage.command_files_inactive %}\n            {{ loop.index }}. {{ command_file }}\n        {%- endfor %}\n{% endif %}\n{#-\nvim:ft=jinja:\n#}\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/ivxv_voting_facts_crontab.jinja",
    "content": "{#\n\nIVXV Internet voting framework\n\nCrontab template file to automate ivxv-storageorder tool.\n\n-#}\n# Crontab for IVXV Management Service ivxv-storageorder automation\n\n# This file is managed by IVXV Management Service\n# Current version is generated @ {{ time_generated }}\n\n# This file is (re)generated automatically on the following events:\n#\n# - `ivxv-cmd-load election <digitally signed election configuration>`\n#\n# This file is removed automatically on the following events:\n#\n# - `ivxv-collector-init`\n\n# Generate .csv file from Logmonitor logs of failed voting facts and run ivxv-storageorder on first active Collector host\n{{ minute }} {{ hour }} {{ day }} {{ month }} {{ weekday }} if [ -x /usr/bin/ivxv-voting-facts ]; then /usr/bin/ivxv-voting-facts; fi\n\n"
  },
  {
    "path": "collector-admin/ivxv_admin/templates/stats.json",
    "content": "{\n  \"data\": {\n    \"TOTAL\": {\n      \"age_group_16-17\": 0,\n      \"age_group_18-24\": 0,\n      \"age_group_25-34\": 0,\n      \"age_group_35-44\": 0,\n      \"age_group_45-54\": 0,\n      \"age_group_55-64\": 0,\n      \"age_group_65-74\": 0,\n      \"age_group_75plus\": 0,\n      \"authentication-methods\": [],\n      \"voting-operating-systems\": [],\n      \"verify-operating-systems\": [],\n      \"revoters-2-times\": 0,\n      \"revoters-3-times\": 0,\n      \"revoters-more-than-3-times\": 0,\n      \"revoters-total\": 0,\n      \"revotes-from-changed-ip\": 0,\n      \"revotes-with-different-card\": 0,\n      \"top-10-revoters\": [],\n      \"total-checkers\": 0,\n      \"total-voters\": 0,\n      \"total-votes-checked\": 0,\n      \"total-votes-collected\": 0,\n      \"votes-by-country\": [],\n      \"voters-females\": 0,\n      \"voters-males\": 0\n    }\n  },\n  \"meta\": {\n    \"generator\": \"IVXV Management Service Agent Daemon\",\n    \"time_generated\": \"2017-09-01T00:00:00\"\n  },\n  \"error\": \"IVXV Agent Daemon has not generated statistics. This is a default placeholder stats file from IVXV Collector Management Service package\"\n}\n"
  },
  {
    "path": "collector-admin/ivxv_admin/wsgi.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nWSGI application for collector management service.\n\nDefault path for application is \"/ivxv/cgi/\"\n\"\"\"\n\nimport datetime\nimport json\nimport logging\nimport os\nimport re\nimport urllib\nimport urllib.request\n\nimport bottle\nfrom bottle import request, response\n\nfrom . import (\n    COLLECTOR_STATE_CONFIGURED,\n    COLLECTOR_STATE_FAILURE,\n    COLLECTOR_STATE_INSTALLED,\n    COLLECTOR_STATE_NOT_INSTALLED,\n    COLLECTOR_STATE_PARTIAL_FAILURE,\n    EVENT_LOG_FILENAME,\n    MANAGEMENT_DAEMON_URL,\n    USER_ROLES,\n    __version__,\n)\nfrom .config import CONFIG, cfg_path\n\nFILE_UPLOAD_PATH = CONFIG['file_upload_path']\n\nAPP = bottle.Bottle()\n#: Default value for \"Expires\" header (don't allow to cache responses)\nEXPIRES_DEFAULT = datetime.datetime(1970, 1, 1)\n\nlog = logging.getLogger(__name__)\n\n\ndef abort(code=500, text='Unknown Error.'):\n    \"\"\"\n    Abort execution and raise a HTTP error.\n\n    :raises bottle.HTTPResponse:\n    \"\"\"\n    body = json.dumps(dict(message=text))\n    raise bottle.HTTPResponse(\n        body, code, headers={'content_type': 'application/json'})\n\n\n@APP.hook('before_request')\ndef log_query():\n    \"\"\"Log query data.\"\"\"\n    log.info('%s %s', request.method, request.urlparts.path)\n    if request.forms:\n        log.info('POST params: %s', dict(request.forms))\n    for key, fileobj in request.files.items():\n        log.info(\"FILE: field %r, filename %r\", key, fileobj.raw_filename)\n\n\n@APP.route('/context.json')\ndef context():\n    \"\"\"\n    Output context data (logged in user, collector state).\n\n    :return: Context data as JSON\n    :rtype: str\n    \"\"\"\n    # read user information from Apache environment\n    user_cn = request.environ.get('SSL_CLIENT_S_DN_CN')\n    assert user_cn\n    assert ',' in user_cn\n\n    # parse user information\n    surname, name, idcode = user_cn.split(',')\n    user_data = {\n        'cn': user_cn,\n        'user_name': f'{name} {surname}',\n        'idcode': idcode,\n        'role': [],\n        'role-description': [],\n        'permissions': [],\n    }\n\n    # detect user role\n    for role in sorted(USER_ROLES):\n        access_filename = cfg_path(\n            'permissions_path', '-'.join([user_cn, role]))\n        if os.path.exists(access_filename):\n            user_data['role'].append(role)\n            user_data['role-description'].append(\n                USER_ROLES[role]['description'])\n            user_data['permissions'] += USER_ROLES[role]['permissions']\n\n    # prepare context data\n    if not user_data['role']:\n        context_data = {}\n        user_data.update({\n            'role': ['none'],\n            'role-description': [USER_ROLES['none']['description']],\n            'permissions': USER_ROLES['none']['permissions'],\n        })\n    else:\n        filepath = cfg_path('admin_ui_data_path', 'status.json')\n        with open(filepath) as ifp:\n            state_json = json.load(ifp)\n            context_data = {\n                'collector': state_json['collector'],\n                'election': {\n                    'id': state_json['election']['election-id'],\n                    'stage': state_json['election']['phase']\n                }\n            }\n            context_data[\"collector\"][\"version\"] = __version__\n    user_data['permissions'] = sorted(set(user_data['permissions']))\n    context_data['current-user'] = user_data\n\n    if 'collector' in context_data:\n        state_info = {\n            COLLECTOR_STATE_NOT_INSTALLED: ['Paigaldamata', 'info'],\n            COLLECTOR_STATE_INSTALLED: ['Paigaldatud', 'info'],\n            COLLECTOR_STATE_CONFIGURED: ['Seadistatud', 'success'],\n            COLLECTOR_STATE_FAILURE: ['Tõrge', 'danger'],\n            COLLECTOR_STATE_PARTIAL_FAILURE: ['Osaline tõrge', 'danger'],\n        }\n        state = context_data['collector']['state']\n        context_data['collector'].update({\n            'state-description': state_info[state][0],\n            'state-indication': state_info[state][1],\n        })\n\n    # start response\n    response.content_type = 'application/json'\n    response.expires = EXPIRES_DEFAULT\n    return json.dumps({'data': context_data})\n\n\n@APP.post('/download-ballot-box')\n@APP.post('/download-consolidated-ballot-box')\n@APP.post('/skip-voters-list')\n@APP.post('/download-voter-detail-stats')\n@APP.post('/download-voting-sessions')\n@APP.post('/download-processor-input')\ndef fwd_request():\n    \"\"\"Forward request to IVXV Management Service Daemon.\"\"\"\n    path = request.fullpath.split('/')[-1]\n    daemon_url = MANAGEMENT_DAEMON_URL + path\n\n    data = urllib.parse.urlencode(request.forms).encode(\"UTF-8\")\n    fwd_request = urllib.request.Request(daemon_url, data=data, method=\"GET\")\n    max_response_size = 1024 * 1024 * 1024  # 1GB\n    with urllib.request.urlopen(fwd_request) as req_fp:\n        daemon_response = req_fp.read(max_response_size)\n        if len(daemon_response) == max_response_size:\n            response.content_type = \"application/json\"\n            return json.dumps(\n                {\n                    \"success\": False,\n                    \"message\": f\"Vastus on suurem kui {max_response_size} baiti\",\n                }\n            )\n        response.content_type = req_fp.headers[\"Content-Type\"] or \"application/json\"\n        for header in [\"Content-Length\", \"Content-Disposition\"]:\n            if req_fp.headers[header]:\n                response.set_header(header, req_fp.headers[header])\n\n    # start response\n    response.expires = EXPIRES_DEFAULT\n    return daemon_response\n\n\n@APP.post('/upload-config')\ndef upload_cfg():\n    \"\"\"\n    Upload command file.\n\n    Command file is a digitally signed file that contains one of the following\n    commands:\n\n    1. User management command (add, change role)\n    2. Collector config command (contains config file or voting list)\n    \"\"\"\n    upload = request.files.get('upload')\n    if upload is None:\n        abort(400, 'Üleslaaditav fail on määramata')\n\n    # save file to local directory\n    assert os.path.isdir(FILE_UPLOAD_PATH)\n    filename = '-'.join(\n        [datetime.datetime.now().strftime('%s.%f'), upload.filename])\n    file_path = os.path.join(FILE_UPLOAD_PATH, filename)\n    upload.save(file_path)\n\n    # forward command to management daemon\n    daemon_url = MANAGEMENT_DAEMON_URL + 'upload-command'\n    post_data = urllib.parse.urlencode({\n        'filename': filename,\n        'original_filename': upload.filename,\n        'cmd_type': request.forms.get('type')\n    })\n    post_data = post_data.encode('UTF-8')\n    with urllib.request.urlopen(daemon_url, post_data) as req_fp:\n        daemon_response = req_fp.read()\n\n    # start response\n    response.content_type = 'application/json'\n    response.expires = EXPIRES_DEFAULT\n    return daemon_response\n\n\n@APP.route('/eventlog')\ndef eventlog():\n    \"\"\"Convert Collector event log file to valid JSON.\n\n    :return: Event log as JSON\n    :rtype: str\n    \"\"\"\n    records = []\n    filepath = cfg_path('ivxv_admin_data_path', EVENT_LOG_FILENAME)\n    try:\n        with open(filepath) as fp:\n            while True:\n                line = fp.readline()\n                if not line:\n                    break\n                records.append(json.loads(line))\n    except OSError as err:\n        log.error(err)\n        abort(text=str(err))\n\n    # start response\n    response.content_type = 'application/json'\n    response.expires = EXPIRES_DEFAULT\n    return json.dumps({'data': records})\n\n\n@APP.route('/ballot-box-state')\ndef get_ballot_box_state():\n    \"\"\"Output state of ballot boxes.\n\n    :return: State as JSON\n    :rtype: str\n    \"\"\"\n    state = []\n    for filename in os.listdir(CONFIG['exported_votes_path']):\n        if not filename.endswith('.log'):\n            continue\n        timestamp = datetime.datetime.strptime(\n            re.sub(r'.+-(\\d.+).log', '\\\\1', filename),\n            '%Y.%m.%d_%H.%M')\n        filepath = os.path.join(CONFIG['exported_votes_path'], filename)\n        with open(filepath) as fp:\n            log_lines = fp.readlines()\n            if len(log_lines) > 12:\n                log_lines = log_lines[:6] + ['...\\n'] + log_lines[-6:]\n        zip_filename = filename.replace('.log', '.zip')\n        file_state = (\n            'ready'\n            if 'Collected votes archive is written to' in log_lines[-1]\n            else 'prepare')\n        state.append({\n            'timestamp': timestamp.strftime('%d.%m.%Y %H:%M'),\n            'filename': zip_filename,\n            'state': file_state,\n            'log': ''.join(log_lines),\n        })\n\n    # start response\n    response.content_type = 'application/json'\n    response.expires = EXPIRES_DEFAULT\n    return json.dumps({'data': state})\n"
  },
  {
    "path": "collector-admin/site/cgi/ivxv-admin.wsgi",
    "content": "# IVXV Internet voting framework\n\"\"\"\nCollectors administrator interface\nWSGI application definition for Apache web server\n\"\"\"\n\nfrom ivxv_admin import wsgi\n\napplication = wsgi.APP\n"
  },
  {
    "path": "collector-admin/site/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta http-equiv=\"refresh\" content=\"0;url=ivxv/index.html\">\n    <title>IVXV Koguja haldusteenus</title>\n    <script language=\"javascript\">\n    window.location.href = \"ivxv/index.html\"\n    </script>\n</head>\n\n<body>\n    Go to <a href=\"ivxv/index.html\">/ivxv/index.html</a>\n</body>\n\n</html>"
  },
  {
    "path": "collector-admin/site/ivxv/about.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Program information page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Tarkvara andmed – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">IVXV Kogumisteenuse haldusteenus</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <!-- div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div -->\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Versioon\n                        </div>\n                        <div class=\"panel-body\">\n                            <div>Elektroonilise hääletamise infosüsteem IVXV.</div>\n                            <div>Kogumisteenuse haldusteenus.</div>\n                            <div>\n                                Versioon:\n                                <span class=\"fa-2x\" id=\"version\">1.10.3</span>\n                            <div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-success\">\n                        <div class=\"panel-heading\">\n                            Kasutajaliides\n                        </div>\n                        <div class=\"panel-body\">\n                            Haldusteenuse kasutajaliidese funktsioonid:\n                            <ul>\n                                <li><a href=\"index.html\">Kogumisteenuse seisundi jälgimine</a>;</li>\n                                <li><a href=\"lists.html\">Valijate ja valikute nimekirjade haldus</a>;</li>\n                                <li><a href=\"stats.html\">Statistika vahendamine e-hääletuse kulgemise kohta</a>;</li>\n                                <li><a href=\"users.html\">Haldusteenuse kasutajate haldus</a>;</li>\n                                <li><a href=\"services.html\">Kogumisteenuse koosseisu haldus</a>;</li>\n                                <li><a href=\"downloads.html\">Allalaadimised</a> (e-valimiskast, hääletamise detailstatistika);</li>\n                                <li><a href=\"log.html\">Kogumisteenuse halduse sündmuste logi kuvamine</a>.</li>\n                            </ul>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-success\">\n                        <div class=\"panel-heading\">\n                            Masinloetavad andmed\n                        </div>\n                        <div class=\"panel-body\">\n                            Masinloetavad andmed (JSON):\n                            <ul>\n                                <li><a href=\"/ivxv/cgi/context.json\">Kasutajaliidese kontekst</a>;</li>\n                                <li><a href=\"/ivxv/data/status.json\">Kogumisteenuse olek</a>;</li>\n                                <li><a href=\"/ivxv/data/stats.json\">Hääletuse statistika</a>;</li>\n                                <li><a href=\"/ivxv/data/districts.json\">Ringkondade nimekiri</a>;</li>\n                                <li><a href=\"/ivxv/cgi/eventlog\">Haldusteenuse sündmuste logi</a>;</li>\n                                <li><a href=\"/ivxv/cgi/ballot-box-state\">Allalaadimiseks koostatud e-valimiskastide seisund</a>.</li>\n                            </ul>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    Kasutajaliidese ehitamiseks on kasutatud BootsTrap teemat\n                    <a href=\"https://github.com/BlackrockDigital/startbootstrap-sb-admin-2\">SB\n                    Admin 2</a>.\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/config.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - User management page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Seadistuste rakendamise seisund – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Seadistuste rakendamise seisund</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\" id=\"upload-row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Seadistuste üleslaadimine\n                        </div>\n                        <div id=\"collapseTwo\">\n                            <div class=\"panel-body\">\n                                <div id=\"upload-message\" class=\"alert alert-danger\" style=\"display: none\">\n                                    Faili üleslaadimise veateade\n                                </div>\n                                <form id=\"config-upload-form\" action=\"/ivxv/cgi/upload-config\" method=\"post\" enctype=\"multipart/form-data\">\n                                    <div class=\"form-group\">\n                                        <label>Teenuse seadistusfail</label>\n                                        <input type=\"file\" name=\"upload\" />\n                                    </div>\n                                    <div class=\"dropdown\">\n                                        Seadistuse tüüp:\n                                        <select id=\"drop\">\n                                            <option value=\"technical\">Tehniline seadistus</option>\n                                            <option value=\"election\">Valimiste seadistus</option>\n                                        </select>\n                                    </div>\n                                    <button id=\"file-upload-submit\" class=\"btn btn-primary\">\n                                        Laadi seadistus haldusteenusesse\n                                    </button>\n                                </form>\n                            </div>\n                        </div>\n                    </div>\n                    <!-- /.panel -->\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/config.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 10000);\n\n        reset_upload_form();\n        // set handler for file uploads\n        $('input[type=file]').on('change', prepareUpload);\n        $('#config-upload-form').on('submit', uploadFiles);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/downloads.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Downloading page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Väljavõtete allalaadimised – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Väljavõtete allalaadimised</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <!-- div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div -->\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n\n            </div>\n            <!-- /.row -->\n\n            <!-- Warning message panel -->\n            <div id=\"common-warning-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-warning\">\n                        <div class=\"panel-heading\">\n                            Hoiatus!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Hoiatusteade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-heading\">\n                            Hääletamise detailstatistika\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>\n                                Hääletamise detailstatistika koostatakse\n                                hääletusteenuses ja see on JSON-vormingus.\n                            </p>\n                            <form action=\"/ivxv/cgi/download-voter-detail-stats\" method=\"post\">\n                                <button class=\"btn btn-primary\">\n                                    <i class=\"fa fa-download\"></i>\n                                    Laadi hääletamise detailstatistika alla\n                                </button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-heading\">\n                            Hääletamise seansid\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>\n                                Hääletamise ja hääle kontrollimise seansside\n                                väljavõte koostatakse logiseire teenuses ja\n                                see on CSV-vormingus.\n                            </p>\n                            <form action=\"/ivxv/cgi/download-voting-sessions\" method=\"post\">\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input name=\"uniq\" type=\"checkbox\"> Unikaalsed seansid\n                                    </label>\n                                </div>\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input name=\"anonymize\" type=\"checkbox\"> Anonüümistatud seansid\n                                    </label>\n                                </div>\n                                <div class=\"checkbox\">\n                                    <label>\n                                        <input name=\"verifications\" type=\"checkbox\"> Hääle kontrollimisega seansid\n                                    </label>\n                                </div>\n                                <button class=\"btn btn-primary\">\n                                    <i class=\"fa fa-download\"></i>\n                                    Laadi hääletamise seansid alla\n                                </button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                    <div id=\"panel-download-form\" class=\"panel panel-default\" style='display: none;'>\n                        <div class=\"panel-heading\">\n                            E-valimiskasti koostamine\n                        </div>\n                        <div class=\"panel-body\">\n                            <div>\n                                <input type=\"checkbox\" name=\"consolidate\" value=\"1\" id=\"consolidate\">\n                                <label for=\"consolidate\">\n                                    Valimiskasti konsolideerimine valimiskastist tehtud\n                                    varukoopiate põhjal.\n                                </label>\n                                <div class=\"text-danger\">\n                                    Konsolideerimine on ressursinõudlik\n                                    protsess ja see on vajalik vaid juhul, kui\n                                    häälte kogumisel on esinenud probleeme\n                                    talletusteenusega.\n                                </div>\n                            </div>\n                            <div>\n                                <button class=\"btn btn-primary\" onclick=\"downloadBallot($('input[name=\\'consolidate\\']:checked').length)\">\n                                    <i class=\"fa fa-download\"></i>\n                                    <span>Koosta e-valimiskast</span>\n                                </button>\n                                <br />\n                                <br />\n                                <img id=\"loading\" src=\"../ivxv/ajax-loader.gif\" style=\"display: none;\" />\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div id=\"panel-download-ballot-box-template\" class=\"panel panel-default\" style='display: none;'>\n                        <div class=\"panel-heading\">\n                            E-valimiskasti allalaadimine\n                        </div>\n                        <div class=\"panel-body\">\n                            <div class=\"list-group\">\n\n                                <div class=\"list-group-item list-group-item-info\">\n                                    <i class=\"fa fa-spinner fa-fw\"></i>\n                                    <span>Varukoopia nimetus</span> (koostamisel)\n                                    <span class=\"pull-right text-muted small\">\n                                        <em>01.01.1970 00:00</em>\n                                    </span>\n                                    <pre style=\"text-align: left; display: none;\">Log lines</pre>\n                                </div>\n\n                                <a href=\"#\" class=\"list-group-item list-group-item-success\">\n                                    <i class=\"fa fa-download fa-fw\"></i>\n                                    <span>Varukoopia nimetus</span>\n                                    <span class=\"pull-right text-muted small\">\n                                        <em>01.01.1970 00:00</em>\n                                    </span>\n                                </a>\n\n                                <div class=\"list-group-item list-group-item-danger\">\n                                    <i class=\"fa fa-warning fa-fw\"></i>\n                                    <span>Veateade</span>\n                                    <span class=\"pull-right text-muted small\">\n                                        <em>01.01.1970 00:00</em>\n                                    </span>\n                                </div>\n\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-heading\">\n                            Töötlemisrakenduse sisendi alus\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>\n                                Töötlemisrakenduse sisendi alus on häälte\n                                töötlemiseks vajalike sisendfailide komplekt\n                                ZIP-vormingus konteinerina ja see koostatakse\n                                haldusteenuses.\n                            </p>\n                            <form action=\"/ivxv/cgi/download-processor-input\" method=\"post\">\n                                <button class=\"btn btn-primary\">\n                                    <i class=\"fa fa-download\"></i>\n                                    Laadi töötlemisrakenduse sisendi alus alla\n                                </button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/downloads.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 10000);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/index.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Index (overview) page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Üldseisund – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Kogumisteenuse üldseisund</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div id=\"electionid\" style=\"display: none\" class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-primary\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-12\">\n                                    <div>Valimiste identifikaator:</div>\n                                    <div id=\"election-id\" class=\"fa-2x\">&nbsp;</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div class=\"col-lg-3 col-md-6\">\n                    <div id=\"collector-state-panel\" class=\"panel\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-12\">\n                                    <div>Kogumisteenuse seisund:</div>\n                                    <div id=\"collector-state\" class=\"fa-2x\"></div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div id=\"voting-phase\" class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-12\">\n                                    <div>Käimasolev hääletusetapp:</div>\n                                    <div id=\"voting-stage\" class=\"fa-2x\">&nbsp;</div>\n                                </div>\n                            </div>\n                        </div>\n                        <ul class=\"list-group\">\n                            <li class=\"list-group-item\">Algus: <span id=\"voting-stage-start\">-</span></li>\n                            <li class=\"list-group-item\">Lõpp: <span id=\"voting-stage-end\">-</span></li>\n                        </ul>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div class=\"col-lg-3 col-md-6\">\n                    <div id=\"voters-list-update-available\" class=\"panel panel-yellow\" style=\"display: none\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-12 fa-2x\">\n                                    <div>Valijanimekirjade uuendus on saadaval!</div>\n                                </div>\n                            </div>\n                        </div>\n                        <a href=\"lists.html\">\n                            <div class=\"panel-footer\">\n                                <span class=\"pull-left\">Vaata lähemalt</span>\n                                <span class=\"pull-right\"><i class=\"fa fa-arrow-circle-right\"></i></span>\n                                <div class=\"clearfix\"></div>\n                            </div>\n                        </a>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n            </div>\n            <!-- /.row -->\n            <div class=\"row\">\n                <div class=\"col-lg-6 col-md-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Haldusteenusesse laaditud seadistuste versioonid:\n                        </div>\n                        <ul class=\"list-group\">\n                            <li id=\"trust\" class=\"list-group-item\"><strong>Usaldusjuure seadistused:</strong> <span id=\"config-trust\">-</span></li>\n                            <li id=\"tech\" class=\"list-group-item\"><strong>Tehnilised seadistused:</strong> <span id=\"config-tech\">-</span></li>\n                            <li id=\"election\" class=\"list-group-item\"><strong>Valimiste seadistused:</strong> <span id=\"config-election\">-</span></li>\n                        </ul>\n                    </div>\n                    <!-- /.panel -->\n                </div>\n                <!-- /.col-lg-6 -->\n                <div class=\"col-lg-6 col-md-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Valimisnimekirjade seisundid ja versioonid:\n                        </div>\n                        <ul class=\"list-group\">\n                            <li id=\"choiceslist\" class=\"list-group-item\">\n                                <strong>Valikute nimekiri:</strong>\n                                <span id=\"list-choices\">-</span></li>\n                            <li id=\"choicesliststatus\" class=\"list-group-item\">\n                                <strong>Valikute nimekirja seisund:</strong>\n                                <span id=\"list-choices-status\">-</span></li>\n                            <li id=\"districtslist\" class=\"list-group-item\">\n                                <strong>Ringkondade nimekiri:</strong>\n                                <span id=\"list-districts\">-</span>\n                            </li>\n                            <li id=\"districtsliststatus\" class=\"list-group-item\">\n                                <strong>Ringkondade nimekirja seisund:</strong>\n                                <span id=\"list-districts-status\">-</span></li>\n                            <li id=\"voterslist\" class=\"list-group-item\">\n                                <strong>Valijate nimekirjad\n                                <span id=\"list-voters-total\" class=\"badge\">-</span>\n                                </strong>\n                                &nbsp;\n                                <span class=\"label label-primary\">\n                                rakendamise ootel <span id=\"list-voters-pending\" class=\"badge\">-</span>\n                                </span>\n                                &nbsp;\n                                <span class=\"label label-primary\">\n                                rakendatud <span id=\"list-voters-loaded\" class=\"badge\">-</span>\n                                </span>\n                                &nbsp;\n                                <span class=\"label label-primary\">\n                                vigaseid <span id=\"list-voters-invalid\" class=\"badge\">-</span>\n                                </span>\n                                &nbsp;\n                                <span class=\"label label-primary\">\n                                vahele jäetud <span id=\"list-voters-skipped\" class=\"badge\">-</span>\n                                </span>\n                                &nbsp;\n                                <span class=\"label label-info\">\n                                saadaval <span id=\"list-voters-available\" class=\"badge\">-</span>\n                                </span>\n                            </li>\n                            <div id=\"list-list\"></div>\n                        </ul>\n                    </div>\n                    <!-- /.panel -->\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div id=\"service_summary\" style=\"display: none\" class=\"col-lg-6\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Teenuste kokkuvõte seisundi järgi\n                        </div>\n                        <div class=\"panel-body\">\n                            Paigaldamata: <span id=\"not_installed\"></span>\n                            <br/> Paigaldatud: <span id=\"installed\"></span>\n                            <br/> Seadistatud: <span id=\"configured\"></span>\n                            <br/> Osaline tõrge: <span id=\"p_failure\"></span>\n                            <br/> Tõrge: <span id=\"failure\"></span>\n                            <br/> Eemaldatud: <span id=\"removed\"></span>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n                <div class=\"col-lg-6\">\n                    <div class=\"panel-group\">\n                        <div id=\"packagepanel\" class=\"panel panel-danger\">\n                            <div class=\"panel-heading\">\n                                Kogumisteenuse tarkvarapakid:\n                                <span id=\"debs-exist-count\">–</span> olemas,\n                                <span id=\"debs-missing-count\">–</span> puudu\n                            </div>\n                            <!-- /.panel-heading -->\n                            <div id=\"missing-debs-list\" class=\"panel-group\" style=\"margin-bottom:0px;\"></div>\n                        </div>\n                        <!-- /.panel -->\n                        <div class=\"panel panel-info\">\n                            <div class=\"panel-heading\">\n                                Kasutajate arv: <span id=\"user-count\"></span>\n                            </div>\n                            <!-- /.panel-heading -->\n                        </div>\n                        <!-- /.panel -->\n                        <div class=\"panel panel-info\">\n                            <div class=\"panel-heading\">\n                                Korraldused <span id=\"command-files-count\" class=\"badge\">-</span>\n                                &nbsp;\n                                <span class=\"label label-primary\">\n                                aktiivseid <span id=\"command-files-active-count\" class=\"badge\">-</span>\n                                </span>\n                                &nbsp;\n                                <span class=\"label label-default\">\n                                arhiveeritud <span id=\"command-files-inactive-count\" class=\"badge\">-</span></li>\n                                </span>\n                            </div>\n                            <!-- /.panel-heading -->\n                        </div>\n                        <!-- /.panel -->\n                    </div>\n                    <!-- /.panel-group -->\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/index.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 5000);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/lists.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Voting lists management page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Valimiste nimekirjad – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Valimiste nimekirjad</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                    <div class=\"alert alert-info\">\n                        Nimekirjade rakendamise logid on jälgitavad\n                        <a href=\"config.html\" class=\"alert-link\">seadistuste lehel</a>.\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div id=\"panel-choices-list\" class=\"panel\">\n                        <div class=\"panel-heading\">\n                            Valikute nimekiri\n                        </div>\n                        <div class=\"panel-body\">\n                            <div class=\"row\">\n                                <div id=\"panel-choices-list-not-loaded\" class=\"col-lg-12\" style=\"display: none\">\n                                    <div class=\"alert alert-danger\">\n                                        Valikute nimekiri on laadimata\n                                    </div>\n                                </div>\n                                <div id=\"panel-choices-list-data\" class=\"col-lg-12\">\n                                    <ul class=\"list-group\">\n                                        <li class=\"list-group-item\"><strong>Valikute nimekiri:</strong> <span id=\"list-choices\">-</span></li>\n                                        <li class=\"list-group-item\"><strong>Valikute nimekirja seisund:</strong> <span id=\"list-choices-status\">-</span></li>\n                                    </ul>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div id=\"panel-voters-list\" class=\"panel\">\n                        <div class=\"panel-heading\">\n                            Valijate nimekirjad\n                        </div>\n\n                        <div class=\"panel-body\">\n                            <div class=\"row\">\n                                <div id=\"panel-voters-list-data\" class=\"col-lg-12\">\n                                    <ul class=\"list-group\">\n                                        <li class=\"list-group-item\">\n                                            <strong>Valijate nimekirjad\n                                                <span id=\"list-voters-total\" class=\"badge\">-</span>\n                                            </strong>\n                                            &nbsp;\n                                            <span class=\"label label-primary\">\n                                            rakendamise ootel <span id=\"list-voters-pending\" class=\"badge\">-</span>\n                                            </span>\n                                            &nbsp;\n                                            <span class=\"label label-primary\">\n                                            rakendatud <span id=\"list-voters-loaded\" class=\"badge\">-</span>\n                                            </span>\n                                            &nbsp;\n                                            <span class=\"label label-primary\">\n                                            vigaseid <span id=\"list-voters-invalid\" class=\"badge\">-</span>\n                                            </span>\n                                            &nbsp;\n                                            <span class=\"label label-primary\">\n                                            vahele jäetud <span id=\"list-voters-skipped\" class=\"badge\">-</span>\n                                            </span>\n                                            &nbsp;\n                                            <span class=\"label label-info\">\n                                            saadaval <span id=\"list-voters-available\" class=\"badge\">-</span>\n                                            </span>\n                                        </li>\n                                        <div id=\"list-list\"></div>\n                                    </ul>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div id=\"panel-districts-list\" class=\"panel\">\n                        <div class=\"panel-heading\">\n                            Ringkondade nimekiri\n                        </div>\n                        <div class=\"panel-body\">\n                            <div class=\"row\">\n                                <div id=\"panel-districts-list-data\" class=\"col-lg-12\">\n                                    <ul class=\"list-group\">\n                                        <li class=\"list-group-item\"><strong>Ringkondade nimekiri:</strong> <span id=\"list-districts\">-</span></li>\n                                        <li class=\"list-group-item\"><strong>Ringkondade nimekirja seisund:</strong> <span id=\"list-districts-status\">-</span></li>\n                                    </ul>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            <h4 class=\"panel-title\">\n                            Nimekirjade üleslaadimine\n                        </h4>\n                        </div>\n                        <div class=\"panel-body\">\n                            <div id=\"upload-message\" class=\"alert alert-danger\" style=\"display: none\">\n                                Faili üleslaadimise veateade\n                            </div>\n                            <form id=\"config-upload-form\" action=\"/ivxv/cgi/upload-config\" method=\"post\" enctype=\"multipart/form-data\">\n                                <div class=\"form-group\">\n                                    <div class=\"form-group\">\n                                        <label>Nimekiri</label>\n                                        <input type=\"file\" name=\"upload\" />\n                                    </div>\n                                </div>\n                                <div class=\"dropdown\" id=\"drop\">\n                                    Nimekirja tüüp:\n                                    <br />\n                                    <select>\n                                        <option id=\"choicesoption\" value=\"choices\">Valikute nimekiri</option>\n                                        <option id=\"votersoption\" value=\"voters\">Valijate nimekiri</option>\n                                        <option value=\"districts\">Ringkondade nimekiri</option>\n                                    </select>\n                                </div>\n                                <button id=\"file-upload-submit\" class=\"btn btn-primary\">Laadi nimekiri haldusteenusesse</button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/lists.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 10000);\n        reset_upload_form();\n        // set handler for file uploads\n        $('input[type=file]').on('change', prepareUpload);\n        $('#config-upload-form').on('submit', uploadFiles);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/log.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Event log watching page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Haldussündmuste logi – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- DataTables CSS -->\n    <link href=\"../vendor/datatables/datatables.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Haldussündmuste logi</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <table id=\"dataTables-log\" width=\"100%\" class=\"table table-striped table-bordered table-hover\">\n                        <thead>\n                            <tr>\n                                <th>Aeg</th>\n                                <th>Teenus</th>\n                                <th>Tase</th>\n                                <th>Sündmus</th>\n                                <th>Kirjeldus</th>\n                            </tr>\n                        </thead>\n                    </table>\n                    <!-- /.table-responsive -->\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- DataTables JavaScript -->\n    <script src=\"../vendor/datatables/datatables.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/log.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/services.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Services overview page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Teenused – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Hallatavad teenused</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Teenuste kokkuvõte seisundi järgi\n                        </div>\n                        <div class=\"panel-body\">\n                            Paigaldamata: <span id=\"not_installed\"></span>\n                            <br/> Paigaldatud: <span id=\"installed\"></span>\n                            <br/> Seadistatud: <span id=\"configured\"></span>\n                            <br/> Tõrge: <span id=\"failure\"></span>\n                            <br/> Eemaldatud: <span id=\"removed\"></span>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n                <div id=\"service_list_table\" style=\"display: none;\" class=\"col-lg-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Kogumisteenuse koosseisus olevad teenused\n                        </div>\n                        <div class=\"panel-body\">\n                            <table width=\"100%\" class=\"table table-striped table-bordered table-hover\">\n                                <thead>\n                                    <tr>\n                                        <th>#</th>\n                                        <th>ID</th>\n                                        <th>Võrk</th>\n                                        <th>Liik</th>\n                                        <th>Seisund</th>\n                                    </tr>\n                                </thead>\n                                <tbody id=\"services-list\">\n                                </tbody>\n                            </table>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/services.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 15000);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/stats.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - Statistics page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>E-hääletamise statistika – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">E-hääletamise statistika</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div id=\"stats-error\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga hääletamise statistika hankimisel\n                        </div>\n                        <div class=\"panel-body\">\n                            <p id=\"stats-error-msg\">Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-6\">\n                </div>\n                <!-- /.col-lg-6 -->\n\n                <div class=\"col-lg-6\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p>Ringkondade ja jaoskondade filter:</p>\n                            <select id=\"districts\" onchange=\"loadPageData();\">\n                                <option value=\"TOTAL\">Kõik</option>\n                            </select>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-6 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-primary\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-3\">\n                                    <i class=\"fa-regular fa-envelope fa-5x\"></i>\n                                </div>\n                                <div class=\"col-xs-9 text-right\">\n                                    <div id=\"total-votes-collected\" class=\"huge\">&nbsp;</div>\n                                    <div>Kogutud häält</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-primary\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-3\">\n                                    <i class=\"fa fa-user-group fa-5x\"></i>\n                                </div>\n                                <div class=\"col-xs-9 text-right\">\n                                    <div id=\"total-voters\" class=\"huge\">&nbsp;</div>\n                                    <div>Hääletajat</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-green\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-3\">\n                                    <i class=\"fa fa-check fa-5x\"></i>\n                                </div>\n                                <div class=\"col-xs-9 text-right\">\n                                    <div id=\"total-votes-checked\" class=\"huge\">&nbsp;</div>\n                                    <div>Kontrollitud häält</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n                <div class=\"col-lg-3 col-md-6\">\n                    <div class=\"panel panel-green\">\n                        <div class=\"panel-heading\">\n                            <div class=\"row\">\n                                <div class=\"col-xs-3\">\n                                    <i class=\"fa fa-mobile-screen-button fa-5x\"></i>\n                                </div>\n                                <div class=\"col-xs-9 text-right\">\n                                    <div id=\"total-checkers\" class=\"huge\">&nbsp;</div>\n                                    <div>Hääle kontrollijat</div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-3 -->\n            </div>\n            <!-- /.row -->\n\n            <div class=\"row\">\n                <div class=\"col-lg-4\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Hääletajate jaotumine\n                        </div>\n                        <div class=\"panel-body\">\n                            <table width=\"100%\" class=\"table table-striped table-bordered table-hover\">\n                                <tbody id=\"voters-table\" class=\"text-right\">\n                                    <tr class=\"info\">\n                                        <th colspan=\"2\">Sugu</th>\n                                    </tr>\n                                    <tr>\n                                        <td>Mehi</td>\n                                        <td id=\"voters-males\" class=\"text-right\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>Naisi</td>\n                                        <td id=\"voters-females\" class=\"text-right\">&nbsp;</td>\n                                    </tr>\n                                    <tr class=\"info\">\n                                        <th colspan=\"2\">Vanusegrupp</th>\n                                    </tr>\n                                    <tr>\n                                        <td>16-17</td>\n                                        <td id=\"age_group_16-17\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>18-24</td>\n                                        <td id=\"age_group_18-24\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>25-34</td>\n                                        <td id=\"age_group_25-34\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>35-44</td>\n                                        <td id=\"age_group_35-44\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>45-54</td>\n                                        <td id=\"age_group_45-54\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>55-64</td>\n                                        <td id=\"age_group_55-64\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>65-74</td>\n                                        <td id=\"age_group_65-74\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                    <tr>\n                                        <td>75+</td>\n                                        <td id=\"age_group_75plus\">&nbsp;</td>\n                                    </tr>\n                                </tbody>\n                                <tbody id=\"auth-os\" class=\"text-right\">\n                                </tbody>\n                            </table>\n                            <!-- /.table-responsive -->\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-4 -->\n                <div class=\"col-lg-4\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Korduvhääletamiste statistika\n                        </div>\n                        <div class=\"panel-body\">\n                            <table width=\"100%\" class=\"table table-striped table-bordered table-hover\">\n                                <tbody class=\"text-right\">\n                                    <tr>\n                                        <td>Täpselt 2 korda hääletanute arv</td>\n                                        <td id=\"revoters-2-times\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>Täpselt 3 korda hääletanute arv</td>\n                                        <td id=\"revoters-3-times\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>Rohkem kui 3 korda hääletanute arv</td>\n                                        <td id=\"revoters-more-than-3-times\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>Korduvhääletamised muutunud IP-aadressilt</td>\n                                        <td id=\"revotes-from-changed-ip\">&nbsp;</td>\n                                    </tr>\n                                    <tr>\n                                        <td>Korduvhääletamised erineva kaardiga</td>\n                                        <td id=\"revotes-with-different-card\">&nbsp;</td>\n                                    </tr>\n                                </tbody>\n                                <tbody id=\"table-revoters\" class=\"text-right\">\n                                </tbody>\n                            </table>\n                            <!-- /.table-responsive -->\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-4 -->\n                <div class=\"col-lg-4\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            Hääletajate jagunemine riigiti\n                        </div>\n                        <div class=\"panel-body\">\n                            <table width=\"100%\" class=\"table table-striped table-bordered table-hover\">\n                                <tbody id=\"table-countries\" class=\"text-right\">\n                                </tbody>\n                            </table>\n                            <!-- /.table-responsive -->\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-4 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/stats.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 10000);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/ivxv/users.html",
    "content": "<!--\nIVXV Internet voting framework\n\nCollector management service - Web interface - User management page\n-->\n<!DOCTYPE html>\n<html lang=\"et\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n    <title>Haldusteenuse kasutajad – IVXV Kogumisteenuse haldusteenus</title>\n    <!-- Bootstrap Core CSS -->\n    <link href=\"../vendor/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <!-- MetisMenu CSS -->\n    <link href=\"../vendor/metisMenu/metisMenu.min.css\" rel=\"stylesheet\">\n    <!-- Custom CSS -->\n    <link href=\"../dist/css/sb-admin-2.css\" rel=\"stylesheet\">\n    <!-- Custom Fonts -->\n    <link href=\"../vendor/font-awesome/css/all.min.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n    <div id=\"wrapper\">\n        <!-- Navigation -->\n        <nav class=\"navbar navbar-default navbar-static-top\" role=\"navigation\" style=\"margin-bottom: 0\">\n            <div class=\"navbar-header\">\n                <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                    <span class=\"sr-only\">Toggle navigation</span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                    <span class=\"icon-bar\"></span>\n                </button>\n                <a class=\"navbar-brand\" href=\"index.html\">Kogumisteenuse haldusteenus</a>\n            </div>\n            <!-- /.navbar-header -->\n            <ul class=\"nav navbar-top-links navbar-right\">\n                <li class=\"dropdown\">\n                    <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                        <i class=\"fa fa-user fa-fw\"></i> <i class=\"fa fa-caret-down\"></i>\n                    </a>\n                    <ul class=\"dropdown-menu dropdown-user\" style=\"padding: 10px;\">\n                        <li><i class=\"fa fa-user fa-fw\"></i> Kasutaja andmed</li>\n                        <li>CN:&nbsp;<span id=\"user-cn\">-</span></li>\n                        <li>Isikukood:&nbsp;<span id=\"user-idcode\">-</span></li>\n                        <li>Roll:&nbsp;<span id=\"user-role-description\">-</span></li>\n                    </ul>\n                    <!-- /.dropdown-user -->\n                </li>\n                <!-- /.dropdown -->\n            </ul>\n            <!-- /.navbar-top-links -->\n            <div class=\"navbar-default sidebar\" role=\"navigation\">\n                <div class=\"sidebar-nav navbar-collapse\">\n                    <ul class=\"nav\" id=\"side-menu\">\n                        <li>\n                            <a href=\"index.html\"><i class=\"fa fa-dashboard fa-fw\"></i> Üldseisund</a>\n                        </li>\n                        <li>\n                            <a href=\"lists.html\"><i class=\"fa fa-list fa-fw\"></i> Nimekirjad</a>\n                        </li>\n                        <li>\n                            <a href=\"stats.html\"><i class=\"fa fa-chart-column fa-fw\"></i> Statistika</a>\n                        </li>\n                        <li>\n                            <a href=\"users.html\"><i class=\"fa fa-user fa-fw\"></i> Kasutajad</a>\n                        </li>\n                        <li>\n                            <a href=\"services.html\"><i class=\"fa fa-sitemap fa-fw\"></i> Teenused</a>\n                        </li>\n                        <li>\n                            <a href=\"downloads.html\"><i class=\"fa fa-download fa-fw\"></i> Allalaadimised</a>\n                        </li>\n                        <li>\n                            <a href=\"config.html\"><i class=\"fa fa-wrench fa-fw\"></i> Seadistused</a>\n                        </li>\n                        <li>\n                            <a href=\"log.html\"><i class=\"fa fa-file-text fa-fw\"></i> Logiraamat</a>\n                        </li>\n                        <li>\n                            <a href=\"about.html\"><i class=\"fa fa-info-circle fa-fw\"></i> Programmi andmed</a>\n                        </li>\n                    </ul>\n                </div>\n                <!-- /.sidebar-collapse -->\n            </div>\n            <!-- /.navbar-static-side -->\n        </nav>\n        <div id=\"page-wrapper\">\n\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <h1 class=\"page-header\">Haldusteenuse kasutajad</h1>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Data loading status -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-default\">\n                        <div class=\"panel-body\">\n                            <p id=\"loadstatus\">Andmete laadimise seisund</p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <!-- /.row -->\n\n            <!-- Error message panel -->\n            <div id=\"common-error-msg\" class=\"row\" style=\"display: none;\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-red\">\n                        <div class=\"panel-heading\">\n                            Viga!\n                        </div>\n                        <div class=\"panel-body\">\n                            <p>Veateade</p>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Users list -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-primary\">\n                        <div class=\"panel-heading\">\n                            <h4 class=\"panel-title\">\n                                Kasutajate nimekiri\n                            </h4>\n                        </div>\n                        <div class=\"panel-body\">\n                            <table class=\"table table-striped\">\n                                <thead>\n                                    <tr>\n                                        <th class=\"col-md-1\">#</th>\n                                        <th>CN</th>\n                                        <th>Rollid</th>\n                                    </tr>\n                                </thead>\n                                <tbody id=\"user-list\">\n                                </tbody>\n                            </table>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n            <!-- Command upload form -->\n            <div class=\"row\">\n                <div class=\"col-lg-12\">\n                    <div class=\"panel panel-info\">\n                        <div class=\"panel-heading\">\n                            <h4 class=\"panel-title\">\n                                Volituste rakendamine\n                            </h4>\n                        </div>\n                        <div class=\"panel-body\">\n                            <div id=\"upload-message\" class=\"alert alert-danger\" style=\"display: none\">\n                                Faili üleslaadimise veateade\n                            </div>\n                            <form id=\"config-upload-form\" action=\"/ivxv/cgi/upload-config\" method=\"post\" enctype=\"multipart/form-data\">\n                                <div class=\"form-group\">\n                                    <div class=\"form-group\">\n                                        <label>Kasutaja volituste seadistusfail</label>\n                                        <input type=\"file\" name=\"upload\" />\n                                    </div>\n                                </div>\n                                <button id=\"file-upload-submit\" class=\"btn btn-primary\">Rakenda volitused</button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                <!-- /.col-lg-12 -->\n            </div>\n            <!-- /.row -->\n\n        </div>\n        <!-- /#page-wrapper -->\n    </div>\n    <!-- /#wrapper -->\n\n    <!-- jQuery -->\n    <script src=\"../vendor/jquery/jquery.min.js\"></script>\n    <!-- Bootstrap Core JavaScript -->\n    <script src=\"../vendor/bootstrap/js/bootstrap.min.js\"></script>\n    <!-- Metis Menu Plugin JavaScript -->\n    <script src=\"../vendor/metisMenu/metisMenu.min.js\"></script>\n    <!-- Custom Theme JavaScript -->\n    <script src=\"../dist/js/sb-admin-2.js\"></script>\n    <!-- IVXV JavaScript -->\n    <script src=\"../js/ivxv.js\"></script>\n    <!-- Page related JavaScript -->\n    <script src=\"../js/users.js\"></script>\n    <!-- Page-Level Scripts -->\n    <script>\n    $(document).ready(function() {\n        getContextData();\n        loadPageData();\n        setInterval(loadPageData, 10000);\n        reset_upload_form();\n        // set handler for file uploads\n        $('input[type=file]').on('change', prepareUpload);\n        $('#config-upload-form').on('submit', uploadFiles);\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "collector-admin/site/js/config.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Configuration status page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n       * HTTP GET response that contains HTML tags is not allowed!\n       * state is always an JSON object\n       */\n      state = sanitizeJSON(state);\n      display_cfg_panel(\n        'trust', state, state['config']['trust'], 'Usaldusjuure seadistus');\n      display_cfg_panel(\n        'technical', state, state['config-apply']['technical'], 'Tehniline seadistus');\n      display_cfg_panel(\n        'election', state, state['config-apply']['election'], 'Valimiste seadistus');\n      display_cfg_panel(\n        'choices', state, state['config-apply']['choices'], 'Valikute nimekiri');\n      display_cfg_panel(\n        'districts', state, state['list']['districts'], 'Ringkondade nimekiri');\n      var changeset_no;\n      display_cfg_panel(\n        'voters0000', state, state['config-apply']['voters0000'], 'Valijate nimekiri (algne)');\n      for (var i = 1; i < 10000; i++) {\n        var iStr = 'voters' + String(i).padStart(4, '0');\n        if (!(iStr + '-state' in state['list']))\n          break;\n        if (iStr in state['config-apply']) {\n          display_cfg_panel(\n            iStr, state, state['config-apply'][iStr],\n            'Valijate muudatusnimekiri nr. ' + i);\n        } else {\n          display_cfg_panel(\n            iStr, state, state['list'][iStr],\n            'Valijate muudatusnimekiri nr. ' + i);\n        }\n      }\n\n      hideErrorMessage();\n\n      // data loading stats\n      var genDate = new Date();\n      genDate.setTime(Date.parse(state['meta']['time_generated']));\n      $('#loadstatus')\n        .removeClass('text-danger')\n        .addClass('text-info')\n        .html(\n          'Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n          'Andmete genereerimise aeg: ' + genDate.toLocaleTimeString('et-EE', {}));\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n}\n\nvar state_filenames = {};\n\n/**\n * Display config state panel\n *\n * @param {string} id_prefix\n * @param {Object} state\n * @param {Object} cfg\n * @param {string} title\n */\nfunction display_cfg_panel(id_prefix, state, cfg, title) {\n  id_prefix = sanitizePrimitive(id_prefix);\n  state = sanitizeJSON(state);\n  cfg = sanitizeJSON(cfg);\n  // Create panel if required\n  var panel = $('#' + id_prefix + '-cfg-state-panel');\n  if (!panel.length) {\n    $('#upload-row').before(\n      '<div class=\"row\">' +\n      '    <div class=\"col-lg-12\">' +\n      '        <div id=\"' + id_prefix + '-cfg-state-panel\" class=\"panel\">' +\n      '            <div class=\"panel-heading\">' +\n      '                <h4 class=\"panel-title\">' + sanitizePrimitive(title) + '</h4>' +\n      '            </div>' +\n      '            <div class=\"panel-body\">' +\n      '              <div />' + // Placeholder for config info text\n      '              <button type=\"button\" class=\"btn btn-default\" onClick=\"toggle_apply_log(this);\">Rakendamise logi</button>' +\n      '              <pre style=\"display: none;\" class=\"pre-scrollable\" />' + // Placeholder for log content\n      '            </div>' +\n      '        </div>' +\n      '    </div>' +\n      '</div>'\n    );\n    panel = $('#' + id_prefix + '-cfg-state-panel');\n  }\n\n  // Configure panel\n  var panel_body = panel.find('.panel-body');\n  panel\n    .removeClass('panel-green')\n    .removeClass('panel-warning')\n    .removeClass('panel-danger');\n  if ((id_prefix === 'trust') || (id_prefix === 'districts')) {\n    var ver_element_id = 'cfg-ver-' + id_prefix;\n    panel.addClass(cfg === null ? 'panel-danger' : 'panel-green');\n    panel_body\n      .find('div:first')\n      .html(\n        '<div>Seisund: ' + (cfg === null ? 'Laadimata' : 'Rakendatud haldusteenusele') + '</div>' +\n        '<div>' +\n        (cfg === null ? '-' : 'Versioon: <span id=\"' + ver_element_id + '\"></span>') +\n        '</div>');\n    outputCmdVersion('#' + ver_element_id, id_prefix, state)\n    panel_body.find('button').hide();\n  } else if (cfg === undefined) {\n    panel.addClass('panel-danger');\n    panel_body.find('div:first').html('<div>Laadimata</div>');\n    panel_body.find('button').hide();\n  } else {\n    state_filenames[id_prefix] = cfg['state_file'];\n    if (cfg['completed']) {\n      panel.addClass('panel-green');\n    } else {\n      panel.addClass('panel-warning');\n    }\n\n    if (id_prefix.startsWith('voters')) {\n      stateStr = voterListStateDescriptions.get(state['list'][id_prefix + '-state']);\n    } else {\n      stateStr = cfg['completed'] ? 'rakendatud' : 'rakendamisel';\n    }\n    if (id_prefix.startsWith('voters') && !(id_prefix in state['config-apply'])) {\n      panel_body\n        .find('div:first')\n        .html(\n          '<div>Seisund: ' + sanitizePrimitive(stateStr) + '</div>' +\n          '<div>Rakendatav versioon: <span id=\"cfg-ver-' + id_prefix + '\">[ määramata ]</a></div>' +\n          '<div>Rakendamise katseid: 0</div>'\n        );\n    } else {\n      panel_body\n        .find('div:first')\n        .html(\n          '<div>Seisund: ' + sanitizePrimitive(stateStr) + '</div>' +\n          '<div>Rakendatav versioon: <span id=\"cfg-ver-' + id_prefix + '\">' + cfg['version'] + '</a></div>' +\n          '<div>Rakendamise katseid: ' + cfg['attempts'] + '</div>'\n        );\n      outputCmdVersion('#cfg-ver-' + id_prefix, id_prefix, state);\n    }\n\n    if (cfg['attempts']) {\n      panel_body.find('button').show('slow');\n      var logbox = panel_body.find('pre');\n      if (logbox.is(':visible')) {\n        refresh_log(logbox, cfg['state_file']);\n      }\n    } else {\n      panel_body.find('button').hide('slow');\n      panel_body.find('pre').hide('slow');\n    }\n  }\n}\n\n/**\n * Toggle config log box\n */\nfunction toggle_apply_log(toggle_button) {\n  var parent_element = $(toggle_button).parent();\n  var logbox = parent_element.find('pre');\n  logbox.toggle();\n  if (logbox.is(':visible')) {\n    var cfg_type = parent_element.parent().attr('id').replace('-cfg-state-panel', '');\n    refresh_log(logbox, state_filenames[cfg_type]);\n  }\n}\n\n\n/**\n * Refresh logbox content\n */\nfunction refresh_log(logbox, filename) {\n  var url = '/ivxv/data/commands/' + filename;\n  $.getJSON(encodeURI(url), function(state) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/commands/??\n       * HTTP GET response that contains HTML tags is not allowed!\n       * state is always an JSON object\n       */\n      state = sanitizeJSON(state);\n      logbox.text(state['log'][state['attempts'] - 1].join('\\n'));\n    })\n    .fail(function(response) {\n      logbox.text(sanitizePrimitive(response.responseText));\n    });\n}\n\n/**\n * Reset upload form\n */\nfunction reset_upload_form() {\n  $('input[type=file]').val(null);\n  $('#file-upload-submit').attr('disabled', '');\n}\n\n// Variable to store uploaded files\nvar files;\n\n/**\n * Grab the files and set them to our variable\n */\nfunction prepareUpload(event) {\n  files = event.target.files;\n  $('#file-upload-submit').attr('disabled', null);\n  $('#upload-message').hide();\n}\n\n/**\n * Catch the form submit and upload the files\n */\nfunction uploadFiles(event) {\n  $('#upload-message')\n    .removeClass('alert-danger')\n    .removeClass('alert-success')\n    .hide();\n\n  event.stopPropagation(); // Stop stuff happening\n  event.preventDefault(); // Totally stop stuff happening\n  // Create a formdata object and add the files\n  var data = new FormData();\n  data.append('upload', files[0]);\n  data.append('type', sanitizePrimitive($('#drop').find(':selected').val()));\n\n  var form = $('#config-upload-form');\n  $.ajax({\n    url: encodeURI(form.attr('action')),\n    type: sanitizePrimitive(form.attr('method')),\n    data: data,\n    cache: false,\n    dataType: 'json',\n    processData: false, // Don't process the files\n    contentType: false, // Set content type to false as jQuery will tell the server its a query string request\n\n    // Success\n    success: function(data, textStatus, jqXHR) {\n      console.log(jqXHR.responseJSON.message);\n      $('#upload-message')\n        .html(\n          sanitizePrimitive(jqXHR.responseJSON.message) +\n          '<hr />' +\n          '<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\\n')) + '</pre>'\n        )\n        .addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')\n        .show();\n      reset_upload_form();\n    },\n\n    // Handle errors\n    error: function(jqXHR, textStatus, errorThrown) {\n      console.log(jqXHR);\n      $('#upload-message')\n        .html(sanitizePrimitive(jqXHR.responseText))\n        .addClass('alert-danger')\n        .show();\n    }\n  });\n}\n"
  },
  {
    "path": "collector-admin/site/js/downloads.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Ballot box download page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n       * HTTP GET response that contains HTML tags is not allowed!\n       * state is always an JSON object\n      */\n      state = sanitizeJSON(state);\n      hideErrorMessage();\n\n      if ((state.collector_state == 'NOT INSTALLED') ||\n        (state.collector_state == 'INSTALLED')) {\n        $('#common-warning-msg').show('slow');\n        $('#common-warning-msg .panel-body p').text('Kogumisteenus pole seadistatud. Valimiskasti allalaadimine pole võimaik.');\n        $('#panel-download-form').hide();\n      } else if (state.election.phase == 'PREPARING') {\n        $('#common-warning-msg').show('slow');\n        $('#common-warning-msg .panel-body p').text('Hääletamine on ettevalmistamise faasis. Valimiskasti allalaadimine pole võimaik.');\n        $('#panel-download-form').hide();\n      } else if (state.election.phase != 'FINISHED') {\n        $('#common-warning-msg').show('slow');\n        $('#common-warning-msg .panel-body p').text('Hääletusperioodil on võimalik väljastada vaid e-valimiskasti varukoopiat.');\n        $('#panel-download-form').show();\n      } else {\n        $('#common-warning-msg').hide('slow');\n        $('#panel-download-form').show();\n      }\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n\n  // load ballot box state\n  $.getJSON('cgi/ballot-box-state', function(state) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/cgi/ballot-box-state\n       * HTTP GET response that contains HTML tags is not allowed!\n       * state is always an JSON object\n       */\n      state = sanitizeJSON(state);\n      $('#panel-download-ballot-box').remove();\n\n      if (state.data.length === 0) {\n        console.log('There is no ballot box created for download');\n        return;\n      }\n\n      var panel = $('#panel-download-ballot-box-template').clone().prop({\n        id: 'panel-download-ballot-box'\n      });\n      panel.find('.list-group').empty();\n\n      $(state.data).each(function() {\n        var line = null;\n        var message = this.filename.search('consolidated') === -1 ?\n          'Eksporditud e-valimiskast' :\n          'Eksporditud ja konsolideeritud e-valimiskast';\n        if (this.state == 'ready') {\n          line = $('#panel-download-ballot-box-template .list-group-item-success')\n            .clone()\n            .attr('href', '/ivxv/data/ballot-box/' + this.filename);\n        } else if (this.state == 'prepare') {\n          line = $('#panel-download-ballot-box-template .list-group-item-info')\n            .clone();\n          line.find('pre').text(this.log).show();\n        } else {\n          line = $('#panel-download-ballot-box-template .list-group-item-danger')\n            .clone();\n          message = 'Unknown ballot box state: ' + this.state;\n        }\n\n        line.find('span').first().text(message);\n        line.find('span em').text(this.timestamp);\n        panel.find('.list-group').append(line);\n      });\n\n      $('#panel-download-ballot-box-template').after(panel);\n      panel.show();\n    })\n    .fail(function(jqXHR, textStatus) {\n      $('#panel-download-ballot-box').remove();\n      var panel = $('#panel-download-ballot-box-template').clone().prop({\n        id: 'panel-download-ballot-box'\n      });\n      panel.find('.list-group').empty();\n      $('#panel-download-ballot-box-template').after(panel);\n\n      var line = $('#panel-download-ballot-box-template .list-group-item-danger')\n        .clone();\n      line\n        .find('span')\n        .first()\n        .text(\n          'Serveri viga allalaadimiseks ettevalmistatud valimiskastide olekuandmete laadimisel');\n      line.find('span em').remove();\n\n      panel.find('.list-group').append(line);\n      panel.show();\n    });\n}\n\n/**\n * Download ballot box\n */\nfunction downloadBallot(consolidate) {\n  $('#loading').show();\n  url = (\n    consolidate === 1 ?\n    '/ivxv/cgi/download-consolidated-ballot-box' :\n    '/ivxv/cgi/download-ballot-box')\n\n  $.ajax({\n    method: 'POST',\n    url: url,\n\n    success: function(url) {\n      $('#loading').hide();\n      $('#panel-download-form .panel-body')\n        .text('Server alustas valimiskasti ettevalmistamist allalaadimiseks');\n    },\n\n    error: function(jqXHR, textStatus, errorThrown) {\n      $('#loading').hide();\n      alert(errorThrown);\n    }\n\n  });\n}\n"
  },
  {
    "path": "collector-admin/site/js/index.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Overview page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // data mappings\n  var states = {\n    'NOT INSTALLED': ['Paigaldamata', 'warning', 0],\n    'INSTALLED': ['Paigaldatud', '', 0],\n    'CONFIGURED': ['Seadistatud', 'success', 0],\n    'PARTIAL FAILURE': ['Osaline tõrge', 'danger', 0],\n    'FAILURE': ['Tõrge', 'danger', 0],\n    'REMOVED': ['Eemaldatud', '', 0]\n  };\n  var phases = {\n    'PREPARING': 'Ettevalmistamine',\n    'WAITING FOR SERVICE START': 'Teenuse käivitamise ootamine',\n    'WAITING FOR ELECTION START': 'Valimise alguse ootamine',\n    'ELECTION': 'Häälte kogumine',\n    'WAITING FOR SERVICE STOP': 'Teenuse seiskamise ootamine',\n    'FINISHED': 'Lõpetatud'\n  };\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n     /*\n      * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n      * HTTP GET response that contains HTML tags is not allowed!\n      * state is always an JSON object\n      */\n      state = sanitizeJSON(state);\n      hideErrorMessage();\n\n      // election ID\n      $('#electionid').toggle(state['election']['election-id'] !== null);\n      $('#election-id').text(state['election']['election-id']);\n\n      // collector state\n      $('#collector-state')\n        .html(\n          state['collector']['state'] in states ?\n          states[state['collector']['state']][0] :\n          'Tundmatu');\n      $('#collector-state-panel')\n        .removeClass('panel-danger')\n        .removeClass('panel-warning')\n        .removeClass('panel-success')\n        .addClass(\n          state['collector']['state'] in states ?\n          'panel-' + states[state['collector']['state']][1] :\n          'panel-danger');\n\n      // voting phase\n      $('#voting-phase').toggle(state['config']['election'] !== null);\n      $('#voting-stage')\n        .text(\n          state['election']['phase'] in phases ?\n          phases[state['election']['phase']] :\n          'Tundmatu');\n      $('#voting-stage-start').text(state['election']['phase-start']);\n      $('#voting-stage-end').text(state['election']['phase-end']);\n\n      // config packages\n      $('#trust')\n        .addClass('list-group-item-warning')\n        .removeClass('list-group-item-success');\n      if (state['config']['trust']) {\n        outputCmdVersion('#config-trust', 'trust', state)\n        $('#trust')\n          .removeClass('list-group-item-warning')\n          .addClass('list-group-item-success');\n      }\n      $('#tech')\n        .addClass('list-group-item-warning')\n        .removeClass('list-group-item-success');\n      if (state['config']['technical']) {\n        outputCmdVersion('#config-tech', 'technical', state)\n        $('#tech')\n          .removeClass('list-group-item-warning')\n          .addClass('list-group-item-success');\n      }\n      $('#election')\n        .addClass('list-group-item-warning')\n        .removeClass('list-group-item-success');\n      if (state['config']['election']) {\n        outputCmdVersion('#config-election', 'election', state)\n        $('#election')\n          .removeClass('list-group-item-warning')\n          .addClass('list-group-item-success');\n      }\n\n      // voting lists - choices\n      outputCmdVersion('#list-choices', 'choices', state)\n      $('#choiceslist')\n        .removeClass('list-group-item-danger')\n        .removeClass('list-group-item-success')\n        .removeClass('list-group-item-warning');\n      $('#choicesliststatus')\n        .removeClass('list-group-item-danger')\n        .removeClass('list-group-item-success')\n        .removeClass('list-group-item-warning');\n      if (!state['list']['choices']) {\n        $('#list-choices-status').text('Laadimata');\n        $('#choiceslist').addClass('list-group-item-danger');\n        $('#choicesliststatus').addClass('list-group-item-danger');\n      } else if (!state['list']['choices-loaded']) {\n        $('#list-choices-status').text('Laaditud haldusteenusesse');\n        $('#choiceslist').addClass('list-group-item-warning');\n        $('#choicesliststatus').addClass('list-group-item-warning');\n      } else if (state['list']['choices'] === state['list']['choices-loaded']) {\n        $('#list-choices-status').text('Rakendatud kogumisteenusele');\n        $('#choiceslist').addClass('list-group-item-success');\n        $('#choicesliststatus').addClass('list-group-item-success');\n      }\n\n      // voting lists - districts\n      outputCmdVersion('#list-districts', 'districts', state)\n      $('#districtslist')\n        .removeClass('list-group-item-danger')\n        .removeClass('list-group-item-success');\n      $('#districtsliststatus')\n        .removeClass('list-group-item-danger')\n        .removeClass('list-group-item-success');\n      if (state['list']['districts']) {\n        $('#districtslist').addClass('list-group-item-success');\n        $('#districtsliststatus').addClass('list-group-item-success');\n        $('#list-districts-status').text('Laaditud haldusteenusesse');\n      } else {\n        $('#districtslist').addClass('list-group-item-danger');\n        $('#districtsliststatus').addClass('list-group-item-danger');\n        $('#list-districts-status').text('-');\n      }\n\n      // voting lists - voters\n      fillVoterListStateCounters(state['list']);\n      $('#voterslist')\n        .removeClass('list-group-item-danger')\n        .removeClass('list-group-item-success');\n      if ((state['list']['voters-list-total'] === 0) ||\n        (state['list']['voters-list-invalid'] !== 0)) {\n        $('#voterslist').addClass('list-group-item-danger');\n      } else if (state['list']['voters-list-pending'] === 0) {\n        $('#voterslist').addClass('list-group-item-success');\n\n        $('#list-list').empty();\n        for (var changeset_no = 0; changeset_no < 10000; changeset_no++) {\n          var iStr = 'voters' + String(changeset_no).padStart(4, '0');\n          if (!(iStr + '-state' in state['list']))\n            break;\n          var listStatus = voterListStateDescriptions.get(state['list'][iStr + '-state']);\n          $('#list-list').append(\n            '<li class=\"list-group-item list-group-item-success\" style=\"padding-left:25px\">' +\n            (changeset_no + 1) + '. ' + sanitizePrimitive(listStatus) + ': ' + state['list'][iStr] +\n            '</li>'\n          );\n        }\n      } else {\n        $('#voterslist').addClass('list-group-item-warning');\n      }\n\n      // service summary\n      var serviceexists = false;\n      $.each(state['service'], function(serviceName, service) {\n        if (service['state'] in states) {\n          states[service['state']][2]++;\n          serviceexists = true;\n        }\n      });\n      $('#service_summary').toggle(serviceexists);\n      if (serviceexists) {\n        $('#not_installed').html(states['NOT INSTALLED'][2]);\n        $('#configured').html(states['CONFIGURED'][2]);\n        $('#installed').html(states['INSTALLED'][2]);\n        $('#failure').html(states['FAILURE'][2]);\n        $('#p_failure').html(states['PARTIAL FAILURE'][2]);\n        $('#removed').html(states['REMOVED'][2]);\n      }\n\n      // debian packages\n      $('#debs-exist-count').text(state['storage']['debs_exists'].length);\n      if (state['storage']['debs_missing'].length === 0) {\n        $('#packagepanel')\n          .removeClass('panel-danger')\n          .addClass('panel-success')\n      } else {\n        $('#packagepanel')\n          .removeClass('panel-success')\n          .addClass('panel-danger')\n      }\n      $('#debs-missing-count').text(state['storage']['debs_missing'].length);\n      $('#missing-debs-list').empty();\n      for (var i = 0; i < state['storage']['debs_missing'].length; i++) {\n        $('#missing-debs-list').append(\n          '<div class=\"panel panel-danger\">' +\n          '<div class=\"panel-heading\">' +\n          state['storage']['debs_missing'][i] +\n          '</div>' +\n          '</div>'\n        );\n      }\n\n      // users\n      var usercount = 0;\n      if (state['user']) {\n        usercount = Object.keys(state['user']).length;\n      }\n      $('#user-count').text(usercount);\n\n      // command packages\n      $('#command-files-count').text(\n        state['storage']['command_files_active'].length +\n        state['storage']['command_files_inactive'].length\n      );\n      $('#command-files-active-count').text(state['storage']['command_files_active'].length);\n      $('#command-files-inactive-count').text(state['storage']['command_files_inactive'].length);\n\n      // data loading stats\n      var genDate = new Date();\n      genDate.setTime(Date.parse(state['meta']['time_generated']));\n      $('#loadstatus')\n        .removeClass('text-danger')\n        .addClass('text-info')\n        .html('Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n          'Andmete genereerimise aeg: ' + genDate.toLocaleTimeString('et-EE', {}));\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n}\n"
  },
  {
    "path": "collector-admin/site/js/ivxv.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - JavaScript helpers\n */\n\nvar pageContext = null; // Page context data\nvar userContext = null; // User context data (subset of pageContext)\n\n/**\n * Query page context data and write it\n * to pageContext and userContext variables.\n */\nfunction getContextData() {\n  // query page context data\n  console.debug('Loading context data');\n  $.getJSON('cgi/context.json', function(context) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/cgi/context.json\n       * HTTP GET response that contains HTML tags is not allowed!\n       * context is always an JSON object\n       */\n      context = sanitizeJSON(context);\n      pageContext = context.data;\n      userContext = pageContext['current-user'];\n      console.debug('Current user: ' + userContext['cn'] +\n        ' ' + userContext['idcode']);\n      console.debug('Current role: ' + userContext['role'] +\n        ' (' + userContext['role-description'] + ')');\n      copyObjectToHtml(userContext, 'user-');\n\n      // append election ID to page title\n      if (pageContext['election']['id']) {\n        $('title').prepend(pageContext['election']['id'] + ' – ');\n      }\n\n      $('.navbar-brand').html(\n        'Kogumisteenuse haldusteenus ' + pageContext['collector']['version']\n      );\n\n      // check user permissons and take action\n      if (userContext['permissions'].length === 0) {\n        // user has no permissons, generate error message and paint navbar to red\n        $('.navbar').addClass('label-danger').find('li').addClass('label-danger');\n        showErrorMessage('Puuduvad kasutajaõigused');\n      }\n\n      // Hide menu items if user has no permissions to access it\n      var permission_page_map = [\n        ['election-conf-admin', 'lists.html'],\n        ['stats-view', 'stats.html'],\n        ['user-admin', 'users.html'],\n        ['tech-conf-admin', 'services.html'],\n        ['tech-conf-admin', 'config.html'],\n        ['download-ballot-box', 'downloads.html'],\n        ['log-view', 'log.html']\n      ];\n      $.each(permission_page_map, function(index, value) {\n        var hide = true;\n        for (var i = 0; i < value.length - 1; i++) {\n          if ($.inArray(value[i], userContext['permissions']) !== -1) {\n            hide = false;\n            break;\n          }\n        }\n        if (hide) {\n          $('#side-menu').find('a[href~=\"' + value[1] + '\"]').parent().hide();\n        }\n      });\n    })\n    .fail(function() {\n      showErrorMessage('Viga kontekstiandmete laadimisel', false);\n    });\n}\n\n/**\n * Show error message\n *\n * @param msg\n * @param {boolean} retain_content - Retain page content\n */\nfunction showErrorMessage(msg, retain_content) {\n  console.error(msg);\n  if (!retain_content) {\n    $('#page-wrapper').find('.row').hide('slow');\n  }\n  $('#common-error-msg').find('p').text(msg);\n  $('#common-error-msg').show('slow');\n  $('#page-wrapper').css({\n    'background-color': 'rgba(217, 83, 79, 0.2)'\n  });\n}\n\n/**\n * Hide error message\n */\nfunction hideErrorMessage() {\n  $('#common-error-msg').hide('slow');\n  $('#page-wrapper').css({\n    'background-color': ''\n  });\n}\n\n/**\n * Check user permissions\n *\n * @param {string} permission - Permission name to check\n * @return {boolean} Do user have permission\n */\nfunction userHasPermission(permission) {\n  return -1 !== $.inArray(permission, userContext['permissions']);\n}\n\n/**\n * Copy values from dictionary object to HTML element text nodes.\n *\n * @param {object} object_val - Dictionary object\n * @param {string} targetPrefix - Prefix for DOM id values\n */\nfunction copyObjectToHtml(object_val, targetPrefix) {\n  if ('undefined' === typeof(targetPrefix))\n    targetPrefix = '';\n  $.each(object_val, function(key, val) {\n    $('#' + sanitizePrimitive(targetPrefix) + sanitizePrimitive(key)).text(val);\n  });\n}\n\n/**\n * Format datetime object as string\n *\n * @param {object} dateTime - datetime object\n * @param {number} offset - time offset\n */\nfunction formatTime(dateTime, offset) {\n  var seconds = dateTime.getSeconds();\n  var minutes = dateTime.getMinutes();\n  var hours = dateTime.getHours() + offset;\n\n  seconds = seconds < 10 ? '0' + seconds : seconds;\n  minutes = minutes < 10 ? '0' + minutes : minutes;\n  hours = hours < 10 ? '0' + hours : hours;\n\n  return hours + ':' + minutes + ':' + seconds;\n}\n\n/**\n * Output config file version\n *\n * Add link to download config file command package.\n *\n * @param {string} selector - DOM selector\n * @param {string} cfg_type - config type\n * @param {Object} cfg - config data from status.json\n */\nfunction outputCmdVersion(selector, cfg_type, cfg) {\n  selector = sanitizePrimitive(selector);\n  cfg = sanitizeJSON(cfg);\n\n  if ((cfg_type === 'trust') ||\n    (cfg_type === 'technical') ||\n    (cfg_type === 'election')) {\n    var cfg_ver = cfg['config'][cfg_type];\n  } else if ((cfg_type === 'choices') ||\n    (cfg_type === 'districts')) {\n    var cfg_ver = cfg['list'][cfg_type];\n  } else if (cfg_type.search('voters') === 0) {\n    var cfg_ver = cfg['config-apply'][cfg_type]['version'];\n  } else {\n    console.error('Unknown config: ' + cfg_type);\n  }\n  if (cfg['config-apply'][cfg_type] === undefined) {\n    $(selector).html('<span>' + (cfg_ver === null ? '-' : cfg_ver) + '</span>');\n  } else {\n    var cmd_file = cfg['config-apply'][cfg_type]['cmd_file'];\n    $(selector).html(\n      '<span>' +\n      '<a href=\"data/commands/' + cmd_file + '\">' + cfg_ver + '</a>' +\n      '</span>'\n    );\n  }\n}\n\n/**\n * Voter list state descriptions\n */\nconst voterListStateDescriptions = new Map();\nvoterListStateDescriptions.set('APPLIED', 'RAKENDATUD')\nvoterListStateDescriptions.set('PENDING', 'OOTEL')\nvoterListStateDescriptions.set('INVALID', 'VIGANE')\nvoterListStateDescriptions.set('SKIPPED', 'VAHELE JÄETUD')\nvoterListStateDescriptions.set('AVAILABLE', 'SAADAVAL')\n\n/**\n * Fill voter list state counters\n */\nfunction fillVoterListStateCounters(list_state) {\n  $('#list-voters-total').text(list_state['voters-list-total']);\n  $('#list-voters-loaded').text(list_state['voters-list-applied']);\n  $('#list-voters-pending').text(list_state['voters-list-pending']);\n  $('#list-voters-invalid').text(list_state['voters-list-invalid']);\n  $('#list-voters-skipped').text(list_state['voters-list-skipped']);\n  $('#list-voters-available').text(list_state['voters-list-available']);\n}\n\n/**\n * If Object (JSON!) contains XSS vulnerable content like HTML attributes\n * or HTML context, it will be replaced and XSS-free Object (JSON!) is returned.\n *\n * If somehow Object type data parsing fails - empty {} is returned.\n * Note, that even if Object data contains XSS vulnerabilities it\n * doesn't automatically mean, that it isn't a valid JavaScript Object,\n * however if data isn't valid Object, then it doesn't make sense to\n * proceed with XSS validation at all.\n *\n * @param {Object} context - JSON object\n * @return {Object} - XSS-free JSON object\n */\nfunction sanitizeJSON(context) {\n  try {\n    return JSON.parse(JSON.stringify(context).replaceAll('<', '&lt;'));\n  } catch (err) {\n    console.error(err);\n    return {};\n  }\n}\n\n/**\n * If primitive type contains XSS vulnerable content like HTML attributes\n * or HTML context, it will be replaced and returned as XSS-free string.\n *\n * If somehow primitive type data parsing fails - empty string is returned.\n * Note, that even if primitive data contains XSS vulnerabilities it\n * doesn't automatically mean, that it isn't a valid JavaScript primitive,\n * however if data isn't valid primitive, then it doesn't make sense to\n * proceed with XSS validation at all.\n *\n * @param {string | number | boolean} context - primitive data\n * @return {string} - XSS-free string\n */\nfunction sanitizePrimitive(context) {\n  try {\n    return String(context).replaceAll('<', '&lt;');\n  } catch (err) {\n    console.error(err);\n    return '';\n  }\n}\n"
  },
  {
    "path": "collector-admin/site/js/lists.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Voting lists management page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n     /*\n      * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n      * HTTP GET response that contains HTML tags is not allowed!\n      * state is always an JSON object\n      */\n      state = sanitizeJSON(state);\n      hideErrorMessage();\n\n      // choices list\n      outputCmdVersion('#list-choices', 'choices', state)\n      if (!state['list']['choices']) {\n        $('#list-choices-status').text('Laadimata');\n        $('#panel-choices-list').attr('class', 'panel panel-red');\n      } else {\n        $('#choicesoption').hide();\n        $('#votersoption').parent().val('voters');\n        $('#drop').find('option[value=\"voters\"]').prop('selected', true);\n        if (!state['list']['choices-loaded']) {\n          $('#list-choices-status').text('Laaditud haldusteenusesse');\n          $('#panel-choices-list').attr('class', 'panel panel-warning');\n        } else if (state['list']['choices'] === state['list']['choices-loaded']) {\n          $('#list-choices-status').text('Rakendatud kogumisteenusele');\n          $('#panel-choices-list').attr('class', 'panel panel-success');\n        }\n      }\n\n      // voters lists\n      if (state['list']['voters-list-applied']) {\n        $('#votersoption').contents().replaceWith('Valijate muudatusnimekirja vahelejätmine');\n      }\n      fillVoterListStateCounters(state['list']);\n      $('#panel-voters-list').attr('class', 'panel panel-warning');\n\n      if (state['list']['voters-list-applied'] === 0) {\n        $('#panel-voters-list').attr('class', 'panel panel-red');\n      } else if (state['list']['voters-list-pending'] === 0) {\n        $('#panel-voters-list').attr('class', 'panel panel-success');\n\n        $('#list-list').empty();\n        for (var changeset_no = 0; changeset_no < 10000; changeset_no++) {\n          var iStr = 'voters' + String(changeset_no).padStart(4, '0');\n          if (!(iStr + '-state' in state['list']))\n            break;\n          var listStatus = voterListStateDescriptions.get(state['list'][iStr + '-state']);\n          $('#list-list').append(\n            '<li class=\"list-group-item\" style=\"padding-left:25px\">' +\n            (changeset_no + 1) + '. ' + sanitizePrimitive(listStatus) + ': ' + state['list'][iStr] +\n            '</li>'\n          );\n        }\n      }\n\n      // districts list\n      outputCmdVersion('#list-districts', 'districts', state)\n      if (!state['list']['districts']) {\n        $('#list-districts-status').text('Laadimata');\n        $('#panel-districts-list').attr('class', 'panel panel-red');\n      } else {\n        $('#list-districts-status').text('Laaditud haldusteenusesse');\n        $('#panel-districts-list').attr('class', 'panel panel-success');\n      }\n\n      // data loading stats\n      var genDate = new Date();\n      genDate.setTime(Date.parse(state['meta']['time_generated']));\n      $('#loadstatus')\n        .removeClass('text-danger')\n        .addClass('text-info')\n        .html('Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n          'Andmete genereerimise aeg: ' + genDate.toLocaleTimeString('et-EE', {}));\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n}\n\n/**\n * Reset upload form\n */\nfunction reset_upload_form() {\n  $('input[type=file]').val(null);\n  $('#file-upload-submit').attr('disabled', '');\n}\n\n// Variable to store uploaded files\nvar files;\n\n/**\n * Grab the files and set them to our variable\n */\nfunction prepareUpload(event) {\n  files = event.target.files;\n  $('#file-upload-submit').attr('disabled', null);\n  $('#upload-message').hide();\n}\n\n/**\n * Catch the form submit and upload the files\n */\nfunction uploadFiles(event) {\n  $('#upload-message').hide()\n    .removeClass('alert-danger')\n    .removeClass('alert-success');\n\n  event.stopPropagation(); // Stop stuff happening\n  event.preventDefault(); // Totally stop stuff happening\n  // Create a formdata object and add the files\n  var data = new FormData();\n  data.append('upload', files[0]);\n  data.append('type', sanitizePrimitive($('#drop').find(':selected').val()));\n\n  var form = $('#config-upload-form');\n  $.ajax({\n    url: encodeURI(form.attr('action')),\n    type: sanitizePrimitive(form.attr('method')),\n    data: data,\n    cache: false,\n    dataType: 'json',\n    processData: false, // Don't process the files\n    contentType: false, // Set content type to false as jQuery will tell the server its a query string request\n    // Success\n    success: function(data, textStatus, jqXHR) {\n      console.log(jqXHR.responseJSON.message);\n      $('#upload-message')\n        .html(\n          sanitizePrimitive(jqXHR.responseJSON.message) +\n          '<hr />' +\n          '<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\\n')) + '</pre>'\n        )\n        .addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')\n        .show();\n      reset_upload_form();\n    },\n    // Handle errors\n    error: function(jqXHR, textStatus, errorThrown) {\n      console.log(jqXHR);\n      $('#upload-message')\n        .html(sanitizePrimitive(jqXHR.responseText))\n        .addClass('alert-danger')\n        .show();\n    }\n  });\n}\n"
  },
  {
    "path": "collector-admin/site/js/log.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Event log browsing page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // fill log table with data\n  $('#dataTables-log').DataTable({\n      'ajax': '/ivxv/cgi/eventlog',\n      stateSave: true,\n      responsive: true,\n      'columns': [{\n        'data': 'timestamp'\n      }, {\n        'data': 'service'\n      }, {\n        'data': 'level'\n      }, {\n        'data': 'event'\n      }, {\n        'data': 'message'\n      }],\n      'order': [\n        [0, 'desc']\n      ],\n      'fnRowCallback': function(nRow, aData) {\n        if (aData.level === 'INFO') {\n          $(nRow).addClass('success');\n        } else {\n          $(nRow).addClass('danger');\n        }\n      }\n    })\n    .on('xhr', function(e, settings, json) {\n      if (json === null) {\n        $('#loadstatus')\n          .removeClass('text-info')\n          .addClass('text-danger')\n          .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      } else {\n        $('#loadstatus')\n          .removeClass('text-danger')\n          .addClass('text-info')\n          .html('Andmete laadimise aeg: ' + formatTime(loadDate, 0));\n      }\n    });\n};\n"
  },
  {
    "path": "collector-admin/site/js/services.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Services management page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // data mappings\n  var service_states = {\n    'NOT INSTALLED': ['Paigaldamata', 'warning', 0],\n    'INSTALLED': ['Paigaldatud', '', 0],\n    'CONFIGURED': ['Seadistatud', 'success', 0],\n    'FAILURE': ['Tõrge', 'danger', 0],\n    'REMOVED': ['Eemaldatud', '', 0]\n  };\n  var service_types = {\n    'backup': 'Varundusteenus',\n    'choices': 'Nimekirjateenus',\n    'mid': 'Mobiil-ID abiteenus',\n    'smartid': 'Smart-ID abiteenus',\n    'webeid': 'Web-eID abiteenus',\n    'sessionstatus': 'SessionID staatust raporteeriv abiteenus',\n    'proxy': 'Vahendusteenus',\n    'storage': 'Talletusteenus',\n    'log': 'Logikogumisteenus',\n    'votesorder': 'Järjekorrateenus',\n    'voting': 'Hääletamisteenus',\n    'verification': 'Kontrollteenus'\n  };\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n    /*\n     * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n     * HTTP GET response that contains HTML tags is not allowed!\n     * state is always an JSON object\n     */\n    state = sanitizeJSON(state);\n    hideErrorMessage();\n\n    var i = 1;\n    $('#service_list_table').toggle(state['service'] !== undefined);\n\n    // register service details block visibility states\n    var is_service_visible = {};\n    $.each(state['service'], function(k, v) {\n      var details_block_id = k.replace(/[@\\.]/g, '_') + '_details';\n      is_service_visible[details_block_id] = $('#' + details_block_id).is(':visible');\n    });\n\n    $('#services-list').empty();\n    $.each(state['service'], function(k, v) {\n      var zone = 'Tundmatu';\n      var state_str = 'Tundmatu';\n      var status_class = 'warning';\n      var service_type = v['service-type'] in service_types ? service_types[v['service-type']] : 'Tundmatu';\n      var show_mobile = 'mid-token-key' in state['service'][k] ? '' : 'display: none';\n      var show_tlskey = 'tls-key' in state['service'][k] ? '' : 'display: none';\n      var show_tlscert = 'tls-cert' in state['service'][k] ? '' : 'display: none';\n      var lastdata = state['service'][k]['last-data'] ? state['service'][k]['last-data'] : '-';\n      var econf = state['service'][k]['election-conf-version'] ? state['service'][k]['election-conf-version'] : '-';\n      var tlsc = state['service'][k]['tls-cert'] ? state['service'][k]['tls-cert'] : '-';\n      var tlsk = state['service'][k]['tls-key'] ? state['service'][k]['tls-key'] : '-';\n      var midk = state['service'][k]['mid-token-key'] ? state['service'][k]['mid-token-key'] : '-';\n      var pingerr = state['service'][k]['ping-errors'] ? state['service'][k]['ping-errors'] : '-';\n      var bg_info = state['service'][k]['bg_info'];\n      var tconf = state['service'][k]['technical-conf-version'] ? state['service'][k]['technical-conf-version'] : '-';\n      var ipaddr = state['service'][k]['ip-address'] ? state['service'][k]['ip-address'] : '-';\n\n      // Service field in json doesn't have info about its zone, have to find it by going through all of them\n      $.each(state['network'], function(network_key, network_value) {\n        if (k in network_value) {\n          zone = network_key;\n          return false;\n        }\n      });\n\n      if (v['state'] in service_states) {\n        service_states[v['state']][2]++;\n        state_str = service_states[v['state']][0];\n        status_class = service_states[v['state']][1];\n      }\n\n      var details_block_id = k.replace(/[@\\.]/g, '_') + '_details';\n      var details_block_style = is_service_visible[details_block_id] ? '' : 'display: none;';\n      $('#services-list').append(\n        '<tr class=\"' + status_class + '\" onclick=\"$(this).next().toggle()\">' +\n        '<td>' + i + '</td>' +\n        '<td>' + k + '</td>' +\n        '<td>' + zone + '</td>' +\n        '<td>' + service_type + '</td>' +\n        '<td>' + state_str + '</td>' +\n        '</tr>' +\n        '<tr class=\"warning\" ' +\n        'style=\"' + details_block_style + '\" ' +\n        'id=\"' + details_block_id + '\" ' +\n        '>' +\n        '<td colspan=\"5\">' +\n        '<table class=\"table table-striped\">' +\n        '<tbody>' +\n        '<tr>' +\n        '<td align=\"right\">Vigade arv:</td>' +\n        '<td>' + pingerr + '</td>' +\n        '</tr>' +\n        '<tr>' +\n        '<td align=\"right\">Viimane kontroll:</td>' +\n        '<td>' + lastdata + '</td>' +\n        '</tr>' +\n        '<tr>' +\n        '<td align=\"right\">Tehniline seadistus:</td>' +\n        '<td>' + tconf + '</td>' +\n        '</tr>' +\n        '<tr>' +\n        '<td align=\"right\">Valimiste seadistus:</td>' +\n        '<td>' + econf + '</td>' +\n        '</tr>' +\n        '<tr>' +\n        '<td align=\"right\">IP-aaddress:</td>' +\n        '<td>' + ipaddr + '</td>' +\n        '</tr>' +\n        '<tr style=\"' + show_tlscert + ';\">' +\n        '<td align=\"right\">TLS-sertifikaat:</td>' +\n        '<td>' + tlsc + '</td>' +\n        '</tr>' +\n        '<tr style=\"' + show_tlskey + ';\">' +\n        '<td align=\"right\">TLS-võti:</td>' +\n        '<td>' + tlsk + '</td>' +\n        '</tr>' +\n        '<tr style=\"' + show_mobile + ';\">' +\n        '<td align=\"right\">Mobiil-ID krüptimissaladus:</td>' +\n        '<td>' + midk + '</td>' +\n        '</tr>' +\n        '</tbody>' +\n        '</table>' +\n        '</td>' +\n        '</tr>'\n      );\n      if (bg_info) {\n        $('#services-list').append(\n          '<tr>' +\n          '<td/>' +\n          '<td colspan=\"4\" class=\"warning\">' +\n          '  <small>Taustainfo: </small>' +\n          '  <small class=\"text-info\">' + bg_info.replace(/\\n/g, '<br />') + '</small>' +\n          '</td>' +\n          '</tr>'\n        );\n      }\n      i++;\n    });\n\n    // data loading stats\n    var genDate = new Date();\n    genDate.setTime(Date.parse(state['meta']['time_generated']));\n    $('#loadstatus')\n      .removeClass('text-danger')\n      .addClass('text-info')\n      .html('Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n        'Andmete genereerimise aeg: ' + genDate.toLocaleTimeString('et-EE', {}));\n\n  }).done(function() {\n    $('#not_installed').html(service_states['NOT INSTALLED'][2]);\n    $('#configured').html(service_states['CONFIGURED'][2]);\n    $('#installed').html(service_states['INSTALLED'][2]);\n    $('#failure').html(service_states['FAILURE'][2]);\n    $('#removed').html(service_states['REMOVED'][2]);\n  }).fail(function() {\n    $('#loadstatus')\n      .removeClass('text-info')\n      .addClass('text-danger')\n      .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n    showErrorMessage('Viga seisundi laadimisel', true);\n  });\n}\n"
  },
  {
    "path": "collector-admin/site/js/stats.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - Stats page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // manage districts selection\n  if ($('#districts option').length == 1) {\n    $.getJSON('data/districts.json', function(data) {\n        /*\n         * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/districts.json\n         * HTTP GET response that contains HTML tags is not allowed!\n         * data is always an JSON object\n         */\n        data = sanitizeJSON(data);\n        var dropdown = $('#districts');\n        $.each(data, function() {\n          dropdown.append($('<option />').val(this[0]).text(this[1]));\n        })\n      })\n      .fail(function() {\n        $('#districts option').text('Ringkondade nimekiri pole saadaval');\n      });\n  }\n\n  // fill page with stats\n  $.getJSON('data/stats.json', function(data) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/stats.json\n       * HTTP GET response that contains HTML tags is not allowed!\n       * data is always an JSON object\n       */\n      data = sanitizeJSON(data)\n      $('#stats-error').hide();\n      $('#stats-error-msg').html();\n      $('#auth-os').empty();\n      $('#table-revoters').empty();\n      $('#table-countries').empty();\n      $.each(data, function(key, val) {\n        if (key === 'data') {\n          var district_stats = $(val).prop($('#districts').val());\n          $.each(district_stats, function(stats_key, stats_val) {\n            if (typeof(stats_val) === 'object') {\n              if (stats_key === 'authentication-methods') {\n                $('#auth-os').append(\n                  '<tr class=\"info\">\\n' +\n                  '<th colspan=\"2\">Autentimisvahend</th>\\n' +\n                  '</tr>\\n'\n                )\n              } else if (stats_key === 'voting-operating-systems') {\n                $('#auth-os').append(\n                  '<tr class=\"info\">\\n' +\n                  '<th colspan=\"2\">Operatsioonisüsteem hääletamisel</th>\\n' +\n                  '</tr>\\n'\n                )\n              } else if (stats_key === 'verify-operating-systems') {\n                $('#auth-os').append(\n                  '<tr class=\"info\">\\n' +\n                  '<th colspan=\"2\">Operatsioonisüsteem kontrollimisel</th>\\n' +\n                  '</tr>\\n'\n                )\n              } else if (stats_key === 'top-10-revoters') {\n                $('#table-revoters').append(\n                  '<tr class=\"info\">\\n' +\n                  '<th colspan=\"2\">TOP 10 korduvhääletajat</th>\\n' +\n                  '</tr>'\n                )\n              }\n              $.each(stats_val, function(stats_table_key, stats_table_val) {\n                if (stats_key === 'authentication-methods') {\n                  var method = 'ID-kaart';\n                  if (stats_table_val[0] === 'ticket') {\n                    method = 'Mobiil-ID/Smart-ID/Web eID';\n                  }\n                  $('#auth-os').append(\n                    '<tr><td>' +\n                    method +\n                    '</td><td>' +\n                    sanitizePrimitive(stats_table_val[1]) +\n                    '</td></tr>'\n                  )\n                } else if (stats_key === 'voting-operating-systems') {\n                  $('#auth-os').append(\n                    '<tr><td>' +\n                    sanitizePrimitive(stats_table_val[0]) +\n                    '</td><td>' +\n                    sanitizePrimitive(stats_table_val[1]) +\n                    '</td></tr>'\n                  )\n                } else if (stats_key === 'verify-operating-systems') {\n                  $('#auth-os').append(\n                    '<tr><td>' +\n                    sanitizePrimitive(stats_table_val[0]) +\n                    '</td><td>' +\n                    sanitizePrimitive(stats_table_val[1]) +\n                    '</td></tr>'\n                  )\n                } else if (stats_key === 'top-10-revoters') {\n                  $('#table-revoters').append(\n                    '<tr><td>' +\n                    sanitizePrimitive(stats_table_val[0]) +\n                    '</td><td>' +\n                    sanitizePrimitive(stats_table_val[1]) +\n                    '</td></tr>'\n                  )\n                } else if (stats_key === 'votes-by-country') {\n                  $('#table-countries').append(\n                    '<tr><td>' +\n                    sanitizePrimitive(stats_table_val[0]) +\n                    '</td><td>' +\n                    sanitizePrimitive(stats_table_val[1]) +\n                    '</td></tr>'\n                  )\n                }\n              });\n            } else {\n              $('#' + sanitizePrimitive(stats_key)).text(stats_val);\n            }\n          });\n        } else if (key === 'error') {\n          $('#stats-error').show();\n          $('#stats-error-msg').html(data[key].replace(/\\n/g, '<br />'));\n        }\n      });\n\n      // data loading stats\n      var genDate = new Date();\n      genDate.setTime(Date.parse(data['meta']['time_generated']));\n      $('#loadstatus')\n        .removeClass('text-danger')\n        .addClass('text-info')\n        .html('Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n          'Andmete genereerimise aeg: ' +\n          genDate.toLocaleTimeString(\n            'et-EE', {\n              year: 'numeric',\n              month: 'numeric',\n              day: 'numeric'\n            }));\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n}\n"
  },
  {
    "path": "collector-admin/site/js/users.js",
    "content": "/*\n * IVXV Internet voting framework\n *\n * Administrator interface - User management page\n */\n\n/**\n * Load page data\n */\nfunction loadPageData() {\n  var loadDate = new Date();\n  loadDate.setTime(Date.now());\n\n  // load collector state\n  $.getJSON('data/status.json', function(state) {\n      /*\n       * HTTP GET on https://admin.?.ivxv.ee/ivxv/data/status.json\n       * HTTP GET response that contains HTML tags is not allowed!\n       * state is always an JSON object\n       */\n      state = sanitizeJSON(state);\n      hideErrorMessage();\n\n      var i = 1;\n\n      $('#user-list').empty();\n      $.each(state['user'], function(k, v) {\n        $('#user-list').append(\n          '<tr>' +\n          '<td>' + i + '</td>' +\n          '<td>' + k + '</td>' +\n          '<td>' + v + '</td>' +\n          '</tr>'\n        );\n\n        i++;\n      });\n\n      // data loading stats\n      var genDate = new Date();\n      genDate.setTime(Date.parse(state['meta']['time_generated']));\n      $('#loadstatus')\n        .removeClass('text-danger')\n        .addClass('text-info')\n        .html(\n          'Andmete laadimise aeg: ' + formatTime(loadDate, 0) + '<br />' +\n          'Andmete genereerimise aeg: ' + genDate.toLocaleTimeString('et-EE', {}));\n    })\n    .fail(function() {\n      $('#loadstatus')\n        .removeClass('text-info')\n        .addClass('text-danger')\n        .html('Viga andmete laadimisel: ' + formatTime(loadDate, 0));\n      showErrorMessage('Viga seisundi laadimisel', true);\n    });\n}\n\n/**\n * Reset upload form\n */\nfunction reset_upload_form() {\n  $('input[type=file]').val(null);\n  $('#file-upload-submit').attr('disabled', '');\n}\n\n// Variable to store uploaded files\nvar files;\n\n/**\n * Grab the files and set them to our variable\n */\nfunction prepareUpload(event) {\n  files = event.target.files;\n  $('#file-upload-submit').attr('disabled', null);\n  $('#upload-message').hide();\n}\n\n/**\n * Catch the form submit and upload the files\n */\nfunction uploadFiles(event) {\n  $('#upload-message')\n    .removeClass('alert-danger')\n    .removeClass('alert-success')\n    .hide();\n\n  event.stopPropagation(); // Stop stuff happening\n  event.preventDefault(); // Totally stop stuff happening\n  // Create a formdata object and add the files\n  var data = new FormData();\n  data.append('upload', files[0]);\n  data.append('type', 'user');\n\n  var form = $('#config-upload-form');\n  $.ajax({\n    url: encodeURI(form.attr('action')),\n    type: sanitizePrimitive(form.attr('method')),\n    data: data,\n    cache: false,\n    dataType: 'json',\n    processData: false, // Don't process the files\n    contentType: false, // Set content type to false as jQuery will tell the server its a query string request\n\n    // Success\n    success: function(data, textStatus, jqXHR) {\n      console.log(jqXHR.responseJSON.message);\n      $('#upload-message')\n        .html(\n          sanitizePrimitive(jqXHR.responseJSON.message) +\n          '<hr />' +\n          '<pre>' + sanitizePrimitive(jqXHR.responseJSON.log.join('\\n')) + '</pre>'\n        )\n        .addClass(jqXHR.responseJSON.success ? 'alert-success' : 'alert-danger')\n        .show();\n      reset_upload_form();\n      loadPageData()\n    },\n\n    // Handle errors\n    error: function(jqXHR, textStatus, errorThrown) {\n      console.log(jqXHR);\n      $('#upload-message')\n        .html(sanitizePrimitive(jqXHR.responseText))\n        .addClass('alert-danger')\n        .show();\n    }\n  });\n}\n"
  },
  {
    "path": "collector-admin/sk-certs/EE-GovCA2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIE+DCCBFmgAwIBAgIQMLOwlXoR0oFbj52nmRsnezAKBggqhkjOPQQDBDBaMQsw\r\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\r\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDkw\r\nNTA5MTEwM1oXDTMzMDkwNTA5MTEwM1owWjELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\r\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFTAT\r\nBgNVBAMMDEVFLUdvdkNBMjAxODCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAMcb\r\n/dmAcVo/b2azEPS6CfW7fEA2KuHKC53D7ShVNvLz4QUjCdTXjds/4u99jUoYEQec\r\nluVVzMlgEJR1nkN2eOrLAZYxPjwG5HiI1iZEyW9QKVdeEgyvhzWWTNHGjV3HdZRv\r\n7L9o4533PtJAyqJq9OTs6mjsqwFXjH49bfZ6CGmzUJsHo4ICvDCCArgwEgYDVR0T\r\nAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAQYwNAYDVR0lAQH/BCowKAYIKwYB\r\nBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYBBQUHAwEwHQYDVR0OBBYEFH4p\r\nVuc0knhOd+FvLjMqmHHB/TSfMB8GA1UdIwQYMBaAFH4pVuc0knhOd+FvLjMqmHHB\r\n/TSfMIICAAYDVR0gBIIB9zCCAfMwCAYGBACPegECMAkGBwQAi+xAAQIwMgYLKwYB\r\nBAGDkSEBAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMA0G\r\nCysGAQQBg5EhAQECMA0GCysGAQQBg5F/AQEBMA0GCysGAQQBg5EhAQEFMA0GCysG\r\nAQQBg5EhAQEGMA0GCysGAQQBg5EhAQEHMA0GCysGAQQBg5EhAQEDMA0GCysGAQQB\r\ng5EhAQEEMA0GCysGAQQBg5EhAQEIMA0GCysGAQQBg5EhAQEJMA0GCysGAQQBg5Eh\r\nAQEKMA0GCysGAQQBg5EhAQELMA0GCysGAQQBg5EhAQEMMA0GCysGAQQBg5EhAQEN\r\nMA0GCysGAQQBg5EhAQEOMA0GCysGAQQBg5EhAQEPMA0GCysGAQQBg5EhAQEQMA0G\r\nCysGAQQBg5EhAQERMA0GCysGAQQBg5EhAQESMA0GCysGAQQBg5EhAQETMA0GCysG\r\nAQQBg5EhAQEUMA0GCysGAQQBg5F/AQECMA0GCysGAQQBg5F/AQEDMA0GCysGAQQB\r\ng5F/AQEEMA0GCysGAQQBg5F/AQEFMA0GCysGAQQBg5F/AQEGMDEGCisGAQQBg5Eh\r\nCgEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93d3cuc2suZWUvQ1BTMBgGCCsGAQUF\r\nBwEDBAwwCjAIBgYEAI5GAQEwCgYIKoZIzj0EAwQDgYwAMIGIAkIBk698EqetY9Tt\r\n6HwO50CfzdIIjKmlfCI34xKdU7J+wz1tNVu2tHJwEhdsH0e92i969sRDp1RNPlVh\r\n4XFJzI3oQFQCQgGVxmcuVnsy7NUscDZ0erwovmbFOsNxELCANxNSWx5xMqzEIhV8\r\n46opxu10UFDIBBPzkbBenL4h+g/WU7lG78fIhA==\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "collector-admin/sk-certs/esteid2018.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIFVzCCBLigAwIBAgIQdUf6rBR0S4tbo2bU/mZV7TAKBggqhkjOPQQDBDBaMQsw\r\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\r\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDky\r\nMDA5MjIyOFoXDTMzMDkwNTA5MTEwM1owWDELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\r\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzAR\r\nBgNVBAMMCkVTVEVJRDIwMTgwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAHHOBlv\r\n7UrRPYP1yHhOb7RA/YBDbtgynSVMqYdxnFrKHUXh6tFkghvHuA1k2DSom1hE5kqh\r\nB5VspDembwWDJBOQWQGOI/0t3EtccLYjeM7F9xOPdzUbZaIbpNRHpQgVBpFX0xpL\r\nTgW27MpIMhU8DHBWFpeAaNX3eUpD4gC5cvhsK0RFEqOCAx0wggMZMB8GA1UdIwQY\r\nMBaAFH4pVuc0knhOd+FvLjMqmHHB/TSfMB0GA1UdDgQWBBTZrHDbX36+lPig5L5H\r\notA0rZoqEjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADCCAc0G\r\nA1UdIASCAcQwggHAMAgGBgQAj3oBAjAJBgcEAIvsQAECMDIGCysGAQQBg5EhAQEB\r\nMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzANBgsrBgEEAYOR\r\nIQEBAjANBgsrBgEEAYORfwEBATANBgsrBgEEAYORIQEBBTANBgsrBgEEAYORIQEB\r\nBjANBgsrBgEEAYORIQEBBzANBgsrBgEEAYORIQEBAzANBgsrBgEEAYORIQEBBDAN\r\nBgsrBgEEAYORIQEBCDANBgsrBgEEAYORIQEBCTANBgsrBgEEAYORIQEBCjANBgsr\r\nBgEEAYORIQEBCzANBgsrBgEEAYORIQEBDDANBgsrBgEEAYORIQEBDTANBgsrBgEE\r\nAYORIQEBDjANBgsrBgEEAYORIQEBDzANBgsrBgEEAYORIQEBEDANBgsrBgEEAYOR\r\nIQEBETANBgsrBgEEAYORIQEBEjANBgsrBgEEAYORIQEBEzANBgsrBgEEAYORIQEB\r\nFDANBgsrBgEEAYORfwEBAjANBgsrBgEEAYORfwEBAzANBgsrBgEEAYORfwEBBDAN\r\nBgsrBgEEAYORfwEBBTANBgsrBgEEAYORfwEBBjAqBgNVHSUBAf8EIDAeBggrBgEF\r\nBQcDCQYIKwYBBQUHAwIGCCsGAQUFBwMEMGoGCCsGAQUFBwEBBF4wXDApBggrBgEF\r\nBQcwAYYdaHR0cDovL2FpYS5zay5lZS9lZS1nb3ZjYTIwMTgwLwYIKwYBBQUHMAKG\r\nI2h0dHA6Ly9jLnNrLmVlL0VFLUdvdkNBMjAxOC5kZXIuY3J0MBgGCCsGAQUFBwED\r\nBAwwCjAIBgYEAI5GAQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2Muc2suZWUv\r\nRUUtR292Q0EyMDE4LmNybDAKBggqhkjOPQQDBAOBjAAwgYgCQgDeuUY4HczUbFKS\r\n002HZ88gclgYdztHqglENyTMtXE6dMBRnCbgUmhBCAA0mJSHbyFJ8W9ikLiSyurm\r\nkJM0hDE9KgJCASOqA405Ia5nKjTJPNsHQlMi7KZsIcTHOoBccx+54N8ZX1MgBozJ\r\nmT59rZY/2/OeE163BAwD0UdUQAnMPP6+W3Vd\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "common/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n------------------\n Common libraries\n------------------\n\nCollection of common code used by other top-level components.\n"
  },
  {
    "path": "common/collector/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "common/collector/Makefile",
    "content": ".DEFAULT_GOAL := all\n\n.PHONY: install\ninstall: all\n\t$(MAKE) -C config $@\n\t$(MAKE) -C scripts $@\n\t$(MAKE) -f ../go/common.mk $@\n\n# Remove additional files in clean.\nCLEAN := common/collector/server/tlsciphersuites.go\n\n.PHONY: clean\nclean:\n\t$(MAKE) -f ../go/common.mk clean\n\trm -rf $(CLEAN)\n\n%:\n\t$(MAKE) -f ../go/common.mk $@\n"
  },
  {
    "path": "common/collector/README.md",
    "content": "### collector/cmd/verifier\n\nCan verify documents that are signed with:\n\n- id-card\n- mobile-id\n- smart-id\n\n#### If and only if you have the following trust.yaml configuration:\n```\n# Usaldusjuure seadistus YAML-struktuurina\n\ncontainer:\nbdoc:\nbdocsize: 104857600  # 100 MiB\nfilesize: 104857600  # 100 MiB\nroots:\n- !container devel_root.crt\n- !container sk_test_root.crt\n- !container EE-GovCA2018.crt\n- !container EE_Certification_Centre_Root_CA.crt\nintermediates:\n- !container devel_intermediate.crt\n- !container sk_test_intermediate.crt\n- !container ESTEID-SK_2015.crt\n- !container esteid2018.crt\n- !container EID-SK_2016.pem.crt\nprofile: TS\nocsp:\nresponders:\n- !container sk_test_ocsp.crt\n- !container SK_OCSP_RESPONDER_2011.crt\n- !container EID-SK_2016_OCSP_RESPONDER_2018.pem.cer\ntsp:\nsigners:\n- !container sk_test_tsa.crt\n- !container SK_TIMESTAMPING_AUTHORITY_2019.crt\n- !container SK_TIMESTAMPING_AUTHORITY_2020.crt\n- !container SK_TIMESTAMPING_AUTHORITY_2021.crt\n- !container SK_TIMESTAMPING_AUTHORITY_2022.crt\ndelaytime: 10\n\nauthorizations:\n- PEREKONNANIMI,NIMI,ISIKUKOOD\n```\n\n#### P.S Don't forget to include all these listed certificates into container\n"
  },
  {
    "path": "common/collector/age/age.go",
    "content": "/*\nPackage age determines a voter's age and enforces checks on it.\n\nIn some systems, voters are added to the list of voters if they are not of age\nyet, but will become during election. In these cases, we must explicitly check\nthat a voter is of age at the moment they wish to vote.\n\nThis requires knowing the date of birth of the voter, determining of which can\ndiffer based on how this information is stored and represented. Package age\nalso acts as a registry for methods to get a voter's date of birth.\n*/\npackage age\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Method identifies the method used to get the voter's date of birth.\ntype Method string\n\n// Enumeration of date of birth methods.\nconst (\n\tEstPIC Method = \"estpic\" // Estonian personal identification code. Built-in.\n)\n\n// Getter is the type of functions that get a voter's date of birth given their\n// identity. Only the year, month, and day of the returned time will be used.\ntype Getter func(voter string) (dob time.Time, err error)\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = map[Method]Getter{\n\t\tEstPIC: estpic,\n\t}\n)\n\n// Register registers a date of birth method. It is intended to be called from\n// init functions of packages that implement date of birth methods.\nfunc Register(m Method, g Getter) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\tregistry[m] = g\n}\n\n// Conf is the age checker configuration.\ntype Conf struct {\n\tMethod   Method // The method used to get a voters date of birth.\n\tTimeZone string // The IANA Time Zone name where to calculate the age. UTC by default.\n\tLimit    uint64 // The minimum age of a voter who is allowed to vote. Age check disabled if zero.\n}\n\n// Checker gets the voter's age using a configured method and checks it againt\n// the limit.\ntype Checker struct {\n\tget   Getter\n\tloc   *time.Location\n\tlimit int\n\tnow   func() time.Time // Returns the current time, can be replaced for testing.\n}\n\n// New initializes a new voter age checker with the provided configuration.\nfunc New(c *Conf) (checker *Checker, err error) {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tg, ok := registry[c.Method]\n\tif !ok {\n\t\treturn nil, UnlinkedMethodError{Method: c.Method,\n\t\t\tDescription: _AGE_CHECKER_UNKNOWN}\n\t}\n\tloc, err := time.LoadLocation(c.TimeZone)\n\tif err != nil {\n\t\treturn nil, LocationError{Err: err,\n\t\t\tDescription: _AGE_TZ}\n\t}\n\treturn &Checker{g, loc, int(c.Limit), time.Now}, nil //nolint:gosec\n}\n\n// Check gets the voters's date of birth, calculates their age in the\n// configured location, and ensures it is at least the configured lower limit.\nfunc (c *Checker) Check(voter string) (err error) {\n\tif c.limit == 0 {\n\t\treturn nil\n\t}\n\n\tdob, err := c.get(voter)\n\tif err != nil {\n\t\treturn GetAgeError{Err: err, Description: _AGE_DOF}\n\t}\n\tnow := c.now().In(c.loc)       // Get current time in configured zone.\n\tage := now.Year() - dob.Year() // Years since date of birth.\n\tif now.Month() < dob.Month() || now.Month() == dob.Month() && now.Day() < dob.Day() {\n\t\tage-- // If before the date, then subtract one year.\n\t}\n\tif age < c.limit {\n\t\terr = TooYoungError{Age: age, Limit: c.limit, Description: _AGE_YOUNG}\n\t}\n\treturn\n}\n\nvar estpicre = regexp.MustCompile(`^[1-6][0-9]{2}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])[0-9]{4}$`)\n\n// estpic checks if the voter's identity is an Estonian personal identification\n// code, which has the voter's date of birth encoded into it, and returns the\n// voter's age.\nfunc estpic(voter string) (dob time.Time, err error) {\n\tif !estpicre.MatchString(voter) {\n\t\treturn dob, EstPICInvalidFormat{Description: _AGE_ID}\n\t}\n\tdobs := make([]byte, 0, 8)   // yyyymmdd.\n\tpre := 18 + (voter[0]-'1')/2 // '1' and '2' = 18xx, '3' and '4' = 19xx, ...\n\tdobs = strconv.AppendUint(dobs, uint64(pre), 10)[:8]\n\tcopy(dobs[2:], voter[1:7]) // Copy yymmdd from pic.\n\tif dob, err = time.Parse(\"20060102\", string(dobs)); err != nil {\n\t\terr = EstPICParseDOBError{Err: err, Description: _AGE_DOF_FORMAT}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/age/age_test.go",
    "content": "package age\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nfunc TestCheck(t *testing.T) {\n\tc := &Checker{\n\t\tget: func(dob string) (time.Time, error) {\n\t\t\treturn time.Parse(\"2006-01-02\", dob)\n\t\t},\n\t\tloc: time.UTC,\n\t}\n\n\ttests := []struct {\n\t\tdob   string\n\t\tnow   string\n\t\tlimit int\n\t\tok    bool\n\t}{\n\t\t{\"1999-01-01\", \"2016-01-01\", 16, true},\n\t\t{\"2000-01-01\", \"2016-01-01\", 16, true},\n\t\t{\"2000-01-02\", \"2016-02-01\", 16, true},\n\t\t{\"2000-02-28\", \"2016-02-29\", 16, true},\n\t\t{\"2000-02-29\", \"2016-02-29\", 16, true},\n\t\t{\"2000-03-01\", \"2016-02-29\", 16, false},\n\t\t{\"2000-03-02\", \"2016-03-01\", 16, false},\n\t\t{\"2001-01-01\", \"2016-01-01\", 16, false},\n\n\t\t{\"1996-02-29\", \"2017-02-28\", 21, false},\n\t\t{\"1996-02-29\", \"2017-03-01\", 21, true},\n\n\t\t{\"1970-01-02\", \"1970-01-01\", 21, false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%s in %s\", test.dob, test.now), func(t *testing.T) {\n\t\t\tc.now = func() time.Time {\n\t\t\t\tn, err := time.Parse(\"2006-01-02\", test.now)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"failed to parse now:\", err)\n\t\t\t\t}\n\t\t\t\treturn n\n\t\t\t}\n\t\t\tc.limit = test.limit\n\n\t\t\terr := c.Check(test.dob)\n\t\t\tif test.ok && err != nil {\n\t\t\t\tt.Error(\"unexpected error:\", err)\n\t\t\t}\n\t\t\tif !test.ok && errors.CausedBy(err, new(TooYoungError)) == nil {\n\t\t\t\tt.Errorf(\"unexpected error: got %v, want TooYoungError\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEstPIC(t *testing.T) {\n\ttests := []struct {\n\t\tidentity string\n\t\tyear     int\n\t\tmonth    time.Month\n\t\tday      int\n\t}{\n\t\t{\"10001010000\", 1800, time.January, 1},\n\t\t{\"25202290000\", 1852, time.February, 29},\n\t\t{\"37001010000\", 1970, time.January, 1},\n\t\t{\"49108200000\", 1991, time.August, 20},\n\t\t{\"51703150000\", 2017, time.March, 15},\n\t\t{\"69912310000\", 2099, time.December, 31},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.identity, func(t *testing.T) {\n\t\t\tgot, err := estpic(test.identity)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to get voter DOB:\", err)\n\t\t\t}\n\t\t\texp := time.Date(test.year, test.month, test.day, 0, 0, 0, 0, time.UTC)\n\t\t\tif !exp.Equal(got) {\n\t\t\t\tt.Errorf(\"unexpected DOB: got %s, want %s\", got, exp)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/age/log_desc.go",
    "content": "package age\n\nconst (\n\t_AGE_CHECKER_UNKNOWN = \"Unknown voter age checking utility\"\n\t_AGE_TZ              = \"Failed to parse timezone from configuration file\"\n\t_AGE_DOF             = \"Unable to calculate voter date of birth from voter ID\"\n\t_AGE_YOUNG           = \"Voter is too young\"\n\t_AGE_ID              = \"Non-Estonian voter ID\"\n\t_AGE_DOF_FORMAT      = \"Invalid format of voter date of birth\"\n)\n"
  },
  {
    "path": "common/collector/auth/auth.go",
    "content": "/*\nPackage auth provides common code for authenticating clients.\n*/\npackage auth\n\nimport (\n\t\"context\"\n\t\"crypto/x509/pkix\"\n\t\"sync\"\n\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// Type identifies an authentication verifier. The actual implementations are\n// in other packages.\ntype Type string\n\n// Enumeration of authentication verifiers.\nconst (\n\tDummy  Type = \"dummy\"\n\tTLS    Type = \"tls\"\n\tTicket Type = \"ticket\"\n)\n\n// Here we \"declare\" errors that authentication modules should use for wrapping\n// errors so they can be classified.\nvar (\n\t// MalformedTokenError wraps errors which are cause by malformed\n\t// authentication tokens.\n\t_ = MalformedTokenError{Err: nil, Description: _AUTH_MALFORMED}\n\n\t// CertificateError wraps errors which are caused by invalid\n\t// authentication certificates.\n\t_ = CertificateError{Err: nil, Description: _AUTH_CERT}\n\n\t// UnauthorizedError wraps errors where authentication succeeded, but\n\t// the client is not authorized to use any services.\n\t_ = UnauthorizedError{Err: nil, Description: _AUTH_AUTHORIZED}\n)\n\n// Verifier is a client authentication token verifier. An authentication token\n// can be anything from a X.509 certificate used during TLS client\n// authentication to an identity proof constructed by a trusted external\n// service.\ntype Verifier interface {\n\t// Verify verifies the authentication token and returns the\n\t// authenticated client's PKIX name. The content of token depends on\n\t// the implementation. The returned error can only be nil if\n\t// authentication succeeded.\n\t//\n\t// Context is provided since some authentication verifiers might need\n\t// to contact external services.\n\tVerify(ctx context.Context, token []byte) (*pkix.Name, error)\n}\n\n// VoteIdentifier is an optional additional interface that Verifiers can\n// implement. If a Verifier is also a VoteIdentifier, then it should be used to\n// retrieve the vote identifier for storing the vote submitted using the\n// authentication token.\n//\n// This is necessary for single-use tokens to ensure that they cannot be\n// used for multiple votes. Therefore an authentication token must always\n// return the same vote identifier.\ntype VoteIdentifier interface {\n\tVoteIdentifier(token []byte) (voteID []byte, err error)\n}\n\n// TokenData is an optional additional interface that Verifiers can\n// implement. If a Verifier is also a VoteIdentifier, then it should be used to\n// retrieve the vote identifier for storing the vote submitted using the\n// authentication token.\ntype TokenData interface {\n\tTokenData(token []byte) (data []byte, err error)\n}\n\n// NewFunc is the type of functions that an authentication verifier with a\n// specified configuration.\ntype NewFunc func(yaml.Node) (Verifier, error)\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = make(map[Type]NewFunc)\n)\n\n// Register registers an authentication verifier implementation. It is intended\n// to be called from init functions of packages that implement authentication\n// verifiers.\nfunc Register(t Type, n NewFunc) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\tregistry[t] = n\n}\n\n// Conf is the authentication verifier set configuration. It maps enabled\n// authentication verifiers to their configurations. The latter is listed as an\n// unspecified YAML Node, which will be applied to the corresponding\n// authentication verifier's configuration structure.\ntype Conf map[Type]yaml.Node\n\n// Auther contains a configured set of authentication verifiers.\ntype Auther map[Type]Verifier\n\n// Configure configures a set of authentication verifiers specified in the\n// configuration.\nfunc Configure(c Conf) (a Auther, err error) {\n\ta = make(Auther)\n\n\t// For each configured implementation, ...\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tfor t, y := range c {\n\t\t// ..check if it is linked...\n\t\tn, ok := registry[t]\n\t\tif !ok {\n\t\t\treturn nil, UnlinkedTypeError{Type: t,\n\t\t\t\tDescription: _AUTH_UNKNOWN}\n\t\t}\n\n\t\t// ..and if creating a verifier succeeds.\n\t\tv, err := n(y)\n\t\tif err != nil {\n\t\t\treturn nil, ConfigureTypeError{Type: t, Err: err,\n\t\t\t\tDescription: _AUTH_CFG}\n\t\t}\n\t\ta[t] = v\n\t}\n\n\treturn\n}\n\n// Verify dispatches the authentication token to the verifier of type t and\n// returns the authenticated client's name.\n//\n// If the type also implements VoteIdentifier, then it returns the vote\n// identifier to store the vote with.\nfunc (a Auther) Verify(ctx context.Context, t Type, token []byte) (\n\tname *pkix.Name, voteID []byte, err error) {\n\n\tv, ok := a[t]\n\tif !ok {\n\t\treturn nil, nil, UnconfiguredTypeError{Type: t,\n\t\t\tDescription: _AUTH_UNKNOWN}\n\t}\n\tif name, err = v.Verify(ctx, token); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif vid, ok := v.(VoteIdentifier); ok {\n\t\tif voteID, err = vid.VoteIdentifier(token); err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\treturn\n}\n\nfunc (a Auther) Data(t Type, token []byte) (data []byte, err error) {\n\tv, ok := a[t]\n\tif !ok {\n\t\treturn nil, UnconfiguredDataTypeError{Type: t,\n\t\t\tDescription: _AUTH_UNKNOWN}\n\t}\n\tif vid, ok := v.(TokenData); ok {\n\t\tif data, err = vid.TokenData(token); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/auth/dummy/dummy.go",
    "content": "//go:development\n/*\nPackage dummy implement a dummy authentication verifier user for testing.\n\nThe dummy authentication verifier expects the authentication token to be a\nDER-encoded Distinguished Name and it just returns that name. The dummy\nimplementation can also be optionally configured with a limited set of names\nfor which authentication succeeds and all other names will be reported as\nunauthenticated.\n*/\npackage dummy\n\nimport (\n\t\"context\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tauth.Register(auth.Dummy, func(n yaml.Node) (auth.Verifier, error) {\n\t\tc := new(Conf)\n\t\tif err := yaml.Apply(n, c); err != nil {\n\t\t\treturn nil, ConfigurationError{Err: err}\n\t\t}\n\t\treturn c, nil\n\t})\n}\n\n// Conf is the dummy authentication verifier configuration.\ntype Conf struct {\n\t// Authenticated is a slice of Common Names. If not empty, then only\n\t// names included in this list will be authenticated.\n\tAuthenticated []string\n}\n\n// Verify verifies a dummy authentication token.\nfunc (c *Conf) Verify(_ context.Context, token []byte) (name *pkix.Name, err error) {\n\tvar dn pkix.RDNSequence\n\tif _, err := asn1.Unmarshal(token, &dn); err != nil {\n\t\treturn nil, auth.MalformedTokenError{Err: UnmarshalTokenError{Err: err}}\n\t}\n\tname = new(pkix.Name)\n\tname.FillFromRDNSequence(&dn)\n\tif len(c.Authenticated) > 0 {\n\t\tfor _, cn := range c.Authenticated {\n\t\t\tif cn == name.CommonName {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\treturn nil, auth.UnauthorizedError{Err: NotAllowedError{Name: name}}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/auth/dummy/dummy_test.go",
    "content": "package dummy\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nconst (\n\tauthenticatedDN = \"\\x30\\x1f\\x31\\x1d\\x30\\x1b\\x06\\x03\\x55\\x04\\x03\\x13\" +\n\t\t\"\\x14\\x61\\x75\\x74\\x68\\x65\\x6e\\x74\\x69\\x63\\x61\\x74\\x65\\x64\" +\n\t\t\"\\x20\\x63\\x6c\\x69\\x65\\x6e\\x74\"\n\n\tunauthenticatedDN = \"\\x30\\x21\\x31\\x1f\\x30\\x1d\\x06\\x03\\x55\\x04\\x03\" +\n\t\t\"\\x13\\x16\\x75\\x6e\\x61\\x75\\x74\\x68\\x65\\x6e\\x74\\x69\\x63\\x61\" +\n\t\t\"\\x74\\x65\\x64\\x20\\x63\\x6c\\x69\\x65\\x6e\\x74\"\n)\n\nfunc TestVerify(t *testing.T) {\n\tv := Conf{\n\t\tAuthenticated: []string{\"authenticated client\"},\n\t}\n\n\tt.Run(\"authenticated\", func(t *testing.T) {\n\t\tif _, err := v.Verify(context.Background(), []byte(authenticatedDN)); err != nil {\n\t\t\tt.Error(\"unexpected authentication failure:\", err)\n\t\t}\n\t})\n\n\tt.Run(\"unauthenticated\", func(t *testing.T) {\n\t\twant := new(NotAllowedError)\n\t\tif _, err := v.Verify(context.Background(), []byte(unauthenticatedDN)); err == nil {\n\t\t\tt.Error(\"unexpected authentication success\")\n\t\t} else if errors.CausedBy(err, want) == nil {\n\t\t\tt.Errorf(\"unexpected authentication error: %v; want cause %T\", err, want)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "common/collector/auth/log_desc.go",
    "content": "package auth\n\nconst (\n\t_AUTH_MALFORMED  = \"Wraps errors which are cause by malformed authentication tokens\"\n\t_AUTH_CERT       = \"Wraps errors which are cause by invalid authentication certificates\"\n\t_AUTH_AUTHORIZED = \"Wraps errors where authentication succeeded, but the client is not authorized to use any services\"\n\t_AUTH_UNKNOWN    = \"Unknown authentication method\"\n\t_AUTH_CFG        = \"Failed to configure authentication utility\"\n)\n"
  },
  {
    "path": "common/collector/auth/ticket/log_desc.go",
    "content": "package ticket\n\nconst (\n\t_TICKET_NEW_COOKIE = \"Failed to create new cookie from provided AES key\"\n\t_TICKET_KEY        = \"Failed to read AES key file from file system\"\n\t_TICKET_NEW        = \"Failed to create authentication ticket (cookie)\"\n\t_TICKET_VOTEID     = \"Failed to create vote ID (16-byte random value)\"\n\t_TICKET_JSON       = \"Failed to JSON marshal authentication ticket content\"\n\t_TICKET_VERIFY     = \"Failed to verify authentication ticket passed by a client\"\n\t_TICKET_UJSON      = \"Failed to JSON unmarshal authentication ticket content\"\n)\n"
  },
  {
    "path": "common/collector/auth/ticket/ticket.go",
    "content": "/*\nPackage ticket implements voter authentication using cryptographic cookies.\n\nAn authentication service verifies the identity of the voter and then issues\nencrypted and authenticated cookies called tickets. Other services that trust\nthe authentication service, i.e., share the encryption key with it, need only\nverify this ticket and not perform the entire authentication protocol\nthemselves.\n\nTo ensure that a ticket can only be used to store a vote once, it contains the\nvote identifier that will be assigned to the vote: once a vote with this\nidentifier exists, the ticket cannot be used to store a new vote.\n*/\npackage ticket\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"os\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/cookie\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tauth.Register(auth.Ticket, func(_ yaml.Node) (auth.Verifier, error) {\n\t\t// Wrap NewFromSystem to convert *T to auth.Verifier.\n\t\treturn NewFromSystem()\n\t})\n}\n\n// Conf is the ticket authentication method configuration.\ntype Conf struct {\n\tKey cookie.Key\n}\n\n// T is a ticket manager which can issue and verify authentication tickets.\ntype T struct {\n\tcookie *cookie.C\n}\n\n// New creates a new ticket manager with the provided cookie key.\nfunc New(key cookie.Key) (t *T, err error) {\n\tc, err := cookie.New(key)\n\tif err != nil {\n\t\treturn nil, NewCookieError{Err: err, Description: _TICKET_NEW_COOKIE}\n\t}\n\treturn &T{cookie: c}, nil\n}\n\n// NewFromSystem creates a new ticket manager with the key read from the\n// filesystem.\nfunc NewFromSystem() (t *T, err error) {\n\tkey, err := os.ReadFile(\"/var/lib/ivxv/service/ticket.key\")\n\tif err != nil {\n\t\treturn nil, ReadKeyError{Err: err, Description: _TICKET_KEY}\n\t}\n\tif t, err = New(key); err != nil {\n\t\treturn nil, NewTicketError{Err: err, Description: _TICKET_NEW}\n\t}\n\treturn\n}\n\n// NewFromSystemAsCookie returns a cookie, which is used as a shared secret,\n// any implementation can build additional encryption logic on top.\nfunc NewFromSystemAsCookie() (*cookie.C, error) {\n\tkey, err := os.ReadFile(\"/var/lib/ivxv/service/ticket.key\")\n\tif err != nil {\n\t\treturn nil, ReadSharedSecretForCookieError{Err: err, Description: _TICKET_KEY}\n\t}\n\treturn cookie.New(key)\n}\n\ntype tt struct {\n\tSubject pkix.RDNSequence\n\tVoteID  []byte\n}\n\n// Create issues a new authentication ticket for the subject.\nfunc (t *T) Create(subject pkix.Name) (ticket []byte, err error) {\n\tvid := make([]byte, 16)\n\tif _, err = rand.Read(vid); err != nil {\n\t\treturn nil, GenerateVoteIDError{Err: err,\n\t\t\tDescription: _TICKET_VOTEID}\n\t}\n\tsubject.ExtraNames = subject.Names // Also marshal unrecognized names.\n\tplain, err := asn1.Marshal(tt{Subject: subject.ToRDNSequence(), VoteID: vid})\n\tif err != nil {\n\t\treturn nil, MarshalTicketError{Err: err,\n\t\t\tDescription: _TICKET_JSON}\n\t}\n\treturn t.cookie.Create(plain), nil\n}\n\n// Verify implements the ivxv.ee/common/collector/auth.Verifier interface. The token must be a\n// ticket issued with the same cookie key as t was configured with.\nfunc (t *T) Verify(_ context.Context, token []byte) (name *pkix.Name, err error) {\n\tticket, err := t.open(token)\n\tif err != nil {\n\t\treturn nil, VerifyOpenError{Err: err, Description: _TICKET_VERIFY}\n\t}\n\tname = new(pkix.Name)\n\tname.FillFromRDNSequence(&ticket.Subject)\n\treturn\n}\n\n// VoteIdentifier implements the ivxv.ee/common/collector/auth.VoteIdentifier interface. The\n// token must be a ticket issued with the same cookie key as t was configured\n// with.\nfunc (t *T) VoteIdentifier(token []byte) (voteID []byte, err error) {\n\tticket, err := t.open(token)\n\tif err != nil {\n\t\treturn nil, VoteIdentifierOpenError{Err: err,\n\t\t\tDescription: _TICKET_VERIFY}\n\t}\n\treturn ticket.VoteID, nil\n}\n\n// CreateData issues a new ticket for the data.\nfunc (t *T) CreateData(plain []byte) (ticket []byte, err error) {\n\treturn t.cookie.Create(plain), nil\n}\n\n// TokenData implements the ivxv.ee/common/collector/auth.TokenData interface. The\n// token must be a ticket issued with the same cookie key as it was configured\n// with.\nfunc (t *T) TokenData(token []byte) (data []byte, err error) {\n\treturn t.openplain(token)\n}\n\nfunc (t *T) open(token []byte) (ticket tt, err error) {\n\tplain, err := t.openplain(token)\n\tif err != nil {\n\t\treturn ticket, err\n\t}\n\trest, err := asn1.Unmarshal(plain, &ticket)\n\tif err != nil {\n\t\treturn ticket, UnmarshalTicketError{Err: err,\n\t\t\tDescription: _TICKET_UJSON}\n\t}\n\tif len(rest) > 0 {\n\t\treturn ticket, TrailingDataError{Rest: rest,\n\t\t\tDescription: _TICKET_UJSON}\n\t}\n\treturn\n}\n\nfunc (t *T) openplain(token []byte) (plain []byte, err error) {\n\tplain, err = t.cookie.Open(token)\n\tif err != nil {\n\t\treturn nil, OpenTicketError{Err: err, Description: _TICKET_VERIFY}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/auth/tls/log_desc.go",
    "content": "package tls\n\nconst (\n\t_TLS_CFG                   = \"Cannot YAML parse TLS authentication configuration\"\n\t_TLS_NEW                   = \"Failed to configure TLS authentication utility\"\n\t_TLS_CA                    = \"No CA certificate that would verify client certificates provided\"\n\t_TLS_CA_PARSE              = \"Failed to parse CA certificate\"\n\t_TLS_INTERMEDIATE_CA_PARSE = \"Failed to parse intermediate CA certificate\"\n\t_TLS_OCSP                  = \"Failed to configure OCSP client for voter certificate validity verification\"\n\t_TLS_TICKET                = \"TLS authenticated client should not support ticket authentication\"\n\t_TLS_CERT                  = \"Voter hasn't provided certificates on TLS authentication\"\n\t_TLS_CERT_VERIFY           = \"Voter certificate verification failed\"\n\t_TLS_CERT_VERIFY_OCSP      = \"Voter certificate OCSP verification failed\"\n\t_TLS_CERT_VERIFY_OCSP_BAD  = \"Voter certificate OCSP status is not good\"\n)\n"
  },
  {
    "path": "common/collector/auth/tls/tls.go",
    "content": "/*\nPackage tls implements voter authentication using TLS client authentication.\n\nThe server package requests any client certificate during the TLS handshake,\nbut does not require or verify it. This package can be used to require it and\nset the parameters used for verification.\n\nNote that only the configured roots and intermediates will be used for\nverification: any intermediate certificates provided by the client will be\nignored.\n*/\npackage tls\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/ocsp\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tauth.Register(auth.TLS, func(n yaml.Node) (auth.Verifier, error) {\n\t\tc := new(Conf)\n\t\tif err := yaml.Apply(n, c); err != nil {\n\t\t\treturn nil, ConfigurationError{Err: err, Description: _TLS_CFG}\n\t\t}\n\t\tv, err := New(c)\n\t\tif err != nil {\n\t\t\treturn nil, CertificateParsingError{Err: err, Description: _TLS_NEW}\n\t\t}\n\t\treturn v, nil\n\t})\n}\n\n// Conf is the TLS authentication verifier configuration.\ntype Conf struct {\n\tRoots         []string   // PEM-encoded client certificate verification roots.\n\tIntermediates []string   // PEM-encoded client certificate verification intermediates.\n\tOCSP          *ocsp.Conf // Optional OCSP-checking configuration.\n}\n\n// V is an auth.Verifier which checks TLS authentication.\ntype V struct {\n\trpool *x509.CertPool\n\tipool *x509.CertPool\n\tocsp  *ocsp.Client\n}\n\n// New returns a new TLS authentication verifier with the provided configuration.\nfunc New(c *Conf) (v *V, err error) {\n\tif len(c.Roots) == 0 {\n\t\treturn nil, UnconfiguredRootsError{Description: _TLS_CA}\n\t}\n\n\tv = new(V)\n\tif v.rpool, err = cryptoutil.PEMCertificatePool(c.Roots...); err != nil {\n\t\treturn nil, RootsParsingError{Err: err,\n\t\t\tDescription: _TLS_CA_PARSE}\n\t}\n\tif v.ipool, err = cryptoutil.PEMCertificatePool(c.Intermediates...); err != nil {\n\t\treturn nil, IntermediatesParsingError{Err: err,\n\t\t\tDescription: _TLS_INTERMEDIATE_CA_PARSE}\n\t}\n\tif c.OCSP != nil {\n\t\tif v.ocsp, err = ocsp.New(c.OCSP); err != nil {\n\t\t\treturn nil, OCSPClientError{Err: err,\n\t\t\t\tDescription: _TLS_OCSP}\n\t\t}\n\t}\n\treturn\n}\n\n// Verify implements the auth.Verifier interface. The token is unused and only\n// the client certificate in context is verified.\nfunc (v *V) Verify(ctx context.Context, token []byte) (*pkix.Name, error) {\n\tif len(token) > 0 {\n\t\treturn nil, auth.MalformedTokenError{\n\t\t\tErr: NonEmptyTokenError{Token: log.Sensitive(token),\n\t\t\t\tDescription: _TLS_TICKET},\n\t\t}\n\t}\n\n\t// Client certificates were added to the context by ivxv.ee/common/collector/server.\n\tcerts := server.TLSClient(ctx)\n\tif len(certs) == 0 {\n\t\treturn nil, auth.CertificateError{Err: NoClientCertificateError{\n\t\t\tDescription: _TLS_CERT,\n\t\t}}\n\t}\n\tcert := certs[0] // The client certificate must be first.\n\n\topts := x509.VerifyOptions{\n\t\tRoots:         v.rpool,\n\t\tIntermediates: v.ipool,\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t}\n\tchains, err := cert.Verify(opts)\n\tif err != nil {\n\t\treturn nil, auth.CertificateError{\n\t\t\tErr: CertificateVerificationError{\n\t\t\t\tCertificate: cert.Raw, // Log entire cert for diagnostics.\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _TLS_CERT_VERIFY,\n\t\t\t},\n\t\t}\n\t}\n\n\tif v.ocsp != nil {\n\t\tissuer := cert\n\t\tif len(chains[0]) > 1 { // At least one chain is guaranteed.\n\t\t\tissuer = chains[0][1]\n\t\t}\n\t\tstatus, err := v.ocsp.Check(ctx, cert, issuer, nil)\n\t\tif err != nil {\n\t\t\treturn nil, CheckCertificateStatusError{\n\t\t\t\tCertificate: cert,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _TLS_CERT_VERIFY_OCSP,\n\t\t\t}\n\t\t}\n\t\tif !status.Good {\n\t\t\treturn nil, auth.CertificateError{\n\t\t\t\tErr: CertificateStatusError{\n\t\t\t\t\tCertificate:      cert,\n\t\t\t\t\tUnknown:          status.Unknown,\n\t\t\t\t\tRevocationReason: status.RevocationReason,\n\t\t\t\t\tDescription:      _TLS_CERT_VERIFY_OCSP_BAD,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &cert.Subject, nil\n}\n"
  },
  {
    "path": "common/collector/cmd/verifier/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t//ivxv:modules common/collector/container\n)\n\nfunc main() {\n\tcode, err := verifierMain()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", err)\n\t}\n\tos.Exit(code)\n}\n\nfunc verifierMain() (int, error) {\n\tflag.Usage = func() {\n\t\tfmt.Fprintln(os.Stderr, \"Usage: \"+os.Args[0]+` [options] <container file>\n\nverifier uses the trust root given in order to verify a container's signatures\nand return the signers and signing times.\n\nThe trust container must contain a single file. This is the trust configuration\nwhich must be identified by the key \"trust.yaml\".\n\nThe container to be verified must have an extension corresponding to the\ncontainer type it is, e.g., foo.bdoc.\n\noptions:`)\n\t\tflag.PrintDefaults()\n\t}\n\n\ttrust := flag.String(\"trust\", \"/etc/ivxv/trust.bdoc\",\n\t\t\"`path` to the trust container. Must have an extension corresponding to\\n\"+\n\t\t\t\"the container type it is, e.g., trust.bdoc.\\n\")\n\tflag.Parse()\n\tif len(flag.Args()) != 1 {\n\t\tflag.Usage()\n\t\treturn exit.Usage, nil\n\t}\n\tpath := flag.Arg(0)\n\n\t// We do not want the verifier application to log anything, but it is\n\t// still assumed that the context has a logger. Use TestContext which\n\t// provides a test logger that does nothing.\n\tctx := log.TestContext(context.Background())\n\n\tcfg, code, err := conf.New(ctx, *trust, \"\", \"\")\n\tif err != nil {\n\t\treturn code, fmt.Errorf(\"failed to load trust root: %v\", err)\n\t}\n\n\tc, err := cfg.Container.OpenFile(path)\n\tif err != nil {\n\t\tcode = exit.DataErr\n\t\tif perr := errors.CausedBy(err, new(os.PathError)); perr != nil {\n\t\t\tif os.IsNotExist(perr) {\n\t\t\t\tcode = exit.NoInput\n\t\t\t}\n\t\t}\n\t\treturn code, fmt.Errorf(\"failed to open container: %v\", err)\n\t}\n\tdefer c.Close()\n\n\tfor _, s := range c.Signatures() {\n\t\tfmt.Println(s.CommonName(), s.SigningTime.Format(time.RFC3339))\n\t}\n\n\treturn exit.OK, nil\n}\n"
  },
  {
    "path": "common/collector/command/command.go",
    "content": "/*\nPackage command provides common code for command-line applications.\n*/\npackage command\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n)\n\nfunc mkflag(name, value, desc string) *string {\n\treturn flag.String(name, fmt.Sprint(\"/etc/ivxv/\", value),\n\t\tfmt.Sprintf(\"`path` to the %s container. Must have an extension\\n\"+\n\t\t\t\"corresponding to the container type it is, e.g., %s.\\n\",\n\t\t\tdesc, value)) // New line for printing the default value.\n}\n\nfunc mkusage(usage string, args ...string) func() {\n\n\tvar argbuf bytes.Buffer\n\tfor _, arg := range args {\n\t\targbuf.WriteString(\" <\")\n\t\targbuf.WriteString(arg)\n\t\targbuf.WriteString(\">\")\n\t}\n\targstr := argbuf.String()\n\n\tif len(usage) > 0 {\n\t\tusage += \"\\n\\n\"\n\t}\n\n\treturn func() {\n\t\tfmt.Fprintf(os.Stderr, \"usage: %s [options]%s\\n\\n%soptions:\\n\",\n\t\t\tos.Args[0], argstr, usage)\n\t\tflag.PrintDefaults()\n\t\tos.Exit(exit.Usage)\n\t}\n}\n\n// Enumeration of C.Until values which indicate how far the command-line\n// application should execute.\nconst (\n\tCheckTrust   = iota // Check the trust root: done by the command package.\n\tCheckConf           // Check configuration values.\n\tCheckVersion        // Check current loaded version.\n\tCheckInput          // Check input values.\n\tExecute             // Execute normally.\n)\n\n// C is used by a command-line application to perform common tasks.\ntype C struct {\n\tCtx     context.Context\n\tArgs    []string\n\tConf    *conf.C\n\tNetwork string        // Network segment for this service instance.\n\tService *conf.Service // Configuration for this service instance.\n\tStorage *storage.Client\n\n\t// Until indicates how far the command-line application should execute.\n\t// The enumerated constants are ordered, so e.g., if Until is\n\t// CheckInput, then that means that it must perform CheckTrust,\n\t// CheckConf and CheckInput, but not Execute.\n\tUntil int\n\n\tcancel func()\n}\n\n// New performs common command-line application startup tasks.\n//\n//  1. It parses command line options and arguments, checking that all mandatory\n//     arguments are given. It uses usage and argument names from args for usage\n//     and error messages. Note that arguments are only mandatory if c.Until is\n//     at least CheckInput and c.Args is nil otherwise.\n//\n//  2. It creates a context with a logger using the provided tag. It is the\n//     caller's responsibility to call log.Close with this context after the\n//     command has finished.\n//\n//  3. The context is set up to be cancelled on an interrupt or termination\n//     signal.\n//\n//  4. It loads the collector configuration. Options for specifying\n//     configuration file locations are automatically added. Callers must\n//     examine c.Until to determine if they should execute normally or only\n//     check configuration and input values and exit early. Also note that if\n//     c.Until is CheckConf, then either c.Conf.Election or c.Conf.Technical is\n//     nil.\n//\n//  5. It creates a storage service client: almost all command-line applications\n//     will be performing some operations on the election data (see\n//     NewWithoutStorage to skip this step).\n//\n// If an error occurs, then New will print (and log if possible) the error and\n// call os.Exit. Because of this, make sure not to call New after defers have\n// been set up, that must fire before exiting.\nfunc New(tag string, usage string, args ...string) (c *C) {\n\treturn newC(tag, usage, true, args...)\n}\n\n// NewWithoutStorage does the same startup tasks as New, but skips creating a\n// storage service client. This method is only needed for services which do not\n// communicate with storage directly.\nfunc NewWithoutStorage(tag string, usage string, args ...string) (c *C) {\n\treturn newC(tag, usage, false, args...)\n}\n\nfunc newC(tag string, usage string, withStorage bool, args ...string) (c *C) {\n\tc = new(C)\n\n\ttrustp := mkflag(\"trust\", \"trust.bdoc\", \"configuration trust\")\n\telecp := mkflag(\"election\", \"election.bdoc\", \"election configuration\")\n\ttechp := mkflag(\"technical\", \"technical.bdoc\", \"technical configuration\")\n\tinstancep := flag.String(\"instance\", \"\",\n\t\t\"instance identifier for the service running this command.\")\n\tcheckp := flag.String(\"check\", \"\", \"check `target` configuration, \"+\n\t\t\"version, and/or input file and exit. target\\nmust be either \"+\n\t\t`\"trust\", \"technical\", \"election\", \"version\", or \"input\".`)\n\n\tflag.Usage = mkusage(usage, args...)\n\tflag.Parse()\n\n\tswitch *checkp {\n\tcase \"trust\":\n\t\tc.Until = CheckTrust\n\t\t*techp = \"\" // Skip loading other configuration files.\n\t\t*elecp = \"\"\n\tcase \"technical\":\n\t\tc.Until = CheckConf\n\t\t*elecp = \"\" // Skip loading election configuration.\n\tcase \"election\":\n\t\tc.Until = CheckConf\n\t\t*techp = \"\" // Skip loading technical configuration.\n\tcase \"version\":\n\t\tc.Until = CheckVersion\n\tcase \"input\":\n\t\tc.Until = CheckInput\n\tcase \"\":\n\t\tc.Until = Execute\n\tdefault:\n\t\tfmt.Fprintln(os.Stderr, `error: bad \"check\" value:`, *checkp)\n\t\tflag.Usage()\n\t}\n\n\tswitch m, n := flag.NArg(), len(args); {\n\tcase m < n:\n\t\t// Only require arguments if at least checking input. Do not\n\t\t// disallow them to maintain backwards-compatibility with when\n\t\t// arguments were always mandatory.\n\t\tif c.Until >= CheckInput {\n\t\t\tfmt.Fprintln(os.Stderr, \"error: missing argument:\", args[m])\n\t\t\tflag.Usage()\n\t\t}\n\tcase m > n:\n\t\tfmt.Fprintln(os.Stderr, \"error: extra argument:\", flag.Arg(n))\n\t\tflag.Usage()\n\tdefault:\n\t\tc.Args = flag.Args()\n\t}\n\n\tvar err error\n\tif c.Ctx, err = log.NewContext(context.Background(), tag); err != nil {\n\t\tos.Exit(c.Error(exit.Unavailable, nil, \"failed to configure logger:\", err))\n\t}\n\tlog.Log(c.Ctx, Started{Args: os.Args, Description: _COMMAND_START})\n\n\tc.Ctx, c.cancel = context.WithCancel(c.Ctx)\n\tch := make(chan os.Signal, 1)\n\tsignal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\ts := <-ch\n\t\tlog.Log(c.Ctx, Cancelled{Signal: s, Description: _COMMAND_OS})\n\t\tc.cancel()\n\t}()\n\n\tvar code int\n\tif c.Conf, code, err = conf.New(c.Ctx, *trustp, *elecp, *techp); err != nil {\n\t\tos.Exit(c.Error(code, ConfigurationError{Err: err,\n\t\t\tDescription: _COMMAND_CFG},\n\t\t\t\"failed to load configuration:\", err))\n\t}\n\n\t// If only checking trust, then already exit here.\n\tif c.Until == CheckTrust {\n\t\tos.Exit(c.Cleanup(exit.OK))\n\t}\n\n\t// If we have the technical configuration, then find our network\n\t// segment and instance configuration.\n\tif c.Conf.Technical != nil {\n\t\tc.Network, c.Service = c.Conf.Technical.Service(*instancep)\n\t\tif c.Service == nil {\n\t\t\tos.Exit(c.Error(exit.Config, UnknownInstanceIDError{ID: *instancep,\n\t\t\t\tDescription: _COMMAND_INSTANCE},\n\t\t\t\t\"no such service ID:\", *instancep))\n\t\t}\n\t}\n\n\tif withStorage && c.Conf.Technical != nil {\n\t\tvar servers []string\n\t\tfor _, s := range c.Conf.Technical.Services(c.Network).Storage {\n\t\t\tservers = append(servers, s.Address)\n\t\t}\n\t\tif c.Storage, err = storage.New(&c.Conf.Technical.Storage,\n\t\t\t&storage.Services{\n\t\t\t\tSensitive: conf.Sensitive(c.Service.ID),\n\t\t\t\tServers:   servers,\n\t\t\t}); err != nil {\n\n\t\t\tos.Exit(c.Error(exit.Config, StorageConfigurationError{Err: err,\n\t\t\t\tDescription: _COMMAND_STORAGE},\n\t\t\t\t\"failed to configure storage client:\", err))\n\t\t}\n\t}\n\treturn\n}\n\n// Cleanup cancels the context to clean up resources and closes the logger.\n// code is the exit code that the caller is exiting with: if it is OK, then it\n// may be replaced by Cleanup if an error occurs, otherwise it is returned\n// unmodified. Any errors will also be printed.\n//\n// Using this design, Cleanup can be deferred as such:\n//\n//\tfunc appmain() (code int) {\n//\t\tc := command.New(...)\n//\t\tdefer func() {\n//\t\t\tcode = c.Cleanup(code)\n//\t\t}()\n//\t\treturn\n//\t}\nfunc (c *C) Cleanup(code int) int {\n\tc.cancel()\n\tlog.Log(c.Ctx, Done{Description: _COMMAND_STOP})\n\tif err := log.Close(c.Ctx); err != nil {\n\t\tccode := c.Error(exit.Unavailable, nil, \"failed to close logger:\", err)\n\t\tif code == exit.OK {\n\t\t\tcode = ccode\n\t\t}\n\t}\n\treturn code\n}\n\n// Error logs err if not nil, prints an error message containing a, and returns\n// the provided code. Meant to easily exit after an error in the main function\n// of a command-line application.\nfunc (c *C) Error(code int, err log.ErrorEntry, a ...interface{}) int {\n\tif err != nil {\n\t\tlog.Error(c.Ctx, err)\n\t}\n\n\tfmt.Fprintln(os.Stderr, append([]interface{}{\"error:\"}, a...)...)\n\treturn code\n}\n"
  },
  {
    "path": "common/collector/command/exit/exit.go",
    "content": "/*\nPackage exit contains exit codes for command-line application.\n\nThis is a separate package to avoid import cycles between ivxv.ee/common/collector/command and\nivxv.ee/common/collector/conf.\n*/\npackage exit\n\n// Exit code contant values and descriptions taken from\n// /usr/include/sysexits.h.\nconst (\n\tOK = 0 // successful termination\n\n\t// EX_USAGE -- The command was used incorrectly, e.g., with the wrong\n\t//         number of arguments, a bad flag, a bad syntax in a\n\t//         parameter, or whatever.\n\tUsage = 64\n\n\t// EX_DATAERR -- The input data was incorrect in some way. This should\n\t//         only be used for user's data & not system files.\n\tDataErr = 65\n\n\t// EX_NOINPUT -- An input file (not a system file) did not exist or was\n\t//         not readable. This could also include errors like \"No\n\t//         message\" to a mailer (if it cared to catch it).\n\tNoInput = 66\n\n\t// EX_NOUSER -- The user specified did not exist. This might be used\n\t//         for mail addresses or remote logins.\n\tNoUser = 67\n\n\t// EX_NOHOST -- The host specified did not exist. This is used in mail\n\t//         addresses or network requests.\n\tNoHost = 68\n\n\t// EX_UNAVAILABLE -- A service is unavailable. This can occur if a\n\t//         support program or file does not exist. This can also be\n\t//         used as a catchall message when something you wanted to do\n\t//         doesn't work, but you don't know why.\n\tUnavailable = 69\n\n\t// EX_SOFTWARE -- An internal software error has been detected. This\n\t//         should be limited to non-operating system related errors as\n\t//         possible.\n\tSoftware = 70\n\n\t// EX_OSERR -- An operating system error has been detected. This is\n\t//         intended to be used for such things as \"cannot fork\",\n\t//         \"cannot create pipe\", or the like. It includes things like\n\t//         getuid returning a user that does not exist in the passwd\n\t//         file.\n\tOSErr = 71\n\n\t// EX_OSFILE -- Some system file (e.g., /etc/passwd, /etc/utmp, etc.)\n\t//         does not exist, cannot be opened, or has some sort of error\n\t//         (e.g., syntax error).\n\tOSFile = 72\n\n\t// EX_CANTCREAT -- A (user specified) output file cannot be created.\n\tCantCreate = 73\n\n\t// EX_IOERR -- An error occurred while doing I/O on some file.\n\tIOErr = 74\n\n\t// EX_TEMPFAIL -- Temporary failure, indicating something that is not\n\t//         really an error. In sendmail, this means that a mailer\n\t//         (e.g.) could not create a connection, and the request should\n\t//         be reattempted later.\n\tTempFail = 75\n\n\t// EX_PROTOCOL -- The remote system returned something that was \"not\n\t//         possible\" during a protocol exchange.\n\tProtocol = 76\n\n\t// EX_NOPERM -- You did not have sufficient permission to perform the\n\t//         operation. This is not intended for file system problems,\n\t//         which should use NOINPUT or CANTCREAT, but rather for higher\n\t//         level permissions.\n\tNoPerm = 77\n\n\tConfig = 78 // configuration error\n)\n"
  },
  {
    "path": "common/collector/command/log_desc.go",
    "content": "package command\n\nconst (\n\t_COMMAND_START    = \"Logger for the command-line application has been configured\"\n\t_COMMAND_OS       = \"OS killed application with either SIGINT or SIGTERM\"\n\t_COMMAND_CFG      = \"Failed to read application configuration file\"\n\t_COMMAND_INSTANCE = \"Application is being loaded on an instance which identifier hasn't been registered in technical configuration\"\n\t_COMMAND_STORAGE  = \"Failed to configure storage service client for the application\"\n\t_COMMAND_STOP     = \"Application is being stopped, cleanup in progress\"\n)\n"
  },
  {
    "path": "common/collector/command/status/isatty_linux.go",
    "content": "//go:build linux\n\npackage status\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\n// Copied from golang.org/x/crypto/ssh/terminal.IsTerminal.\nfunc isatty(fd uintptr) bool {\n\tvar termios syscall.Termios\n\t_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, syscall.TCGETS,\n\t\tuintptr(unsafe.Pointer(&termios)), 0, 0, 0)\n\treturn err == 0\n}\n"
  },
  {
    "path": "common/collector/command/status/isatty_other.go",
    "content": "//go:build !linux\n\npackage status\n\nfunc isatty(fd uintptr) bool {\n\treturn false\n}\n"
  },
  {
    "path": "common/collector/command/status/status.go",
    "content": "/*\nPackage status implements displaying the status of a process.\n\nIf running on Linux and os.Stdout is a terminal then any updates to the status\nline will be reflected on the same line. These lines are called \"rewritable\".\nOtherwise any updates will print a new line.\n*/\npackage status\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"unicode/utf8\"\n)\n\n// Line is the current status of the process displayed in a single line.\n//\n// All methods of a Line can also be called on nil without any effect. This\n// simplifies optional quieting the status line.\ntype Line struct {\n\ttty    bool\n\tstack  []interface{}\n\tlength int\n\tmu     sync.Mutex\n}\n\n// New initializes a new status line.\nfunc New() *Line {\n\treturn &Line{tty: isatty(os.Stdout.Fd())}\n}\n\n// Redraw redraws the current status line.\nfunc (l *Line) Redraw() {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tl.redraw()\n}\n\nfunc (l *Line) redraw() {\n\tl.setLine(fmt.Sprint(l.stack...))\n}\n\nfunc (l *Line) setLine(s string) {\n\tif !l.tty {\n\t\tfmt.Println(s)\n\t\treturn\n\t}\n\n\tfmt.Print(\"\\r\", s)\n\n\tn := utf8.RuneCountInString(s)\n\tif tail := l.length - n; tail > 0 {\n\t\tfmt.Print(strings.Repeat(\" \", tail), strings.Repeat(\"\\b\", tail))\n\t}\n\n\tl.length = n\n}\n\n// Static appends a static string to the status line.\nfunc (l *Line) Static(s string) {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tl.push(updatable{s: s}) // Reuse updatable but with no way of updating.\n}\n\n// Update is the type of functions used to update an updatable string. Returns\n// the previous value.\ntype Update func(updated string) string\n\n// Updatable appends an updatable string to the status line. If redraw is true,\n// then the status line will be redrawn after each call to Update. If l is nil,\n// then Update is a noop.\nfunc (l *Line) Updatable(initial string, redraw bool) Update {\n\tif l == nil {\n\t\treturn func(string) string { return \"\" }\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tu := &updatable{l: l, s: initial, redraw: redraw}\n\tl.push(u)\n\treturn u.update\n}\n\ntype updatable struct {\n\tl      *Line\n\tredraw bool\n\ts      string\n}\n\nfunc (u *updatable) update(s string) string {\n\tu.l.mu.Lock()\n\tdefer u.l.mu.Unlock()\n\told := u.s\n\tu.s = s\n\tif u.redraw {\n\t\tu.l.redraw()\n\t}\n\treturn old\n}\n\nfunc (u updatable) String() string { return u.s }\n\n// Add is the type of function used to update Count and Percentage displays.\n// Returns the new total value.\ntype Add func(count uint64) uint64\n\n// Count appends a counter to the status line. If total is non-zero, then the\n// count is displayed as \"<current>/<total>\". If redraw is true, then the\n// status line will be redrawn after each call to Add. If l is nil, then Add is\n// a noop, but the returned total will still be accurate.\nfunc (l *Line) Count(total uint64, redraw bool) Add {\n\tif l == nil {\n\t\treturn (&count{l: new(Line)}).add\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tp := &count{l: l, format: \"%d\", redraw: redraw}\n\tif total > 0 {\n\t\ttstr := fmt.Sprint(total)\n\t\tp.format = fmt.Sprintf(\"%%%dd/%s\", len(tstr), tstr)\n\t}\n\tl.push(p)\n\treturn p.add\n}\n\ntype count struct {\n\tl       *Line\n\tredraw  bool\n\tformat  string\n\tcurrent uint64\n}\n\nfunc (c *count) add(count uint64) uint64 {\n\tc.l.mu.Lock()\n\tdefer c.l.mu.Unlock()\n\tc.current += count\n\tif c.redraw {\n\t\tc.l.redraw()\n\t}\n\treturn c.current\n}\n\nfunc (c count) String() string { return fmt.Sprintf(c.format, c.current) }\n\n// Percent appends a percentage indicator to the status line. It calculates the\n// percentage itself based on current and total values. If total is 0, then\n// 100% is always displayed. If redraw is true, then the status line will be\n// redrawn after each call to Add that changes the percentage value. If l is\n// nil, then Add is a noop, but the returned total will still be accurate.\nfunc (l *Line) Percent(total uint64, redraw bool) Add {\n\tif l == nil {\n\t\treturn (&percent{l: new(Line)}).add\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tp := &percent{l: l, total: total, redraw: redraw}\n\tif p.total == 0 {\n\t\tp.percent = 100\n\t}\n\tp.step = p.total / 100\n\tp.recalc = p.step\n\tl.push(p)\n\treturn p.add\n}\n\ntype percent struct {\n\tl       *Line\n\tredraw  bool\n\tcurrent uint64\n\ttotal   uint64\n\tpercent uint64\n\tstep    uint64 // Minimum count that must be added for percent to change.\n\trecalc  uint64 // Next value that percent must be recalculated on.\n}\n\nfunc (p *percent) add(count uint64) uint64 {\n\tp.l.mu.Lock()\n\tdefer p.l.mu.Unlock()\n\tp.current += count\n\tif p.total > 0 && (p.step == 0 || p.current >= p.recalc) {\n\t\toldpc := p.percent // To check if percent actually changed.\n\t\tp.percent = p.current * 100 / p.total\n\t\tp.recalc = p.percent*p.step + p.step\n\t\tif p.redraw && oldpc != p.percent {\n\t\t\tp.l.redraw()\n\t\t}\n\t}\n\treturn p.current\n}\n\nfunc (p percent) String() string { return fmt.Sprintf(\"%3d%%\", p.percent) }\n\nfunc (l *Line) push(stringer fmt.Stringer) {\n\tl.stack = append(l.stack, stringer)\n}\n\n// Pop removes the last appended element from the status line.\nfunc (l *Line) Pop() {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tif len(l.stack) > 0 {\n\t\tl.stack = l.stack[:len(l.stack)-1]\n\t}\n}\n\n// Hide hides a rewritable status line. This can be used to display a permanent\n// line before redrawing the rewritable line.\n//\n// Hide does nothing if the line is not rewritable.\nfunc (l *Line) Hide() {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tif l.tty {\n\t\tl.setLine(\"\")\n\t}\n}\n\n// Show unhides a rewritable status line.\n//\n// Show does nothing if the line is not rewritable.\nfunc (l *Line) Show() {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tif l.tty {\n\t\tl.redraw()\n\t}\n}\n\n// Keep keeps the last output of a rewritable line and resets the line. This\n// can be used to keep the end result of a progress counter visible and start a\n// new status line.\n//\n// If the line is not rewritable, then Keep only resets it.\nfunc (l *Line) Keep() {\n\tif l == nil {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tif l.tty {\n\t\tfmt.Println()\n\t}\n\tl.stack = nil\n\tl.length = 0\n}\n"
  },
  {
    "path": "common/collector/conf/conf.go",
    "content": "/*\nPackage conf provides common code for accessing the configuration of collector\nservices.\n*/\npackage conf\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/age\"\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/mid\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/smartid\"\n\t\"ivxv.ee/common/collector/status\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// C wraps the configuration container parser, election and technical\n// configuration into one.\ntype C struct {\n\tVersion   version.V\n\tContainer container.Opener\n\tElection  *Election\n\tTechnical *Technical\n}\n\n// Election contains the election parameters.\ntype Election struct {\n\tIdentifier string   // The election identifier.\n\tQuestions  []string // Identifiers of questions asked during this election.\n\n\t// Election period. All times must be formatted according to RFC3339.\n\tPeriod struct {\n\t\tServiceStart     string // The time when to start serving requests.\n\t\tElectionStart    string // The start time of the election: votes before this are not counted.\n\t\tElectionStop     string // The end time of the election: choices are no longer distributed.\n\t\tServiceStop      string // The time when to stop serving requests.\n\t\tVerificationStop string // The time when to stop verification requests.\n\t}\n\n\tVoting struct {\n\t\tRateLimitStart   uint64 // After how many votes do we rate limit submissions? 0 means immediately.\n\t\tRateLimitMinutes uint64 // How many minutes between submission when limiting? 0 disables rate limiting.\n\t}\n\n\tVerification struct {\n\t\tCount      uint64 // How many times a vote can be verified? 0 means unlimited.\n\t\tMinutes    uint64 // How much time is given to verify a vote? 0 means unlimited.\n\t\tLatestOnly bool   // If true, then only the latest vote of a voter can be verified.\n\t}\n\n\t// VoterForeignEHAK specifies the administrative unit code (EHAK) to\n\t// use for determining voter districts if the voter is foreign. If\n\t// VoterForeignEHAK is empty, then the default value \"0000\" is used.\n\tVoterForeignEHAK string\n\n\t// IgnoreVoterList is an option used for public testing and should NOT\n\t// be set during production! If not empty, then voter lists will be\n\t// ignored, meaning that everybody who can authenticate and produce a\n\t// valid signed container can vote, and the specified choices list will\n\t// be presented to all users.\n\tIgnoreVoterList string\n\n\tVoterList struct {\n\t\tKey string // PEM-encoding of the public key used to verify voter list signatures.\n\t}\n\n\tXRoad struct {\n\t\tCA string // PEM-encoded authentication certificate.\n\t}\n\n\t// Ballot configuration contains everything a service may need to check\n\t// ballot correctness without decrypting it.\n\t//\n\t// One can have only EncPkeyGroup set and then EncPkey becomes optional.\n\t// The same way we can have only EncPkey set while EncPkeyGroup left empty.\n\t// However, if both are set then EncPkey takes precedence.\n\tBallot struct {\n\t\t// EncPkeyGroup is an abstract group that encryption public\n\t\t// key belongs to, and which is used for voter ballot encryption in a\n\t\t// voting application.\n\t\t//\n\t\t// Check for all available groups in Documentation.\n\t\tEncPkeyGroup string\n\n\t\t// x509 voter ballot encryption public key\n\t\tEncPkey string\n\t}\n\n\t// Composited configuration structures defined in other packages.\n\tAuth          auth.Conf\n\tIdentity      identity.Type\n\tAge           age.Conf\n\tVote          container.Conf\n\tMID           mid.Conf\n\tSmartID       smartid.Conf\n\tQualification q11n.Conf\n}\n\n// ServiceStartTime parses c.Period.ServiceStart and returns the result.\nfunc (e Election) ServiceStartTime() (time.Time, error) {\n\treturn time.Parse(time.RFC3339, e.Period.ServiceStart)\n}\n\n// ElectionStartTime parses c.Period.ElectionStart and returns the result.\nfunc (e Election) ElectionStartTime() (time.Time, error) {\n\treturn time.Parse(time.RFC3339, e.Period.ElectionStart)\n}\n\n// ElectionStopTime parses c.Period.ElectionStop and returns the result.\nfunc (e Election) ElectionStopTime() (time.Time, error) {\n\treturn time.Parse(time.RFC3339, e.Period.ElectionStop)\n}\n\n// ServiceStopTime parses c.Period.ElectionStop and returns the result.\nfunc (e Election) ServiceStopTime() (time.Time, error) {\n\treturn time.Parse(time.RFC3339, e.Period.ServiceStop)\n}\n\n// VerificationStopTime parses c.Period.VerificationStop and returns the result.\nfunc (e Election) VerificationStopTime() (time.Time, error) {\n\treturn time.Parse(time.RFC3339, e.Period.VerificationStop)\n}\n\n// VoterForeignEHAKDefault returns e.VoterForeignEHAK or its default value if\n// it is not specified.\nfunc (e Election) VoterForeignEHAKDefault() string {\n\tif e.VoterForeignEHAK != \"\" {\n\t\treturn e.VoterForeignEHAK\n\t}\n\treturn \"0000\"\n}\n\n// Technical contains the collector services' technical parameters.\ntype Technical struct {\n\tDebug bool // Should debug logging be enabled?\n\n\tSniDomain string\n\n\tNetwork []struct {\n\t\tID       string   // Network segment identifier.\n\t\tServices Services // Configured services in this segment.\n\t}\n\n\t// Composited configuration structures defined in other packages.\n\tFilter  server.FilterConf\n\tStorage storage.Conf\n\n\tStatus status.Conf\n}\n\n// Services is a block of configured services for each service type.\ntype Services struct {\n\tProxy         []*Service\n\tMID           []*Service\n\tSmartID       []*Service\n\tWebeID        []*Service\n\tChoices       []*Service\n\tVoting        []*Service\n\tVerification  []*Service\n\tStorage       []*Service\n\tVotesOrder    []*Service\n\tSessionStatus []*Service\n}\n\n// Services finds the configured services for the requested network segment.\nfunc (t Technical) Services(network string) *Services {\n\tfor _, n := range t.Network {\n\t\tif n.ID == network {\n\t\t\treturn &n.Services\n\t\t}\n\t}\n\treturn nil\n}\n\n// Service is the configuration for a single service instance.\ntype Service struct {\n\tID          string // Unique identifier of the service instance.\n\tAddress     string // The host:port to listen on for requests.\n\tPeerAddress string // The host:port to listen on for peer messages.\n\tOrigin      string // In case of proxy, this is an FQDN that client sees.\n}\n\n// Service finds the network and configuration for a service instance with id.\nfunc (t Technical) Service(id string) (network string, service *Service) {\n\tfields := reflect.TypeOf(new(Services)).Elem().NumField()\n\tfor _, n := range t.Network {\n\t\tservices := reflect.ValueOf(n.Services)\n\t\tfor i := 0; i < fields; i++ {\n\t\t\tfor _, s := range services.Field(i).Interface().([]*Service) {\n\t\t\t\tif s.ID == id {\n\t\t\t\t\treturn n.ID, s\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\n// Sensitive returns the path to the service directory for the service instance\n// with ID. This directory contains sensitive service information which can not\n// be included in the configuration, e.g., secret keys.\nfunc Sensitive(id string) string {\n\treturn filepath.Join(\"/var/lib/ivxv/service\", id)\n}\n\n// TLS returns the path to the TLS-certificate and corresponding private key\n// given the service directory.\nfunc TLS(sensitive string) (cert, key string) {\n\treturn filepath.Join(sensitive, \"tls.pem\"),\n\t\tfilepath.Join(sensitive, \"tls.key\")\n}\n\n// New opens and parses configuration files. All files must be signature\n// containers and the names must have extensions corresponding to the type of\n// container that they are. If err is not nil, then code is the code that the\n// caller should exit with.\n//\n// New first opens trust without verification and parses the file named\n// \"trust.yaml\" in the container to obtain verification parameters to construct\n// a configuration container parser. It uses this to verify the signature\n// provided on the trust container itself to check for errors in the\n// self-signed trust container.\n//\n// Next, election and technical are verified and opened and the files\n// \"election.yaml\" and \"technical.yaml\" are parsed for the election and\n// technical configuration, respectively.\n//\n// The configuration container parser, election and technical configurations\n// are returned nested into C.\n//\n// If election or technical are empty strings, then parsing the election or\n// technical configuration is skipped, respectively. Trust must not be empty,\n// because it is needed for verifying other configurations.\nfunc New(ctx context.Context, trust, election, technical string) (c *C, code int, err error) {\n\tlog.Log(ctx, Parsing{Trust: trust, Election: election, Technical: technical,\n\t\tDescription: _CONF_START})\n\n\tc = new(C)\n\tif code, err = c.trust(ctx, trust); err != nil {\n\t\treturn nil, code, ParseTrustError{Path: trust, Err: err,\n\t\t\tDescription: _CONF_TRUST}\n\t}\n\tif len(election) > 0 {\n\t\tc.Election = new(Election)\n\t\tif c.Version.Election, code, err = c.parse(\n\t\t\tctx, election, \"election.yaml\", &c.Election); err != nil {\n\n\t\t\treturn nil, code, ParseElectionError{Path: election, Err: err,\n\t\t\t\tDescription: _CONF_ELECTION}\n\t\t}\n\t}\n\tif len(technical) > 0 {\n\t\tc.Technical = new(Technical)\n\t\tif c.Version.Technical, code, err = c.parse(\n\t\t\tctx, technical, \"technical.yaml\", &c.Technical); err != nil {\n\n\t\t\treturn nil, code, ParseTechnicalError{Path: technical, Err: err,\n\t\t\t\tDescription: _CONF_TECH}\n\t\t}\n\t\tlog.SetDebug(ctx, c.Technical.Debug)\n\t}\n\treturn\n}\n\nfunc (c *C) trust(ctx context.Context, path string) (code int, err error) {\n\t// First open trust without verifying signatures.\n\tfp, err := os.Open(path)\n\tif err != nil {\n\t\treturn exit.NoInput, OpenTrustFileError{Err: err,\n\t\t\tDescription: _CONF_TRUST_READ}\n\t}\n\tdefer fp.Close()\n\n\t// Check the extension after we have ensured that the file even exists.\n\tt := container.Type(strings.TrimPrefix(filepath.Ext(path), \".\"))\n\tif len(t) == 0 {\n\t\treturn exit.DataErr, TrustMissingExtensionError{\n\t\t\tDescription: _CONF_TRUST_BDOC_EXT}\n\t}\n\n\tcnt, err := container.UnverifiedOpen(t, fp)\n\tif err != nil {\n\t\treturn exit.DataErr, OpenTrustError{Err: err,\n\t\t\tDescription: _CONF_TRUST_OPEN}\n\t}\n\tdefer cnt.Close()\n\n\tvar conf struct {\n\t\tContainer container.Conf\n\t}\n\tif err = unmarshal(\"trust.yaml\", cnt.Data(), &conf); err != nil {\n\t\treturn exit.DataErr, UnmarshalTrustError{Err: err,\n\t\t\tDescription: _CONF_TRUST_CONF}\n\t}\n\n\t// Then configure the configuration container parser and verify the\n\t// trust container.\n\tif c.Container, err = container.Configure(conf.Container); err != nil {\n\t\treturn exit.DataErr, ConfigureContainerOpenerError{Err: err,\n\t\t\tDescription: _CONF_BDOC_OPENER}\n\t}\n\tif _, err = fp.Seek(0, io.SeekStart); err != nil {\n\t\treturn exit.IOErr, RewindTrustError{Err: err,\n\t\t\tDescription: _CONF_REWIND}\n\t}\n\n\tcnt, err = c.Container.Open(t, fp)\n\tif err != nil {\n\t\treturn exit.DataErr, VerifyTrustError{Err: err,\n\t\t\tDescription: _CONF_BDOC_VERIFY}\n\t}\n\tdefer cnt.Close()\n\tc.Version.Trust = version.Signatures(cnt.Signatures())\n\tif len(c.Version.Trust) == 0 {\n\t\treturn exit.DataErr, UnsignedTrustError{\n\t\t\tDescription: _CONF_TRUST_NO_SIG}\n\t}\n\tfor _, s := range c.Version.Trust {\n\t\tlog.Log(ctx, TrustSignature{Signer: s.Signer, SigningTime: s.SigningTime,\n\t\t\tDescription: _CONF_TRUST_BDOC_SIG_INFO})\n\t}\n\treturn\n}\n\nfunc (c *C) parse(ctx context.Context, path, key string, v interface{}) (\n\tsignatures version.Signatures, code int, err error) {\n\n\tcnt, err := c.Container.OpenFile(path)\n\tif err != nil {\n\t\tcode = exit.DataErr\n\t\tif perr := errors.CausedBy(err, new(os.PathError)); perr != nil {\n\t\t\tif os.IsNotExist(perr) {\n\t\t\t\tcode = exit.NoInput\n\t\t\t}\n\t\t}\n\t\treturn nil, code, ConfigurationContainerError{Err: err,\n\t\t\tDescription: _CONF_BDOC_READ}\n\t}\n\tdefer cnt.Close()\n\n\tsignatures = version.Signatures(cnt.Signatures())\n\tif len(signatures) == 0 {\n\t\treturn nil, exit.DataErr, UnsignedConfigurationError{\n\t\t\tDescription: _CONF_BDOC_NO_SIG}\n\t}\n\tfor _, s := range signatures {\n\t\tlog.Log(ctx, ConfigurationSignature{\n\t\t\tPath:        path,\n\t\t\tSigner:      s.Signer,\n\t\t\tSigningTime: s.SigningTime,\n\t\t\tDescription: _CONF_BDOC_SIG_INFO,\n\t\t})\n\t}\n\n\tif err = unmarshal(key, cnt.Data(), v); err != nil {\n\t\treturn nil, exit.DataErr, UnmarshalConfigurationError{Err: err,\n\t\t\tDescription: _CONF_BDOC_DATA}\n\t}\n\treturn\n}\n\n// unmarshal searches a container for the an entry with the given key or that\n// ends in \".<key>\" and unmarshals its contents within into v.\nfunc unmarshal(key string, data map[string][]byte, v interface{}) (err error) {\n\tfor name, content := range data {\n\t\tif name == key || strings.HasSuffix(name, \".\"+key) {\n\t\t\tif err = yaml.Unmarshal(bytes.NewReader(content), data, v); err != nil {\n\t\t\t\treturn UnmarhsalConfError{Err: err,\n\t\t\t\t\tDescription: _CONF_YAML_READ}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn MissingContainerKeyError{Key: key, Description: _CONF_YAML_NO_DATA}\n}\n"
  },
  {
    "path": "common/collector/conf/conf_test.go",
    "content": "package conf\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/log\"\n\n\t_ \"ivxv.ee/common/collector/container/dummy\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\ttrust     string\n\t\telection  string\n\t\ttechnical string\n\t}{\n\t\t{\"dummy\", \"testdata/trust.dummy\", \"testdata/election.dummy\", \"testdata/technical.dummy\"},\n\t\t{\"prefixed\", \"testdata/prefix.trust.dummy\", \"\", \"\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif _, _, err := New(log.TestContext(context.Background()),\n\t\t\t\ttest.trust, test.election, test.technical); err != nil {\n\n\t\t\t\tt.Fatal(\"New failed:\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/conf/log_desc.go",
    "content": "package conf\n\nconst (\n\t_CONF_START               = \"Starting to parse configuration .bdoc container\"\n\t_CONF_TRUST               = \"Failed to parse trust configuration .bdoc container\"\n\t_CONF_ELECTION            = \"Failed to parse election configuration .bdoc container\"\n\t_CONF_TECH                = \"Failed to parse technical configuration .bdoc container\"\n\t_CONF_TRUST_READ          = \"Failed to read trust configuration .bdoc container\"\n\t_CONF_TRUST_BDOC_EXT      = \"trust configuration container doesn't have an extension (.bdoc)\"\n\t_CONF_TRUST_OPEN          = \"Failed to open trust configuration .bdoc container\"\n\t_CONF_TRUST_CONF          = \"Failed to read trust configuration file\"\n\t_CONF_BDOC_OPENER         = \"Configure container opener based on a trust configuration file\"\n\t_CONF_REWIND              = \"Rewind (back to beginning) trust configuration .bdoc container reader's position pointer\"\n\t_CONF_BDOC_VERIFY         = \"Failed to verify .bdoc container\"\n\t_CONF_TRUST_NO_SIG        = \"No signatures found in trust configuration .bdoc container\"\n\t_CONF_TRUST_BDOC_SIG_INFO = \"Summary info of trust configuration .bdoc container\"\n\t_CONF_BDOC_READ           = \"Unable to read .bdoc container\"\n\t_CONF_BDOC_NO_SIG         = \"No signatures found in .bdoc container\"\n\t_CONF_BDOC_SIG_INFO       = \"Summary info of .bdoc container\"\n\t_CONF_BDOC_DATA           = \"Failed to read .bdoc container data\"\n\t_CONF_YAML_READ           = \"Failed to unmarshal YAML data from a .bdoc container\"\n\t_CONF_YAML_NO_DATA        = \"No such data in a .bdoc container\"\n)\n"
  },
  {
    "path": "common/collector/conf/testdata/election.dummy",
    "content": "signatures:\n  - signer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBijCCATCgAwIBAgIJAJldSxM2hh55MAoGCCqGSM49BAMCMB8xHTAbBgNVBAMM\n      FENvbmZpZ3VyYXRpb24gU2lnbmVyMCAXDTE3MDExOTE0MDYxN1oYDzIxMTYxMjI2\n      MTQwNjE3WjAfMR0wGwYDVQQDDBRDb25maWd1cmF0aW9uIFNpZ25lcjBZMBMGByqG\n      SM49AgEGCCqGSM49AwEHA0IABLMo1qGUQNBKYoYHjZ9iEl8kfaCiqATqV7JKqdWc\n      dKXs/RJC/0Mi8HQGOOxxenvpErRnNPnxQvrOyejIp1mq7bijUzBRMB0GA1UdDgQW\n      BBRDNaElGLEWCMQqE/TGCOrfr+MwRDAfBgNVHSMEGDAWgBRDNaElGLEWCMQqE/TG\n      COrfr+MwRDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIAvrkzDr\n      29i4BRrqDJ7QXvLo9KFlm4a80/ME7uv3/CMqAiEAhbuBnbkyOqKzKZ5ceJefZSU2\n      YsQ3obk0n54TF/Xmibc=\n      -----END CERTIFICATE-----\ndata:\n  election.yaml: |\n\n    identifier: test election\n    questions:\n      - test question 1\n\n    period:\n      servicestart:  1970-01-01T00:00:00Z\n      electionstart: 1970-01-01T00:10:00Z\n      electionstop:  2038-01-19T03:00:00Z\n      servicestop:   2038-01-19T03:14:07Z\n      verificationstop:   2038-01-19T03:20:07Z\n\n    voting:\n      ratelimitstart:   50\n      ratelimitminutes:  5\n\n    verification:\n      count:    3\n      minutes: 30\n\n    voterlist:\n      key: |\n        -----BEGIN PUBLIC KEY-----\n        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3s/ikp3TlatLUW28Aq0C\n        QyD6q4mtiJJx+eYXN47ikN6/UJ9xHJpKwpNSHJBCHqW6PjIFFr5TM+b1FzEjMrkI\n        RZXJR7IVRZ8TOpAW5z4+l7icoEYL5mtUmsmrnLHAePljzpxyV4ZnqzZhnDSfwjuF\n        msE3EQmMbGab57FE4p3bBeSuZGMuiQXwKDCkVk6zGWdkFG31m++deGd5BtD6oMlL\n        3j28hFVNeQUDRVMknLk8MmUhIFKDECwmuE3KwHI6FeDJPlK2RgM913JNHupr/H+J\n        fsL5+GW6T6G/7WUxy+0pDRI27C79XA22BCXsRAI9vOBfmk9gKv0e/sCbrAp53PBl\n        1QIDAQAB\n        -----END PUBLIC KEY-----\n\n    xroad:\n      ca: |\n       -----BEGIN CERTIFICATE-----\n       MIIC8DCCAdigAwIBAgIUGkfCoWPHJ0tZDNzd98p0bV9coVgwDQYJKoZIhvcNAQEL\n       BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDQwNDA3NDIzOFoXDTIyMDUw\n       NDA3NDIzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\n       AAOCAQ8AMIIBCgKCAQEAwQamYbWYsJp7pxxG6nEvFbqe/tItwSTGQnvepSRD+3nv\n       MOJlkBjAQ+S7Yec6ay2/ZytMXnxwXMK9/l2tjAtRE4pkDoikTEwn6XPztha+lFnX\n       ewAC9TTbQ4O0UCdKUqp0lAPs49jdCI8V03iLWFF+7iJTuc9rERS1iuA3RQOhZ/I/\n       IQdruXZ9FBdUR8I0QZg7jaPjkpCiM38lcd39zRwXXEFdqOVgsUBvN2KYSyXR1u/c\n       3UWdzAquYKt583mUuortXfmdlEB+HSRRrw0wXlp708NS8qlfdm8nDbT7B8KXWdTV\n       EOAbRmY1ZRFiuGOpp3Ry7RKq6YibAwXy79p0w4EjfQIDAQABozowODAUBgNVHREE\n       DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB\n       MA0GCSqGSIb3DQEBCwUAA4IBAQB0Dmvb4OskZz+9BzbmAJhtz/yxatxFWdRHipqq\n       pUQexAgPSu1iUQMECAnya5cquQSXjHAXDqdsD4Yg+1r4zCcWwpec5UPjOg37GZid\n       8K5BYTawltywvWRLJ1sDWFVZENiDgP2Lxmq/PkG4rg2tG5C9IdfIlnAuop1XLcNc\n       1sfMyhMG1WAktvTil+acJmGtnlBAO5kovM31sAXr9isOrOGLsJo8mxiGZVWecqKQ\n       NoHGPflxCwXVXI5W2Cj9oXjZIzsJtu4zb4hM40prin9gBqQGku7wYDrRH0e0+C8z\n       NeDVbEWIbU7ZDBTIKAIkcSGZuB6lz1lxNRbD0Y9kaIXUpnxd\n       -----END CERTIFICATE-----\n\n    auth:\n      dummy:\n        authenticated:\n          - ÅLT-DELETÈ,CØNTROLINA,48908209998\n      ticket:\n      tls:\n        roots:\n          - |\n            -----BEGIN CERTIFICATE-----\n            MIIB1DCCAXqgAwIBAgIJALqVrrrcihzxMAoGCCqGSM49BAMCMEUxLTArBgNVBAMM\n            JMOFTFQtREVMRVTDiCxDw5hOVFJPTElOQSw0ODkwODIwOTk5ODEUMBIGA1UEBRML\n            NDg5MDgyMDk5OTgwHhcNMTcwMjAzMTQ0NDM4WhcNMjYxMjEzMTQ0NDM4WjBFMS0w\n            KwYDVQQDDCTDhUxULURFTEVUw4gsQ8OYTlRST0xJTkEsNDg5MDgyMDk5OTgxFDAS\n            BgNVBAUTCzQ4OTA4MjA5OTk4MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEO6yc\n            WaTUJpzWsUA0OThH8ifPsH2F1zXvrocPk0766PCO/WAfq+go1bqwwTX7btGf1bLj\n            5xUcxAOM58CgJGWVMKNTMFEwHQYDVR0OBBYEFE+jmkruIfWQee6yjGwfKr/SRlhs\n            MB8GA1UdIwQYMBaAFE+jmkruIfWQee6yjGwfKr/SRlhsMA8GA1UdEwEB/wQFMAMB\n            Af8wCgYIKoZIzj0EAwIDSAAwRQIhAJWqOFoVdS1drLGJ1WznJWCObwK4uQ04vwUR\n            bPDn71DyAiAmJ7w6aQ2twba+HqonlhCMXAzdiqtWgCUKZ9oIEvTGtg==\n            -----END CERTIFICATE-----\n        ocsp:\n          url: http://demo.sk.ee/ocsp\n          responders:\n            - |\n              -----BEGIN CERTIFICATE-----\n              MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n              MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n              czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n              IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n              MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n              Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n              b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n              LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n              gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n              r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n              z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n              OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n              wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n              RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n              FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n              AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n              aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n              JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n              GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n              czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n              BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n              aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n              MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n              nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n              mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n              dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n              -----END CERTIFICATE-----\n\n    identity: pnoee\n\n    age:\n      method:   estpic\n      timezone: Europe/Tallinn\n      limit:    18\n\n    vote:\n      dummy:\n        trusted:\n          - ÅLT-DELETÈ,CØNTROLINA,48908209998\n\n    mid:\n      url: https://tsp.demo.sk.ee/mid-api\n      relyingpartyuuid: 00000000-0000-0000-0000-000000000000\n      relyingpartyname: DEMO\n      language: EST\n      authmessage: Mobiil-ID isikutuvastus\n      signmessage: Mobiil-ID hääle allkirjastamine\n      messageformat: GSM-7\n      authchallengesize: 64\n      statustimeoutms: 5000\n      roots:\n        - |\n          -----BEGIN CERTIFICATE-----\n          MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n          MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n          czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n          IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n          GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n          cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n          aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n          xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n          ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n          MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n          VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n          kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n          pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n          A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n          AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n          BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n          I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n          Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n          SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n          tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n          SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n          -----END CERTIFICATE-----\n      intermediates:\n        - |\n          -----BEGIN CERTIFICATE-----\n          MIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\n          MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n          czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n          IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\n          MjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n          aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\n          A1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\n          ggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n          8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\n          z8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n          5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\n          zs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n          46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n          3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\n          J7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\n          h5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\n          ot3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n          6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\n          AAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\n          HQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\n          IARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\n          ay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\n          AQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\n          AAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\n          BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\n          fTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\n          AQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\n          aWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\n          Mmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\n          Y3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\n          AH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\n          DgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\n          yPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\n          WFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\n          PKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n          -----END CERTIFICATE-----\n      ocsp:\n        url: http://demo.sk.ee/ocsp\n        responders:\n          - |\n            -----BEGIN CERTIFICATE-----\n            MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n            MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n            czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n            IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n            MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n            Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n            b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n            LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n            gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n            r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n            z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n            OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n            wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n            RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n            FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n            AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n            aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n            JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n            GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n            czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n            BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n            aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n            MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n            nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n            mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n            dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n            -----END CERTIFICATE-----\n\n    smartid:\n      url: https://sid.demo.sk.ee/smart-id-rp/v2/\n      relyingpartyuuid: 00000000-0000-0000-0000-000000000000\n      relyingpartyname: DEMO\n      certificatelevel: QUALIFIED\n      authinteractionsorder:\n        - type: verificationCodeChoice\n          displayText60: authenticating\n        - type: displayTextAndPIN\n          displayText60: authenticating\n      signinteractionsorder:\n        - type: verificationCodeChoice\n          displayText60: signing\n        - type: displayTextAndPIN\n          displayText60: signing\n      authchallengesize: 64\n      statustimeoutms: 5000\n      roots:\n        - |\n          -----BEGIN CERTIFICATE-----\n          MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n          MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n          czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n          IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n          GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n          cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n          aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n          xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n          ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n          MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n          VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n          kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n          pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n          A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n          AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n          BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n          I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n          Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n          SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n          tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n          SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n          -----END CERTIFICATE-----\n      intermediates:\n        - |\n           -----BEGIN CERTIFICATE-----\n           MIIG+DCCBeCgAwIBAgIQUkCP5k8r59RXxWzfbx+GsjANBgkqhkiG9w0BAQwFADB9\n           MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n           czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n           IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTYwODMwMTEyNDE1WhgP\n           MjAzMDEyMTcyMzU5NTlaMGgxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n           aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEcMBoG\n           A1UEAwwTVEVTVCBvZiBFSUQtU0sgMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP\n           ADCCAgoCggIBAOrKOByrJqS1QsKD4tXhqkZafPMd5sfxem6iVbMAAHKpvOs4Ia2o\n           XdSvJ2FjrMl5szeT4lpHyzfECzO3nx7pvRLKHufi6lMwMGjtSI6DK8BiH9z7Lm+k\n           NLunNFdIir0hPijjbIkjg9iwfaeST9Fi5502LsK7duhKuCnH7O0uMrS/MynJ4StA\n           NGY13X2FvPW4qkrtbwsmhdN0Btro72O6/3O+0vbnq/yCWtcQrBGv3+8XEBdCqH5S\n           /Rt0EugKX4UlVy5l0QUc8IrjGtdMsr9KDtvmVwlefXYKoLqkC7guMGOUNf6Y4AYG\n           sPqfY4dG3N5YNp5FHDL7IO93h7TpRV3gyR38LiJsPHk5nES5mdPkNuEkCyg0zEKI\n           7uJ4LUuBbjzZPp2gP7PN8Iqi9GP7V2NCz8vUVN3WpHvctsf0DMvZdV5pxqLY5ojy\n           fhMsU4aMcGSQA9EK8ES3O1zBK1DW+btjbQjUFW1SIwCkB2yofFxge+vvzZGbvt2U\n           GOE8oAL8/JzNxi9FbjTAbycrGWgEMQ0sM1fKc+OsvoaSy9m3ZQGph0+dbsouQpl3\n           kpJvjDMzxxkrMqxdhlVMreLKGCMMxJMAGQEwVS5P93Nnmz8UbkmeomUJr3NrBo4+\n           V9L5S4Kx1vTvD0p72xRYFyfifLOjs8qs7lR3yhkcBPQI78ERqxv31FWDAgMBAAGj\n           ggKFMIICgTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNVHQ4E\n           FgQUrrDq4Tb4JqulzAtmVf46HQK/ErQwDgYDVR0PAQH/BAQDAgEGMIHEBgNVHSAE\n           gbwwgbkwPAYHBACL7EABAjAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5l\n           ZS9yZXBvc2l0b29yaXVtL0NQUzA8BgcEAIvsQAEAMDEwLwYIKwYBBQUHAgEWI2h0\n           dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMDsGBgQAj3oBAjAxMC8G\n           CCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS\n           BgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD\n           AgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v\n           b2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0\n           cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e\n           BDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA\n           AAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs\n           SQEBMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRv\n           cnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQAiw1VN\n           xp1Ho7FwcPlFqlLl6zb225IvpNelFX2QMbq1SPe41LuBW7WRZIV4b6bRQug55k8l\n           Am8eX3zEXL9I+4Bzai/IBlMSTYNpqAQGNVImQVwMa64uN8DWo8LNWSYNYYxQzO7s\n           TnqsqxLPWeKZRMkREI0RaVNoIPsciJvid9iBKTcGnMVkbrgyLzlXblLMU4I0pL2R\n           Wlfs2tr+XtCtWAvJPFskM2QZ2NnLjW8WroZr8TooocRA1vl/ruIAPC3FxW7zebKc\n           A2B66j4tW7uyF2kPx4WWA3xgR5QZnn4ePEAYjJdu1eWd9KbeAbxPCfFOST43t0fm\n           20HfV2Wp2PMEq4b2\n           -----END CERTIFICATE-----\n\n      ocsp:\n        url: http://demo.sk.ee/ocsp\n        responders:\n          - |\n            -----BEGIN CERTIFICATE-----\n            MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n            MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n            czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n            IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n            MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n            Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n            b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n            LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n            gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n            r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n            z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n            OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n            wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n            RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n            FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n            AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n            aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n            JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n            GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n            czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n            BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n            aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n            MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n            nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n            mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n            dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n            -----END CERTIFICATE-----\n\n    qualification:\n      - protocol: tspreg\n        conf:\n          url: http://demo.sk.ee/tsa\n          signers:\n            - |\n              -----BEGIN CERTIFICATE-----\n              MIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\n              MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n              czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n              IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\n              MjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\n              VVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\n              A1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\n              ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n              7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\n              QOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\n              QZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\n              EuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\n              nAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\n              AwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\n              AwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\n              Cp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\n              hhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\n              LnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\n              dHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\n              n8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\n              fbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\n              F5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\n              dknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\n              Agm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\n              pIX8UV3N9w==\n              -----END CERTIFICATE-----\n          delaytime: 1\n      - protocol: ocsp\n        conf:\n          url: http://demo.sk.ee/ocsp\n          responders:\n            - |\n              -----BEGIN CERTIFICATE-----\n              MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n              MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n              czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n              IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n              MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n              Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n              b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n              LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n              gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n              r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n              z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n              OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n              wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n              RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n              FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n              AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n              aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n              JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n              GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n              czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n              BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n              aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n              MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n              nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n              mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n              dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n              -----END CERTIFICATE-----\n\n# vim: set ft=yaml sw=2:\n"
  },
  {
    "path": "common/collector/conf/testdata/prefix.trust.dummy",
    "content": "signatures:\n  - signer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBijCCATCgAwIBAgIJAJldSxM2hh55MAoGCCqGSM49BAMCMB8xHTAbBgNVBAMM\n      FENvbmZpZ3VyYXRpb24gU2lnbmVyMCAXDTE3MDExOTE0MDYxN1oYDzIxMTYxMjI2\n      MTQwNjE3WjAfMR0wGwYDVQQDDBRDb25maWd1cmF0aW9uIFNpZ25lcjBZMBMGByqG\n      SM49AgEGCCqGSM49AwEHA0IABLMo1qGUQNBKYoYHjZ9iEl8kfaCiqATqV7JKqdWc\n      dKXs/RJC/0Mi8HQGOOxxenvpErRnNPnxQvrOyejIp1mq7bijUzBRMB0GA1UdDgQW\n      BBRDNaElGLEWCMQqE/TGCOrfr+MwRDAfBgNVHSMEGDAWgBRDNaElGLEWCMQqE/TG\n      COrfr+MwRDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIAvrkzDr\n      29i4BRrqDJ7QXvLo9KFlm4a80/ME7uv3/CMqAiEAhbuBnbkyOqKzKZ5ceJefZSU2\n      YsQ3obk0n54TF/Xmibc=\n      -----END CERTIFICATE-----\ndata:\n  prefix.trust.yaml: |\n\n    container:\n      dummy:\n        trusted:\n          - Configuration Signer\n\n# vim: set ft=yaml sw=2:\n"
  },
  {
    "path": "common/collector/conf/testdata/technical.dummy",
    "content": "signatures:\n  - signer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBijCCATCgAwIBAgIJAJldSxM2hh55MAoGCCqGSM49BAMCMB8xHTAbBgNVBAMM\n      FENvbmZpZ3VyYXRpb24gU2lnbmVyMCAXDTE3MDExOTE0MDYxN1oYDzIxMTYxMjI2\n      MTQwNjE3WjAfMR0wGwYDVQQDDBRDb25maWd1cmF0aW9uIFNpZ25lcjBZMBMGByqG\n      SM49AgEGCCqGSM49AwEHA0IABLMo1qGUQNBKYoYHjZ9iEl8kfaCiqATqV7JKqdWc\n      dKXs/RJC/0Mi8HQGOOxxenvpErRnNPnxQvrOyejIp1mq7bijUzBRMB0GA1UdDgQW\n      BBRDNaElGLEWCMQqE/TGCOrfr+MwRDAfBgNVHSMEGDAWgBRDNaElGLEWCMQqE/TG\n      COrfr+MwRDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIAvrkzDr\n      29i4BRrqDJ7QXvLo9KFlm4a80/ME7uv3/CMqAiEAhbuBnbkyOqKzKZ5ceJefZSU2\n      YsQ3obk0n54TF/Xmibc=\n      -----END CERTIFICATE-----\ndata:\n  technical.yaml: |\n\n    debug: true\n\n    snidomain: inttest.ivxv.ee\n\n    filter:\n      tls:\n        handshaketimeout: 10\n        ciphersuites:\n          - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n          - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\n          # Support for older smartphones.\n          - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\n          - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n      codec:\n        rwtimeout: 5\n        requestsize: 16834  # 16 KiB\n        logrequests: true\n\n    network:\n      - id: default\n        services:\n          proxy:\n            - id:      proxy@localhost\n              address: localhost:4440\n          mid:\n            - id:      mid@localhost\n              address: localhost:4441\n          choices:\n            - id:      choices@localhost\n              address: localhost:4442\n          voting:\n            - id:      voting@localhost\n              address: localhost:4443\n          verification:\n            - id:      verification@localhost\n              address: localhost:4444\n          smartid:\n            - id:      smartid@localhost\n              address: localhost:4445\n          votesorder:\n            - id:      votesorder@localhost\n              address: localhost:4446\n\n    storage:\n      protocol: file\n      conf:\n        wd: testdata/storage\n      ordertimeout: 10\n\n# vim: set ft=yaml sw=2:\n"
  },
  {
    "path": "common/collector/conf/testdata/trust.dummy",
    "content": "signatures:\n  - signer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBijCCATCgAwIBAgIJAJldSxM2hh55MAoGCCqGSM49BAMCMB8xHTAbBgNVBAMM\n      FENvbmZpZ3VyYXRpb24gU2lnbmVyMCAXDTE3MDExOTE0MDYxN1oYDzIxMTYxMjI2\n      MTQwNjE3WjAfMR0wGwYDVQQDDBRDb25maWd1cmF0aW9uIFNpZ25lcjBZMBMGByqG\n      SM49AgEGCCqGSM49AwEHA0IABLMo1qGUQNBKYoYHjZ9iEl8kfaCiqATqV7JKqdWc\n      dKXs/RJC/0Mi8HQGOOxxenvpErRnNPnxQvrOyejIp1mq7bijUzBRMB0GA1UdDgQW\n      BBRDNaElGLEWCMQqE/TGCOrfr+MwRDAfBgNVHSMEGDAWgBRDNaElGLEWCMQqE/TG\n      COrfr+MwRDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIAvrkzDr\n      29i4BRrqDJ7QXvLo9KFlm4a80/ME7uv3/CMqAiEAhbuBnbkyOqKzKZ5ceJefZSU2\n      YsQ3obk0n54TF/Xmibc=\n      -----END CERTIFICATE-----\ndata:\n  trust.yaml: |\n\n    container:\n      dummy:\n        trusted:\n          - Configuration Signer\n\n# vim: set ft=yaml sw=2:\n"
  },
  {
    "path": "common/collector/conf/version/log_desc.go",
    "content": "package version\n\nconst (\n\t_VERSION_JSON     = \"Failed to JSON marshal signature as {<CN> <timestamp>}\"\n\t_VERSION_JSON_ALL = \"Failed to JSON marshal all signatures as [{'<CN> <timestamp>}, ...]\"\n)\n"
  },
  {
    "path": "common/collector/conf/version/version.go",
    "content": "/*\nPackage version contains types for denoting the version of a configuration\nfile.\n\nThis is a separate package to avoid import cycles between ivxv.ee/common/collector/conf and\nivxv.ee/common/collector/server.\n*/\npackage version\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/container\"\n)\n\n// V contains metadata about all signatures on the loaded trust, election, and\n// technical configuration.\ntype V struct {\n\tTrust     Signatures\n\tElection  Signatures\n\tTechnical Signatures\n}\n\n// Signatures is a slice of container signatures with custom JSON formatting.\n//\n// We define the type on the entire slice, although custom formatting is only\n// applied to the single signatures, but this way when converting a slice of\n// container signatures, we do not need to create a new slice and can just cast\n// the entire slice.\ntype Signatures []container.Signature\n\n// MarshalJSON marshals the signatures as a JSON array of \"<CN> <timestamp>\".\nfunc (s Signatures) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\te := json.NewEncoder(&b)\n\tb.WriteByte('[')\n\tfor i, c := range s {\n\t\tif i > 0 {\n\t\t\tb.WriteByte(',')\n\t\t}\n\t\tif err := e.Encode(fmt.Sprint(\n\t\t\tc.CommonName(),\n\t\t\t\" \",\n\t\t\tc.SigningTime.Format(time.RFC3339),\n\t\t)); err != nil {\n\t\t\treturn nil, MarshalSignatureError{Err: err, Description: _VERSION_JSON}\n\t\t}\n\t}\n\tb.WriteByte(']')\n\treturn b.Bytes(), nil\n}\n\n// Container returns a container's version string.\nfunc Container(c container.Container) (version string, err error) {\n\tb, err := json.Marshal(Signatures(c.Signatures()))\n\tversion = string(b)\n\tif err != nil {\n\t\terr = ContainerError{Err: err, Description: _VERSION_JSON_ALL}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/config/Makefile",
    "content": "ETC := rsyslog-ivxv-templates.conf rsyslog-logcollector.conf sudoers\n\ninstall:\n\tmkdir -p $(DESTDIR)/etc/ivxv && cp $(ETC) $(DESTDIR)/etc/ivxv\n"
  },
  {
    "path": "common/collector/config/rsyslog-ivxv-templates.conf",
    "content": "# IVXV Internet voting framework\n\n# Templates for rsyslog log messages\n# /etc/rsyslog.d/10-ivxv-templates.conf\n\n# Log message in JSON\ntemplate(name=\"ivxv-json\" type=\"list\") {\n    property(name=\"jsonmesg\")\n    constant(value=\"\\n\")\n}\n\n# Default log file for IVXV microservices, one file per hour:\n# /var/log/ivxv/ivxv-YYYY-MM-DD-HH.log\ntemplate(name=\"IVXV_DEFAULT_LOG_FILENAME\" type=\"list\") {\n    constant(value=\"/var/log/ivxv/ivxv-\")\n    property(name=\"timegenerated\" dateFormat=\"year\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"month\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"day\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"hour\")\n    constant(value=\".log\")\n}\n\n# Request log file for IVXV microservices, one file per hour:\n# /var/log/ivxv/ivxv-request-YYYY-MM-DD-HH.log\ntemplate(name=\"IVXV_REQUEST_LOG_FILENAME\" type=\"list\") {\n    constant(value=\"/var/log/ivxv/ivxv-request-\")\n    property(name=\"timegenerated\" dateFormat=\"year\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"month\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"day\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"hour\")\n    constant(value=\".log\")\n}\n\n# Debug log file for IVXV microservices, one file per hour:\n# /var/log/ivxv/ivxv-debug-YYYY-MM-DD-HH.log\ntemplate(name=\"IVXV_DEBUG_LOG_FILENAME\" type=\"list\") {\n    constant(value=\"/var/log/ivxv/ivxv-debug-\")\n    property(name=\"timegenerated\" dateFormat=\"year\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"month\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"day\")\n    constant(value=\"-\")\n    property(name=\"timegenerated\" dateFormat=\"hour\")\n    constant(value=\".log\")\n}\n"
  },
  {
    "path": "common/collector/config/rsyslog-logcollector.conf",
    "content": "# IVXV Internet voting framework\n\n# Rsyslog configuration file for log collector service\n# /etc/rsyslog.d/ivxv-logcollector.conf\n\n# Collect log messages over RELP protocol\nmodule(load=\"imrelp\")\ninput(type=\"imrelp\" port=\"20514\" maxDataSize=\"32k\")\n\n# write IVXV log to /var/log/ivxv/ivxv.log (up to level INFO)\nif ($programname startswith 'ivxv-') and ($syslogfacility-text == 'local0') and\n   ($syslogseverity <= '6') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_DEFAULT_LOG_FILENAME\"\n    template=\"ivxv-json\"\n)\n\n# write IVXV request log to /var/log/ivxv/ivxv-request.log\nif ($programname startswith 'ivxv-') and ($syslogfacility-text == 'local1') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_REQUEST_LOG_FILENAME\"\n)\n\n# write IVXV debug log and log of related\n# services (haproxy, etcd, rsyslog, sshd) to /var/log/ivxv/ivxv-debug-YYYY-MM-DD-HH.log\nif ($programname startswith 'ivxv-') or ($programname startswith 'rsyslog') or\n   ($programname == 'haproxy') or ($programname == 'sshd') or ($programname == 'etcd') then\naction(\n    type=\"omfile\"\n    dynaFile=\"IVXV_DEBUG_LOG_FILENAME\"\n)\n"
  },
  {
    "path": "common/collector/config/sudoers",
    "content": "# IVXV Internet voting framework\n\n# /etc/sudoers.d/ivxv\n\n# allow ivxv-admin user to execute ivxv-admin-sudo srcipt\nivxv-admin      ALL = (ALL)NOPASSWD: /usr/bin/ivxv-admin-sudo\n"
  },
  {
    "path": "common/collector/container/bdoc/asice.go",
    "content": "package bdoc\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"ivxv.ee/common/collector/safereader\"\n\tzip2 \"ivxv.ee/common/collector/zip\"\n)\n\n// metainf is the name of the folder containing meta information.\nconst metainf = \"META-INF/\"\n\n// signatureRE is used to match names of signature files.\nvar signatureRE = regexp.MustCompile(`^` + metainf + `[^/]*signatures[^/]*\\.xml$`)\n\n// asiceFile is a file read from an ASiC-E container.\ntype asiceFile struct {\n\tname     string\n\tmimetype string // Only used for non-signatures.\n\tdata     *bytes.Buffer\n}\n\n// signature returns true if the file is a signature.\nfunc (f asiceFile) signature() bool {\n\treturn signatureRE.MatchString(f.name)\n}\n\n// close releases any resources held by asiceFile.\nfunc (f *asiceFile) close() {\n\trelease(f.data)\n\tf.data = nil\n}\n\n// openASiCE opens an ASiC-E XAdES container and decompresses all the files in\n// it. It checks that \"mimetype\" and \"META-INF/manifest.xml\" comply with all\n// requirements, so these will not be returned.\n//\n// zipLimit is the maximum allowed length of the Zip archive and fileLimit is\n// the maximum allowed decompressed length of a file in the archive.\n//\n// If readSigs is false, then signature files will be skipped: used if only the\n// container contents are requested.\n//\n// All returned asiceFiles should be closed after use.\nfunc openASiCE(r io.Reader, fileCount, zipLimit, fileLimit int64, readSigs bool) (\n\tfiles map[string]*asiceFile, err error) {\n\n\t// Convert the stream to io.ReaderAt needed for zip.NewReader.\n\trat, size, err := toReaderAt(r, zipLimit)\n\tif err != nil {\n\t\treturn nil, ToReaderAtError{Err: err, Description: _CONTAINER_READER_TO_READER_AT}\n\t}\n\tdefer rat.close()\n\n\t// Fail fast: Check the ASiC-E magic number before reading the ZIP.\n\tif err = asiceMagic(rat); err != nil {\n\t\treturn nil, NotASiCEError{Err: err,\n\t\t\tDescription: _CONTAINER_ASICE_MAGIC}\n\t}\n\n\t// Read the ZIP directory and parse file headers.\n\trzip, err := zip.NewReader(rat, size)\n\tif err != nil {\n\t\treturn nil, ZIPReaderError{Err: err,\n\t\t\tDescription: _CONTAER_ZIP_READER}\n\t}\n\n\t// Check the files and decompress data.\n\tfiles = make(map[string]*asiceFile)\n\t// Pass files as explicit argument so that it gets evaluated now and we\n\t// can later safely return nil. Do not do the same for err, because we\n\t// want to know its value on return, not now.\n\tdefer func(files map[string]*asiceFile) {\n\t\t// Close all files on error.\n\t\tif err != nil {\n\t\t\tfor _, file := range files {\n\t\t\t\tfile.close()\n\t\t\t}\n\t\t}\n\t}(files)\n\n\tseen := make(map[string]struct{})\n\tvar manifest *asiceFile\n\tvar signatures int\n\tfor i, file := range rzip.File {\n\n\t\t// Check that we have not seen this file yet.\n\t\tif _, ok := seen[file.Name]; ok {\n\t\t\treturn nil, DuplicateFileNameError{FileName: file.Name,\n\t\t\t\tDescription: _CONTAINER_DUP}\n\t\t}\n\t\tseen[file.Name] = struct{}{}\n\n\t\t// asiceMagic checked that the first file in the archive was a\n\t\t// correct \"mimetype\" using the local file header: now check\n\t\t// that it is also first in the central directory and that the\n\t\t// directory header matches.\n\t\tif i == 0 {\n\t\t\tif err = asiceMagicCentral(file); err != nil {\n\t\t\t\treturn nil, ASiCEMagicCentralError{Err: err,\n\t\t\t\t\tDescription: _CONTAINER_ASICE_CENTRAL_MAGIC,\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\t// Check that the file name is allowed.\n\t\tswitch {\n\t\tcase file.Name == metainf:\n\t\t\tcontinue\n\t\tcase file.Name == metainf+\"manifest.xml\":\n\t\t\t// manifest.xml is not returned, so decompress here and\n\t\t\t// defer the recover.\n\t\t\tif manifest, err = decompress(file, fileLimit); err != nil {\n\t\t\t\treturn nil, DecompressManifestError{Err: err,\n\t\t\t\t\tDescription: _CONTAINER_ZIP_MANIFEST}\n\t\t\t}\n\t\t\tdefer manifest.close()\n\t\t\tcontinue\n\t\tcase signatureRE.MatchString(file.Name):\n\t\t\tsignatures++\n\t\t\tif !readSigs {\n\t\t\t\tcontinue // Skip signatures if not requested.\n\t\t\t}\n\t\t// No more META-INF/ files allowed below this case.\n\t\tcase strings.HasPrefix(file.Name, metainf):\n\t\t\treturn nil, UnknownMetaInfoFile{FileName: file.Name,\n\t\t\t\tDescription: _CONTAINER_META}\n\n\t\tcase strings.Count(file.Name, \"/\") > 0:\n\t\t\treturn nil, FileInSubfolderError{FileName: file.Name,\n\t\t\t\tDescription: _CONTAINER_DIRS}\n\t\t}\n\n\t\tvar asice *asiceFile\n\t\tif asice, err = decompress(file, fileLimit); err != nil {\n\t\t\treturn nil, DecompressFileError{\n\t\t\t\tFileName:    file.Name,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CONTAINER_UNZIP_FILE,\n\t\t\t}\n\t\t}\n\t\tfiles[file.Name] = asice\n\t}\n\n\t// Locate .ZIP end of central directory\n\toffset, err := zip2.EndOfCentralDirectory(rat, zip2.DecompressOptions{\n\t\tFilesLimit:    int(fileCount),\n\t\tFileSizeLimit: int(fileLimit),\n\t\tZipSizeLimit:  size,\n\t})\n\tif err != nil {\n\t\treturn nil, EndOfCentralDirectoryError{\n\t\t\tErr:         err,\n\t\t\tDescription: _CONTAINER_EOCD,\n\t\t}\n\t}\n\n\t// If at least a single byte can be read after end of central directory, then there are appended bytes present\n\tatLeastOneByte := [1]byte{}\n\tif _, err = rat.ReadAt(atLeastOneByte[:], offset); err != io.EOF {\n\t\treturn nil, MalformedZIPError{\n\t\t\tErr:         err,\n\t\t\tDescription: _CONTAINER_MALFORMED_ZIP,\n\t\t}\n\t}\n\n\t// The manifest, at least one signature, and at least one data file\n\t// must be present. If they are, then we can be certain that \"mimetype\"\n\t// is too, because we require it to be the first file.\n\tif manifest == nil {\n\t\treturn nil, MissingManifestError{\n\t\t\tDescription: _CONTAINER_BDOC_MANI,\n\t\t}\n\t}\n\tif signatures == 0 {\n\t\treturn nil, NoSignaturesError{\n\t\t\tDescription: _CONTAINER_BDOC_DATA,\n\t\t}\n\t}\n\tdatafiles := len(files)\n\tif readSigs {\n\t\tdatafiles -= signatures\n\t}\n\tif datafiles == 0 {\n\t\treturn nil, NoDataFilesError{\n\t\t\tDescription: _CONTAINER_BDOC_DATA,\n\t\t}\n\t}\n\n\t// Check that the manifest has references for all non-signature files,\n\t// and only those files, and retrieve their MIME types from it.\n\tif err = readManifest(manifest.data.Bytes(), files); err != nil {\n\t\treturn nil, ManifestError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_MANI_READ}\n\t}\n\treturn files, nil\n}\n\nconst (\n\t// magic is the name of the magic number file used to recognize ASiC\n\t// containers.\n\tmagic = \"mimetype\"\n\n\t// mimetype is the mimetype used for ASiC-E containers.\n\tmimetype = \"application/vnd.etsi.asic-e+zip\"\n)\n\n// asiceMagic checks the magic number \"mimetype\" file as described in clause A.1\n// of the ASiC specification and that it specifies the ASiC-E MIME type.\nfunc asiceMagic(rat io.ReaderAt) error {\n\tbuf := make([]byte, 30+len(magic)+len(mimetype))\n\tif n, err := rat.ReadAt(buf, 0); err != nil {\n\t\treturn ReadMagicFileHeaderError{Header: buf[:n], Err: err,\n\t\t\tDescription: _CONTAINER_ASICE_MAGIC}\n\t}\n\tle := binary.LittleEndian\n\n\tif marker := string(buf[:4]); marker != \"PK\\x03\\x04\" {\n\t\treturn NoZIPMagicError{Marker: marker,\n\t\t\tDescription: _CONTAINER_ZIP_MAGIC}\n\t}\n\n\tif length := le.Uint16(buf[26:28]); int(length) != len(magic) {\n\t\treturn MagicLengthError{Length: length, Description: _CONTAINER_ZIP_MAGIC_LEN}\n\t}\n\tif filename := string(buf[30 : 30+len(magic)]); filename != magic {\n\t\treturn NotASiCMagicError{FileName: filename, Description: _CONTAINER_ASICE_MAGIC_FORMAT}\n\t}\n\n\tif method := le.Uint16(buf[8:10]); method != zip.Store {\n\t\treturn CompressedMIMETypeError{Method: method,\n\t\t\tDescription: _CONTAINER_ZIP_MIME}\n\t}\n\tif length := le.Uint16(buf[28:30]); length != 0 {\n\t\treturn ExtraFieldError{Length: length,\n\t\t\tDescription: _CONTAINER_ZIP_EXTRA}\n\t}\n\n\tif length := le.Uint32(buf[18:22]); int(length) != len(mimetype) {\n\t\treturn MIMETypeLengthError{Length: length, Description: _CONTAINER_ZIP_MIME_LEN}\n\t}\n\tif mime := string(buf[30+len(magic):]); mime != mimetype {\n\t\treturn NotASiCEMIMETypeError{MIMEType: mime, Description: _CONTAINER_ASICE_MIME}\n\t}\n\treturn nil\n}\n\n// asiceMagicCentral checks that the ZIP central directory header contains the\n// same information as the local header checked in asiceMagic.\nfunc asiceMagicCentral(file *zip.File) error {\n\t// Check the file name.\n\tif file.Name != magic {\n\t\treturn MIMETypeCentralNameError{Name: file.Name, Description: CONTAINER_MIME}\n\t}\n\n\t// Check if the central directory is referring to the file at the start\n\t// of the archive. We cannot check the header offset, but data offset\n\t// 30 + file name length is the same, since we have no extra data.\n\toffset, err := file.DataOffset()\n\tif err != nil {\n\t\treturn GetMIMETypeOffsetError{Err: err, Description: _CONTAINER_MIME_OFF}\n\t}\n\tif offset != int64(30+len(magic)) {\n\t\treturn MIMETypeCentralOffsetError{Offset: offset,\n\t\t\tDescription: _CONTAINER_MIME_C_OFF}\n\t}\n\n\t// Check directory header compression, data size, and extra length --\n\t// file name length was already checked by comparing the file name.\n\tif file.Method != zip.Store {\n\t\treturn MIMETypeCentralMethodError{Method: file.Method,\n\t\t\tDescription: _CONTAINER_HEADER_COMPRESS}\n\t}\n\tif file.CompressedSize64 != uint64(len(mimetype)) {\n\t\treturn MIMETypeCentralSizeError{Size: file.CompressedSize64,\n\t\t\tDescription: _CONTAINER_MIME_CONTENT}\n\t}\n\tif len(file.Extra) != 0 {\n\t\treturn MIMETypeCentralExtraError{Extra: file.Extra,\n\t\t\tDescription: _CONTAINER_MIME_CONTENT_EXT}\n\t}\n\treturn nil\n}\n\n// decompress decompresses the ZIP file into an asiceFile. Note that mimetype\n// is not set for these files.\nfunc decompress(file *zip.File, limit int64) (*asiceFile, error) {\n\tfd, err := file.Open()\n\tif err != nil {\n\t\treturn nil, OpenZIPFileError{Err: err, Description: _CONTAINER_ZIP_OPEN}\n\t}\n\tdefer fd.Close()\n\n\tasice := &asiceFile{name: file.Name, data: buffer()}\n\tsize, err := asice.data.ReadFrom(safereader.New(fd, limit))\n\tif err != nil {\n\t\tasice.close()\n\t\treturn nil, ReadZIPFileError{Err: err, Description: _CONTAINER_ZIP_READ}\n\t}\n\n\tif file.UncompressedSize64 != uint64(size) { //nolint:gosec\n\t\tasice.close()\n\t\treturn nil, UncompressedZIPFileSizeError{\n\t\t\tDeclared:    file.UncompressedSize64,\n\t\t\tActual:      size,\n\t\t\tDescription: _CONTAINER_ZIP_DIFF,\n\t\t}\n\t}\n\treturn asice, nil\n}\n\n// Specified in section 17.7.2 of the Open Document Format for Office\n// Applications (OpenDocument) v1.0 (page 687 of\n// https://docs.oasis-open.org/office/v1.0/OpenDocument-v1.0-os.pdf).\ntype manifest struct {\n\tXMLElement  c14n `xmlx:\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0 manifest\"`\n\tFileEntries []fileEntry\n}\n\n// Specified in section 17.7.3 of the Open Document Format for Office\n// Applications (OpenDocument) v1.0 (page 687 of\n// https://docs.oasis-open.org/office/v1.0/OpenDocument-v1.0-os.pdf).\ntype fileEntry struct {\n\tXMLElement c14n   `xmlx:\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0 file-entry\"`\n\tFullPath   string `xmlx:\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0 full-path,attr\"`\n\tMediaType  string `xmlx:\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0 media-type,attr\"`\n}\n\n// readManifest parses the OpenDocument manifest and fills the MIME types for\n// all non-signature files. The file entries in the manifest must match exactly\n// the set of non-signature files plus the \"/\" element, which must have the\n// same MIME type as specified in \"mimetype\".\n//\n// Although BDOC 2.1.2:2014 references OpenDocument v1.2, the official\n// implementations actually only produce v1.0, so we only support that.\nfunc readManifest(data []byte, files map[string]*asiceFile) error {\n\tvar m manifest\n\tif err := parseXML(data, &m); err != nil {\n\t\treturn ManifestXMLError{Data: data, Err: err,\n\t\t\tDescription: _CONTAINER_ZIP_XML}\n\t}\n\n\tseen := make(map[string]struct{})\n\tfor _, entry := range m.FileEntries {\n\t\t// Check that we have not seen this entry yet.\n\t\tif _, ok := seen[entry.FullPath]; ok {\n\t\t\treturn DuplicateManifestEntryError{Path: entry.FullPath,\n\t\t\t\tDescription: _CONTAINER_ZIP_XML_MANY}\n\t\t}\n\t\tseen[entry.FullPath] = struct{}{}\n\n\t\t// The OpenDocument should contain a root element, but it is\n\t\t// not mandatory.\n\t\tif entry.FullPath == \"/\" {\n\t\t\tif entry.MediaType != mimetype {\n\t\t\t\treturn ManifestRootMIMETypeError{MIMEType: entry.MediaType,\n\t\t\t\t\tDescription: _CONTAINER_MIME_ROOT}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tfile, ok := files[entry.FullPath]\n\t\tif !ok {\n\t\t\treturn ExtraManifestEntryError{Path: entry.FullPath,\n\t\t\t\tDescription: _CONTAINER_FILE_FROM_MANI}\n\t\t}\n\t\tif file.signature() {\n\t\t\treturn SignatureInManifestError{Path: entry.FullPath,\n\t\t\t\tDescription: _CONTAINER_SIG_FROM_MANI}\n\t\t}\n\t\tfile.mimetype = entry.MediaType\n\t}\n\n\tfor _, file := range files {\n\t\tif !file.signature() && len(file.mimetype) == 0 {\n\t\t\treturn MissingManifestEntryError{Path: file.name,\n\t\t\t\tDescription: _CONTAINER_SIG_FILE_MANI}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/asice_test.go",
    "content": "package bdoc\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nfunc TestASiCEMagic(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\theader *zip.FileHeader\n\t\tdata   []byte\n\t\terr    error\n\t}{\n\t\t{\"short\", nil, []byte(\"too short\"), new(ReadMagicFileHeaderError)},\n\n\t\t{\"non-ZIP\", nil, make([]byte, 30+len(magic)+len(mimetype)), new(NoZIPMagicError)},\n\n\t\t{\"magic length\", &zip.FileHeader{Name: \"short\"}, nil, new(MagicLengthError)},\n\n\t\t{\"magic\", &zip.FileHeader{Name: \"MIMETYPE\"}, nil, new(NotASiCMagicError)},\n\n\t\t{\"compressed\", &zip.FileHeader{\n\t\t\tName:   magic,\n\t\t\tMethod: zip.Deflate,\n\t\t}, nil, new(CompressedMIMETypeError)},\n\n\t\t{\"extra\", &zip.FileHeader{\n\t\t\tName:  magic,\n\t\t\tExtra: make([]byte, 10),\n\t\t}, nil, new(ExtraFieldError)},\n\n\t\t{\"MIME type length\", &zip.FileHeader{\n\t\t\tName:             magic,\n\t\t\tCompressedSize64: 20,\n\t\t}, make([]byte, 20), new(MIMETypeLengthError)},\n\n\t\t{\"MIME type\", &zip.FileHeader{\n\t\t\tName:             magic,\n\t\t\tCompressedSize64: uint64(len(mimetype)),\n\t\t}, make([]byte, len(mimetype)), new(NotASiCEMIMETypeError)},\n\n\t\t{\"OK\", &zip.FileHeader{\n\t\t\tName:             magic,\n\t\t\tCompressedSize64: uint64(len(mimetype)),\n\t\t}, []byte(mimetype), nil},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tif test.header != nil {\n\t\t\t\tz := zip.NewWriter(&buf)\n\t\t\t\tw, err := z.CreateHeader(test.header)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"failed to create file header:\", err)\n\t\t\t\t}\n\t\t\t\tif _, err = w.Write(test.data); err != nil {\n\t\t\t\t\tt.Fatal(\"failed to write file data:\", err)\n\t\t\t\t}\n\t\t\t\tif err = z.Close(); err != nil {\n\t\t\t\t\tt.Fatal(\"failed to close container:\", err)\n\t\t\t\t}\n\n\t\t\t\t// Since the zip package uses data descriptors,\n\t\t\t\t// we need to write the compressed size\n\t\t\t\t// ourself.\n\t\t\t\tbinary.LittleEndian.PutUint64(buf.Bytes()[18:26],\n\t\t\t\t\ttest.header.CompressedSize64)\n\t\t\t} else {\n\t\t\t\tbuf.Write(test.data)\n\t\t\t}\n\t\t\terr := asiceMagic(bytes.NewReader(buf.Bytes()))\n\t\t\tif err != test.err && errors.CausedBy(err, test.err) == nil {\n\t\t\t\tt.Errorf(\"expected error %T, got %v\", test.err, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Linter will complain about long code lines, however in case of manifest.xml\n// file we just can't add newline to the file, since that could violate\n// formatting of the specific XML entries.\n//\n// Also note that these unit tests implement Open Document Format for Office\n// Applications (OpenDocument) v1.0 as well as v1.2\n//\n//nolint:lll\nfunc TestReadManifest(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmanifest string\n\t\tfiles    map[string]*asiceFile\n\t\terr      error\n\t}{\n\t\t{\"XML\", \"not xml\", nil, new(ManifestXMLError)},\n\n\t\t{\"duplicate\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"foo\"\n\t\t\t\t\tmanifest:media-type=\"bar\"/>\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"foo\"\n\t\t\t\t\tmanifest:media-type=\"bar\"/>\n\t\t\t</manifest:manifest>`,\n\t\t\tmap[string]*asiceFile{\"foo\": {name: \"foo\"}},\n\t\t\tnew(DuplicateManifestEntryError)},\n\n\t\t{\"root\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"/\"\n\t\t\t\t\tmanifest:media-type=\"foobar\"/>\n\t\t\t</manifest:manifest>`, nil, new(ManifestRootMIMETypeError)},\n\n\t\t{\"extra\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\" manifest:version=\"1.2\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"foo\"\n\t\t\t\t\tmanifest:media-type=\"bar\"/>\n\t\t\t</manifest:manifest>`, nil, new(ExtraManifestEntryError)},\n\n\t\t{\"signature\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\" manifest:version=\"1.2\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"META-INF/signatures0.xml\"\n\t\t\t\t\tmanifest:media-type=\"foobar\"/>\n\t\t\t</manifest:manifest>`,\n\t\t\tmap[string]*asiceFile{\"META-INF/signatures0.xml\": {\n\t\t\t\tname: \"META-INF/signatures0.xml\"}},\n\t\t\tnew(SignatureInManifestError)},\n\n\t\t{\"missing\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\" manifest:version=\"1.2\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"/\"\n\t\t\t\t\tmanifest:media-type=\"application/vnd.etsi.asic-e+zip\"/>\n\t\t\t</manifest:manifest>`, // Must include at least one entry.\n\t\t\tmap[string]*asiceFile{\"foo\": {name: \"foo\"}},\n\t\t\tnew(MissingManifestEntryError)},\n\n\t\t{\"OK\", `<manifest:manifest\n\t\t\t\txmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\" manifest:version=\"1.2\">\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"/\"\n\t\t\t\t\tmanifest:media-type=\"application/vnd.etsi.asic-e+zip\"/>\n\t\t\t\t<manifest:file-entry\n\t\t\t\t\tmanifest:full-path=\"foo\"\n\t\t\t\t\tmanifest:media-type=\"bar\"/>\n\t\t\t</manifest:manifest>`,\n\t\t\tmap[string]*asiceFile{\"foo\": {name: \"foo\"}},\n\t\t\tnil},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := readManifest([]byte(test.manifest), test.files)\n\t\t\tif err != test.err && errors.CausedBy(err, test.err) == nil {\n\t\t\t\tt.Errorf(\"expected error %T, got %v\", test.err, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/bdoc.go",
    "content": "/*\nPackage bdoc implements BDOC verification following the BDOC specification\nversion 2.1.2:2014.\nhttp://www.id.ee/public/bdoc-spec212-est.pdf\nhttp://www.id.ee/public/bdoc-spec212-eng.pdf\n*/\npackage bdoc // import \"ivxv.ee/container/bdoc\"\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/ocsp\"\n\t\"ivxv.ee/common/collector/tsp\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tcontainer.Register(container.BDOC, configure, unverifiedOpen, container.ASiCE)\n}\n\n// configure configures a new Opener and returns its Open function.\nfunc configure(n yaml.Node) (container.OpenFunc, error) {\n\tvar c Conf\n\tif err := yaml.Apply(n, &c); err != nil {\n\t\treturn nil, ConfigurationYAMLError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_CFG}\n\t}\n\n\to, err := New(&c)\n\tif err != nil {\n\t\treturn nil, ConfigurationError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_NEW}\n\t}\n\n\t// Wrap o.Open in a short function to cast the returned value from\n\t// *BDOC to container.Container.\n\treturn func(r io.Reader) (container.Container, error) {\n\t\treturn o.Open(r)\n\t}, nil\n}\n\nfunc unverifiedOpen(encoded io.Reader) (container.Container, error) {\n\t// The bootstrap container and files in it should fit inside 10 MiB.\n\tconst limit = 10 * 1024 * 1024\n\tconst fileCount = 50\n\tfiles, err := openASiCE(encoded, fileCount, limit, limit, false)\n\tif err != nil {\n\t\treturn nil, UnverifiedOpenError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_OPEN}\n\t}\n\treturn &BDOC{files: files}, nil\n}\n\n// Profile identifies a BDOC signature profile.\ntype Profile string\n\n// Enumeration of BDOC signature profiles.\nconst (\n\tBES Profile = \"BES\" // Base profile.\n\tTS  Profile = \"TS\"  // Timestamp profile.\n)\n\n// Conf contains the configurable options for the BDOC container opener. It\n// only contains serialized values such that it can easily be unmarshaled from\n// a file.\ntype Conf struct {\n\t// FileCount is the maximum allowed files amount in the container.\n\tFileCount int64\n\n\t// BDOCSize is the maximum accepted size of BDOC containers read from\n\t// streams into memory. Note that this limit does not apply to sources\n\t// which already support random access.\n\tBDOCSize int64\n\n\t// FileSize is the maximum accepted decompressed size of files in the container.\n\tFileSize int64\n\n\t// Roots contains the root certificates used for verification in PEM format\n\tRoots []string\n\n\t// Roots contains the intermediate certificates used for verification in PEM format\n\tIntermediates []string\n\n\t// Profile specifies the profile for opened BDOC containers.\n\tProfile Profile\n\n\t// OCSP is the configuration for the OCSP client used to check a\n\t// certificate's revocation status if Profile is TS. Only offline\n\t// checks are performed.\n\tOCSP ocsp.Conf\n\n\t// TSP is the configuration for the TSP client used to check timestamps\n\t// if Profile is TS.\n\tTSP tsp.Conf\n\n\t// TSDelayTime is the maximum time in seconds that the timestamp and\n\t// the OCSP response can differ if Profile is TS.\n\tTSDelayTime int64\n}\n\n// Opener is a configured BDOC container opener.\ntype Opener struct {\n\tfileCount int64\n\tbdocSize  int64\n\tfileSize  int64\n\trpool     *x509.CertPool\n\tipool     *x509.CertPool\n\tocsp      *ocsp.Client\n\ttsp       *tsp.Client\n\tprofile   Profile\n\ttsdelay   time.Duration\n}\n\n// New returns a new BDOC container opener.\nfunc New(c *Conf) (*Opener, error) {\n\to := &Opener{\n\t\tfileCount: c.FileCount,\n\t\tbdocSize:  c.BDOCSize,\n\t\tfileSize:  c.FileSize,\n\t\tprofile:   c.Profile,\n\t}\n\n\tif o.fileCount <= 0 {\n\t\treturn nil, InvalidBDOCFileCountError{Size: o.fileCount,\n\t\t\tDescription: _CONTAINER_BDOC_FILE_COUNT}\n\t}\n\n\tif o.bdocSize <= 0 {\n\t\treturn nil, InvalidBDOCSizeError{Size: o.bdocSize,\n\t\t\tDescription: _CONTAINER_BDOC_SIZE}\n\t}\n\n\tif o.fileSize <= 0 {\n\t\treturn nil, InvalidFileSizeError{Size: o.fileSize,\n\t\t\tDescription: _CONTAINER_BDOC_DATA_SIZE}\n\t}\n\n\tif len(c.Roots) == 0 {\n\t\treturn nil, UnconfiguredRootsError{Description: _CONTAINER_BDOC_CA}\n\t}\n\tvar err error\n\tif o.rpool, err = cryptoutil.PEMCertificatePool(c.Roots...); err != nil {\n\t\treturn nil, RootsParseError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_CA_PARSE}\n\t}\n\tif o.ipool, err = cryptoutil.PEMCertificatePool(c.Intermediates...); err != nil {\n\t\treturn nil, IntermediatesParseError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_ICA_PARSE}\n\t}\n\n\tswitch c.Profile {\n\tcase TS:\n\t\t// Create TSP client for checking timestamps.\n\t\tif o.tsp, err = tsp.New(&c.TSP); err != nil {\n\t\t\treturn nil, TSPClientError{Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_TSA}\n\t\t}\n\t\to.tsdelay = time.Duration(c.TSDelayTime) * time.Second\n\t\t// Create OCSP client for checking revocation status.\n\t\tif o.ocsp, err = ocsp.New(&c.OCSP); err != nil {\n\t\t\treturn nil, OCSPClientError{Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_OCSP}\n\t\t}\n\tcase BES: // No additional setup.\n\tdefault:\n\t\treturn nil, UnsupportedProfileError{Profile: c.Profile,\n\t\t\tDescription: _CONTAINER_BDOC_PROFILE}\n\t}\n\treturn o, nil\n}\n\n// BDOC is a parsed BDOC container\ntype BDOC struct {\n\tsignatures []container.Signature\n\tfiles      map[string]*asiceFile\n\tocspValues map[string][]byte\n\ttspData    map[string]*bytes.Buffer\n}\n\n// Open opens and verifies a BDOC signature container.\nfunc (o *Opener) Open(encoded io.Reader) (bdoc *BDOC, err error) {\n\t// Open the container and get all the files from it.\n\tfiles, err := openASiCE(encoded, o.fileCount, o.bdocSize, o.fileSize, true)\n\tif err != nil {\n\t\treturn nil, OpenBDOCContainerError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_OPEN}\n\t}\n\n\tbdoc = &BDOC{\n\t\tfiles:      files,\n\t\tocspValues: make(map[string][]byte),\n\t\ttspData:    make(map[string]*bytes.Buffer),\n\t}\n\t// Pass b as explicit argument so that it gets evaluated now and we can\n\t// later safely return nil. Do not do the same for err, because we want\n\t// to know its value on return, not now.\n\tdefer func(bdoc *BDOC) {\n\t\t// Close all files on error.\n\t\tif err != nil {\n\t\t\tbdoc.Close()\n\t\t}\n\t}(bdoc)\n\n\t// Parse signing time from BDOC container\n\tparseSigningTime := func(xadesSignature *signature) (t time.Time, err error) {\n\t\tt, err = time.Parse(time.RFC3339, xadesSignature.Object.\n\t\t\tQualifyingProperties.SignedProperties.\n\t\t\tSignedSignatureProperties.SigningTime.Value)\n\t\tif err != nil {\n\t\t\treturn time.Time{}, SigningTimeParseError{Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_XADES_SIG_TIME}\n\t\t}\n\t\treturn\n\t}\n\n\t// Parse the signatures from the container.\n\tvar signatures []*signature\n\tfor key, file := range bdoc.files {\n\t\tif !file.signature() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Parse the XAdES signatures.\n\t\tvar x xadesSignatures\n\t\tif err = parseXML(file.data.Bytes(), &x); err != nil {\n\t\t\treturn nil, SignatureXMLError{Name: file.name, Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_XADES_SIG}\n\t\t}\n\t\ts := &x.Signature // We allow only one signature per file.\n\t\tsignatures = append(signatures, s)\n\n\t\t// Remove the signature from BDOC data files and close it: we\n\t\t// do not need it anymore after it is parsed.\n\t\tdelete(bdoc.files, key)\n\t\tfile.close()\n\n\t\t// Parse signer certificate.\n\t\tvar signer *x509.Certificate\n\t\tif signer, err = cryptoutil.Base64Certificate(strings.TrimSpace(\n\t\t\ts.KeyInfo.X509Data.X509Certificate.Value)); err != nil {\n\n\t\t\treturn nil, SignerCertificateParseError{\n\t\t\t\tFileName:    file.name,\n\t\t\t\tSignatureID: s.ID,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CONTAINER_BDOC_CERT_SIG,\n\t\t\t}\n\t\t}\n\n\t\t// If profile is TS, then this will get replaced by the timestamp time.\n\t\tvar signingTime time.Time\n\t\tsigningTime, err = parseSigningTime(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Verify signer certificate and determine the issuer. We use\n\t\t// the declared signing time because the certificate might have\n\t\t// expired by now. The declared time cannot be trusted and the\n\t\t// certificate might have been revoked before expiration:\n\t\t// additional OCSP checks are performed, either here if the\n\t\t// profile is TS, or during signature qualification.\n\t\tvar issuer *x509.Certificate\n\t\tif issuer, err = o.verifyCertificate(signer, signingTime); err != nil {\n\t\t\treturn nil, SignerCertificateVerificationError{\n\t\t\t\tCertificate: signer.Raw, // Log entire cert for diagnostics.\n\t\t\t\tSigningTime: signingTime,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CONTAINER_BDOC_CERT_SIG_VERIFY,\n\t\t\t}\n\t\t}\n\n\t\t// Append to BDOC signatures.\n\t\tbdoc.signatures = append(bdoc.signatures, container.Signature{\n\t\t\tID:          s.ID,\n\t\t\tSigner:      signer,\n\t\t\tIssuer:      issuer,\n\t\t\tSigningTime: signingTime,\n\t\t})\n\n\t\t// Decode and store the ivxv.ee/q11n/ocsp.SignatureValuer value.\n\t\tbdoc.ocspValues[s.ID], err = b64d(s.SignatureValue.Value)\n\t\tif err != nil {\n\t\t\treturn nil, SignatureValueDecodeError{Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_SIG_B64}\n\t\t}\n\n\t\t// Canonicalize and store the ivxv.ee/q11n/tsp.TimestampDataer data.\n\t\tsigval := buffer()\n\t\twriteXML(&s.SignatureValue, sigval)\n\t\tbdoc.tspData[s.ID] = sigval\n\t}\n\n\t// Check the parsed signatures against the data files in the container.\n\tfor i, s := range signatures {\n\t\tsigningTime, err := parseSigningTime(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err = o.check(s, &bdoc.signatures[i], bdoc.files, signingTime); err != nil {\n\t\t\treturn nil, CheckSignatureError{Signature: s.ID, Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_CERT_OCSP}\n\t\t}\n\t}\n\treturn bdoc, nil\n}\n\n// Signatures implements the container.Container interface.\nfunc (b *BDOC) Signatures() []container.Signature {\n\treturn b.signatures\n}\n\n// Data implements the container.Container interface.\nfunc (b *BDOC) Data() map[string][]byte {\n\tfiles := make(map[string][]byte)\n\tfor _, file := range b.files {\n\t\tfiles[file.name] = file.data.Bytes()\n\t}\n\treturn files\n}\n\n// Close implements the container.Container interface.\nfunc (b *BDOC) Close() error {\n\tfor _, file := range b.files {\n\t\tfile.close()\n\t}\n\tb.files = nil\n\tfor _, buf := range b.tspData {\n\t\trelease(buf)\n\t}\n\tb.tspData = nil\n\treturn nil\n}\n\n// SignatureValue implements the ivxv.ee/q11n/ocsp.SignatureValuer interface.\n// It returns the raw signature value for the requested ID.\nfunc (b *BDOC) SignatureValue(id string) ([]byte, error) {\n\tvalue, ok := b.ocspValues[id]\n\tif !ok {\n\t\treturn nil, SignatureValueNoSuchIDError{ID: id,\n\t\t\tDescription: _CONTAINER_BDOC_OCSP_ID}\n\t}\n\treturn value, nil\n}\n\n// TimestampData implements the ivxv.ee/q11n/tsp.TimestampDataer interface. It\n// returns the canonical SignatureValue XML element for the requested ID.\nfunc (b *BDOC) TimestampData(id string) ([]byte, error) {\n\tdata, ok := b.tspData[id]\n\tif !ok {\n\t\treturn nil, TimestampDataNoSuchIDError{ID: id,\n\t\t\tDescription: _CONTAINER_BDOC_TSA_ID}\n\t}\n\treturn data.Bytes(), nil\n}\n\nconst xmlc14n11 = \"http://www.w3.org/2006/12/xml-c14n11\"\n\nfunc (o *Opener) check(\n\ts *signature, c *container.Signature, files map[string]*asiceFile, sigTime time.Time) error {\n\t_, err := checkSignatureValue(s, c.Signer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = checkReferences(s, files); err != nil {\n\t\treturn err\n\t}\n\n\t// Check that QualifyingProperties targets this Signature.\n\tqprop := &s.Object.QualifyingProperties\n\tif qprop.Target != \"#\"+s.ID {\n\t\treturn QualifyingPropertiesTargetError{\n\t\t\tSignatureID: s.ID,\n\t\t\tTarget:      qprop.Target,\n\t\t\tDescription: _CONTAINER_BDOC_Q,\n\t\t}\n\t}\n\n\tif err = checkSignedProperties(&qprop.SignedProperties, c.Signer); err != nil {\n\t\treturn err\n\t}\n\n\tusp := &qprop.UnsignedProperties.UnsignedSignatureProperties\n\tocsp := &usp.RevocationValues.OCSPValues\n\n\t// BES profile doesn't have a timestamp nor ocsp response\n\tif o.profile == TS {\n\t\tc.SigningTime, err = checkTimestamp(&usp.SignatureTimeStamp, &s.SignatureValue, o.tsp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproducedAt, _, err := checkOCSP(ocsp, c.Signer, c.Issuer, o.ocsp, sigTime)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif diff := producedAt.Sub(c.SigningTime); diff < 0 || diff > o.tsdelay {\n\t\t\treturn TimestampAndOCSPTimeMismatchError{\n\t\t\t\tTimestampGenTime: c.SigningTime,\n\t\t\t\tOCSPProducedAt:   producedAt,\n\t\t\t\tDescription:      _CONTAINER_BDOC_TS,\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n\nvar signAlgorithms = map[string]x509.SignatureAlgorithm{\n\t\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\":   x509.SHA256WithRSA,\n\t\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384\":   x509.SHA384WithRSA,\n\t\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512\":   x509.SHA512WithRSA,\n\t\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256\": x509.ECDSAWithSHA256,\n\t\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384\": x509.ECDSAWithSHA384,\n\t\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512\": x509.ECDSAWithSHA512,\n}\n\nfunc checkSignatureValue(s *signature, c *x509.Certificate) (signature []byte, err error) {\n\tsigning := s.SignedInfo.SignatureMethod.Algorithm\n\tx509sa, ok := signAlgorithms[signing]\n\tif !ok {\n\t\treturn nil, UnsupportedSigningAlgorithmError{Algorithm: signing,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_ALG}\n\t}\n\n\tc14n := s.SignedInfo.CanonicalizationMethod.Algorithm\n\tif c14n != xmlc14n11 {\n\t\treturn nil, UnsupportedSignedInfoCanonicalizationAlgorithmError{\n\t\t\tAlgorithm:   c14n,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_CALG}\n\t}\n\n\tsiginfo := buffer()\n\tdefer release(siginfo)\n\twriteXML(&s.SignedInfo, siginfo)\n\n\tif signature, err = b64d(s.SignatureValue.Value); err != nil {\n\t\treturn nil, DecodeSignatureValueError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_B64}\n\t}\n\n\t// We need to re-encode ECDSA signatures for the Go standard library.\n\trecode := signature\n\tswitch x509sa {\n\tcase x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:\n\t\tif recode, err = cryptoutil.ReEncodeECDSASignature(signature); err != nil {\n\t\t\treturn nil, ReEncodeECDSASignatureError{\n\t\t\t\tSignature:   signature,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CONTAINER_BDOC_XML2ASN1,\n\t\t\t}\n\t\t}\n\t}\n\n\tif err = c.CheckSignature(x509sa, siginfo.Bytes(), recode); err != nil {\n\t\treturn nil, SignatureVerificationError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_VERIFY}\n\t}\n\treturn signature, nil\n}\n\n// checkReferences checks the references for SignedProperties and each file.\nfunc checkReferences(s *signature, files map[string]*asiceFile) error {\n\t// Check the SignedProperties reference.\n\tsigPropID := s.Object.QualifyingProperties.SignedProperties.ID\n\tref, err := findReference(s.SignedInfo.Reference, \"#\"+sigPropID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ref.Type != \"http://uri.etsi.org/01903#SignedProperties\" {\n\t\treturn InvalidSignedPropertiesReferenceTypeError{Type: ref.Type,\n\t\t\tDescription: _CONTAINER_BDOC_REF}\n\t}\n\n\t// We use a fixed canonicalization algorithm, so Transforms should\n\t// either be missing or designate the algorithm that we use.\n\tfor _, tr := range ref.Transforms.Transform {\n\t\tif tr.Algorithm != xmlc14n11 {\n\t\t\treturn UnsupportedSignedPropertiesTransformError{\n\t\t\t\tAlgorithm:   tr.Algorithm,\n\t\t\t\tDescription: _CONTAINER_BDOC_SIG_PROP_CALG,\n\t\t\t}\n\t\t}\n\t}\n\n\tsigprop := buffer()\n\tdefer release(sigprop)\n\twriteXML(&s.Object.QualifyingProperties.SignedProperties, sigprop)\n\tif err = checkXMLDigest(ref.DigestMethod, sigprop.Bytes(), ref.DigestValue); err != nil {\n\t\treturn SignedPropertiesReferenceDigestError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_PROP_DIG}\n\t}\n\n\t// Check the data file references.\n\tdofs := s.Object.QualifyingProperties.SignedProperties.\n\t\tSignedDataObjectProperties.DataObjectFormat\n\tfor _, file := range files {\n\t\tref, err = findReference(s.SignedInfo.Reference, file.name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(ref.Type) > 0 {\n\t\t\treturn UnsupportedReferenceTypeError{URI: file.name, Type: ref.Type,\n\t\t\t\tDescription: _CONTAINER_BDOC_REF}\n\t\t}\n\t\tif len(ref.Transforms.Transform) > 0 {\n\t\t\treturn FileReferenceWithTransformsError{URI: file.name,\n\t\t\t\tDescription: _CONTAINER_BDOC_FILE_REF}\n\t\t}\n\n\t\tif err = checkXMLDigest(ref.DigestMethod, file.data.Bytes(), ref.DigestValue); err != nil {\n\t\t\treturn FileReferenceDigestError{URI: file.name, Err: err,\n\t\t\t\tDescription: _CONTAINER_BDOC_FILE_DIG}\n\t\t}\n\n\t\t// Since we already have the reference for file here, also\n\t\t// check the DataObjectFormat instead of having to look up the\n\t\t// reference again later when checking SignedProperties.\n\t\tdof, err := findDataObjectFormat(dofs, \"#\"+ref.ID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif dof.MimeType.Value != file.mimetype {\n\t\t\treturn DataObjectFormatMIMETypeMismatchError{\n\t\t\t\tURI:              file.name,\n\t\t\t\tManifest:         file.mimetype,\n\t\t\t\tDataObjectFormat: dof.MimeType.Value,\n\t\t\t\tDescription:      _CONTAINER_BDOC_DO,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for extra References. We know that there cannot be any less,\n\t// because we had a reference for each unique file name.\n\tif count, expected := len(s.SignedInfo.Reference), len(files)+1; count > expected {\n\t\treturn ExtraReferencesError{Count: count, Expected: expected,\n\t\t\tDescription: _CONTAINER_BDOC_N_REF}\n\t}\n\n\t// Check for extra DataObjectFormats.\n\tif count, expected := len(dofs), len(files); count > expected {\n\t\treturn ExtraDataObjectFormatsError{Count: count, Expected: expected,\n\t\t\tDescription: _CONTAINER_BDOC_DO_EX}\n\t}\n\treturn nil\n}\n\nfunc findReference(refs []reference, uri string) (reference, error) {\n\tfor _, ref := range refs {\n\t\truri, err := url.QueryUnescape(ref.URI)\n\t\tif err != nil {\n\t\t\treturn ref, ReferenceURIUnescapeError{\n\t\t\t\tURI:         ref.URI,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CONTAINER_BDOC_REF_URI,\n\t\t\t}\n\t\t}\n\t\tif ruri == uri {\n\t\t\treturn ref, nil\n\t\t}\n\t}\n\treturn reference{}, NoReferenceWithURIError{URI: uri,\n\t\tDescription: _CONTAINER_BDOC_REF_N_URI}\n}\n\nfunc findDataObjectFormat(dofs []dataObjectFormat, ref string) (dataObjectFormat, error) {\n\tfor _, dof := range dofs {\n\t\tif dof.ObjectReference == ref {\n\t\t\treturn dof, nil\n\t\t}\n\t}\n\treturn dataObjectFormat{}, NoDataObjectFormatWithReferenceError{\n\t\tReference: ref, Description: _CONTAINER_BDOC_REF_NO}\n}\n\nfunc checkSignedProperties(p *signedProperties, c *x509.Certificate) error {\n\t// Check that SigningCertificate is correct\n\tsc := &p.SignedSignatureProperties.SigningCertificate\n\tif err := checkSigningCertificate(sc, c); err != nil {\n\t\treturn err\n\t}\n\n\t// DataObjectFormats were already checked in checkReferences.\n\n\tspi := &p.SignedSignatureProperties.SignaturePolicyIdentifier\n\t// TS/BES profiles require that there be no signature policy which\n\t// might affect the interpretation of the signature container.\n\t//\n\t// Only legacy TM profile had that spi.XMLElement property,\n\t// but TM profile is not supported anymore\n\tif spi.XMLElement.isPresent() {\n\t\treturn UnexpectedSignaturePolicyIdentifierError{\n\t\t\t// Use TrimSpace to remove all \\n, \\r, \\s chars\n\t\t\t// in case XML is not canonicalized\n\t\t\tIdentifier:  strings.TrimSpace(spi.SignaturePolicyID.SigPolicyID.Identifier.Value),\n\t\t\tDescription: _CONTAINER_BDOC_SIG_POLICY,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkSigningCertificate(s *signingCertificate, c *x509.Certificate) error {\n\t// Check CertDigest to ensure that KeyInfo was not replaced.\n\tmethod := s.Cert.CertDigest.DigestMethod\n\tdigest := s.Cert.CertDigest.DigestValue\n\tif err := checkXMLDigest(method, c.Raw, digest); err != nil {\n\t\treturn SigningCertificateDigestError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_SIG_PARSE}\n\t}\n\n\t// Check IssuerSerial to ensure that the correct certificate is indicated.\n\tserial := strings.TrimSpace(s.Cert.IssuerSerial.X509SerialNumber.Value)\n\tif c.SerialNumber.String() != serial {\n\t\treturn SigningCertificateSerialError{\n\t\t\tKeyInfo:            c.SerialNumber,\n\t\t\tSigningCertificate: serial,\n\t\t\tDescription:        _CONTAINER_BDOC_SIG_SER,\n\t\t}\n\t}\n\n\tissuer, err := cryptoutil.DecodeRDNSequence(s.Cert.IssuerSerial.X509IssuerName.Value)\n\tif err != nil {\n\t\treturn SigningCertificateIssuerParseError{\n\t\t\tIssuer:      s.Cert.IssuerSerial.X509IssuerName.Value,\n\t\t\tErr:         err,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_ISS,\n\t\t}\n\t}\n\n\tc.Issuer.ExtraNames = c.Issuer.Names\n\tif !cryptoutil.RDNSequenceEqual(c.Issuer.ToRDNSequence(), issuer) {\n\t\treturn SigningCertificateIssuerError{\n\t\t\tKeyInfo:            c.Issuer,\n\t\t\tSigningCertificate: s.Cert.IssuerSerial.X509IssuerName.Value,\n\t\t\tDescription:        _CONTAINER_BDOC_CERT_ISS_VERIFY,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkOCSP(values *ocspValues, c, issuer *x509.Certificate,\n\tocsp *ocsp.Client, sigTime time.Time) (\n\tproducedAt time.Time, nonce []byte, err error) {\n\n\tvalue := values.EncapsulatedOCSPValue.Value\n\tif len(value) == 0 {\n\t\treturn producedAt, nil, OCSPResponseMissingError{\n\t\t\tDescription: _CONTAINER_BDOC_CERT_N_OCSP,\n\t\t}\n\t}\n\n\tresponse, err := b64d(value)\n\tif err != nil {\n\t\treturn producedAt, nil, OCSPResponseDecodeError{\n\t\t\tValue: value, Err: err, Description: _CONTAINER_BDOC_CERT_OCSP_B64}\n\t}\n\n\tstatus, err := ocsp.CheckFullResponse(response, c, issuer, nil, sigTime)\n\tif err != nil {\n\t\treturn producedAt, nil, OCSPResponceVerificationError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_OCSP_VERIFY}\n\t}\n\tif !status.Good {\n\t\treturn producedAt, nil, OCSPStatusNotGoodError{Response: *status,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_OCSP_BAD}\n\t}\n\treturn status.ProducedAt, status.Nonce, nil\n}\n\nfunc checkTimestamp(timestamp *xadesTimeStamp, sigval *signatureValue, tsp *tsp.Client) (\n\tgenTime time.Time, err error) {\n\n\tc14n := timestamp.CanonicalizationMethod\n\tif c14n.XMLElement.isPresent() && c14n.Algorithm != xmlc14n11 {\n\t\treturn genTime, UnsupportedTimestampCanonicalizationAlgorithmError{\n\t\t\tAlgorithm:   c14n.Algorithm,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_TSA_CALG,\n\t\t}\n\t}\n\n\tvalue := timestamp.EncapsulatedTimeStamp.Value\n\tif len(value) == 0 {\n\t\treturn genTime, TimestampMissingError{\n\t\t\tDescription: _CONTAINER_BDOC_CERT_N_TSA,\n\t\t}\n\t}\n\n\tresponse, err := b64d(value)\n\tif err != nil {\n\t\treturn genTime, TimestampDecodeError{Value: value, Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_TSA_B64}\n\t}\n\n\tdata := buffer()\n\tdefer release(data)\n\twriteXML(sigval, data)\n\n\tif genTime, err = tsp.Check(response, data.Bytes(), nil); err != nil {\n\t\treturn genTime, TimestampVerificationError{Err: err,\n\t\t\tDescription: _CONTAINER_BDOC_CERT_VALIDATE}\n\t}\n\treturn genTime, nil\n}\n\nvar hashXMLMap = map[string]crypto.Hash{\n\t\"http://www.w3.org/2001/04/xmlenc#sha256\":       crypto.SHA256,\n\t\"http://www.w3.org/2001/04/xmldsig-more#sha384\": crypto.SHA384,\n\t\"http://www.w3.org/2001/04/xmlenc#sha512\":       crypto.SHA512,\n}\n\nfunc checkXMLDigest(method digestMethod, data []byte, digest digestValue) error {\n\th, ok := hashXMLMap[method.Algorithm]\n\tif !ok {\n\t\treturn UnsupportedDigestAlgorithm{Algorithm: method.Algorithm,\n\t\t\tDescription: _CONTAINER_DIG_ALG}\n\t}\n\n\tdecoded, err := b64d(digest.Value)\n\tif err != nil {\n\t\treturn DigestValueDecodeError{Err: err,\n\t\t\tDescription: _CONTAINER_DIG_ALG_B64}\n\t}\n\n\thash := h.New()\n\thash.Write(data)\n\tcalculated := hash.Sum(nil)\n\tif !bytes.Equal(decoded, calculated) {\n\t\treturn DigestMismatchError{Expected: decoded, Calculated: calculated,\n\t\t\tDescription: _CONTAINER_DIG_ALG_VERIFY}\n\t}\n\treturn nil\n}\n\nfunc (o *Opener) verifyCertificate(c *x509.Certificate, time time.Time) (\n\tissuer *x509.Certificate, err error) {\n\n\tif c.KeyUsage&x509.KeyUsageContentCommitment == 0 {\n\t\treturn nil, NotANonRepudiationCertificateError{\n\t\t\tKeyUsage:    c.KeyUsage,\n\t\t\tDescription: _CONTAINER_KEY_USAGE_CONTENT_COMMIT,\n\t\t}\n\t}\n\n\topts := x509.VerifyOptions{\n\t\tRoots:         o.rpool,\n\t\tIntermediates: o.ipool,\n\t\tCurrentTime:   time,\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},\n\t}\n\n\tchains, err := c.Verify(opts)\n\tif err == nil {\n\t\t// At least one chain is guaranteed: use the first one.\n\t\tissuer = c\n\t\tif len(chains[0]) > 1 {\n\t\t\tissuer = chains[0][1]\n\t\t}\n\t}\n\treturn issuer, err\n}\n\nfunc b64d(data string) ([]byte, error) {\n\treturn base64.StdEncoding.DecodeString(strings.TrimSpace(data))\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/bdoc_test.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nconst (\n\t// Path to the trust.yml for BDOC TS profile.\n\ttrustConfTS = \"testdata/trustTS.yaml\"\n\t// Path to the trust.yml for deprecated BDOC TM profile.\n\ttrustConfTM = \"testdata/trustTM.yaml\"\n\t// Path to the trust.yml for BDOC BES profile.\n\ttrustConfBES = \"testdata/trustBES.yaml\"\n\n\tdataKey   = \"test.txt\"\n\tdataValue = \"Test data\"\n\n\t// Deprecated: TM\n\t//\n\t// BDOC Opener should not be configured to support TM profiles anymore.\n\t//\n\t// SK ID Solutions:\n\t//\n\t// The modified service cannot be used to create signatures in BDOC-TM\n\t// format. Signatures created in BDOC-TM format from November 1, 2023 can\n\t// no longer be validated.\n\t//\n\t// Reference:\n\t// https://www.skidsolutions.eu/news/important-changes-in-validity-confirmation-service/\n\tTM = \"TM\"\n\n\t// In unzipped BDOC container, in META-INF/signatures0.xml, <ds:Signature Id=\"S0\">\n\tsignatureIDS0 = \"S0\"\n\t// In unzipped BDOC container, in META-INF/signatures1.xml, <ds:Signature Id=\"S1\">\n\tsignatureIDS1 = \"S1\"\n)\n\nconst expectedEqualObjects = \"Expected o1 == o2, got o1: %v, o2: %v\\n\"\n\n// Supplier is an any function that takes no parameters and returns some value.\ntype Supplier func() interface{}\n\nfunc unsupportedProfileErrorMessageSupplier(profile Profile) Supplier {\n\treturn func() interface{} {\n\t\tprofileErr := new(UnsupportedProfileError)\n\t\tprofileErr.Profile = profile\n\t\tprofileErr.Description = _CONTAINER_BDOC_PROFILE\n\t\treturn *profileErr\n\t}\n}\n\nfunc tsProfileOCSPDelayedErrorMessageSupplier(signatureID string) Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(CheckSignatureError)\n\t\tsignatureError.Signature = signatureID\n\t\ttimestampErr := new(TimestampAndOCSPTimeMismatchError)\n\t\ttimestampErr.OCSPProducedAt = time.Date(2021, 4, 13, 9, 56, 6, 0, time.UTC)\n\t\ttimestampErr.TimestampGenTime = time.Date(2021, 4, 8, 8, 20, 41, 0, time.UTC)\n\t\ttimestampErr.Description = _CONTAINER_BDOC_TS\n\t\tsignatureError.Err = *timestampErr\n\t\tsignatureError.Description = _CONTAINER_BDOC_CERT_OCSP\n\t\treturn *signatureError\n\t}\n}\n\nfunc tsProfileOCSPOldErrorMessageSupplier(signatureID string) Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(CheckSignatureError)\n\t\tsignatureError.Signature = signatureID\n\t\ttimestampErr := new(TimestampAndOCSPTimeMismatchError)\n\t\ttimestampErr.TimestampGenTime = time.Date(2021, 4, 13, 9, 56, 6, 0, time.UTC)\n\t\ttimestampErr.OCSPProducedAt = time.Date(2021, 4, 8, 8, 20, 41, 0, time.UTC)\n\t\ttimestampErr.Description = _CONTAINER_BDOC_TS\n\t\tsignatureError.Err = *timestampErr\n\t\tsignatureError.Description = _CONTAINER_BDOC_CERT_OCSP\n\t\treturn *signatureError\n\t}\n}\n\nfunc noDataFilesErrorMessageSupplier() Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(OpenBDOCContainerError)\n\t\tmanifestError := new(NoDataFilesError)\n\t\tmanifestError.Description = _CONTAINER_BDOC_DATA\n\t\tsignatureError.Err = *manifestError\n\t\tsignatureError.Description = _CONTAINER_BDOC_OPEN\n\t\treturn *signatureError\n\t}\n}\n\nfunc noSignatureErrorMessageSupplier() Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(OpenBDOCContainerError)\n\t\tmanifestError := new(NoSignaturesError)\n\t\tmanifestError.Description = _CONTAINER_BDOC_DATA\n\t\tsignatureError.Err = *manifestError\n\t\tsignatureError.Description = _CONTAINER_BDOC_OPEN\n\t\treturn *signatureError\n\t}\n}\n\nfunc manifestErrorMessageSupplier() Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(OpenBDOCContainerError)\n\t\tmanifestError := new(MissingManifestError)\n\t\tmanifestError.Description = _CONTAINER_BDOC_MANI\n\t\tsignatureError.Err = *manifestError\n\t\tsignatureError.Description = _CONTAINER_BDOC_OPEN\n\t\treturn *signatureError\n\t}\n}\n\nfunc tmProfilePolicyErrorMessageSupplier(signatureID string) Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(CheckSignatureError)\n\t\tsignatureError.Signature = signatureID\n\t\tpolicyErr := new(UnexpectedSignaturePolicyIdentifierError)\n\t\tpolicyErr.Identifier = \"urn:oid:1.3.6.1.4.1.10015.1000.3.2.1\"\n\t\tpolicyErr.Description = _CONTAINER_BDOC_SIG_POLICY\n\t\tsignatureError.Err = *policyErr\n\t\tsignatureError.Description = _CONTAINER_BDOC_CERT_OCSP\n\t\treturn *signatureError\n\t}\n}\n\nfunc tmProfileTimestampErrorMessageSupplier(signatureID string) Supplier {\n\treturn func() interface{} {\n\t\tsignatureError := new(CheckSignatureError)\n\t\tsignatureError.Signature = signatureID\n\t\ttimestampErr := new(TimestampMissingError)\n\t\ttimestampErr.Description = _CONTAINER_BDOC_CERT_N_TSA\n\t\tsignatureError.Err = *timestampErr\n\t\tsignatureError.Description = _CONTAINER_BDOC_CERT_OCSP\n\t\treturn *signatureError\n\t}\n}\n\n// loadTrustConf loads necessary trust.yml for the given profile.\nfunc loadTrustConf(profile Profile) (*Opener, error) {\n\tvar confPath string\n\n\tswitch profile {\n\tcase TS:\n\t\tconfPath = trustConfTS\n\tcase TM:\n\t\tconfPath = trustConfTM\n\tcase BES:\n\t\tconfPath = trustConfBES\n\t}\n\n\treturn testLoadConf(confPath)\n}\n\n// testLoadConf configures BDOC Opener using confPath.\nfunc testLoadConf(confPath string) (o *Opener, err error) {\n\tyamlTestConf, err := os.Open(confPath)\n\tif err != nil {\n\t\treturn\n\t}\n\tnode, err := yaml.Parse(yamlTestConf, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tvar c Conf\n\tif err = yaml.Apply(node, &c); err != nil {\n\t\treturn\n\t}\n\n\treturn New(&c)\n}\n\n//nolint:lll\nfunc TestOpen(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tsigners           []string\n\t\tprofile           Profile\n\t\texpectedFailure   bool\n\t\texpectedFileCount int\n\t\tfailure           Supplier // nil means no error is expected\n\t}{\n\t\t// ID-card signature with TS profile and AIA OCSP response is\n\t\t// valid for BES and TS.\n\t\t{\"EIDTS\", []string{\"JÕEORG,JAAK-KRISTJAN,38001085718\"}, BES, false, 1, nil},\n\t\t{\"EIDTS\", []string{\"JÕEORG,JAAK-KRISTJAN,38001085718\"}, TS, false, 1, nil},\n\t\t// TM profile is not supported anymore\n\t\t{\"EIDTS\", []string{\"JÕEORG,JAAK-KRISTJAN,38001085718\"}, TM, true, 1, unsupportedProfileErrorMessageSupplier(TM)},\n\n\t\t// Mobile-ID signature with TS profile and non-AIA OCSP response is\n\t\t// valid for BES and TS.\n\t\t{\"MIDTS\", []string{\"O’CONNEŽ-ŠUSLIK TESTNUMBER,MARY ÄNN,60001018800\"}, BES, false, 1, nil},\n\t\t{\"MIDTS\", []string{\"O’CONNEŽ-ŠUSLIK TESTNUMBER,MARY ÄNN,60001018800\"}, TS, false, 1, nil},\n\t\t// TM profile is not supported anymore\n\t\t{\"MIDTS\", []string{\"O’CONNEŽ-ŠUSLIK TESTNUMBER,MARY ÄNN,60001018800\"}, TM, true, 1, unsupportedProfileErrorMessageSupplier(TM)},\n\n\t\t// TM profiles are not supported anymore, and even if operator configured\n\t\t// BDOC Opener to support only TS or BES profiles via trust.yml config\n\t\t// file, it doesn't restrict user to sign BDOC containers with ID card TM\n\t\t// profile. Backend should fail with SignaturePolicyIdentifier\n\t\t{\"EIDTM\", []string{\"MÄNNIK,MARI-LIIS,47101010033\"}, TM, true, 1,\n\t\t\tunsupportedProfileErrorMessageSupplier(TM)},\n\t\t{\"EIDTM\", []string{\"MÄNNIK,MARI-LIIS,47101010033\"}, BES, true, 1,\n\t\t\ttmProfilePolicyErrorMessageSupplier(signatureIDS0)},\n\t\t{\"EIDTM\", []string{\"MÄNNIK,MARI-LIIS,47101010033\"}, TS, true, 1,\n\t\t\ttmProfilePolicyErrorMessageSupplier(signatureIDS0)},\n\n\t\t// TM profiles are not supported anymore, and even if operator configured\n\t\t// BDOC Opener to support only TS or BES profiles via trust.yml config\n\t\t// file, it doesn't restrict user to sign BDOC containers with Mobile-ID\n\t\t// TM profile. Backend should fail with SignaturePolicyIdentifier\n\t\t{\"MIDTM\", []string{\"O’CONNEŽ-ŠUSLIK,MARY ÄNN,11412090004\"}, TM, true, 1, unsupportedProfileErrorMessageSupplier(TM)},\n\t\t{\"MIDTM\", []string{\"O’CONNEŽ-ŠUSLIK,MARY ÄNN,11412090004\"}, BES, true, 1, tmProfilePolicyErrorMessageSupplier(signatureIDS1)},\n\t\t{\"MIDTM\", []string{\"O’CONNEŽ-ŠUSLIK,MARY ÄNN,11412090004\"}, TS, true, 1, tmProfilePolicyErrorMessageSupplier(signatureIDS1)},\n\n\t\t// ID-card signature with no qualification are only valid for BES\n\t\t{\"EIDBES\", []string{\"ŽAIKOVSKI,IGOR,37101010021\"}, BES, false, 1, nil},\n\t\t{\"EIDBES\", []string{\"ŽAIKOVSKI,IGOR,37101010021\"}, TM, true, 1, unsupportedProfileErrorMessageSupplier(TM)},\n\t\t{\"EIDBES\", []string{\"ŽAIKOVSKI,IGOR,37101010021\"}, TS, true, 1, tmProfileTimestampErrorMessageSupplier(signatureIDS0)},\n\n\t\t// Containers with multiple signers and files.\n\t\t{\n\t\t\t\"MultipleSigners\",\n\t\t\t[]string{\"ORAV,IVAN,30809010001\", \"ROPKA,KIVIVALVUR,32608320001\"},\n\t\t\tTS, false, 1, nil,\n\t\t},\n\t\t{\"MultipleFiles\", []string{\"ROPKA,KIVIVALVUR,32608320001\"}, TS, false, 2, nil},\n\n\t\t// Containers with missing files.\n\t\t{\"NoManifest\", nil, BES, true, 1, manifestErrorMessageSupplier()},\n\t\t{\"NoSignatures\", nil, BES, true, 1, noSignatureErrorMessageSupplier()},\n\t\t{\"NoFiles\", []string{\"ŽAIKOVSKI,IGOR,37101010021\"}, BES, true, 0, noDataFilesErrorMessageSupplier()},\n\n\t\t// Containers with invalid OCSP response times.\n\t\t{\"OCSPOld\", nil, TS, true, 1, tsProfileOCSPOldErrorMessageSupplier(signatureIDS0)},\n\t\t{\"OCSPDelayed\", nil, TS, true, 1, tsProfileOCSPDelayedErrorMessageSupplier(signatureIDS0)},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"%s as %s\", test.name, test.profile), func(t *testing.T) {\n\t\t\t// Open test bdoc file\n\t\t\tfile, err := os.Open(filepath.Join(\"testdata\",\n\t\t\t\tfmt.Sprintf(\"test%s.bdoc\", test.name)))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Failed to open BDOC:\", err)\n\t\t\t}\n\t\t\tdefer file.Close()\n\n\t\t\ttestOpener, err := loadTrustConf(test.profile)\n\t\t\tif err != nil {\n\t\t\t\tfail := test.failure()\n\t\t\t\tif !reflect.DeepEqual(fail, err) {\n\t\t\t\t\terrMsg := fmt.Sprintf(expectedEqualObjects, fail, err)\n\t\t\t\t\tt.Fatal(errMsg)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbdoc, err := testOpener.Open(file)\n\t\t\tswitch {\n\t\t\tcase test.expectedFailure && err != nil:\n\t\t\t\tfail := test.failure()\n\t\t\t\tif !reflect.DeepEqual(fail, err) {\n\t\t\t\t\terrMsg := fmt.Sprintf(expectedEqualObjects, fail, err)\n\t\t\t\t\tt.Fatal(errMsg)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\tcase test.expectedFailure && err == nil:\n\t\t\t\tt.Fatal(\"Expected failure verifying BDOC\")\n\t\t\tcase !test.expectedFailure && err != nil:\n\t\t\t\tt.Fatalf(\"Failure verifying BDOC: %v\", err)\n\t\t\t}\n\n\t\t\ts := bdoc.Signatures()\n\t\t\tif len(s) != len(test.signers) {\n\t\t\t\tt.Fatal(\"unexpected signers count:\", len(s))\n\t\t\t}\n\t\t\tif ret := testCompareNames(s, test.signers); ret != \"\" {\n\t\t\t\tt.Fatalf(\"Signer common name error: %s\", ret)\n\t\t\t}\n\n\t\t\tdoc := bdoc.Data()\n\t\t\tif len(doc) != test.expectedFileCount {\n\t\t\t\tt.Fatal(\"unexpected data key count:\", len(doc))\n\t\t\t}\n\t\t\tif len(doc) == 1 {\n\t\t\t\tif val, ok := doc[dataKey]; !ok {\n\t\t\t\t\tt.Fatalf(\"missing data key %q\", dataKey)\n\t\t\t\t} else if !bytes.Equal(val, []byte(dataValue)) {\n\t\t\t\t\tt.Fatalf(\"unexpected data value of key %q: %x\", dataKey, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc testCompareNames(signatures []container.Signature, signers []string) string {\n\tfor _, signer := range signers {\n\t\tvar found bool\n\t\tfor _, signature := range signatures {\n\t\t\tif signature.Signer.Subject.CommonName == signer {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn fmt.Sprintf(\"Expected name %s not found\", signer)\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/c14n.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// c14n contains metadata for a parsed XML element required to reconstruct\n// canonical XML from it.\ntype c14n struct {\n\t// ns specifies the set of namespace bindings that apply to this\n\t// element. Since copying this to every element of an XML document\n\t// would be extremely wasteful, this field is non-nil only for elements\n\t// that are marked as canonicalization roots. See parse on how to mark\n\t// roots.\n\tns    map[string]string\n\tstart startElement // Start token of the XML element.\n\n\t// whitespace contains the whitespace character data between\n\t// sub-elements. If the XML element contains a character data field,\n\t// then this is nil. Otherwise it will always contain at least one\n\t// entry - the whitespace following the start token - and additional\n\t// entries for the whitespace following each sub-element.\n\twhitespace []string\n}\n\n// isPresent reports if an XML element has been parsed into c14n. Useful for\n// checking the presence of optional elements.\nfunc (c c14n) isPresent() bool {\n\treturn len(c.start.name.Local) > 0\n}\n\nvar c14nType = reflect.TypeOf(c14n{})\n\n// c14nHeader returns a pointer to the c14n header in v.\nfunc c14nHeader(v reflect.Value) *c14n {\n\tt := v.Type()\n\tif t.Kind() != reflect.Struct {\n\t\tpanic(fmt.Sprint(\"cannot process \", t, \" as XML element (must be struct)\"))\n\t}\n\tif t.NumField() < 1 {\n\t\tpanic(fmt.Sprint(t, \" must have at least one field\"))\n\t}\n\tf := t.Field(0)\n\tif f.Name != \"XMLElement\" || f.Type != c14nType {\n\t\tpanic(fmt.Sprint(t, \" must have XMLElement of type c14n as first field\"))\n\t}\n\treturn v.Field(0).Addr().Interface().(*c14n)\n}\n\n// writer formats a parsed XML structure as canonical XML as specified in\n// https://www.w3.org/TR/xml-c14n11/.\ntype writer struct {\n\t// bytes.Buffer is used as the write destination instead of a generic\n\t// io.Writer because of its guaranteed writes. If a use case appears\n\t// which requires a generic destination, then writer can be rewritten.\n\tb  *bytes.Buffer\n\tns mapstack\n}\n\n// write write the canonical form of the struct reflected in v into p.w. v must\n// satisfy the requirements set in parser.parse or write will panic. Cannot use\n// encoding/xml.Encoder.EncodeToken here because it writes non-canonical XML.\n//\n// If root is true, then write is processing the root element of the document\n// and the namespace bindings in c14nHeader(v).ns are written.\nfunc (w *writer) write(v reflect.Value, root bool) {\n\theader := c14nHeader(v)\n\n\t// Check that it is properly tagged.\n\tname, c14nroot := xmlxElement(v.Type().Field(0))\n\tif name != header.start.name {\n\t\tpanic(fmt.Sprint(`\"xmlx\" tag name `, name,\n\t\t\t\" does not match start element\", header.start.name))\n\t}\n\tif root && !c14nroot {\n\t\tpanic(fmt.Sprint(\"document root \", v.Type(),\n\t\t\t` \"xmlx\" tag does not contain \"c14nroot\" flag`))\n\t}\n\n\tw.ns.push()\n\tdefer w.ns.pop()\n\n\tw.writeRaw(\"<\")\n\tif header.start.nsprefix != \"\" {\n\t\tw.writeRaw(header.start.nsprefix, \":\")\n\t}\n\tw.writeRaw(header.start.name.Local)\n\tw.writeAttributes(header, root)\n\tw.writeRaw(\">\")\n\n\tw.writeSubelements(v, header)\n\n\tw.writeRaw(\"</\")\n\tif header.start.nsprefix != \"\" {\n\t\tw.writeRaw(header.start.nsprefix, \":\")\n\t}\n\tw.writeRaw(header.start.name.Local, \">\")\n}\n\nfunc (w *writer) writeAttributes(header *c14n, root bool) {\n\tif root {\n\t\tns := make([]attr, 0, len(header.ns))\n\t\tfor prefix, uri := range header.ns {\n\t\t\tvar decl attr\n\t\t\tif prefix == \"\" {\n\t\t\t\tdecl.Name.Local = xmlns\n\t\t\t} else {\n\t\t\t\tdecl.nsprefix = xmlns\n\t\t\t\tdecl.Name.Local = prefix\n\t\t\t}\n\t\t\tdecl.Value = uri\n\t\t\tns = append(ns, decl)\n\t\t\tw.ns.set(prefix, uri)\n\t\t}\n\t\tsortAttr(ns)\n\t\tfor _, decl := range ns {\n\t\t\tw.writeAttribute(decl)\n\t\t}\n\t}\n\tsortAttr(header.start.attr) // Can do in-place, order only matters for c14n.\n\tfor _, attr := range header.start.attr {\n\t\tif attr.namespace() {\n\t\t\tvar prefix string\n\t\t\tif attr.nsprefix == xmlns {\n\t\t\t\tprefix = attr.Name.Local\n\t\t\t}\n\t\t\tif uri, ok := w.ns.get(prefix); ok && uri == attr.Value {\n\t\t\t\tcontinue // Skip superfluous declarations.\n\t\t\t}\n\t\t\tif root { // Should never reach if root.\n\t\t\t\tpanic(fmt.Sprintf(\"XML namespace map does not \"+\n\t\t\t\t\t\"include %q declared by root element\", prefix))\n\t\t\t}\n\t\t\tw.ns.set(prefix, attr.Value)\n\t\t}\n\t\tw.writeAttribute(attr)\n\t}\n}\n\nfunc (w *writer) writeAttribute(a attr) {\n\tw.writeRaw(\" \")\n\tif a.nsprefix != \"\" {\n\t\tw.writeRaw(a.nsprefix, \":\")\n\t}\n\tw.writeRaw(a.Name.Local, `=\"`)\n\tw.writeString(a.Value, true)\n\tw.writeRaw(`\"`)\n}\n\nfunc (w *writer) writeSubelements(v reflect.Value, header *c14n) {\n\tt := v.Type()\n\ti := 1                        // Skip first field.\n\tfor ; i < v.NumField(); i++ { // Find first non-attribute field.\n\t\tif _, attr, _, _ := xmlxAttribute(t.Field(i)); !attr {\n\t\t\tbreak\n\t\t}\n\t}\n\tvar subfields bool\n\tfor ws := 0; i < v.NumField(); i++ {\n\t\tfield := v.Field(i)\n\t\tftype := t.Field(i)\n\t\tif !subfields { // First field after attributes.\n\t\t\tsubfields = true\n\t\t\tif xmlxCharacterData(ftype) {\n\t\t\t\tw.writeString(field.String(), false)\n\t\t\t\tif v.NumField() > i+1 {\n\t\t\t\t\tpanic(fmt.Sprint(t, \" must not have \",\n\t\t\t\t\t\t`sub-elements along with character data`))\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tw.writeRaw(header.whitespace[ws])\n\t\t\tws++\n\t\t}\n\t\toptional := xmlxSubelement(ftype)\n\n\t\tif field.Kind() == reflect.Struct {\n\t\t\t// Skip optional field if it is not present.\n\t\t\tif optional && !c14nHeader(field).isPresent() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tw.write(field, false)\n\t\t\tw.writeRaw(header.whitespace[ws])\n\t\t\tws++\n\t\t\tcontinue\n\t\t}\n\n\t\t// []struct\n\t\tif optional && field.IsNil() {\n\t\t\tcontinue\n\t\t}\n\t\tfor j := 0; j < field.Len(); j++ {\n\t\t\tw.write(field.Index(j), false)\n\t\t\tw.writeRaw(header.whitespace[ws])\n\t\t\tws++\n\t\t}\n\t}\n\tif !subfields {\n\t\t// Even without subfields there can be internal whitespace.\n\t\tw.writeRaw(header.whitespace[0])\n\t}\n}\n\nvar (\n\tescapeAttr = map[byte]string{\n\t\t'\\t': \"&#x9;\",\n\t\t'\\n': \"&#xA;\",\n\t\t'\\r': \"&#xD;\",\n\t\t'\"':  \"&quot;\",\n\t\t'&':  \"&amp;\",\n\t\t'<':  \"&lt;\",\n\t}\n\tescapeText = map[byte]string{\n\t\t'\\r': \"&#xD;\",\n\t\t'&':  \"&amp;\",\n\t\t'<':  \"&lt;\",\n\t\t'>':  \"&gt;\",\n\t}\n)\n\nfunc (w *writer) writeString(s string, attr bool) {\n\t// Cannot use encoding/xml.EscapeText since it does not provide\n\t// canonical output and does not consider context for what to escape.\n\tesc := escapeText\n\tif attr {\n\t\tesc = escapeAttr\n\t}\n\t// Iterate over bytes instead of runes to avoid decode-encode overhead.\n\tvar r int\n\tfor i := 0; i < len(s); i++ {\n\t\tif e, ok := esc[s[i]]; ok {\n\t\t\tw.writeRaw(s[r:i], e)\n\t\t\tr = i + 1\n\t\t}\n\t}\n\tw.writeRaw(s[r:])\n}\n\nfunc (w *writer) writeRaw(str ...string) {\n\t// Avoid fmt.Fprint which needs to use intermediate buffers.\n\tfor _, s := range str {\n\t\tw.b.WriteString(s)\n\t}\n}\n\nfunc sortAttr(attrs []attr) {\n\tfor i := 1; i < len(attrs); i++ {\n\t\tinsert := attrs[i]\n\t\tj := i\n\t\tfor ; j > 0 && lessAttr(insert, attrs[j-1]); j-- {\n\t\t\tattrs[j] = attrs[j-1]\n\t\t}\n\t\tattrs[j] = insert\n\t}\n}\n\n// lessAttr return true if a should come before b in canonical order.\nfunc lessAttr(a, b attr) bool {\n\t// Default namespace declarations come first.\n\tif a.nsprefix == \"\" && a.Name.Local == xmlns {\n\t\treturn true\n\t}\n\tif b.nsprefix == \"\" && b.Name.Local == xmlns {\n\t\treturn false\n\t}\n\n\t// Then come namespace declaration sorted by local name.\n\tif a.nsprefix == xmlns {\n\t\treturn b.nsprefix != xmlns || a.Name.Local < b.Name.Local\n\t}\n\tif b.nsprefix == xmlns {\n\t\treturn false\n\t}\n\n\t// Remaining attributes are first sorted by namespace URI (not the\n\t// namespace binding nsprefix!) and secondly by local name.\n\treturn a.Name.Space < b.Name.Space || a.Name.Local < b.Name.Local\n}\n\nfunc writeXML(v interface{}, b *bytes.Buffer) {\n\t(&writer{b: b}).write(valueOfPtr(v).Elem(), true)\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/c14n_test.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestWriteXML(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\telement  interface{}\n\t\texpected string\n\t}{\n\t\t{\n\t\t\t\"simple\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"default foo,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns:         make(map[string]string),\n\t\t\t\tstart:      startElement{name: xml.Name{Space: \"default\", Local: \"foo\"}},\n\t\t\t\twhitespace: []string{\"\"},\n\t\t\t}}, \"<foo></foo>\",\n\t\t},\n\t\t{\n\t\t\t\"prefix\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns: make(map[string]string),\n\t\t\t\tstart: startElement{\n\t\t\t\t\tnsprefix: \"foo\",\n\t\t\t\t\tname:     xml.Name{Space: \"foo\", Local: \"bar\"},\n\t\t\t\t},\n\t\t\t\twhitespace: []string{\"\"},\n\t\t\t}}, \"<foo:bar></foo:bar>\",\n\t\t},\n\t\t{\n\t\t\t\"internal whitespace\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns: make(map[string]string),\n\t\t\t\tstart: startElement{\n\t\t\t\t\tnsprefix: \"foo\",\n\t\t\t\t\tname:     xml.Name{Space: \"foo\", Local: \"bar\"},\n\t\t\t\t},\n\t\t\t\twhitespace: []string{\"\\n\"},\n\t\t\t}}, \"<foo:bar>\\n</foo:bar>\",\n\t\t},\n\t\t{\n\t\t\t\"root namespace\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"namespace bar,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns: map[string]string{\n\t\t\t\t\t\"foo\": \"namespace\",\n\t\t\t\t\t\"\":    \"default\",\n\t\t\t\t},\n\t\t\t\tstart: startElement{\n\t\t\t\t\tnsprefix: \"foo\",\n\t\t\t\t\tname:     xml.Name{Space: \"namespace\", Local: \"bar\"},\n\t\t\t\t},\n\t\t\t\twhitespace: []string{\"\"},\n\t\t\t}}, `<foo:bar xmlns=\"default\" xmlns:foo=\"namespace\"></foo:bar>`,\n\t\t},\n\t\t{\n\t\t\t\"attributes\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"default foo,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns: map[string]string{\n\t\t\t\t\t\"\":  \"default\",\n\t\t\t\t\t\"a\": \"ns2\",\n\t\t\t\t\t\"b\": \"ns1\",\n\t\t\t\t},\n\t\t\t\tstart: startElement{\n\t\t\t\t\tname: xml.Name{Space: \"default\", Local: \"foo\"},\n\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"a\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"ns2\",\n\t\t\t\t\t\t\t\t\tLocal: \"attr\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"fifth\\t\\n\\r\\\"&<>\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"b\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"ns1\",\n\t\t\t\t\t\t\t\t\tLocal: \"attr2\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"fourth\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"b\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"ns1\",\n\t\t\t\t\t\t\t\t\tLocal: \"attr\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"third\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName:  xml.Name{Local: \"attr\"},\n\t\t\t\t\t\t\t\tValue: \"first\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName:  xml.Name{Local: \"attr2\"},\n\t\t\t\t\t\t\t\tValue: \"second\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twhitespace: []string{\"\"},\n\t\t\t}}, `<foo xmlns=\"default\" xmlns:a=\"ns2\" xmlns:b=\"ns1\" ` +\n\t\t\t\t`attr=\"first\" attr2=\"second\" ` +\n\t\t\t\t`b:attr=\"third\" b:attr2=\"fourth\" ` +\n\t\t\t\t`a:attr=\"fifth&#x9;&#xA;&#xD;&quot;&amp;&lt;>\"></foo>`,\n\t\t},\n\t\t{\n\t\t\t\"character data\", &struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"default foo,c14nroot\"`\n\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{\n\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\tValue: \" character data\\t\\n\\r\\\"&<> \",\n\t\t\t}, \"<foo> character data\\t\\n&#xD;\\\"&amp;&lt;&gt; </foo>\",\n\t\t},\n\t\t{\n\t\t\t\"sub-elements\", &struct {\n\t\t\t\tXMLElement c14n `xmlx:\"default foo,c14nroot\"`\n\t\t\t\tStruct     struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default struct\"`\n\t\t\t\t}\n\t\t\t\tSlice []struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"default slice\"`\n\t\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\t}\n\t\t\t\tOptionalMissing struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default optional-m\"`\n\t\t\t\t} `xmlx:\",optional\"`\n\t\t\t\tOptionalPresent struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default optional-p\"`\n\t\t\t\t} `xmlx:\",optional\"`\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{\n\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t}},\n\t\t\t\t\twhitespace: []string{\n\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t\"\\n\\t\\t\\t\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStruct: struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default struct\"`\n\t\t\t\t}{c14n{\n\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\tLocal: \"struct\",\n\t\t\t\t\t}},\n\t\t\t\t\twhitespace: []string{\"\"},\n\t\t\t\t}},\n\t\t\t\tSlice: []struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"default slice\"`\n\t\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLElement: c14n{\n\t\t\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\t\t\tLocal: \"slice\",\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tValue: \"first\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLElement: c14n{\n\t\t\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\t\t\tLocal: \"slice\",\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tValue: \"second\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tOptionalPresent: struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default optional-p\"`\n\t\t\t\t}{c14n{\n\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\tLocal: \"optional-p\",\n\t\t\t\t\t}},\n\t\t\t\t\twhitespace: []string{\"\"},\n\t\t\t\t}},\n\t\t\t}, `<foo>\n\t\t\t\t<struct></struct>\n\t\t\t\t<slice>first</slice>\n\t\t\t\t<slice>second</slice>\n\t\t\t\t<optional-p></optional-p>\n\t\t\t</foo>`,\n\t\t},\n\t\t{\n\t\t\t\"superfluous namespaces\", &struct {\n\t\t\t\tXMLElement   c14n `xmlx:\"default root,c14nroot\"`\n\t\t\t\tOverwriteFoo struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default overwrite-foo\"`\n\t\t\t\t\tRestoreFoo struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default restore-foo\"`\n\t\t\t\t\t}\n\t\t\t\t\tOverwriteBar struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default overwrite-bar\"`\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{\n\t\t\t\t\tns: map[string]string{\n\t\t\t\t\t\t\"foo\": \"root-foo\",\n\t\t\t\t\t\t\"bar\": \"root-bar\",\n\t\t\t\t\t},\n\t\t\t\t\tstart: startElement{name: xml.Name{\n\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\tLocal: \"root\",\n\t\t\t\t\t}},\n\t\t\t\t\twhitespace: []string{\n\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t\"\\n\\t\\t\\t\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tOverwriteFoo: struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"default overwrite-foo\"`\n\t\t\t\t\tRestoreFoo struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default restore-foo\"`\n\t\t\t\t\t}\n\t\t\t\t\tOverwriteBar struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default overwrite-bar\"`\n\t\t\t\t\t}\n\t\t\t\t}{\n\t\t\t\t\tXMLElement: c14n{\n\t\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\t\t\tLocal: \"overwrite-foo\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"overwritten-foo\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhitespace: []string{\n\t\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\",\n\t\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\",\n\t\t\t\t\t\t\t\"\\n\\t\\t\\t\\t\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tRestoreFoo: struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default restore-foo\"`\n\t\t\t\t\t}{c14n{\n\t\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\t\t\tLocal: \"restore-foo\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"root-foo\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"baz\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"new-baz\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhitespace: []string{\"\\n\\t\\t\\t\\t\\t\"},\n\t\t\t\t\t}},\n\t\t\t\t\tOverwriteBar: struct {\n\t\t\t\t\t\tXMLElement c14n `xmlx:\"default overwrite-bar\"`\n\t\t\t\t\t}{c14n{\n\t\t\t\t\t\tns: make(map[string]string),\n\t\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\t\tSpace: \"default\",\n\t\t\t\t\t\t\t\tLocal: \"overwrite-bar\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"overwritten-foo\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"bar\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"overwritten-bar\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tnsprefix: xmlns,\n\t\t\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\t\t\tLocal: \"baz\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tValue: \"\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhitespace: []string{\"\\n\\t\\t\\t\\t\\t\"},\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t}, `<root xmlns:bar=\"root-bar\" xmlns:foo=\"root-foo\">\n\t\t\t\t<overwrite-foo xmlns:foo=\"overwritten-foo\">\n\t\t\t\t\t<restore-foo xmlns:baz=\"new-baz\" xmlns:foo=\"root-foo\">\n\t\t\t\t\t</restore-foo>\n\t\t\t\t\t<overwrite-bar xmlns:bar=\"overwritten-bar\" xmlns:baz=\"\">\n\t\t\t\t\t</overwrite-bar>\n\t\t\t\t</overwrite-foo>\n\t\t\t</root>`,\n\t\t},\n\t}\n\n\tvar buf bytes.Buffer\n\tfor _, test := range tests {\n\t\tbuf.Reset()\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\twriteXML(test.element, &buf)\n\t\t\tif xml1 := buf.String(); xml1 != test.expected {\n\t\t\t\tt.Errorf(\"unexpected result: %q; want %q\", xml1, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteXMLSignedInfo(t *testing.T) {\n\tvar buf bytes.Buffer\n\tfor _, test := range []string{\"EID\", \"MID\", \"Compact\"} {\n\t\tbuf.Reset()\n\t\tt.Run(test, func(t *testing.T) {\n\t\t\tprec14n := fmt.Sprint(\"signatures0\", test, \".xml\")\n\t\t\txml1, err := os.ReadFile(filepath.Join(\"testdata\", prec14n))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tpostc14n := fmt.Sprint(\"canonicalSignedInfo\", test)\n\t\t\texpected, err := os.ReadFile(filepath.Join(\"testdata\", postc14n))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tvar signatures xadesSignatures\n\t\t\tif err := parseXML(xml1, &signatures); err != nil {\n\t\t\t\tt.Fatal(\"unexpected error parsing signatures:\", err)\n\t\t\t}\n\t\t\twriteXML(&signatures.Signature.SignedInfo, &buf)\n\n\t\t\tif !bytes.Equal(buf.Bytes(), expected) {\n\t\t\t\tt.Error(\"canonicalized \", prec14n, \" does not match \", postc14n)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/log_desc.go",
    "content": "package bdoc\n\nconst (\n\t_CONTAINER_READER_TO_READER_AT      = \"Cannot convert from io.Reader to io.ReaderAt\"\n\t_CONTAINER_ASICE_MAGIC              = \"Failed to read ASiCE magic number\"\n\t_CONTAER_ZIP_READER                 = \"Failed to read zip container\"\n\t_CONTAINER_DUP                      = \"Duplicate file found in a zip container\"\n\t_CONTAINER_ASICE_CENTRAL_MAGIC      = \"We have checked that the first file in the archive was a correct 'mimetype' using the local file header, now check that it is also first in the central directory and that the directory header matches\"\n\t_CONTAINER_ZIP_MANIFEST             = \"Failed to unzip manifest.xml\"\n\t_CONTAINER_META                     = \"META-INF/ directory contains unknown file\"\n\t_CONTAINER_DIRS                     = \"Container contains directories, but shouldn't\"\n\t_CONTAINER_UNZIP_FILE               = \"Unzipping file failed\"\n\t_CONTAINER_EOCD                     = \"Failed to locate .ZIP end of central directory\"\n\t_CONTAINER_MALFORMED_ZIP            = \".ZIP has appended bytes at the end\"\n\t_CONTAINER_BDOC_MANI                = \"No manifest.xml file found in a .bdoc container\"\n\t_CONTAINER_BDOC_SIG                 = \"No signature*.xml file found in a .bdoc container\"\n\t_CONTAINER_BDOC_DATA                = \"No data in a .bdoc container\"\n\t_CONTAINER_BDOC_MANI_READ           = \"Reading manifest.xml failed\"\n\t_CONTAINER_ZIP_MAGIC                = \"Failed to read ZIP magic number\"\n\t_CONTAINER_ZIP_MAGIC_LEN            = \"ZIP magic incorrect length\"\n\t_CONTAINER_ASICE_MAGIC_FORMAT       = \"Not an ASiCE magic\"\n\t_CONTAINER_ZIP_MIME                 = \"Invalid compressed MIME type\"\n\t_CONTAINER_ZIP_EXTRA                = \"No extra data allowed in a .bdoc container\"\n\t_CONTAINER_ZIP_MIME_LEN             = \"Invalid MIME type length\"\n\t_CONTAINER_ASICE_MIME               = \"Not an ASiCE MIME type\"\n\tCONTAINER_MIME                      = \"File name is not 'mimetype'\"\n\t_CONTAINER_MIME_OFF                 = \"Offset of mimetype file is wrong in a .bdoc container\"\n\t_CONTAINER_MIME_C_OFF               = \"Central offset of mimetype type is wrong in a .bdoc container\"\n\t_CONTAINER_HEADER_COMPRESS          = \"Checking directory header compression failed\"\n\t_CONTAINER_MIME_CONTENT             = \"'mimetype' file content is not 'application/vnd.etsi.asic-e+zip'\"\n\t_CONTAINER_MIME_CONTENT_EXT         = \"Extra data found in 'mimetype' file\"\n\t_CONTAINER_ZIP_OPEN                 = \"Failed to open zipped file\"\n\t_CONTAINER_ZIP_READ                 = \"Failed to read zipped file\"\n\t_CONTAINER_ZIP_DIFF                 = \"Uncompressed file size differs from a declared in a .zip container manifest.xml\"\n\t_CONTAINER_ZIP_XML                  = \"XML parse manifest.xml error\"\n\t_CONTAINER_ZIP_XML_MANY             = \"Multiple manifest.xml found\"\n\t_CONTAINER_MIME_ROOT                = \"manifest.xml should have a root MIME type\"\n\t_CONTAINER_FILE_FROM_MANI           = \"Reading ZIP container files list from a manifest.xml failed\"\n\t_CONTAINER_SIG_FROM_MANI            = \"Reading ZIP container signatures list from a manifest.xml failed\"\n\t_CONTAINER_SIG_FILE_MANI            = \"No signature for a given file found in a manifest.xml\"\n\t_CONTAINER_BDOC_CFG                 = \"Failed to read YAML configuration for .bdoc container opener\"\n\t_CONTAINER_BDOC_NEW                 = \"Failed to create .bdoc container opener\"\n\t_CONTAINER_BDOC_OPEN                = \"Failed to open .bdoc container\"\n\t_CONTAINER_BDOC_FILE_COUNT          = \".bdoc files count <= 0\"\n\t_CONTAINER_BDOC_SIZE                = \".bdoc container size <= 0\"\n\t_CONTAINER_BDOC_DATA_SIZE           = \".bdoc container data size <= 0\"\n\t_CONTAINER_BDOC_CA                  = \"No CA certificates provided that should verify voter .bdoc container intermediate CA certificates\"\n\t_CONTAINER_BDOC_CA_PARSE            = \"Failed to parse CA certificates\"\n\t_CONTAINER_BDOC_ICA_PARSE           = \"Failed to parse intermediate CA certificates\"\n\t_CONTAINER_BDOC_TSA                 = \"Failed to create a TSA provider's client\"\n\t_CONTAINER_BDOC_OCSP                = \"Failed to create a OCSP provider's client\"\n\t_CONTAINER_BDOC_PROFILE             = \"Unknown .bdoc profile\"\n\t_CONTAINER_BDOC_XADES_SIG_TIME      = \".bdoc container xades signature not in RFC3339 format\"\n\t_CONTAINER_BDOC_XADES_SIG           = \"Failed to parse .bdoc container xades signature\"\n\t_CONTAINER_BDOC_CERT_SIG            = \"Failed to parse certificate of a voter from .bdoc container\"\n\t_CONTAINER_BDOC_CERT_SIG_VERIFY     = \"Failed to verify certificate of a voter from .bdoc container\"\n\t_CONTAINER_BDOC_SIG_B64             = \"Failed to base-64 decode signature\"\n\t_CONTAINER_BDOC_CERT_OCSP           = \"Failed to check voter certificate against OCSP provider\"\n\t_CONTAINER_BDOC_OCSP_ID             = \"No OCSP response found for this signature\"\n\t_CONTAINER_BDOC_TSA_ID              = \"No TSA response found for this signature\"\n\t_CONTAINER_BDOC_Q                   = \"QualifyingProperties in XML signature doesn't target this signature ID\"\n\t_CONTAINER_BDOC_TS                  = \"OCSP producedAt > (TSA timestamp + TSA delay)\"\n\t_CONTAINER_BDOC_SIG_ALG             = \"Unsupported signing algorithm\"\n\t_CONTAINER_BDOC_SIG_CALG            = \"Unsupported canonicalized signing algorithm\"\n\t_CONTAINER_BDOC_XML2ASN1            = \"Failed to transform signature from XML to ASN.1 format\"\n\t_CONTAINER_BDOC_SIG_VERIFY          = \"Failed to verify signature\"\n\t_CONTAINER_BDOC_REF                 = \"Invalid signed properties reference type\"\n\t_CONTAINER_BDOC_SIG_PROP_CALG       = \"Unsupported signed properties canonicalization algorithm\"\n\t_CONTAINER_BDOC_SIG_PROP_DIG        = \"Signed properties reference digest error\"\n\t_CONTAINER_BDOC_FILE_REF            = \"File reference with transforms\"\n\t_CONTAINER_BDOC_FILE_DIG            = \"File reference digest error\"\n\t_CONTAINER_BDOC_DO                  = \"Data object format mismatches MIME type\"\n\t_CONTAINER_BDOC_N_REF               = \"References to unprocessed files exist\"\n\t_CONTAINER_BDOC_DO_EX               = \"Extra data object formats error\"\n\t_CONTAINER_BDOC_REF_URI             = \"Reference URI unescape error\"\n\t_CONTAINER_BDOC_REF_N_URI           = \"No reference with URI found\"\n\t_CONTAINER_BDOC_REF_NO              = \"No data object format with reference found\"\n\t_CONTAINER_BDOC_SIG_POLICY          = \"Unexpected signature policy identifier\"\n\t_CONTAINER_BDOC_SIG_PARSE           = \"Signing certificate digest error\"\n\t_CONTAINER_BDOC_SIG_SER             = \"Checking signing certificate serial failed\"\n\t_CONTAINER_BDOC_CERT_ISS            = \"Parsing issuer from certificate failed\"\n\t_CONTAINER_BDOC_CERT_ISS_VERIFY     = \"Failed to verify issuer from certificate\"\n\t_CONTAINER_BDOC_CERT_N_OCSP         = \"No OCSP response found in a certificate\"\n\t_CONTAINER_BDOC_CERT_OCSP_B64       = \"Failed to base-64 decode OCSP response\"\n\t_CONTAINER_BDOC_CERT_OCSP_VERIFY    = \"OCSP response verification failed\"\n\t_CONTAINER_BDOC_CERT_OCSP_BAD       = \"OCSP response status is not good\"\n\t_CONTAINER_BDOC_CERT_TSA_CALG       = \"Unsupported TSA canonicalization algorithm\"\n\t_CONTAINER_BDOC_CERT_N_TSA          = \"No TSA response found in a certificate\"\n\t_CONTAINER_BDOC_CERT_TSA_B64        = \"Failed to base-64 decode TSA response\"\n\t_CONTAINER_BDOC_CERT_VALIDATE       = \"Failed to validate TSA response\"\n\t_CONTAINER_DIG_ALG                  = \"Unsupported digest algorithm\"\n\t_CONTAINER_DIG_ALG_B64              = \"base-64 decoding digest failed\"\n\t_CONTAINER_DIG_ALG_VERIFY           = \"Digest verification failed\"\n\t_CONTAINER_KEY_USAGE_CONTENT_COMMIT = \"Not a non repudiation certificate\"\n\t_CONTAINER_READER                   = \"Reading into in-memory io.ReaderAt failed\"\n\t_CONTAINER_READER_REWIND            = \"Rewind io.ReaderAt pointer at current position failed\"\n\t_CONTAINER_READER_REWIND_END        = \"Rewind io.ReaderAt pointer at end position failed\"\n\t_CONTAINER_READER_REWIND_START      = \"Rewind io.ReaderAt pointer at start position failed\"\n\t_CONTAINER_READER_BUF               = \"Cannot read io.ReaderAt buffer size\"\n\t_CONTAINER_READER_BUF_LIM           = \"io.ReaderAt buffer size exceeds limit\"\n\t_CONTAINER_XML_EOF                  = \"XML current token is EOF, but still has more data to read\"\n\t_CONTAINER_XML_TOKEN_PARSE          = \"Failed to parse XML token\"\n\t_CONTAINER_XML_ATTR_DUP             = \"Duplicate XML attributes found\"\n\t_CONTAINER_XML_ATTR_EMPTY           = \"Empty XML attribute found\"\n\t_CONTAINER_XML_NS                   = \"Undeclared XML namespace prefix error\"\n\t_CONTAINER_XML_NS_EMPTY             = \"Empty XML namespace prefix\"\n\t_CONTAINER_XML_NS_INNER             = \"No inner xmlns namespace allowed\"\n\t_CONTAINER_XML_NS_UNDEC             = \"Undeclared namespace found\"\n\t_CONTAINER_XML_NS_COLON             = \"Namespace or prefix contain semicolon\"\n\t_CONTAINER_XML_END                  = \"Unexpected XML end element\"\n\t_CONTAINER_XML_TAGS                 = \"Mismatching XML element tags\"\n\t_CONTAINER_XML_START_TOKEN          = \"Parse XML start token error\"\n\t_CONTAINER_XML_START_TOKEN_FAIL     = \"Parsed XML start token, but found non-start element\"\n\t_CONTAINER_XML_TOKEN                = \"Parsed XML token, but found unexpected token\"\n\t_CONTAINER_XML_END_TOKEN            = \"Parse XML end token error\"\n\t_CONTAINER_XML_TOKEN_TRAIL_EL       = \"Parsed XML token, but found trailing element\"\n\t_CONTAINER_XML_TOKEN_TRAIL          = \"Parsed XML token, but found trailing token\"\n\t_CONTAINER_XML_TOKEN_ATTR           = \"Parsed XML token, but found missing attribute\"\n\t_CONTAINER_XML_TOKEN_ATTR_UNIQ      = \"Parse XML token, but found non-unique attribute\"\n\t_CONTAINER_XML_TOKEN_ATTR_EX        = \"Parse extra XML attribute error\"\n\t_CONTAINER_XML_TOKEN_CHAR_DATA      = \"Parse XML character data token error\"\n\t_CONTAINER_XML_EL_STRUCT            = \"Failed to parse XML struct element\"\n\t_CONTAINER_XML_EL_SLICE             = \"Failed to parse XML slice element\"\n)\n"
  },
  {
    "path": "common/collector/container/bdoc/mapstack.go",
    "content": "package bdoc\n\n// mapstack is a stack of string to string maps. Modifications to the map are\n// made in layers and can be removedd as one. Lookup is performed recursively\n// through all layers.\ntype mapstack []map[string]string\n\n// push adds a new empty layer to the stack.\nfunc (s *mapstack) push() {\n\t*s = append(*s, nil)\n}\n\n// set sets a key-value in the top-most layer. Panics if there are no layers.\nfunc (s *mapstack) set(key, value string) {\n\tds := *s // Dereferenced copy to simplify following code.\n\tm := ds[len(ds)-1]\n\tif m == nil {\n\t\tm = make(map[string]string)\n\t\t(*s)[len(ds)-1] = m\n\t}\n\tm[key] = value\n}\n\n// pop removes the top-most layer. Panics if there are no layers.\nfunc (s *mapstack) pop() {\n\t*s = (*s)[:len(*s)-1]\n}\n\n// get gets the value of key, looking from the top-most layer down.\nfunc (s *mapstack) get(key string) (string, bool) {\n\tfor i := len(*s) - 1; i >= 0; i-- {\n\t\tif value, ok := (*s)[i][key]; ok {\n\t\t\treturn value, ok\n\t\t}\n\t}\n\treturn \"\", false\n}\n\n// flatten returns a flat map with all key-values currently in the stack.\nfunc (s *mapstack) flatten() map[string]string {\n\tflat := make(map[string]string)\n\tfor _, m := range *s {\n\t\tfor key, value := range m {\n\t\t\tflat[key] = value\n\t\t}\n\t}\n\treturn flat\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/pool.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n)\n\n// Shared pool of buffers used by other functions in this package. Great care\n// must be taken to not retain any references to the buffer after releasing it\n// for reuse, especially to any byte slices returned by Buffer.Bytes().\nvar buffers = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn new(bytes.Buffer)\n\t},\n}\n\nfunc buffer() *bytes.Buffer {\n\treturn buffers.Get().(*bytes.Buffer)\n}\n\nfunc release(buf *bytes.Buffer) {\n\tif buf != nil {\n\t\tbuf.Reset()\n\t\tbuffers.Put(buf)\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/reader.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\n// readAtCloser is a wrapper around the io.ReaderAt returned by toReaderAt\n// which on close releases any resources allocated by it.\ntype readAtCloser struct {\n\tio.ReaderAt\n\tbuffer *bytes.Buffer\n}\n\nfunc (ratc *readAtCloser) close() {\n\trelease(ratc.buffer)\n\tratc.buffer = nil\n\tratc.ReaderAt = nil // Reader references ratc.buffer.Bytes() so set nil.\n}\n\n// toReaderAt transforms an io.Reader into an io.ReaderAt. This is achieved by\n// first checking whether the io.Reader already implements io.ReaderAt. If not,\n// r is read until EOF or the safe limit is reached and the read data is\n// returned in a bytes.Reader.\n//\n// toReaderAt also finds the size of the data to be read, either by calling\n// Size or Seek on r or again just by reading until EOF or the safe limit is\n// reached. An error is returned if the size is greater than limit.\n//\n// The resulting io.ReaderAt is wrapped in a readAtCloser, whose Close method\n// frees any resources acquired during this process. Therefore the Close method\n// of ratc MUST be called when it is no longer used.\nfunc toReaderAt(r io.Reader, limit int64) (ratc *readAtCloser, size int64, err error) {\n\t// Convert r into an io.ReaderAt.\n\tvar ok bool\n\tratc = new(readAtCloser)\n\tratc.ReaderAt, ok = r.(io.ReaderAt)\n\tif !ok {\n\t\t// r is not an io.ReaderAt: read everything into memory and\n\t\t// wrap in a bytes.Reader.\n\t\tratc.buffer = buffer()\n\t\tif size, err = ratc.buffer.ReadFrom(safereader.New(r, limit)); err != nil {\n\t\t\tratc.close()\n\t\t\terr = ReaderAtReadBufferError{Err: err, Description: _CONTAINER_READER}\n\t\t\treturn\n\t\t}\n\t\tratc.ReaderAt = bytes.NewReader(ratc.buffer.Bytes())\n\t\treturn // We have both ratc and size.\n\t}\n\n\t// Determine the size of ratc.ReaderAt.\n\ttype sizer interface {\n\t\tSize() int64\n\t}\n\n\tswitch s := ratc.ReaderAt.(type) {\n\tcase sizer:\n\t\tsize = s.Size()\n\tcase io.Seeker:\n\t\t// Seek to end to get length and return to current location.\n\t\tvar current int64\n\t\tif current, err = s.Seek(0, io.SeekCurrent); err != nil {\n\t\t\terr = ReaderAtSeekCurrentError{Err: err,\n\t\t\t\tDescription: _CONTAINER_READER_REWIND}\n\t\t\treturn\n\t\t}\n\t\tif size, err = s.Seek(0, io.SeekEnd); err != nil {\n\t\t\terr = ReaderAtSeekEndError{Err: err,\n\t\t\t\tDescription: _CONTAINER_READER_REWIND_END}\n\t\t\treturn\n\t\t}\n\t\tif _, err = s.Seek(current, io.SeekStart); err != nil {\n\t\t\terr = ReaderAtSeekStartError{Err: err,\n\t\t\t\tDescription: _CONTAINER_READER_REWIND_START}\n\t\t\treturn\n\t\t}\n\tdefault:\n\t\t// As a fallback, just read everything into memory, determine\n\t\t// the size, and discard the read data. This alters the read\n\t\t// offset, but ReadAt does not depend on it anyway.\n\t\tbuf := buffer()\n\t\tdefer release(buf)\n\t\tif size, err = buf.ReadFrom(safereader.New(r, limit)); err != nil {\n\t\t\terr = ReaderAtReadSizeError{Err: err, Description: _CONTAINER_READER_BUF}\n\t\t\treturn\n\t\t}\n\t}\n\n\tif size > limit {\n\t\terr = ReaderAtSizeError{Size: size, Limit: limit, Description: _CONTAINER_READER_BUF_LIM}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/reader_test.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n)\n\n// testReader only implements io.Reader.\ntype testReader bytes.Reader\n\nfunc newReader(data []byte) *testReader {\n\treturn (*testReader)(bytes.NewReader(data))\n}\n\nfunc (r *testReader) Read(p []byte) (int, error) {\n\treturn (*bytes.Reader)(r).Read(p)\n}\n\n// testReaderAt only implements io.Reader and io.ReaderAt.\ntype testReaderAt struct{ *testReader }\n\nfunc newReaderAt(data []byte) testReaderAt {\n\treturn testReaderAt{newReader(data)}\n}\n\nfunc (r *testReaderAt) ReadAt(p []byte, off int64) (int, error) {\n\treturn (*bytes.Reader)(r.testReader).ReadAt(p, off)\n}\n\n// testReadAtSeeker only implements io.Reader, io.ReaderAt and io.Seeker.\ntype testReadAtSeeker struct{ testReaderAt }\n\nfunc (r testReadAtSeeker) Seek(offset int64, whence int) (int64, error) {\n\treturn (*bytes.Reader)(r.testReader).Seek(offset, whence)\n}\n\n// testReadAtSizer only implements io.Reader, io.ReaderAt and sizer.\ntype testReadAtSizer struct{ testReaderAt }\n\nfunc (r testReadAtSizer) Size() int64 {\n\treturn (*bytes.Reader)(r.testReader).Size()\n}\n\nvar data = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n\nfunc TestToReaderAt(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\treader io.Reader\n\t}{\n\t\t{\"Reader\", newReader(data)},\n\t\t{\"ReaderAt\", newReaderAt(data)},\n\t\t{\"ReadAtSeeker\", testReadAtSeeker{newReaderAt(data)}},\n\t\t{\"ReadAtSizer\", testReadAtSizer{newReaderAt(data)}},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tratc, size, err := toReaderAt(test.reader, int64(len(data)))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"toReaderAt error:\", err)\n\t\t\t}\n\t\t\tratc.close()\n\t\t\tif size != int64(len(data)) {\n\t\t\t\tt.Fatalf(\"unexpected size: want %d, got %d\", len(data), size)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/testdata/canonicalSignedInfoCompact",
    "content": "<ds:SignedInfo xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\"><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"></ds:SignatureMethod><ds:Reference Id=\"S0-RefId0\" URI=\"test.txt\"><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod><ds:DigestValue>4nyCFL6LfPW8zHwIJH48sMFRSkjuH2MZf+TvPvUdfm8=</ds:DigestValue></ds:Reference><ds:Reference Id=\"S0-RefId1\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\"><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod><ds:DigestValue>+3pD2CLeOxWobO0U6BhPrpfuA4b4IrdKaQq8Npv+GyY=</ds:DigestValue></ds:Reference></ds:SignedInfo>"
  },
  {
    "path": "common/collector/container/bdoc/testdata/canonicalSignedInfoEID",
    "content": "<ds:SignedInfo xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\">\n      <ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\"></ds:CanonicalizationMethod>\n      <ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"></ds:SignatureMethod>\n      <ds:Reference Id=\"S0-RefId0\" URI=\"test.txt\">\n        <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod>\n        <ds:DigestValue>4nyCFL6LfPW8zHwIJH48sMFRSkjuH2MZf+TvPvUdfm8=\n</ds:DigestValue>\n      </ds:Reference>\n      <ds:Reference Id=\"S0-RefId1\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\">\n        <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod>\n        <ds:DigestValue>+3pD2CLeOxWobO0U6BhPrpfuA4b4IrdKaQq8Npv+GyY=\n</ds:DigestValue>\n      </ds:Reference>\n    </ds:SignedInfo>"
  },
  {
    "path": "common/collector/container/bdoc/testdata/canonicalSignedInfoMID",
    "content": "<ds:SignedInfo xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\" Id=\"S0-SignedInfo\">\n<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\">\n</ds:CanonicalizationMethod>\n<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256\">\n</ds:SignatureMethod>\n<ds:Reference Id=\"S0-ref-0\" URI=\"EE_Certification_Centre_Root_CA.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>IwP0Vozcm1MsmLemnYwpnXjHXTs70rlPhUqteU7ZIa8=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-1\" URI=\"ESTEID-SK_2011.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>5XIpC9qJ8MkgH3tqWNPmU8XsiY9nq6HcNRPmmPShfLY=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-2\" URI=\"ESTEID-SK_2015.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>l7BSx7yM00dFGC5MWsvSdto4FdPvzWP9zQ3/CqZpA2o=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-3\" URI=\"ivxv.properties\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>utNu6sqhFu3VUSamEU1POJGY6CV99FIfJjd/IGJVRFg=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-4\" URI=\"SK_OCSP_RESPONDER_2011.pem.cer\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>jnEAlCTbnYGTpVV2Q4+y5vAlDOA7lBR8EJiM5OKgj7U=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-5\" URI=\"SK_TIMESTAMPING_AUTHORITY.pem.cer\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>FqVY3feXnohBOgCY/43dv3Wd0Rhe1whpB014HCRe8JY=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-sp\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>/mU9ss4AFlBieyjICaNA9EoXnMMeYZ+qUPHnprupIVk=</ds:DigestValue>\n</ds:Reference>\n</ds:SignedInfo>"
  },
  {
    "path": "common/collector/container/bdoc/testdata/confNoCheckTM.yaml",
    "content": "checkTimeMark: false\nroots:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n    GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n    cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n    aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n    xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n    ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n    MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n    VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n    kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n    pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n    A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n    AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n    BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n    I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n    Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n    SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n    tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n    SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n    -----END CERTIFICATE-----\nintermediates:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMwNjA5WhcN\n    MjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n    aXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAx\n    MTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOC\n    AQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu60MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD\n    79o+8r4SRYp9kowSFA8E1v38XXTHRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/H\n    rl9VmikIgAlaHvv3Z+MzS6qeLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+\n    Ky7MZ9jbBr+Nx4py/V7xm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQ\n    xrLjv3B67i19d7sNh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxW\n    adHDvVYUCJgW2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0T\n    AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYB\n    BAHOHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\n    aQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4A\n    ZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYDVR0OBBYE\n    FEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSx\n    ZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9z\n    aXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBd\n    h5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obPmJ7jRax3PX0pFhaUE6EhAR0d\n    gS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0SGj7aFLkcC+ULwu1k6yiortFJ0Ds\n    49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cHUijnf39Szq7E2z7MDfZkYg8HZeHrO493\n    EFghXcnSH7J7z47cgP3GWFNUKv1V2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+T\n    orEjxz//WzcF/Sklp4FeD0MU39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu6\n    3kW7RDqr+wQFSxKr9DIH\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\n    MjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n    aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\n    A1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\n    ggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n    8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\n    z8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n    5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\n    zs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n    46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n    3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\n    J7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\n    h5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\n    ot3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n    6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\n    AAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\n    HQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\n    IARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\n    ay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\n    AQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\n    AAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\n    BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\n    fTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\n    AQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\n    aWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\n    Mmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\n    Y3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\n    AH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\n    DgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\n    yPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\n    WFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\n    PKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n    -----END CERTIFICATE-----\nocsp:\n  url: http://demo.sk.ee/ocsp\n  responders:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEijCCA3KgAwIBAgIQaI8x6BnacYdNdNwlYnn/mzANBgkqhkiG9w0BAQUFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMyMjQ1WhcN\n      MjQwOTA3MTIyMjQ1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n      Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n      b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n      LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cw6Cja17BbYbHi6\n      frwccDI4BIQLk/fiCE8L45os0xhPgEGR+EHE8LPCIqofPgf4gwN1vDE6cQNUlK0O\n      d+Ush39i9Z45esnfpGq+2HsDJaFmFr5+uC1MEz5Kn1TazEvKbRjkGnSQ9BertlGe\n      r2BlU/kqOk5qA5RtJfhT0psc1ixKdPipv59wnf+nHx1+T+fPWndXVZLoDg4t3w8l\n      IvIE/KhOSMlErvBIHIAKV7yH1hOxyeGLghqzMiAn3UeTEOgoOS9URv0C/T5C3mH+\n      Y/uakMSxjNuz41PneimCzbEJZJRiEaMIj8qPAubcbL8GtY03MWmfNtX6/wh6u6TM\n      fW8S2wIDAQABo4H+MIH7MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMJMB0GA1UdDgQW\n      BBR9/5CuRokEgGiqSzYuZGYAogl8TzCBoAYDVR0gBIGYMIGVMIGSBgorBgEEAc4f\n      AwEBMIGDMFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQAaQBt\n      AGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4AZwAu\n      MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LnNrLmVlL2FqYXRlbXBlbC8wHwYDVR0j\n      BBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wDQYJKoZIhvcNAQEFBQADggEBAAba\n      j7kTruTAPHqToye9ZtBdaJ3FZjiKug9/5RjsMwDpOeqFDqCorLd+DBI4tgdu0g4l\n      haI3aVnKdRBkGV18kqp84uU97JRFWQEf6H8hpJ9k/LzAACkP3tD+0ym+md532mV+\n      nRz1Jj+RPLAUk9xYMV7KPczZN1xnl2wZDJwBbQpcSVH1DjlZv3tFLHBLIYTS6qOK\n      4SxStcgRq7KdRczfW6mfXzTCRWM3G9nmDei5Q3+XTED41j8szRWglzYf6zOv4djk\n      ja64WYraQ5zb4x8Xh7qTCk6UupZ7je+0oRfuz0h/3zyRdjcRPkjloSpQp/NG8Rmr\n      cnr874p8d9fdwCrRI7U=\n      -----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/container/bdoc/testdata/signatures0Compact.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?><asic:XAdESSignatures xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\"><ds:Signature Id=\"S0\"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\"/><ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"/><ds:Reference Id=\"S0-RefId0\" URI=\"test.txt\"><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/><ds:DigestValue>4nyCFL6LfPW8zHwIJH48sMFRSkjuH2MZf+TvPvUdfm8=</ds:DigestValue></ds:Reference><ds:Reference Id=\"S0-RefId1\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\"><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/><ds:DigestValue>+3pD2CLeOxWobO0U6BhPrpfuA4b4IrdKaQq8Npv+GyY=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue Id=\"S0-SIG\">rjaH6bQtJspdFAengOnxIjIan1bWBDbTCEWbE7X9/f4EUOyVZd1sDoTnOUART0C25FuXy2JlxAiClEWtKN3t9EBcZy3S2nioEFxsW3KwI+8rOgrxXw0iI1znAqfws1kWA08F0KBEudAOnk8TC5UUTCYPCH2zXFY/cPftP9emTA9KDQmCviCi3x9BiuySZM54ofQdOrb/V0XZe5OG6xkKM7XqVoN252pW2lyEvbHXfQNN98EsnrKHmeBadTXWmfcTjnDqyUv8cEg+r4VSXi5txWsc8hf9HuJ4gAe5RyawIIQ9S3ypigMJ2psa0jr1pnVGrgFQ1ioTBgr1s62f5Iwtnw==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIE/jCCA+agAwIBAgIQKpa4J4PNPChTajHdDM3l1DANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE0MDUwNzEzMTUwOVoXDTE5MDQwNzIwNTk1OVowgZ4xCzAJBgNVBAYTAkVFMQ8wDQYDVQQKDAZFU1RFSUQxGjAYBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMSYwJAYDVQQDDB1Nw4ROTklLLE1BUkktTElJUyw0NzEwMTAxMDAzMzEQMA4GA1UEBAwHTcOETk5JSzESMBAGA1UEKgwJTUFSSS1MSUlTMRQwEgYDVQQFEws0NzEwMTAxMDAzMzCCASMwDQYJKoZIhvcNAQEBBQADggEQADCCAQsCggEBAMAKzkzptfsm2tgu2ZFePsFB9OIUc6U7ytG2izO7gO5RV2SiDzx7RgskUmSRJlU7pBiGq0i8R/Lnf0hr7yu3l13u27EwFARx0LizrUQhlWya7MjXU/RCQwG6ctQagfNzV3M+Ds6340eGu/80VHyV8kUrf3ikzv0tzu3h1O59yFHs0s8yxujTp2J6XWs8s4cIp8XMvl6E6z9OTzN6lnSLm6wqoT5UcC42mwxSajlMRqD0XzPDLKrqt1BAEkpsikI0BEVTLKzKZNtzJktTr+0sKUqHiGfsY8ibhJTl1BNOjPhuTlClD69pjz2H+2RG0q0EDlddfI6lQ2biorC4wmu7r6MCBGx3P9mjggFmMIIBYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIGQDCBmQYDVR0gBIGRMIGOMIGLBgorBgEEAc4fAwEBMH0wWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0AaQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4wIQYIKwYBBQUHAgEWFWh0dHA6Ly93d3cuc2suZWUvY3BzLzAdBgNVHQ4EFgQUfSof04AL6iqi97jvXUdPzhDNAbMwIgYIKwYBBQUHAQMEFjAUMAgGBgQAjkYBATAIBgYEAI5GAQQwHwYDVR0jBBgwFoAUQbb+xbGxtFMTjPr6YtA0bW0iNAowRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lc3RlaWQyMDExLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAQzo6tEBrrnKnTP7MLu2LlIpZ7Gh1zWtqktNM42oyKE5+iUp815h2Bq04N9PrRdfVf+p3T5+UfgcZrTKAp4SagNtJwgy8HZUInvo9XB32lT/hUOudveHvgEm+VBnkD+zXf+HczDliuMglehfSMVQ3sewxYPy1+eU1IQHpZ5HjwamU74Ob0CZbkzbJU3/QvyVdz0kicP4nMFEcLKoZzqKZ9BldV1sgoeaJ4X+wAWVeHHdtz2QWAIyX4B1tMtQpjk/bzpnsxEaFleW0uW+mfECYGLQcEWW2iKWkoBBFcwNM2Ou48CL0t80rQ0ZBq5kTh5CvqF/MSdsvcIwJ+zXmiqt/EQ==</ds:X509Certificate></ds:X509Data></ds:KeyInfo><ds:Object><xades:QualifyingProperties Target=\"#S0\"><xades:SignedProperties Id=\"S0-SignedProperties\"><xades:SignedSignatureProperties><xades:SigningTime>2016-10-27T09:44:26Z</xades:SigningTime><xades:SigningCertificate><xades:Cert><xades:CertDigest><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/><ds:DigestValue>Lm7A+6id6in1RilLAzog9uCgfV1FLVnILhCsFGxSH8E=</ds:DigestValue></xades:CertDigest><xades:IssuerSerial><ds:X509IssuerName>emailAddress=pki@sk.ee,CN=TEST of ESTEID-SK 2011,O=AS Sertifitseerimiskeskus,C=EE</ds:X509IssuerName><ds:X509SerialNumber>56610155445802750935393253012615849428</ds:X509SerialNumber></xades:IssuerSerial></xades:Cert></xades:SigningCertificate><xades:SignaturePolicyIdentifier><xades:SignaturePolicyId><xades:SigPolicyId><xades:Identifier Qualifier=\"OIDAsURN\">urn:oid:1.3.6.1.4.1.10015.1000.3.2.1</xades:Identifier><xades:Description>BDOC – FORMAT FOR DIGITAL SIGNATURES</xades:Description></xades:SigPolicyId><xades:SigPolicyHash><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/><ds:DigestValue>3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs=</ds:DigestValue></xades:SigPolicyHash><xades:SigPolicyQualifiers><xades:SigPolicyQualifier><xades:SPURI>https://www.sk.ee/repository/bdoc-spec21.pdf</xades:SPURI></xades:SigPolicyQualifier></xades:SigPolicyQualifiers></xades:SignaturePolicyId></xades:SignaturePolicyIdentifier></xades:SignedSignatureProperties><xades:SignedDataObjectProperties><xades:DataObjectFormat ObjectReference=\"#S0-RefId0\"><xades:MimeType>application/octet-stream</xades:MimeType></xades:DataObjectFormat></xades:SignedDataObjectProperties></xades:SignedProperties><xades:UnsignedProperties><xades:UnsignedSignatureProperties><xades:CertificateValues><xades:EncapsulatedX509Certificate Id=\"S0-RESPONDER_CERT\">MIIEijCCA3KgAwIBAgIQaI8x6BnacYdNdNwlYnn/mzANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMyMjQ1WhcNMjQwOTA3MTIyMjQ1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qgb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cw6Cja17BbYbHi6frwccDI4BIQLk/fiCE8L45os0xhPgEGR+EHE8LPCIqofPgf4gwN1vDE6cQNUlK0Od+Ush39i9Z45esnfpGq+2HsDJaFmFr5+uC1MEz5Kn1TazEvKbRjkGnSQ9BertlGer2BlU/kqOk5qA5RtJfhT0psc1ixKdPipv59wnf+nHx1+T+fPWndXVZLoDg4t3w8lIvIE/KhOSMlErvBIHIAKV7yH1hOxyeGLghqzMiAn3UeTEOgoOS9URv0C/T5C3mH+Y/uakMSxjNuz41PneimCzbEJZJRiEaMIj8qPAubcbL8GtY03MWmfNtX6/wh6u6TMfW8S2wIDAQABo4H+MIH7MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMJMB0GA1UdDgQWBBR9/5CuRokEgGiqSzYuZGYAogl8TzCBoAYDVR0gBIGYMIGVMIGSBgorBgEEAc4fAwEBMIGDMFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQAaQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4AZwAuMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LnNrLmVlL2FqYXRlbXBlbC8wHwYDVR0jBBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wDQYJKoZIhvcNAQEFBQADggEBAAbaj7kTruTAPHqToye9ZtBdaJ3FZjiKug9/5RjsMwDpOeqFDqCorLd+DBI4tgdu0g4lhaI3aVnKdRBkGV18kqp84uU97JRFWQEf6H8hpJ9k/LzAACkP3tD+0ym+md532mV+nRz1Jj+RPLAUk9xYMV7KPczZN1xnl2wZDJwBbQpcSVH1DjlZv3tFLHBLIYTS6qOK4SxStcgRq7KdRczfW6mfXzTCRWM3G9nmDei5Q3+XTED41j8szRWglzYf6zOv4djkja64WYraQ5zb4x8Xh7qTCk6UupZ7je+0oRfuz0h/3zyRdjcRPkjloSpQp/NG8Rmrcnr874p8d9fdwCrRI7U=</xades:EncapsulatedX509Certificate><xades:EncapsulatedX509Certificate Id=\"S0-CA-CERT\">MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMwNjA5WhcNMjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu60MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD79o+8r4SRYp9kowSFA8E1v38XXTHRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/Hrl9VmikIgAlaHvv3Z+MzS6qeLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+Ky7MZ9jbBr+Nx4py/V7xm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQxrLjv3B67i19d7sNh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxWadHDvVYUCJgW2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYBBAHOHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQAaQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4AZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYDVR0OBBYEFEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSxZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBdh5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obPmJ7jRax3PX0pFhaUE6EhAR0dgS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0SGj7aFLkcC+ULwu1k6yiortFJ0Ds49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cHUijnf39Szq7E2z7MDfZkYg8HZeHrO493EFghXcnSH7J7z47cgP3GWFNUKv1V2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+TorEjxz//WzcF/Sklp4FeD0MU39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu63kW7RDqr+wQFSxKr9DIH</xades:EncapsulatedX509Certificate></xades:CertificateValues><xades:RevocationValues><xades:OCSPValues><xades:EncapsulatedOCSPValue Id=\"N0\">MIIHDgoBAKCCBwcwggcDBgkrBgEFBQcwAQEEggb0MIIG8DCCAUKhgYYwgYMxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMQ0wCwYDVQQLDARPQ1NQMScwJQYDVQQDDB5URVNUIG9mIFNLIE9DU1AgUkVTUE9OREVSIDIwMTExGDAWBgkqhkiG9w0BCQEWCXBraUBzay5lZRgPMjAxNjEwMjcwOTQ0MzJaMGAwXjBJMAkGBSsOAwIaBQAEFJlSx0SY5H6TNo4LfCcJivmxW5RQBBRBtv7FsbG0UxOM+vpi0DRtbSI0CgIQKpa4J4PNPChTajHdDM3l1IAAGA8yMDE2MTAyNzA5NDQzMlqhRDBCMEAGCSsGAQUFBzABAgQzMDEwDQYJYIZIAWUDBAIBBQAEIL2rIkY6lynfhZdFDdKPDirhgfmxN0jKTJoJ4RG9eNTTMA0GCSqGSIb3DQEBCwUAA4IBAQDObM44Dl00Ub5WutM7PcNRHG7i6lQIIO1nlCwX/n/nGH9OQda3sXB+jIQEvdF9X0ivonG6jXPLv+Y5QSK53XFDOeZSqfa2MKFmieXwN4C+1cc7NCX9J+mJawUxpRQwCxw7fkNgrwiKnfNqn9mN/PSZXuMJXwxmtKLt5o8QBCd84ftpivc/V8XRX4YX1Ol+Q9+zI5UoFjYFkgNvz7f1ZMIJewcsesr4aJkx/8BKDjNUrQYHkArkWFgZbKkAPbxNdQaZTNveidMLl3SlxcSqAt6NIIQ/yXoxQD7uhVMhQ6+Xjn7D39F6okGWHZDVzhDi9jY6zgZwLVZ273F/pzuD1D/SoIIEkjCCBI4wggSKMIIDcqADAgECAhBojzHoGdpxh0103CVief+bMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMTAwLgYDVQQDDCdURVNUIG9mIEVFIENlcnRpZmljYXRpb24gQ2VudHJlIFJvb3QgQ0ExGDAWBgkqhkiG9w0BCQEWCXBraUBzay5lZTAeFw0xMTAzMDcxMzIyNDVaFw0yNDA5MDcxMjIyNDVaMIGDMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsGA1UECwwET0NTUDEnMCUGA1UEAwweVEVTVCBvZiBTSyBPQ1NQIFJFU1BPTkRFUiAyMDExMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRzDoKNrXsFthseLp+vBxwMjgEhAuT9+IITwvjmizTGE+AQZH4QcTws8Iiqh8+B/iDA3W8MTpxA1SUrQ535SyHf2L1njl6yd+kar7YewMloWYWvn64LUwTPkqfVNrMS8ptGOQadJD0F6u2UZ6vYGVT+So6TmoDlG0l+FPSmxzWLEp0+Km/n3Cd/6cfHX5P589ad1dVkugODi3fDyUi8gT8qE5IyUSu8EgcgApXvIfWE7HJ4YuCGrMyICfdR5MQ6Cg5L1RG/QL9PkLeYf5j+5qQxLGM27PjU+d6KYLNsQlklGIRowiPyo8C5txsvwa1jTcxaZ821fr/CHq7pMx9bxLbAgMBAAGjgf4wgfswFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYEFH3/kK5GiQSAaKpLNi5kZgCiCXxPMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8DAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0AaQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4wJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jANBgkqhkiG9w0BAQUFAAOCAQEABtqPuROu5MA8epOjJ71m0F1oncVmOIq6D3/lGOwzAOk56oUOoKist34MEji2B27SDiWFojdpWcp1EGQZXXySqnzi5T3slEVZAR/ofyGkn2T8vMAAKQ/e0P7TKb6Z3nfaZX6dHPUmP5E8sBST3FgxXso9zNk3XGeXbBkMnAFtClxJUfUOOVm/e0UscEshhNLqo4rhLFK1yBGrsp1FzN9bqZ9fNMJFYzcb2eYN6LlDf5dMQPjWPyzNFaCXNh/rM6/h2OSNrrhZitpDnNvjHxeHupMKTpS6lnuN77ShF+7PSH/fPJF2NxE+SOWhKlCn80bxGatyevzvinx3193AKtEjtQ==</xades:EncapsulatedOCSPValue></xades:OCSPValues></xades:RevocationValues></xades:UnsignedSignatureProperties></xades:UnsignedProperties></xades:QualifyingProperties></ds:Object></ds:Signature></asic:XAdESSignatures>"
  },
  {
    "path": "common/collector/container/bdoc/testdata/signatures0EID.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n<asic:XAdESSignatures xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\">\n  <ds:Signature Id=\"S0\">\n    <ds:SignedInfo>\n      <ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\"/>\n      <ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"/>\n      <ds:Reference Id=\"S0-RefId0\" URI=\"test.txt\">\n        <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>\n        <ds:DigestValue>4nyCFL6LfPW8zHwIJH48sMFRSkjuH2MZf+TvPvUdfm8=\n</ds:DigestValue>\n      </ds:Reference>\n      <ds:Reference Id=\"S0-RefId1\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\">\n        <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>\n        <ds:DigestValue>+3pD2CLeOxWobO0U6BhPrpfuA4b4IrdKaQq8Npv+GyY=\n</ds:DigestValue>\n      </ds:Reference>\n    </ds:SignedInfo>\n    <ds:SignatureValue Id=\"S0-SIG\">rjaH6bQtJspdFAengOnxIjIan1bWBDbTCEWbE7X9/f4EUOyVZd1sDoTnOUAR\nT0C25FuXy2JlxAiClEWtKN3t9EBcZy3S2nioEFxsW3KwI+8rOgrxXw0iI1zn\nAqfws1kWA08F0KBEudAOnk8TC5UUTCYPCH2zXFY/cPftP9emTA9KDQmCviCi\n3x9BiuySZM54ofQdOrb/V0XZe5OG6xkKM7XqVoN252pW2lyEvbHXfQNN98Es\nnrKHmeBadTXWmfcTjnDqyUv8cEg+r4VSXi5txWsc8hf9HuJ4gAe5RyawIIQ9\nS3ypigMJ2psa0jr1pnVGrgFQ1ioTBgr1s62f5Iwtnw==\n</ds:SignatureValue>\n    <ds:KeyInfo>\n      <ds:X509Data>\n        <ds:X509Certificate>MIIE/jCCA+agAwIBAgIQKpa4J4PNPChTajHdDM3l1DANBgkqhkiG9w0BAQUF\nADBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1p\nc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxMTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTE0MDUwNzEzMTUwOVoXDTE5MDQw\nNzIwNTk1OVowgZ4xCzAJBgNVBAYTAkVFMQ8wDQYDVQQKDAZFU1RFSUQxGjAY\nBgNVBAsMEWRpZ2l0YWwgc2lnbmF0dXJlMSYwJAYDVQQDDB1Nw4ROTklLLE1B\nUkktTElJUyw0NzEwMTAxMDAzMzEQMA4GA1UEBAwHTcOETk5JSzESMBAGA1UE\nKgwJTUFSSS1MSUlTMRQwEgYDVQQFEws0NzEwMTAxMDAzMzCCASMwDQYJKoZI\nhvcNAQEBBQADggEQADCCAQsCggEBAMAKzkzptfsm2tgu2ZFePsFB9OIUc6U7\nytG2izO7gO5RV2SiDzx7RgskUmSRJlU7pBiGq0i8R/Lnf0hr7yu3l13u27Ew\nFARx0LizrUQhlWya7MjXU/RCQwG6ctQagfNzV3M+Ds6340eGu/80VHyV8kUr\nf3ikzv0tzu3h1O59yFHs0s8yxujTp2J6XWs8s4cIp8XMvl6E6z9OTzN6lnSL\nm6wqoT5UcC42mwxSajlMRqD0XzPDLKrqt1BAEkpsikI0BEVTLKzKZNtzJktT\nr+0sKUqHiGfsY8ibhJTl1BNOjPhuTlClD69pjz2H+2RG0q0EDlddfI6lQ2bi\norC4wmu7r6MCBGx3P9mjggFmMIIBYjAJBgNVHRMEAjAAMA4GA1UdDwEB/wQE\nAwIGQDCBmQYDVR0gBIGRMIGOMIGLBgorBgEEAc4fAwEBMH0wWAYIKwYBBQUH\nAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0AaQBzAGUAawBzAC4A\nIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4wIQYIKwYBBQUH\nAgEWFWh0dHA6Ly93d3cuc2suZWUvY3BzLzAdBgNVHQ4EFgQUfSof04AL6iqi\n97jvXUdPzhDNAbMwIgYIKwYBBQUHAQMEFjAUMAgGBgQAjkYBATAIBgYEAI5G\nAQQwHwYDVR0jBBgwFoAUQbb+xbGxtFMTjPr6YtA0bW0iNAowRQYDVR0fBD4w\nPDA6oDigNoY0aHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVz\ndF9lc3RlaWQyMDExLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAQzo6tEBrrnKn\nTP7MLu2LlIpZ7Gh1zWtqktNM42oyKE5+iUp815h2Bq04N9PrRdfVf+p3T5+U\nfgcZrTKAp4SagNtJwgy8HZUInvo9XB32lT/hUOudveHvgEm+VBnkD+zXf+Hc\nzDliuMglehfSMVQ3sewxYPy1+eU1IQHpZ5HjwamU74Ob0CZbkzbJU3/QvyVd\nz0kicP4nMFEcLKoZzqKZ9BldV1sgoeaJ4X+wAWVeHHdtz2QWAIyX4B1tMtQp\njk/bzpnsxEaFleW0uW+mfECYGLQcEWW2iKWkoBBFcwNM2Ou48CL0t80rQ0ZB\nq5kTh5CvqF/MSdsvcIwJ+zXmiqt/EQ==\n</ds:X509Certificate>\n      </ds:X509Data>\n    </ds:KeyInfo>\n    <ds:Object>\n      <xades:QualifyingProperties Target=\"#S0\">\n        <xades:SignedProperties Id=\"S0-SignedProperties\">\n          <xades:SignedSignatureProperties>\n            <xades:SigningTime>2016-10-27T09:44:26Z</xades:SigningTime>\n            <xades:SigningCertificate>\n              <xades:Cert>\n                <xades:CertDigest>\n                  <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>\n                  <ds:DigestValue>Lm7A+6id6in1RilLAzog9uCgfV1FLVnILhCsFGxSH8E=\n</ds:DigestValue>\n                </xades:CertDigest>\n                <xades:IssuerSerial>\n                  <ds:X509IssuerName>emailAddress=pki@sk.ee,CN=TEST of ESTEID-SK 2011,O=AS Sertifitseerimiskeskus,C=EE</ds:X509IssuerName>\n                  <ds:X509SerialNumber>56610155445802750935393253012615849428</ds:X509SerialNumber>\n                </xades:IssuerSerial>\n              </xades:Cert>\n            </xades:SigningCertificate>\n            <xades:SignaturePolicyIdentifier>\n              <xades:SignaturePolicyId>\n                <xades:SigPolicyId>\n                  <xades:Identifier Qualifier=\"OIDAsURN\">urn:oid:1.3.6.1.4.1.10015.1000.3.2.1</xades:Identifier>\n                  <xades:Description>BDOC – FORMAT FOR DIGITAL SIGNATURES</xades:Description>\n                </xades:SigPolicyId>\n                <xades:SigPolicyHash>\n                  <ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>\n                  <ds:DigestValue>3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs=\n</ds:DigestValue>\n                </xades:SigPolicyHash>\n                <xades:SigPolicyQualifiers>\n                  <xades:SigPolicyQualifier>\n                    <xades:SPURI>https://www.sk.ee/repository/bdoc-spec21.pdf</xades:SPURI>\n                  </xades:SigPolicyQualifier>\n                </xades:SigPolicyQualifiers>\n              </xades:SignaturePolicyId>\n            </xades:SignaturePolicyIdentifier>\n          </xades:SignedSignatureProperties>\n          <xades:SignedDataObjectProperties>\n            <xades:DataObjectFormat ObjectReference=\"#S0-RefId0\">\n              <xades:MimeType>application/octet-stream</xades:MimeType>\n            </xades:DataObjectFormat>\n          </xades:SignedDataObjectProperties>\n        </xades:SignedProperties>\n        <xades:UnsignedProperties>\n          <xades:UnsignedSignatureProperties>\n            <xades:CertificateValues>\n              <xades:EncapsulatedX509Certificate Id=\"S0-RESPONDER_CERT\">MIIEijCCA3KgAwIBAgIQaI8x6BnacYdNdNwlYnn/mzANBgkqhkiG9w0BAQUF\nADB9MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1p\nc2tlc2t1czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENl\nbnRyZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEw\nMzA3MTMyMjQ1WhcNMjQwOTA3MTIyMjQ1WjCBgzELMAkGA1UEBhMCRUUxIjAg\nBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9D\nU1AxJzAlBgNVBAMMHlRFU1Qgb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEY\nMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA0cw6Cja17BbYbHi6frwccDI4BIQLk/fiCE8L45os0xhP\ngEGR+EHE8LPCIqofPgf4gwN1vDE6cQNUlK0Od+Ush39i9Z45esnfpGq+2HsD\nJaFmFr5+uC1MEz5Kn1TazEvKbRjkGnSQ9BertlGer2BlU/kqOk5qA5RtJfhT\n0psc1ixKdPipv59wnf+nHx1+T+fPWndXVZLoDg4t3w8lIvIE/KhOSMlErvBI\nHIAKV7yH1hOxyeGLghqzMiAn3UeTEOgoOS9URv0C/T5C3mH+Y/uakMSxjNuz\n41PneimCzbEJZJRiEaMIj8qPAubcbL8GtY03MWmfNtX6/wh6u6TMfW8S2wID\nAQABo4H+MIH7MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMJMB0GA1UdDgQWBBR9\n/5CuRokEgGiqSzYuZGYAogl8TzCBoAYDVR0gBIGYMIGVMIGSBgorBgEEAc4f\nAwEBMIGDMFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\naQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABp\nAG4AZwAuMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LnNrLmVlL2FqYXRlbXBl\nbC8wHwYDVR0jBBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wDQYJKoZIhvcN\nAQEFBQADggEBAAbaj7kTruTAPHqToye9ZtBdaJ3FZjiKug9/5RjsMwDpOeqF\nDqCorLd+DBI4tgdu0g4lhaI3aVnKdRBkGV18kqp84uU97JRFWQEf6H8hpJ9k\n/LzAACkP3tD+0ym+md532mV+nRz1Jj+RPLAUk9xYMV7KPczZN1xnl2wZDJwB\nbQpcSVH1DjlZv3tFLHBLIYTS6qOK4SxStcgRq7KdRczfW6mfXzTCRWM3G9nm\nDei5Q3+XTED41j8szRWglzYf6zOv4djkja64WYraQ5zb4x8Xh7qTCk6UupZ7\nje+0oRfuz0h/3zyRdjcRPkjloSpQp/NG8Rmrcnr874p8d9fdwCrRI7U=\n</xades:EncapsulatedX509Certificate>\n              <xades:EncapsulatedX509Certificate Id=\"S0-CA-CERT\">MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUF\nADB9MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1p\nc2tlc2t1czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENl\nbnRyZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEw\nMzA3MTMwNjA5WhcNMjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAG\nA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVT\nVCBvZiBFU1RFSUQtU0sgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu6\n0MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD79o+8r4SRYp9kowSFA8E1v38XXTH\nRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/Hrl9VmikIgAlaHvv3Z+MzS6qe\nLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+Ky7MZ9jbBr+Nx4py/V7x\nm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQxrLjv3B67i19d7sN\nh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxWadHDvVYUCJgW\n2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUw\nAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYBBAHO\nHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\naQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABp\nAG4AZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYD\nVR0OBBYEFEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2l\nLxDF5yEOvsSxZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3\nLnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqG\nSIb3DQEBBQUAA4IBAQBdh5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obP\nmJ7jRax3PX0pFhaUE6EhAR0dgS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0\nSGj7aFLkcC+ULwu1k6yiortFJ0Ds49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cH\nUijnf39Szq7E2z7MDfZkYg8HZeHrO493EFghXcnSH7J7z47cgP3GWFNUKv1V\n2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+TorEjxz//WzcF/Sklp4FeD0MU\n39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu63kW7RDqr+wQFSxKr9DIH\n</xades:EncapsulatedX509Certificate>\n            </xades:CertificateValues>\n            <xades:RevocationValues>\n              <xades:OCSPValues>\n                <xades:EncapsulatedOCSPValue Id=\"N0\">MIIHDgoBAKCCBwcwggcDBgkrBgEFBQcwAQEEggb0MIIG8DCCAUKhgYYwgYMx\nCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vz\na3VzMQ0wCwYDVQQLDARPQ1NQMScwJQYDVQQDDB5URVNUIG9mIFNLIE9DU1Ag\nUkVTUE9OREVSIDIwMTExGDAWBgkqhkiG9w0BCQEWCXBraUBzay5lZRgPMjAx\nNjEwMjcwOTQ0MzJaMGAwXjBJMAkGBSsOAwIaBQAEFJlSx0SY5H6TNo4LfCcJ\nivmxW5RQBBRBtv7FsbG0UxOM+vpi0DRtbSI0CgIQKpa4J4PNPChTajHdDM3l\n1IAAGA8yMDE2MTAyNzA5NDQzMlqhRDBCMEAGCSsGAQUFBzABAgQzMDEwDQYJ\nYIZIAWUDBAIBBQAEIL2rIkY6lynfhZdFDdKPDirhgfmxN0jKTJoJ4RG9eNTT\nMA0GCSqGSIb3DQEBCwUAA4IBAQDObM44Dl00Ub5WutM7PcNRHG7i6lQIIO1n\nlCwX/n/nGH9OQda3sXB+jIQEvdF9X0ivonG6jXPLv+Y5QSK53XFDOeZSqfa2\nMKFmieXwN4C+1cc7NCX9J+mJawUxpRQwCxw7fkNgrwiKnfNqn9mN/PSZXuMJ\nXwxmtKLt5o8QBCd84ftpivc/V8XRX4YX1Ol+Q9+zI5UoFjYFkgNvz7f1ZMIJ\newcsesr4aJkx/8BKDjNUrQYHkArkWFgZbKkAPbxNdQaZTNveidMLl3SlxcSq\nAt6NIIQ/yXoxQD7uhVMhQ6+Xjn7D39F6okGWHZDVzhDi9jY6zgZwLVZ273F/\npzuD1D/SoIIEkjCCBI4wggSKMIIDcqADAgECAhBojzHoGdpxh0103CVief+b\nMA0GCSqGSIb3DQEBBQUAMH0xCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBT\nZXJ0aWZpdHNlZXJpbWlza2Vza3VzMTAwLgYDVQQDDCdURVNUIG9mIEVFIENl\ncnRpZmljYXRpb24gQ2VudHJlIFJvb3QgQ0ExGDAWBgkqhkiG9w0BCQEWCXBr\naUBzay5lZTAeFw0xMTAzMDcxMzIyNDVaFw0yNDA5MDcxMjIyNDVaMIGDMQsw\nCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczENMAsGA1UECwwET0NTUDEnMCUGA1UEAwweVEVTVCBvZiBTSyBPQ1NQIFJF\nU1BPTkRFUiAyMDExMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRzDoKNrXsFthseLp+vBxwMjgE\nhAuT9+IITwvjmizTGE+AQZH4QcTws8Iiqh8+B/iDA3W8MTpxA1SUrQ535SyH\nf2L1njl6yd+kar7YewMloWYWvn64LUwTPkqfVNrMS8ptGOQadJD0F6u2UZ6v\nYGVT+So6TmoDlG0l+FPSmxzWLEp0+Km/n3Cd/6cfHX5P589ad1dVkugODi3f\nDyUi8gT8qE5IyUSu8EgcgApXvIfWE7HJ4YuCGrMyICfdR5MQ6Cg5L1RG/QL9\nPkLeYf5j+5qQxLGM27PjU+d6KYLNsQlklGIRowiPyo8C5txsvwa1jTcxaZ82\n1fr/CHq7pMx9bxLbAgMBAAGjgf4wgfswFgYDVR0lAQH/BAwwCgYIKwYBBQUH\nAwkwHQYDVR0OBBYEFH3/kK5GiQSAaKpLNi5kZgCiCXxPMIGgBgNVHSAEgZgw\ngZUwgZIGCisGAQQBzh8DAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUA\nbAB0ACAAdABlAHMAdABpAG0AaQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBv\nAHIAIAB0AGUAcwB0AGkAbgBnAC4wJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cu\nc2suZWUvYWphdGVtcGVsLzAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVH\nGwN2/jANBgkqhkiG9w0BAQUFAAOCAQEABtqPuROu5MA8epOjJ71m0F1oncVm\nOIq6D3/lGOwzAOk56oUOoKist34MEji2B27SDiWFojdpWcp1EGQZXXySqnzi\n5T3slEVZAR/ofyGkn2T8vMAAKQ/e0P7TKb6Z3nfaZX6dHPUmP5E8sBST3Fgx\nXso9zNk3XGeXbBkMnAFtClxJUfUOOVm/e0UscEshhNLqo4rhLFK1yBGrsp1F\nzN9bqZ9fNMJFYzcb2eYN6LlDf5dMQPjWPyzNFaCXNh/rM6/h2OSNrrhZitpD\nnNvjHxeHupMKTpS6lnuN77ShF+7PSH/fPJF2NxE+SOWhKlCn80bxGatyevzv\ninx3193AKtEjtQ==\n</xades:EncapsulatedOCSPValue>\n              </xades:OCSPValues>\n            </xades:RevocationValues>\n          </xades:UnsignedSignatureProperties>\n        </xades:UnsignedProperties>\n      </xades:QualifyingProperties>\n    </ds:Object>\n  </ds:Signature>\n</asic:XAdESSignatures>\n"
  },
  {
    "path": "common/collector/container/bdoc/testdata/signatures0MID.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<asic:XAdESSignatures xmlns:asic=\"http://uri.etsi.org/02918/v1.2.1#\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\">\n<ds:Signature Id=\"S0\">\n<ds:SignedInfo Id=\"S0-SignedInfo\">\n<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2006/12/xml-c14n11\">\n</ds:CanonicalizationMethod>\n<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256\">\n</ds:SignatureMethod>\n<ds:Reference Id=\"S0-ref-0\" URI=\"EE_Certification_Centre_Root_CA.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>IwP0Vozcm1MsmLemnYwpnXjHXTs70rlPhUqteU7ZIa8=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-1\" URI=\"ESTEID-SK_2011.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>5XIpC9qJ8MkgH3tqWNPmU8XsiY9nq6HcNRPmmPShfLY=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-2\" URI=\"ESTEID-SK_2015.pem.crt\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>l7BSx7yM00dFGC5MWsvSdto4FdPvzWP9zQ3/CqZpA2o=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-3\" URI=\"ivxv.properties\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>utNu6sqhFu3VUSamEU1POJGY6CV99FIfJjd/IGJVRFg=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-4\" URI=\"SK_OCSP_RESPONDER_2011.pem.cer\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>jnEAlCTbnYGTpVV2Q4+y5vAlDOA7lBR8EJiM5OKgj7U=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-5\" URI=\"SK_TIMESTAMPING_AUTHORITY.pem.cer\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>FqVY3feXnohBOgCY/43dv3Wd0Rhe1whpB014HCRe8JY=</ds:DigestValue>\n</ds:Reference>\n<ds:Reference Id=\"S0-ref-sp\" Type=\"http://uri.etsi.org/01903#SignedProperties\" URI=\"#S0-SignedProperties\">\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>/mU9ss4AFlBieyjICaNA9EoXnMMeYZ+qUPHnprupIVk=</ds:DigestValue>\n</ds:Reference>\n</ds:SignedInfo><ds:SignatureValue Id=\"S0-SIG\">\n2CkBcIW8j5T33zD/dFvTJQRdduo2s26f/XCvTfny33fwsIHH70LfjGJMD3SpmrEAcpswpcN4YtJ5KbQ+V4qCDA==</ds:SignatureValue>\n<ds:KeyInfo Id=\"S0-KeyInfo\">\n<ds:X509Data><ds:X509Certificate>MIIE5TCCAs2gAwIBAgIQAVwVqrdtcARW3zreJ0KJ+DANBgkqhkiG9w0BAQsFADBj\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVTVEVJRC1TSyAy\nMDE1MB4XDTE2MDMwODIwNDkzNFoXDTIxMDMwODIxNTk1OVowgaIxCzAJBgNVBAYT\nAkVFMRswGQYDVQQKDBJFU1RFSUQgKE1PQklJTC1JRCkxGjAYBgNVBAsMEWRpZ2l0\nYWwgc2lnbmF0dXJlMSIwIAYDVQQDDBlKQUVHRVIsSkFBTlVTLDM4MDA3MjQyNzE5\nMQ8wDQYDVQQEDAZKQUVHRVIxDzANBgNVBCoMBkpBQU5VUzEUMBIGA1UEBRMLMzgw\nMDcyNDI3MTkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATdtEkDXOUj+VAJ0YI3\ncOBSiJVfitmrn7J3zllgK2+o3een32K7982nfALWaY/ZyIwQJYiHWWYukr6vQhP7\nBJQYo4IBHjCCARowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBkAwWwYDVR0gBFQw\nUjBQBgorBgEEAc4fAQMDMEIwHQYIKwYBBQUHAgIwEQwPQ29udHJhY3QgMS4xMS05\nMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9jcHMwHQYDVR0OBBYEFOY2\nI79Rh5vtYrmHNMIQs2/xVJ82MCIGCCsGAQUFBwEDBBYwFDAIBgYEAI5GAQEwCAYG\nBACORgEEMB8GA1UdIwQYMBaAFLOriLyZ1WKkhSoIzbQdcjuDckdRMDwGA1UdHwQ1\nMDMwMaAvoC2GK2h0dHA6Ly93d3cuc2suZWUvY3Jscy9lc3RlaWQvZXN0ZWlkMjAx\nNS5jcmwwDQYJKoZIhvcNAQELBQADggIBAFjicT5+WrrMuStbLI+VMIw99amjKh0N\nf71AkMB/Zs8Joy851loIGK6rdLOet5m4FB1Sn09NTB67iNCl6cPNIChWe3SZ9E8q\n3LbC/OwZ77mF2BnXhKNertu/TXoZH7PMHGvWZlQnr1pt0vZdIJm0LyHPkYXEc/Ue\n3OgIufrm/Y4zFBwwowvSjNdHCZDkEq3Lhat/qe6DAQJyrPHxuzynKn0cl/iBhak3\nuJDi2GTLLUpGV98d2NN6PDSikvRrHYyn6WOhsZl7z5kaNOlaNgdGRvU5iwxhE9TL\nkNakWI8H6DfZSlNU1Gdwv6lrYBHqZlLGpTgg7FmCnr+iTE9jS6/zxbrZ3y9Lpl1J\n7isnoI+eja8uHvr888pefBOPII09jqJwdpQZfxqU2nS9u5FrGgQKcYzWg7aP3DNC\nbPpRbVfbTF2fqsldJ7jpOOn/rheywiVN0muC+j522cLxD/f8OUPXIlg9FoHYpCOJ\nvcaMt7c6oR8zCheHQ87vGF+gZhJ81TxOKoSfpNDxza6lQ4ttesKgjvohSdLQJ+IT\nT2ftgs3vsmE/UQ6hh1gD+b24q5bdNCDO7pOwBHAbXHkNh0yV1DdP+8lPCLG9n9S+\nmvhEtUlxVDVjp9OYMm0tT9sO3Uhmcp6DPyHoz+jj5Rni1WodvNrrHxHc10qiorto\nABegOEP+mYBd</ds:X509Certificate></ds:X509Data></ds:KeyInfo>\n<ds:Object Id=\"S0-object-xades\"><xades:QualifyingProperties Id=\"S0-QualifyingProperties\" Target=\"#S0\" xmlns:xades=\"http://uri.etsi.org/01903/v1.3.2#\"><xades:SignedProperties Id=\"S0-SignedProperties\">\n<xades:SignedSignatureProperties Id=\"S0-SignedSignatureProperties\">\n<xades:SigningTime>2016-11-07T15:57:53Z</xades:SigningTime>\n<xades:SigningCertificate>\n<xades:Cert>\n<xades:CertDigest>\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>Cc+MtelmZEA5+W4xRUr/XJOd15QNsOYwWwG0uHJVRnQ=</ds:DigestValue>\n</xades:CertDigest>\n<xades:IssuerSerial>\n<ds:X509IssuerName>CN=ESTEID-SK 2015,2.5.4.97=#0c0e4e545245452d3130373437303133,O=AS Sertifitseerimiskeskus,C=EE</ds:X509IssuerName>\n<ds:X509SerialNumber>1807358762927482821481893101981305336</ds:X509SerialNumber>\n</xades:IssuerSerial>\n</xades:Cert>\n</xades:SigningCertificate>\n<xades:SignaturePolicyIdentifier>\n<xades:SignaturePolicyId>\n<xades:SigPolicyId>\n<xades:Identifier Qualifier=\"OIDAsURN\">\nurn:oid:1.3.6.1.4.1.10015.1000.3.2.1</xades:Identifier>\n</xades:SigPolicyId>\n<xades:SigPolicyHash>\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\">\n</ds:DigestMethod>\n<ds:DigestValue>3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs=</ds:DigestValue>\n</xades:SigPolicyHash>\n<xades:SigPolicyQualifiers>\n<xades:SigPolicyQualifier>\n<xades:SPURI>\nhttps://www.sk.ee/repository/bdoc-spec21.pdf</xades:SPURI>\n</xades:SigPolicyQualifier>\n</xades:SigPolicyQualifiers>\n</xades:SignaturePolicyId>\n</xades:SignaturePolicyIdentifier>\n\n<xades:SignatureProductionPlace>\n</xades:SignatureProductionPlace>\n</xades:SignedSignatureProperties>\n<xades:SignedDataObjectProperties>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-0\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-1\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-2\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-3\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-4\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n<xades:DataObjectFormat ObjectReference=\"#S0-ref-5\">\n<xades:MimeType>application/octet-stream</xades:MimeType>\n</xades:DataObjectFormat>\n</xades:SignedDataObjectProperties>\n</xades:SignedProperties><xades:UnsignedProperties Id=\"S0-UnsigedProperties\">\n<xades:UnsignedSignatureProperties Id=\"S0-UnsigedSignatureProperties\">\n<xades:CertificateValues Id=\"S0-CertificateValues\">\n<xades:EncapsulatedX509Certificate Id=\"S0-CA_CERT1\">\nMIIGcDCCBVigAwIBAgIQRUgJC4ec7yFWcqzT3mwbWzANBgkqhkiG9w0BAQwFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE1MTIxNzEyMzg0M1oYDzIwMzAxMjE3\nMjM1OTU5WjBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy\naW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVT\nVEVJRC1TSyAyMDE1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oH6\n1NDxbdW9k8nLA1qGaL4B7vydod2Ewp/STBZB3wEtIJCLdkpEsS8pXfFiRqwDVsgG\nGbu+Q99trlb5LI7yi7rIkRov5NftBdSNPSU5rAhYPQhvZZQgOwRaHa5Ey+BaLJHm\nLqYQS9hQvQsCYyws+xVvNFUpK0pGD64iycqdMuBl/nWq3fLuZppwBh0VFltm4nhr\n/1S0R9TRJpqFUGbGr4OK/DwebQ5PjhdS40gCUNwmC7fPQ4vIH+x+TCk2aG+u3MoA\nz0IrpVWqiwzG/vxreuPPAkgXeFCeYf6fXLsGz4WivsZFbph2pMjELu6sltlBXfAG\n3fGv43t91VXicyzR/eT5dsB+zFsW1sHV+1ONPr+qzgDxCH2cmuqoZNfIIq+buob3\neA8ee+XpJKJQr+1qGrmhggjvAhc7m6cU4x/QfxwRYhIVNhJf+sKVThkQhbJ9XxuK\nk3c18wymwL1mpDD0PIGJqlssMeiuJ4IzagFbgESGNDUd4icm0hQT8CmQeUm1GbWe\nBYseqPhMQX97QFBLXJLVy2SCyoAz7Bq1qA43++EcibN+yBc1nQs2Zoq8ck9MK0bC\nxDMeUkQUz6VeQGp69ImOQrsw46qTz0mtdQrMSbnkXCuLan5dPm284J9HmaqiYi6j\n6KLcZ2NkUnDQFesBVlMEm+fHa2iR6lnAFYZ06UECAwEAAaOCAgowggIGMB8GA1Ud\nIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBSzq4i8mdVipIUq\nCM20HXI7g3JHUTAOBgNVHQ8BAf8EBAMCAQYwdwYDVR0gBHAwbjAIBgYEAI96AQIw\nCQYHBACL7EABAjAwBgkrBgEEAc4fAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93\nd3cuc2suZWUvQ1BTMAsGCSsGAQQBzh8BAjALBgkrBgEEAc4fAQMwCwYJKwYBBAHO\nHwEEMBIGA1UdEwEB/wQIMAYBAf8CAQAwQQYDVR0eBDowOKE2MASCAiIiMAqHCAAA\nAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCcGA1Ud\nJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEE\ncDBuMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5zay5lZS9DQTBKBggrBgEFBQcw\nAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRy\nZV9Sb290X0NBLmRlci5jcnQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5z\nay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEMBQAD\nggEBAHRWDGI3P00r2sOnlvLHKk9eE7X93eT+4e5TeaQsOpE5zQRUTtshxN8Bnx2T\noQ9rgi18q+MwXm2f0mrGakYYG0bix7ZgDQvCMD/kuRYmwLGdfsTXwh8KuL6uSHF+\nU/ZTss6qG7mxCHG9YvebkN5Yj/rYRvZ9/uJ9rieByxw4wo7b19p22PXkAkXP5y3+\nqK/Oet98lqwI97kJhiS2zxFYRk+dXbazmoVHnozYKmsZaSUvoYNNH19tpS7BLdsg\ni9KpbvQLb5ywIMq9ut3+b2Xvzq8yzmHMFtLIJ6Afu1jJpqD82BUAFcvi5vhnP8M7\nb974R18WCOpgNQvXDI+2/8ZINeU=</xades:EncapsulatedX509Certificate>\n<xades:EncapsulatedX509Certificate Id=\"S0-RESPONDER_CERT\">\nMIIEvDCCA6SgAwIBAgIQcpyVmdruRVxNgzI3N/NZQTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTExMDMxODEwMjE0M1oXDTI0MDMxODEw\nMjE0M1owgZ0xCzAJBgNVBAYTAkVFMQ4wDAYDVQQIEwVIYXJqdTEQMA4GA1UEBxMH\nVGFsbGlubjEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsG\nA1UECxMET0NTUDEfMB0GA1UEAxMWU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAihvGyhMVrgReHluKln1za6gvCE/mlSREmWjJFpL9llvuEUZoPFIypYA8\ng5u1VfgkeW5gDq25jAOq4FyXeDGIa+pJn2h0o2Wc2aeppVG/emfGm/jA8jjeyMrw\nH8fAJrqVQ7c9X2xSwJEch/P2d8CfMZt5YF6gqLtPvG1b+n6otBZA5wjIFfJ/inJB\nMUvqHSz3+PLfxO2/T3Wyk/c8M9HIMqTelqyiMGRgWehiU1OsL9armv3dQrHs1wm6\nvHaxfpfWB9YAFpeo9aYqhPCxVt/zo2NQB6vxyZS0hsOrXL7SxRToOJaqsnvlbf0e\nrPPFtRHUvbojYYgl+fzlz0Jt6QJoNwIDAQABo4IBHTCCARkwEwYDVR0lBAwwCgYI\nKwYBBQUHAwkwHQYDVR0OBBYEFKWhSGFt537NmJ50nCm7vYrecgxZMIGCBgNVHSAE\nezB5MHcGCisGAQQBzh8EAQIwaTA+BggrBgEFBQcCAjAyHjAAUwBLACAAdABpAG0A\nZQAgAHMAdABhAG0AcABpAG4AZwAgAHAAbwBsAGkAYwB5AC4wJwYIKwYBBQUHAgEW\nG2h0dHBzOi8vd3d3LnNrLmVlL2FqYXRlbXBlbDAfBgNVHSMEGDAWgBQS8lo+6lYc\nv80GrPHxJcmpS9QUmTA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vd3d3LnNrLmVl\nL3JlcG9zaXRvcnkvY3Jscy9lZWNjcmNhLmNybDANBgkqhkiG9w0BAQUFAAOCAQEA\nw2sKwvTHtYGtD8Jw9mNUuj/mWiBSBEBeY2LhW8V6tjBPAPp3s6iWOh0FbVR2LUyr\nqRwgT3fyWiGsiDm/6cIqM+IblLp/8ztfRQjquhW6XCD9SK02OQ9ZSdBwcmoAApZL\nGXQC34wdgmV/hLTTNxONnDACBKz9U+Dy9a4ZT4tpNkbH8jq/BMne8FzbvRt1bjpX\nBP7gjLX+zdx8/hp0Wq4tD+f9NVX0+vm9ahEKuzx4QzPnSB7hhWM9OnLZT7noRQa+\nKWk5c+e5VoR5R2t7MjVl8Cd+2llxiSxqMSbU5/23BzAKgN+NQdrBZAzpZ7lfaAuL\nFaICP+bAm6uW2JUrM6abOw==</xades:EncapsulatedX509Certificate>\n<xades:EncapsulatedX509Certificate Id=\"N0-CA_CERT3\">\nMIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy\nMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl\nZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS\nb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy\neuuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO\nbntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw\nWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d\nMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE\n1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD\nVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/\nzQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB\nBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF\nBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV\nv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG\nE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u\nuSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW\niAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v\nGVCJYMzpJJUPwssd8m92kMfMdcGWxZ0=</xades:EncapsulatedX509Certificate>\n</xades:CertificateValues>\n<xades:RevocationValues Id=\"S0-RevocationValues\">\n<xades:OCSPValues><xades:EncapsulatedOCSPValue Id=\"N0\">\nMIIHWgoBAKCCB1MwggdPBgkrBgEFBQcwAQEEggdAMIIHPDCCAVyhgaAwgZ0xCzAJ\nBgNVBAYTAkVFMQ4wDAYDVQQIEwVIYXJqdTEQMA4GA1UEBxMHVGFsbGlubjEiMCAG\nA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czENMAsGA1UECxMET0NTUDEf\nMB0GA1UEAxMWU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYGCSqGSIb3DQEJARYJ\ncGtpQHNrLmVlGA8yMDE2MTEwNzE1NTgwNlowYDBeMEkwCQYFKw4DAhoFAAQU/ycP\npUs92lD7/iLb2ohCQQ7zE+4EFLOriLyZ1WKkhSoIzbQdcjuDckdRAhABXBWqt21w\nBFbfOt4nQon4gAAYDzIwMTYxMTA3MTU1ODA2WqFEMEIwQAYJKwYBBQUHMAECBDMw\nMTANBglghkgBZQMEAgEFAAQg/IHZ1+8JoBbK4CC+EUBLUr5k3nggZ4zBtJopMd+w\nsoswDQYJKoZIhvcNAQELBQADggEBAFISp9wRki3uSgHysh7POHOXxs8+jDAAekzA\nKrz57hSxjYQ+Y9VLFxfPIBtNP57r/gwXegYclfMg5MyTM9q9UXmMB++K8/W7+SiC\nr2qUNxp6JnHeOnqqkrmP7G9fmbznX5D2qNRfFXw2L3A1y/r5DEyFFtP4kKSYpwCt\nOw85vAco/7LHtWasWTIJRT15T6xBCAkXdwYg51yoLyycAYVx+Sj7fsfraXqxZ3ZG\nkyQdHP4tmEkmvYFQyme2IcP+UISuKaw7b/FEivmqQcDTBMCVSozr03OwTqMAHIqB\nux49GJH4YeUeBdH+0Nm6wbPFyH+H1wCfUtxaKaj5jR4q/vU8P86gggTEMIIEwDCC\nBLwwggOkoAMCAQICEHKclZna7kVcTYMyNzfzWUEwDQYJKoZIhvcNAQEFBQAwdTEL\nMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMx\nKDAmBgNVBAMMH0VFIENlcnRpZmljYXRpb24gQ2VudHJlIFJvb3QgQ0ExGDAWBgkq\nhkiG9w0BCQEWCXBraUBzay5lZTAeFw0xMTAzMTgxMDIxNDNaFw0yNDAzMTgxMDIx\nNDNaMIGdMQswCQYDVQQGEwJFRTEOMAwGA1UECBMFSGFyanUxEDAOBgNVBAcTB1Rh\nbGxpbm4xIjAgBgNVBAoTGUFTIFNlcnRpZml0c2VlcmltaXNrZXNrdXMxDTALBgNV\nBAsTBE9DU1AxHzAdBgNVBAMTFlNLIE9DU1AgUkVTUE9OREVSIDIwMTExGDAWBgkq\nhkiG9w0BCQEWCXBraUBzay5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBAIobxsoTFa4EXh5bipZ9c2uoLwhP5pUkRJloyRaS/ZZb7hFGaDxSMqWAPIOb\ntVX4JHluYA6tuYwDquBcl3gxiGvqSZ9odKNlnNmnqaVRv3pnxpv4wPI43sjK8B/H\nwCa6lUO3PV9sUsCRHIfz9nfAnzGbeWBeoKi7T7xtW/p+qLQWQOcIyBXyf4pyQTFL\n6h0s9/jy38Ttv091spP3PDPRyDKk3pasojBkYFnoYlNTrC/Wq5r93UKx7NcJurx2\nsX6X1gfWABaXqPWmKoTwsVbf86NjUAer8cmUtIbDq1y+0sUU6DiWqrJ75W39Hqzz\nxbUR1L26I2GIJfn85c9CbekCaDcCAwEAAaOCAR0wggEZMBMGA1UdJQQMMAoGCCsG\nAQUFBwMJMB0GA1UdDgQWBBSloUhhbed+zZiedJwpu72K3nIMWTCBggYDVR0gBHsw\neTB3BgorBgEEAc4fBAECMGkwPgYIKwYBBQUHAgIwMh4wAFMASwAgAHQAaQBtAGUA\nIABzAHQAYQBtAHAAaQBuAGcAIABwAG8AbABpAGMAeQAuMCcGCCsGAQUFBwIBFhto\ndHRwczovL3d3dy5zay5lZS9hamF0ZW1wZWwwHwYDVR0jBBgwFoAUEvJaPupWHL/N\nBqzx8SXJqUvUFJkwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5zay5lZS9y\nZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEFBQADggEBAMNr\nCsL0x7WBrQ/CcPZjVLo/5logUgRAXmNi4VvFerYwTwD6d7OoljodBW1Udi1Mq6kc\nIE938lohrIg5v+nCKjPiG5S6f/M7X0UI6roVulwg/UitNjkPWUnQcHJqAAKWSxl0\nAt+MHYJlf4S00zcTjZwwAgSs/VPg8vWuGU+LaTZGx/I6vwTJ3vBc270bdW46VwT+\n4Iy1/s3cfP4adFquLQ/n/TVV9Pr5vWoRCrs8eEMz50ge4YVjPTpy2U+56EUGvilp\nOXPnuVaEeUdrezI1ZfAnftpZcYksajEm1Of9twcwCoDfjUHawWQM6We5X2gLixWi\nAj/mwJurltiVKzOmmzs=</xades:EncapsulatedOCSPValue>\n</xades:OCSPValues></xades:RevocationValues></xades:UnsignedSignatureProperties>\n</xades:UnsignedProperties></xades:QualifyingProperties></ds:Object>\n</ds:Signature>\n</asic:XAdESSignatures>"
  },
  {
    "path": "common/collector/container/bdoc/testdata/trustBES.yaml",
    "content": "filecount: 50\nbdocsize: 102400  # 100 KiB\nfilesize: 102400  # 100 KiB\nroots:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n    GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n    cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n    aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n    xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n    ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n    MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n    VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n    kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n    pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n    A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n    AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n    BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n    I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n    Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n    SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n    tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n    SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFLDCCBI2gAwIBAgIQImvqKVwtGyZbh+ecdKPc7zAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwODMwMTI0ODI4WhcNMzMwODMwMTI0ODI4WjBiMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgwgZswEAYHKoZIzj0C\n    AQYFK4EEACMDgYYABABZN0DFpEKsj3SzsySoR/bcwAUoLc+S2HrvHY0xIDkFFTtU\n    QXfjxXyexNIx+ALe2IYJZLTl0T79C5by4/mO/5H7UgCxZZCRKtdcKqSGYJOVpT0X\n    oA51yX8eBk8aPVrTcwABcBhU6nTNGEoNXfeS7mrZB6Gs3eFxEVdejIEjNObWVFYM\n    bqOCAuAwggLcMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMDQG\n    A1UdJQEB/wQqMCgGCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQGCCsGAQUF\n    BwMBMB0GA1UdDgQWBBR/DHDY9OWPAXfux20pKbn0yfxqwDAfBgNVHSMEGDAWgBR/\n    DHDY9OWPAXfux20pKbn0yfxqwDCCAiQGA1UdIASCAhswggIXMAgGBgQAj3oBAjAJ\n    BgcEAIvsQAECMDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8v\n    d3d3LnNrLmVlL0NQUzANBgsrBgEEAYORIQECAjANBgsrBgEEAYORfwECATANBgsr\n    BgEEAYORIQECBTANBgsrBgEEAYORIQECBjANBgsrBgEEAYORIQECBzANBgsrBgEE\n    AYORIQECAzANBgsrBgEEAYORIQECBDANBgsrBgEEAYORIQECCDANBgsrBgEEAYOR\n    IQECCTANBgsrBgEEAYORIQECCjANBgsrBgEEAYORIQECCzANBgsrBgEEAYORIQEC\n    DDANBgsrBgEEAYORIQECDTANBgsrBgEEAYORIQECDjANBgsrBgEEAYORIQECDzAN\n    BgsrBgEEAYORIQECEDANBgsrBgEEAYORIQECETANBgsrBgEEAYORIQECEjANBgsr\n    BgEEAYORIQECEzANBgsrBgEEAYORIQECFDANBgsrBgEEAYORfwECAjANBgsrBgEE\n    AYORfwECAzANBgsrBgEEAYORfwECBDANBgsrBgEEAYORfwECBTANBgsrBgEEAYOR\n    fwECBjBVBgorBgEEAYORIQoBMEcwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNr\n    LmVlL0NQUzAiBggrBgEFBQcCAjAWGhRURVNUIG9mIEVFLUdvdkNBMjAxODAYBggr\n    BgEFBQcBAwQMMAowCAYGBACORgEBMAoGCCqGSM49BAMEA4GMADCBiAJCAeTjfRrM\n    t+4ecVYozAfdpTjCikf332XcuRkuJ6fbLqqMm7C3v/d5ebyOqvDG6wWAp8Z0GZA5\n    ONIvS2rm8kJ7HR5tAkIAoFn7n5ZW62dXMmPk+LReR1hUyTpxrxC31QjqvMqM2AbM\n    8luw0f/AaC5qsEdwKrKT+p1xvnjSyIVfcMiu6Q3T2EE=\n    -----END CERTIFICATE-----\nintermediates:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMwNjA5WhcN\n    MjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n    aXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAx\n    MTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOC\n    AQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu60MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD\n    79o+8r4SRYp9kowSFA8E1v38XXTHRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/H\n    rl9VmikIgAlaHvv3Z+MzS6qeLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+\n    Ky7MZ9jbBr+Nx4py/V7xm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQ\n    xrLjv3B67i19d7sNh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxW\n    adHDvVYUCJgW2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0T\n    AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYB\n    BAHOHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\n    aQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4A\n    ZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYDVR0OBBYE\n    FEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSx\n    ZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9z\n    aXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBd\n    h5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obPmJ7jRax3PX0pFhaUE6EhAR0d\n    gS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0SGj7aFLkcC+ULwu1k6yiortFJ0Ds\n    49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cHUijnf39Szq7E2z7MDfZkYg8HZeHrO493\n    EFghXcnSH7J7z47cgP3GWFNUKv1V2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+T\n    orEjxz//WzcF/Sklp4FeD0MU39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu6\n    3kW7RDqr+wQFSxKr9DIH\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\n    MjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n    aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\n    A1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\n    ggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n    8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\n    z8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n    5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\n    zs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n    46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n    3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\n    J7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\n    h5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\n    ot3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n    6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\n    AAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\n    HQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\n    IARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\n    ay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\n    AQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\n    AAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\n    BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\n    fTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\n    AQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\n    aWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\n    Mmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\n    Y3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\n    AH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\n    DgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\n    yPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\n    WFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\n    PKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFfDCCBN2gAwIBAgIQNhjzSfd2UEpbkO14EY4ORTAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwOTA2MDkwMzUyWhcNMzMwODMwMTI0ODI4WjBgMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MIGbMBAGByqGSM49AgEG\n    BSuBBAAjA4GGAAQBxYug4cEqwmIj+3TVaUlhfxCV9FQgfuglC2/0Ux1Ieqw11mDj\n    NvnGJhkWxaLbWJi7QtthMG5R104l7Np7lBevrBgBDtfgja9e3MLTQkY+cFS+UQxj\n    t9ZihTUJVsR7lowYlaGEiqqsGbEhlwfu27Xsm8b2rhSiTOvNdjTtG57NnwVAX+ij\n    ggMyMIIDLjAfBgNVHSMEGDAWgBR/DHDY9OWPAXfux20pKbn0yfxqwDAdBgNVHQ4E\n    FgQUwISZKcROnzsCNPaZ4QpWAAgpPnswDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n    /wQIMAYBAf8CAQAwggHNBgNVHSAEggHEMIIBwDAIBgYEAI96AQIwCQYHBACL7EAB\n    AjAyBgsrBgEEAYORIQECATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5l\n    ZS9DUFMwDQYLKwYBBAGDkSEBAgIwDQYLKwYBBAGDkX8BAgEwDQYLKwYBBAGDkSEB\n    AgUwDQYLKwYBBAGDkSEBAgYwDQYLKwYBBAGDkSEBAgcwDQYLKwYBBAGDkSEBAgMw\n    DQYLKwYBBAGDkSEBAgQwDQYLKwYBBAGDkSEBAggwDQYLKwYBBAGDkSEBAgkwDQYL\n    KwYBBAGDkSEBAgowDQYLKwYBBAGDkSEBAgswDQYLKwYBBAGDkSEBAgwwDQYLKwYB\n    BAGDkSEBAg0wDQYLKwYBBAGDkSEBAg4wDQYLKwYBBAGDkSEBAg8wDQYLKwYBBAGD\n    kSEBAhAwDQYLKwYBBAGDkSEBAhEwDQYLKwYBBAGDkSEBAhIwDQYLKwYBBAGDkSEB\n    AhMwDQYLKwYBBAGDkSEBAhQwDQYLKwYBBAGDkX8BAgIwDQYLKwYBBAGDkX8BAgMw\n    DQYLKwYBBAGDkX8BAgQwDQYLKwYBBAGDkX8BAgUwDQYLKwYBBAGDkX8BAgYwKgYD\n    VR0lAQH/BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB3BggrBgEF\n    BQcBAQRrMGkwLgYIKwYBBQUHMAGGImh0dHA6Ly9haWEuZGVtby5zay5lZS9lZS1n\n    b3ZjYTIwMTgwNwYIKwYBBQUHMAKGK2h0dHA6Ly9jLnNrLmVlL1Rlc3Rfb2ZfRUUt\n    R292Q0EyMDE4LmRlci5jcnQwGAYIKwYBBQUHAQMEDDAKMAgGBgQAjkYBATA4BgNV\n    HR8EMTAvMC2gK6AphidodHRwOi8vYy5zay5lZS9UZXN0X29mX0VFLUdvdkNBMjAx\n    OC5jcmwwCgYIKoZIzj0EAwQDgYwAMIGIAkIBIF+LqytyaV4o5wUSm30VysB8LdWt\n    oOrzNq2QhB6tGv4slg5z+CR58e60eRFqNxT7eccA/HgoPWs0B1Z+L067qtUCQgCB\n    8OP0kHx/j1t7htN2CXjpSjGFZw5TTI4s1eGyTbe0UJRBXEkUKfFbZVmzGPFPprwU\n    dSPi8PpO7+xGBYlFHA4z+Q==\n    -----END CERTIFICATE-----\n\nprofile: BES\n"
  },
  {
    "path": "common/collector/container/bdoc/testdata/trustTM.yaml",
    "content": "filecount: 50\nbdocsize: 102400  # 100 KiB\nfilesize: 102400  # 100 KiB\nroots:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n    GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n    cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n    aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n    xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n    ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n    MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n    VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n    kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n    pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n    A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n    AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n    BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n    I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n    Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n    SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n    tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n    SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFLDCCBI2gAwIBAgIQImvqKVwtGyZbh+ecdKPc7zAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwODMwMTI0ODI4WhcNMzMwODMwMTI0ODI4WjBiMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgwgZswEAYHKoZIzj0C\n    AQYFK4EEACMDgYYABABZN0DFpEKsj3SzsySoR/bcwAUoLc+S2HrvHY0xIDkFFTtU\n    QXfjxXyexNIx+ALe2IYJZLTl0T79C5by4/mO/5H7UgCxZZCRKtdcKqSGYJOVpT0X\n    oA51yX8eBk8aPVrTcwABcBhU6nTNGEoNXfeS7mrZB6Gs3eFxEVdejIEjNObWVFYM\n    bqOCAuAwggLcMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMDQG\n    A1UdJQEB/wQqMCgGCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQGCCsGAQUF\n    BwMBMB0GA1UdDgQWBBR/DHDY9OWPAXfux20pKbn0yfxqwDAfBgNVHSMEGDAWgBR/\n    DHDY9OWPAXfux20pKbn0yfxqwDCCAiQGA1UdIASCAhswggIXMAgGBgQAj3oBAjAJ\n    BgcEAIvsQAECMDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8v\n    d3d3LnNrLmVlL0NQUzANBgsrBgEEAYORIQECAjANBgsrBgEEAYORfwECATANBgsr\n    BgEEAYORIQECBTANBgsrBgEEAYORIQECBjANBgsrBgEEAYORIQECBzANBgsrBgEE\n    AYORIQECAzANBgsrBgEEAYORIQECBDANBgsrBgEEAYORIQECCDANBgsrBgEEAYOR\n    IQECCTANBgsrBgEEAYORIQECCjANBgsrBgEEAYORIQECCzANBgsrBgEEAYORIQEC\n    DDANBgsrBgEEAYORIQECDTANBgsrBgEEAYORIQECDjANBgsrBgEEAYORIQECDzAN\n    BgsrBgEEAYORIQECEDANBgsrBgEEAYORIQECETANBgsrBgEEAYORIQECEjANBgsr\n    BgEEAYORIQECEzANBgsrBgEEAYORIQECFDANBgsrBgEEAYORfwECAjANBgsrBgEE\n    AYORfwECAzANBgsrBgEEAYORfwECBDANBgsrBgEEAYORfwECBTANBgsrBgEEAYOR\n    fwECBjBVBgorBgEEAYORIQoBMEcwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNr\n    LmVlL0NQUzAiBggrBgEFBQcCAjAWGhRURVNUIG9mIEVFLUdvdkNBMjAxODAYBggr\n    BgEFBQcBAwQMMAowCAYGBACORgEBMAoGCCqGSM49BAMEA4GMADCBiAJCAeTjfRrM\n    t+4ecVYozAfdpTjCikf332XcuRkuJ6fbLqqMm7C3v/d5ebyOqvDG6wWAp8Z0GZA5\n    ONIvS2rm8kJ7HR5tAkIAoFn7n5ZW62dXMmPk+LReR1hUyTpxrxC31QjqvMqM2AbM\n    8luw0f/AaC5qsEdwKrKT+p1xvnjSyIVfcMiu6Q3T2EE=\n    -----END CERTIFICATE-----\nintermediates:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMwNjA5WhcN\n    MjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n    aXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAx\n    MTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOC\n    AQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu60MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD\n    79o+8r4SRYp9kowSFA8E1v38XXTHRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/H\n    rl9VmikIgAlaHvv3Z+MzS6qeLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+\n    Ky7MZ9jbBr+Nx4py/V7xm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQ\n    xrLjv3B67i19d7sNh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxW\n    adHDvVYUCJgW2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0T\n    AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYB\n    BAHOHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\n    aQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4A\n    ZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYDVR0OBBYE\n    FEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSx\n    ZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9z\n    aXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBd\n    h5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obPmJ7jRax3PX0pFhaUE6EhAR0d\n    gS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0SGj7aFLkcC+ULwu1k6yiortFJ0Ds\n    49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cHUijnf39Szq7E2z7MDfZkYg8HZeHrO493\n    EFghXcnSH7J7z47cgP3GWFNUKv1V2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+T\n    orEjxz//WzcF/Sklp4FeD0MU39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu6\n    3kW7RDqr+wQFSxKr9DIH\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\n    MjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n    aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\n    A1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\n    ggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n    8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\n    z8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n    5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\n    zs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n    46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n    3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\n    J7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\n    h5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\n    ot3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n    6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\n    AAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\n    HQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\n    IARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\n    ay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\n    AQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\n    AAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\n    BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\n    fTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\n    AQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\n    aWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\n    Mmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\n    Y3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\n    AH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\n    DgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\n    yPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\n    WFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\n    PKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFfDCCBN2gAwIBAgIQNhjzSfd2UEpbkO14EY4ORTAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwOTA2MDkwMzUyWhcNMzMwODMwMTI0ODI4WjBgMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MIGbMBAGByqGSM49AgEG\n    BSuBBAAjA4GGAAQBxYug4cEqwmIj+3TVaUlhfxCV9FQgfuglC2/0Ux1Ieqw11mDj\n    NvnGJhkWxaLbWJi7QtthMG5R104l7Np7lBevrBgBDtfgja9e3MLTQkY+cFS+UQxj\n    t9ZihTUJVsR7lowYlaGEiqqsGbEhlwfu27Xsm8b2rhSiTOvNdjTtG57NnwVAX+ij\n    ggMyMIIDLjAfBgNVHSMEGDAWgBR/DHDY9OWPAXfux20pKbn0yfxqwDAdBgNVHQ4E\n    FgQUwISZKcROnzsCNPaZ4QpWAAgpPnswDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n    /wQIMAYBAf8CAQAwggHNBgNVHSAEggHEMIIBwDAIBgYEAI96AQIwCQYHBACL7EAB\n    AjAyBgsrBgEEAYORIQECATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5l\n    ZS9DUFMwDQYLKwYBBAGDkSEBAgIwDQYLKwYBBAGDkX8BAgEwDQYLKwYBBAGDkSEB\n    AgUwDQYLKwYBBAGDkSEBAgYwDQYLKwYBBAGDkSEBAgcwDQYLKwYBBAGDkSEBAgMw\n    DQYLKwYBBAGDkSEBAgQwDQYLKwYBBAGDkSEBAggwDQYLKwYBBAGDkSEBAgkwDQYL\n    KwYBBAGDkSEBAgowDQYLKwYBBAGDkSEBAgswDQYLKwYBBAGDkSEBAgwwDQYLKwYB\n    BAGDkSEBAg0wDQYLKwYBBAGDkSEBAg4wDQYLKwYBBAGDkSEBAg8wDQYLKwYBBAGD\n    kSEBAhAwDQYLKwYBBAGDkSEBAhEwDQYLKwYBBAGDkSEBAhIwDQYLKwYBBAGDkSEB\n    AhMwDQYLKwYBBAGDkSEBAhQwDQYLKwYBBAGDkX8BAgIwDQYLKwYBBAGDkX8BAgMw\n    DQYLKwYBBAGDkX8BAgQwDQYLKwYBBAGDkX8BAgUwDQYLKwYBBAGDkX8BAgYwKgYD\n    VR0lAQH/BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB3BggrBgEF\n    BQcBAQRrMGkwLgYIKwYBBQUHMAGGImh0dHA6Ly9haWEuZGVtby5zay5lZS9lZS1n\n    b3ZjYTIwMTgwNwYIKwYBBQUHMAKGK2h0dHA6Ly9jLnNrLmVlL1Rlc3Rfb2ZfRUUt\n    R292Q0EyMDE4LmRlci5jcnQwGAYIKwYBBQUHAQMEDDAKMAgGBgQAjkYBATA4BgNV\n    HR8EMTAvMC2gK6AphidodHRwOi8vYy5zay5lZS9UZXN0X29mX0VFLUdvdkNBMjAx\n    OC5jcmwwCgYIKoZIzj0EAwQDgYwAMIGIAkIBIF+LqytyaV4o5wUSm30VysB8LdWt\n    oOrzNq2QhB6tGv4slg5z+CR58e60eRFqNxT7eccA/HgoPWs0B1Z+L067qtUCQgCB\n    8OP0kHx/j1t7htN2CXjpSjGFZw5TTI4s1eGyTbe0UJRBXEkUKfFbZVmzGPFPprwU\n    dSPi8PpO7+xGBYlFHA4z+Q==\n    -----END CERTIFICATE-----\n\nprofile: TM\nocsp:\n  responders:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEijCCA3KgAwIBAgIQaI8x6BnacYdNdNwlYnn/mzANBgkqhkiG9w0BAQUFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMyMjQ1WhcN\n      MjQwOTA3MTIyMjQ1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n      Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n      b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n      LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cw6Cja17BbYbHi6\n      frwccDI4BIQLk/fiCE8L45os0xhPgEGR+EHE8LPCIqofPgf4gwN1vDE6cQNUlK0O\n      d+Ush39i9Z45esnfpGq+2HsDJaFmFr5+uC1MEz5Kn1TazEvKbRjkGnSQ9BertlGe\n      r2BlU/kqOk5qA5RtJfhT0psc1ixKdPipv59wnf+nHx1+T+fPWndXVZLoDg4t3w8l\n      IvIE/KhOSMlErvBIHIAKV7yH1hOxyeGLghqzMiAn3UeTEOgoOS9URv0C/T5C3mH+\n      Y/uakMSxjNuz41PneimCzbEJZJRiEaMIj8qPAubcbL8GtY03MWmfNtX6/wh6u6TM\n      fW8S2wIDAQABo4H+MIH7MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMJMB0GA1UdDgQW\n      BBR9/5CuRokEgGiqSzYuZGYAogl8TzCBoAYDVR0gBIGYMIGVMIGSBgorBgEEAc4f\n      AwEBMIGDMFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQAaQBt\n      AGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4AZwAu\n      MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LnNrLmVlL2FqYXRlbXBlbC8wHwYDVR0j\n      BBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wDQYJKoZIhvcNAQEFBQADggEBAAba\n      j7kTruTAPHqToye9ZtBdaJ3FZjiKug9/5RjsMwDpOeqFDqCorLd+DBI4tgdu0g4l\n      haI3aVnKdRBkGV18kqp84uU97JRFWQEf6H8hpJ9k/LzAACkP3tD+0ym+md532mV+\n      nRz1Jj+RPLAUk9xYMV7KPczZN1xnl2wZDJwBbQpcSVH1DjlZv3tFLHBLIYTS6qOK\n      4SxStcgRq7KdRczfW6mfXzTCRWM3G9nmDei5Q3+XTED41j8szRWglzYf6zOv4djk\n      ja64WYraQ5zb4x8Xh7qTCk6UupZ7je+0oRfuz0h/3zyRdjcRPkjloSpQp/NG8Rmr\n      cnr874p8d9fdwCrRI7U=\n      -----END CERTIFICATE-----\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n      MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n      Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n      b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n      LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n      gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n      r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n      z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n      OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n      wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n      RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n      FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n      AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n      aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n      JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n      GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n      czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n      BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n      aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n      MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n      nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n      mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n      dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n      -----END CERTIFICATE-----\n\ntsp:\n  signers:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEFTCCAv2gAwIBAgIQTqz7bCP8W45UBZa7tztTTDANBgkqhkiG9w0BAQsFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTQwOTAyMTAwNjUxWhcN\n      MjQwOTAyMTAwNjUxWjBdMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n      aXRzZWVyaW1pc2tlc2t1czEMMAoGA1UECwwDVFNBMRwwGgYDVQQDDBNERU1PIG9m\n      IFNLIFRTQSAyMDE0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAysgr\n      VnVPxH8jNgCsJw0y+7fmmBDTM/tNB+xielnP9KcuQ+nyTgNu1JMpnry7Rh4ndr54\n      rPLXNGVdb/vsgsi8B558DisPVUn3Rur3/8XQ+BCkhTQIg1cSmyCsWxJgeaQKJi6W\n      GVaQWB2he35aVhL5F6ae/gzXT3sGGwnWujZkY9o5RapGV15+/b7Uv+7jWYFAxcD6\n      ba5jI00RY/gmsWwKb226Rnz/pXKDBfuN3ox7y5/lZf5+MyIcVe1qJe7VAJGpJFjN\n      q+BEEdvfqvJ1PiGQEDJAPhRqahVjBSzqZhJQoL3HI42NRCFwarvdnZYoCPxjeYpA\n      ynTHgNR7kKGX1iQ8OQIDAQABo4GwMIGtMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUB\n      Af8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUJwScZQxzlzySVqZXviXpKZDV5Nww\n      HwYDVR0jBBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wQwYDVR0fBDwwOjA4oDag\n      NIYyaHR0cHM6Ly93d3cuc2suZWUvcmVwb3NpdG9yeS9jcmxzL3Rlc3RfZWVjY3Jj\n      YS5jcmwwDQYJKoZIhvcNAQELBQADggEBAIq02SVKwP1UolKjqAQe7SVY/Kgi++G2\n      kqAd40UmMqa94GTu91LFZR5TvdoyZjjnQ2ioXh5CV2lflUy/lUrZMDpqEe7IbjZW\n      5+b9n5aBvXYJgDua9SYjMOrcy3siytqq8UbNgh79ubYgWhHhJSnLWK5YJ+5vQjTp\n      OMdRsLp/D+FhTUa6mP0UDY+U82/tFufkd9HW4zbalUWhQgnNYI3oo0CsZ0HExuyn\n      OOZmM1Bf8PzD6etlLSKkYB+mB77Omqgflzz+Jjyh45o+305MRzHDFeJZx7WxC+XT\n      NWQ0ZFTFfc0ozxxzUWUlfNfpWyQh3+4LbeSQRWrNkbNRfCpYotyM6AY=\n      -----END CERTIFICATE-----\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\n      MjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\n      VVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\n      A1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\n      ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n      7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\n      QOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\n      QZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\n      EuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\n      nAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\n      AwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\n      AwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\n      Cp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\n      hhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\n      LnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\n      dHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\n      n8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\n      fbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\n      F5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\n      dknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\n      Agm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\n      pIX8UV3N9w==\n      -----END CERTIFICATE-----\ntsdelaytime: 60\n"
  },
  {
    "path": "common/collector/container/bdoc/testdata/trustTS.yaml",
    "content": "filecount: 50\nbdocsize: 102400  # 100 KiB\nfilesize: 102400  # 100 KiB\nroots:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\n    GA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\n    cnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\n    aWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\n    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\n    xRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\n    ywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\n    MmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\n    VNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\n    kV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\n    pwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\n    A1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\n    AgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\n    BwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\n    I+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\n    Bmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\n    SnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\n    tH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\n    SfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFLDCCBI2gAwIBAgIQImvqKVwtGyZbh+ecdKPc7zAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwODMwMTI0ODI4WhcNMzMwODMwMTI0ODI4WjBiMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgwgZswEAYHKoZIzj0C\n    AQYFK4EEACMDgYYABABZN0DFpEKsj3SzsySoR/bcwAUoLc+S2HrvHY0xIDkFFTtU\n    QXfjxXyexNIx+ALe2IYJZLTl0T79C5by4/mO/5H7UgCxZZCRKtdcKqSGYJOVpT0X\n    oA51yX8eBk8aPVrTcwABcBhU6nTNGEoNXfeS7mrZB6Gs3eFxEVdejIEjNObWVFYM\n    bqOCAuAwggLcMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMDQG\n    A1UdJQEB/wQqMCgGCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQGCCsGAQUF\n    BwMBMB0GA1UdDgQWBBR/DHDY9OWPAXfux20pKbn0yfxqwDAfBgNVHSMEGDAWgBR/\n    DHDY9OWPAXfux20pKbn0yfxqwDCCAiQGA1UdIASCAhswggIXMAgGBgQAj3oBAjAJ\n    BgcEAIvsQAECMDIGCysGAQQBg5EhAQIBMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8v\n    d3d3LnNrLmVlL0NQUzANBgsrBgEEAYORIQECAjANBgsrBgEEAYORfwECATANBgsr\n    BgEEAYORIQECBTANBgsrBgEEAYORIQECBjANBgsrBgEEAYORIQECBzANBgsrBgEE\n    AYORIQECAzANBgsrBgEEAYORIQECBDANBgsrBgEEAYORIQECCDANBgsrBgEEAYOR\n    IQECCTANBgsrBgEEAYORIQECCjANBgsrBgEEAYORIQECCzANBgsrBgEEAYORIQEC\n    DDANBgsrBgEEAYORIQECDTANBgsrBgEEAYORIQECDjANBgsrBgEEAYORIQECDzAN\n    BgsrBgEEAYORIQECEDANBgsrBgEEAYORIQECETANBgsrBgEEAYORIQECEjANBgsr\n    BgEEAYORIQECEzANBgsrBgEEAYORIQECFDANBgsrBgEEAYORfwECAjANBgsrBgEE\n    AYORfwECAzANBgsrBgEEAYORfwECBDANBgsrBgEEAYORfwECBTANBgsrBgEEAYOR\n    fwECBjBVBgorBgEEAYORIQoBMEcwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNr\n    LmVlL0NQUzAiBggrBgEFBQcCAjAWGhRURVNUIG9mIEVFLUdvdkNBMjAxODAYBggr\n    BgEFBQcBAwQMMAowCAYGBACORgEBMAoGCCqGSM49BAMEA4GMADCBiAJCAeTjfRrM\n    t+4ecVYozAfdpTjCikf332XcuRkuJ6fbLqqMm7C3v/d5ebyOqvDG6wWAp8Z0GZA5\n    ONIvS2rm8kJ7HR5tAkIAoFn7n5ZW62dXMmPk+LReR1hUyTpxrxC31QjqvMqM2AbM\n    8luw0f/AaC5qsEdwKrKT+p1xvnjSyIVfcMiu6Q3T2EE=\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIClDCCAfagAwIBAgIUL1GpFX1ZsPJlEY4io20PlqVc97YwCgYIKoZIzj0EAwIw\n    WzELMAkGA1UEBhMCRUUxEjAQBgNVBAoMCVNDQ0VJViBPWTEfMB0GA1UECwwWSVZY\n    ViBUZXN0IENlcnRpZmljYXRlczEXMBUGA1UEAwwOUGVyc29uIENBIFJvb3QwIBcN\n    MjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMFsxCzAJBgNVBAYTAkVFMRIw\n    EAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0\n    ZXMxFzAVBgNVBAMMDlBlcnNvbiBDQSBSb290MIGbMBAGByqGSM49AgEGBSuBBAAj\n    A4GGAAQAmkJKRNcmA3sLfHOpeYEtt8+4k9RSynfP/BXu/1w2MMAcds81V+6K73By\n    bGMgOYTWqm50JrGxWAolJq8nN3+2nrMA8O5oijY9jDgeJfCpwGdEOcc/13/SOpUD\n    yRqmlk2ICQH29BCn3568q/Zl+fOsejEyUhb9M4mciuG1X6y8zyrxcKejUzBRMB0G\n    A1UdDgQWBBSDwaLGCvo/hFGYi6kknnJTGbX6gDAfBgNVHSMEGDAWgBSDwaLGCvo/\n    hFGYi6kknnJTGbX6gDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA4GLADCB\n    hwJBDv+4in/2r6IYZwAoDWT6nTNg+3Fg+lBj9kX+tGcuyOS3YwaJqEgiOrw1LKqf\n    G8SNHSw7Xw4Nydwp5/P9LdjK3qICQgC7tV2J3Ks8p7O9B1qy4H/GcIjdY9Jklrnv\n    nb7r6RfKneV/2vlyxz7PXeZndFw01J6ow267uww2yntaoSZFsB4Axw==\n    -----END CERTIFICATE-----\nintermediates:\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIEuzCCA6OgAwIBAgIQSxRID7FoIaNNdNhBeucLvDANBgkqhkiG9w0BAQUFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMwNjA5WhcN\n    MjMwOTA3MTIwNjA5WjBsMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n    aXRzZWVyaW1pc2tlc2t1czEfMB0GA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAx\n    MTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOC\n    AQ8AMIIBCgKCAQEA0SMr+A2QGMJuNpu60MgqKG0yLL7JfvjNtgs2hqWADDn1AQeD\n    79o+8r4SRYp9kowSFA8E1v38XXTHRq3nSZeToOC5DMAWjsKlm4x8hwwp31BXCs/H\n    rl9VmikIgAlaHvv3Z+MzS6qeLdzyYi/glPVrY42A6/kBApOJlOVLvAFdySNmFkY+\n    Ky7MZ9jbBr+Nx4py/V7xm9VD62Oe1lku4S4qd+VYcQ5jftbr4OFjBp9Nn58/5svQ\n    xrLjv3B67i19d7sNh7UPnMiO6BeBb6yb3P1lqdHofE1lElStIPViJlzjPOh4puxW\n    adHDvVYUCJgW2aM58mTfjFhZbVfcrVn5OyIiTQIDAQABo4IBRjCCAUIwDwYDVR0T\n    AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgZkGA1UdIASBkTCBjjCBiwYKKwYB\n    BAHOHwMBATB9MFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQA\n    aQBtAGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4A\n    ZwAuMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5lZS9DUFMwHQYDVR0OBBYE\n    FEG2/sWxsbRTE4z6+mLQNG1tIjQKMB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEOvsSx\n    ZUcbA3b+MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9z\n    aXRvcnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBd\n    h5R23K7qkrO78j51xN6CR2qwxUcK/cgcTLWv0obPmJ7jRax3PX0pFhaUE6EhAR0d\n    gS4u6XZrjPgVrt/mwq1h8lJP1MF2ueAHKyS0SGj7aFLkcC+ULwu1k6yiortFJ0Ds\n    49ZGA+ioGzYWPQ+g1Zl4wSDIz52ot0cHUijnf39Szq7E2z7MDfZkYg8HZeHrO493\n    EFghXcnSH7J7z47cgP3GWFNUKv1V2c0eVE4OxRulZ3KmBLPWbJKZ0TyGa/Aooc+T\n    orEjxz//WzcF/Sklp4FeD0MU39UURIlg7LfEcm832bPzZzVGFd4drBd5Dy0Uquu6\n    3kW7RDqr+wQFSxKr9DIH\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\n    MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n    czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n    IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\n    MjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\n    aWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\n    A1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\n    ggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n    8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\n    z8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n    5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\n    zs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n    46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n    3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\n    J7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\n    h5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\n    ot3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n    6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\n    AAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\n    HQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\n    IARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\n    ay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\n    AQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\n    AAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\n    BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\n    fTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\n    AQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\n    aWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\n    Mmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\n    Y3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\n    AH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\n    DgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\n    yPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\n    WFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\n    PKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIIFfDCCBN2gAwIBAgIQNhjzSfd2UEpbkO14EY4ORTAKBggqhkjOPQQDBDBiMQsw\n    CQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\n    DA5OVFJFRS0xMDc0NzAxMzEdMBsGA1UEAwwUVEVTVCBvZiBFRS1Hb3ZDQTIwMTgw\n    HhcNMTgwOTA2MDkwMzUyWhcNMzMwODMwMTI0ODI4WjBgMQswCQYDVQQGEwJFRTEb\n    MBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRhDA5OVFJFRS0xMDc0\n    NzAxMzEbMBkGA1UEAwwSVEVTVCBvZiBFU1RFSUQyMDE4MIGbMBAGByqGSM49AgEG\n    BSuBBAAjA4GGAAQBxYug4cEqwmIj+3TVaUlhfxCV9FQgfuglC2/0Ux1Ieqw11mDj\n    NvnGJhkWxaLbWJi7QtthMG5R104l7Np7lBevrBgBDtfgja9e3MLTQkY+cFS+UQxj\n    t9ZihTUJVsR7lowYlaGEiqqsGbEhlwfu27Xsm8b2rhSiTOvNdjTtG57NnwVAX+ij\n    ggMyMIIDLjAfBgNVHSMEGDAWgBR/DHDY9OWPAXfux20pKbn0yfxqwDAdBgNVHQ4E\n    FgQUwISZKcROnzsCNPaZ4QpWAAgpPnswDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB\n    /wQIMAYBAf8CAQAwggHNBgNVHSAEggHEMIIBwDAIBgYEAI96AQIwCQYHBACL7EAB\n    AjAyBgsrBgEEAYORIQECATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5zay5l\n    ZS9DUFMwDQYLKwYBBAGDkSEBAgIwDQYLKwYBBAGDkX8BAgEwDQYLKwYBBAGDkSEB\n    AgUwDQYLKwYBBAGDkSEBAgYwDQYLKwYBBAGDkSEBAgcwDQYLKwYBBAGDkSEBAgMw\n    DQYLKwYBBAGDkSEBAgQwDQYLKwYBBAGDkSEBAggwDQYLKwYBBAGDkSEBAgkwDQYL\n    KwYBBAGDkSEBAgowDQYLKwYBBAGDkSEBAgswDQYLKwYBBAGDkSEBAgwwDQYLKwYB\n    BAGDkSEBAg0wDQYLKwYBBAGDkSEBAg4wDQYLKwYBBAGDkSEBAg8wDQYLKwYBBAGD\n    kSEBAhAwDQYLKwYBBAGDkSEBAhEwDQYLKwYBBAGDkSEBAhIwDQYLKwYBBAGDkSEB\n    AhMwDQYLKwYBBAGDkSEBAhQwDQYLKwYBBAGDkX8BAgIwDQYLKwYBBAGDkX8BAgMw\n    DQYLKwYBBAGDkX8BAgQwDQYLKwYBBAGDkX8BAgUwDQYLKwYBBAGDkX8BAgYwKgYD\n    VR0lAQH/BCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDB3BggrBgEF\n    BQcBAQRrMGkwLgYIKwYBBQUHMAGGImh0dHA6Ly9haWEuZGVtby5zay5lZS9lZS1n\n    b3ZjYTIwMTgwNwYIKwYBBQUHMAKGK2h0dHA6Ly9jLnNrLmVlL1Rlc3Rfb2ZfRUUt\n    R292Q0EyMDE4LmRlci5jcnQwGAYIKwYBBQUHAQMEDDAKMAgGBgQAjkYBATA4BgNV\n    HR8EMTAvMC2gK6AphidodHRwOi8vYy5zay5lZS9UZXN0X29mX0VFLUdvdkNBMjAx\n    OC5jcmwwCgYIKoZIzj0EAwQDgYwAMIGIAkIBIF+LqytyaV4o5wUSm30VysB8LdWt\n    oOrzNq2QhB6tGv4slg5z+CR58e60eRFqNxT7eccA/HgoPWs0B1Z+L067qtUCQgCB\n    8OP0kHx/j1t7htN2CXjpSjGFZw5TTI4s1eGyTbe0UJRBXEkUKfFbZVmzGPFPprwU\n    dSPi8PpO7+xGBYlFHA4z+Q==\n    -----END CERTIFICATE-----\n  - |\n    -----BEGIN CERTIFICATE-----\n    MIICajCCAcygAwIBAgIDBwoAMAoGCCqGSM49BAMCMFsxCzAJBgNVBAYTAkVFMRIw\n    EAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0\n    ZXMxFzAVBgNVBAMMDlBlcnNvbiBDQSBSb290MCAXDTIxMDQyODE0MjcxMFoYDzIx\n    MjEwNDA0MTQyNzEwWjBjMQswCQYDVQQGEwJFRTESMBAGA1UECgwJU0NDRUlWIE9Z\n    MR8wHQYDVQQLDBZJVlhWIFRlc3QgQ2VydGlmaWNhdGVzMR8wHQYDVQQDDBZQZXJz\n    b24gQ0EgSW50ZXJtZWRpYXRlMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBCKJT\n    G8rwbahrnCbQBpe63faLfrnAek9ncNn7ny49qbgP3O3V6XuNM8XOAtUCtgdV2yNU\n    H6E/YUn651shS72gjeYBe8k9Pk8tC/4vlWDwwiVJV0TZmzG21+Rgtm/hxwoDlt+W\n    4BsTIU666UimGpRmlFngcCUZrFYwDLmwDlJCBIXxkuijMjAwMB0GA1UdDgQWBBSB\n    /0fgkaiRfrYt+Emw9CeK2MmmNjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMC\n    A4GLADCBhwJBLRYBoSix1j9/x8C5E+mt0vwGL5oxerR7JoJo/4WbUWkEt7e90Skl\n    0Liy3bH6X2v14MxjdeW72wyIvb50pnTFdJQCQgHBEy3ebBQoopEN8ioDJBFVuYLd\n    lY6RO9hzt5HZ6LrNkliPjPeqPSDOAl8BtiNYZVOa9C9RFDX0hzUmpOTiZMcbww==\n    -----END CERTIFICATE-----\n\nprofile: TS\nocsp:\n  responders:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEijCCA3KgAwIBAgIQaI8x6BnacYdNdNwlYnn/mzANBgkqhkiG9w0BAQUFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTEwMzA3MTMyMjQ1WhcN\n      MjQwOTA3MTIyMjQ1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n      Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n      b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAxMTEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n      LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0cw6Cja17BbYbHi6\n      frwccDI4BIQLk/fiCE8L45os0xhPgEGR+EHE8LPCIqofPgf4gwN1vDE6cQNUlK0O\n      d+Ush39i9Z45esnfpGq+2HsDJaFmFr5+uC1MEz5Kn1TazEvKbRjkGnSQ9BertlGe\n      r2BlU/kqOk5qA5RtJfhT0psc1ixKdPipv59wnf+nHx1+T+fPWndXVZLoDg4t3w8l\n      IvIE/KhOSMlErvBIHIAKV7yH1hOxyeGLghqzMiAn3UeTEOgoOS9URv0C/T5C3mH+\n      Y/uakMSxjNuz41PneimCzbEJZJRiEaMIj8qPAubcbL8GtY03MWmfNtX6/wh6u6TM\n      fW8S2wIDAQABo4H+MIH7MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMJMB0GA1UdDgQW\n      BBR9/5CuRokEgGiqSzYuZGYAogl8TzCBoAYDVR0gBIGYMIGVMIGSBgorBgEEAc4f\n      AwEBMIGDMFgGCCsGAQUFBwICMEweSgBBAGkAbgB1AGwAdAAgAHQAZQBzAHQAaQBt\n      AGkAcwBlAGsAcwAuACAATwBuAGwAeQAgAGYAbwByACAAdABlAHMAdABpAG4AZwAu\n      MCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LnNrLmVlL2FqYXRlbXBlbC8wHwYDVR0j\n      BBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wDQYJKoZIhvcNAQEFBQADggEBAAba\n      j7kTruTAPHqToye9ZtBdaJ3FZjiKug9/5RjsMwDpOeqFDqCorLd+DBI4tgdu0g4l\n      haI3aVnKdRBkGV18kqp84uU97JRFWQEf6H8hpJ9k/LzAACkP3tD+0ym+md532mV+\n      nRz1Jj+RPLAUk9xYMV7KPczZN1xnl2wZDJwBbQpcSVH1DjlZv3tFLHBLIYTS6qOK\n      4SxStcgRq7KdRczfW6mfXzTCRWM3G9nmDei5Q3+XTED41j8szRWglzYf6zOv4djk\n      ja64WYraQ5zb4x8Xh7qTCk6UupZ7je+0oRfuz0h/3zyRdjcRPkjloSpQp/NG8Rmr\n      cnr874p8d9fdwCrRI7U=\n      -----END CERTIFICATE-----\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\n      MjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\n      Zml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\n      b2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\n      LmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\n      gOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\n      r1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\n      z00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\n      OM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\n      wB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\n      RrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\n      FIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\n      AQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\n      aQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\n      JwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\n      GDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\n      czovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\n      BgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\n      aC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\n      MHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\n      nH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\n      mawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\n      dh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n      -----END CERTIFICATE-----\n\ntsp:\n  signers:\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEFTCCAv2gAwIBAgIQTqz7bCP8W45UBZa7tztTTDANBgkqhkiG9w0BAQsFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTQwOTAyMTAwNjUxWhcN\n      MjQwOTAyMTAwNjUxWjBdMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlm\n      aXRzZWVyaW1pc2tlc2t1czEMMAoGA1UECwwDVFNBMRwwGgYDVQQDDBNERU1PIG9m\n      IFNLIFRTQSAyMDE0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAysgr\n      VnVPxH8jNgCsJw0y+7fmmBDTM/tNB+xielnP9KcuQ+nyTgNu1JMpnry7Rh4ndr54\n      rPLXNGVdb/vsgsi8B558DisPVUn3Rur3/8XQ+BCkhTQIg1cSmyCsWxJgeaQKJi6W\n      GVaQWB2he35aVhL5F6ae/gzXT3sGGwnWujZkY9o5RapGV15+/b7Uv+7jWYFAxcD6\n      ba5jI00RY/gmsWwKb226Rnz/pXKDBfuN3ox7y5/lZf5+MyIcVe1qJe7VAJGpJFjN\n      q+BEEdvfqvJ1PiGQEDJAPhRqahVjBSzqZhJQoL3HI42NRCFwarvdnZYoCPxjeYpA\n      ynTHgNR7kKGX1iQ8OQIDAQABo4GwMIGtMA4GA1UdDwEB/wQEAwIGwDAWBgNVHSUB\n      Af8EDDAKBggrBgEFBQcDCDAdBgNVHQ4EFgQUJwScZQxzlzySVqZXviXpKZDV5Nww\n      HwYDVR0jBBgwFoAUtTQKnaUvEMXnIQ6+xLFlRxsDdv4wQwYDVR0fBDwwOjA4oDag\n      NIYyaHR0cHM6Ly93d3cuc2suZWUvcmVwb3NpdG9yeS9jcmxzL3Rlc3RfZWVjY3Jj\n      YS5jcmwwDQYJKoZIhvcNAQELBQADggEBAIq02SVKwP1UolKjqAQe7SVY/Kgi++G2\n      kqAd40UmMqa94GTu91LFZR5TvdoyZjjnQ2ioXh5CV2lflUy/lUrZMDpqEe7IbjZW\n      5+b9n5aBvXYJgDua9SYjMOrcy3siytqq8UbNgh79ubYgWhHhJSnLWK5YJ+5vQjTp\n      OMdRsLp/D+FhTUa6mP0UDY+U82/tFufkd9HW4zbalUWhQgnNYI3oo0CsZ0HExuyn\n      OOZmM1Bf8PzD6etlLSKkYB+mB77Omqgflzz+Jjyh45o+305MRzHDFeJZx7WxC+XT\n      NWQ0ZFTFfc0ozxxzUWUlfNfpWyQh3+4LbeSQRWrNkbNRfCpYotyM6AY=\n      -----END CERTIFICATE-----\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\n      MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\n      czEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\n      IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\n      MjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\n      VVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\n      A1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\n      ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n      7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\n      QOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\n      QZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\n      EuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\n      nAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\n      AwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\n      AwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\n      Cp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\n      hhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\n      LnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\n      dHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\n      n8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\n      fbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\n      F5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\n      dknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\n      Agm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\n      pIX8UV3N9w==\n      -----END CERTIFICATE-----\n    - |\n      -----BEGIN CERTIFICATE-----\n      MIIDEjCCApigAwIBAgIQM7BQCImkdt18qWDYdbfOtjAKBggqhkjOPQQDAjBlMSAw\n      HgYDVQQDDBdURVNUIG9mIFNLIFRTQSBDQSAyMDIzRTEXMBUGA1UEYQwOTlRSRUUt\n      MTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMC\n      RUUwHhcNMjMwNjE1MDcxMjA0WhcNMjkwNjE0MDcxMjAzWjByMS0wKwYDVQQDDCRE\n      RU1PIFNLIFRJTUVTVEFNUElORyBBVVRIT1JJVFkgMjAyM0UxFzAVBgNVBGEMDk5U\n      UkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNV\n      BAYTAkVFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFlmfS6324KQUsz5xSbkG\n      0PxwZfi94mYeuZkculhxkgmIAD3/sSOIoNqRTHg9Jl4tR2VNcMocjLRli474M6SK\n      LqOCARswggEXMB8GA1UdIwQYMBaAFGkForSjh0uOXxhFLdWxlzTPZzu3MG8GCCsG\n      AQUFBwEBBGMwYTA7BggrBgEFBQcwAoYvaHR0cHM6Ly9jLnNrLmVlL1RFU1Rfb2Zf\n      U0tfVFNBX0NBXzIwMjNFLmRlci5jcnQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9kZW1v\n      LnNrLmVlL29jc3AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwPAYDVR0fBDUwMzAx\n      oC+gLYYraHR0cHM6Ly9jLnNrLmVlL1RFU1Rfb2ZfU0tfVFNBX0NBXzIwMjNFLmNy\n      bDAdBgNVHQ4EFgQUPmDgaUB5qWkDeoNoc62C/QKk93YwDgYDVR0PAQH/BAQDAgbA\n      MAoGCCqGSM49BAMCA2gAMGUCMAK0/sP+jVQFNFakD4SeVy9xAZovv7T9WuaKfztg\n      defdJNMm8gaS9HpAa/wwVvnjqQIxAOU2sPULdJMNC6qw563eDasMq9fRUnAf17+/\n      I+byednRNGW3SGYtyGWN8IKKBut4lA==\n      -----END CERTIFICATE-----\ntsdelaytime: 60\n"
  },
  {
    "path": "common/collector/container/bdoc/xades.go",
    "content": "package bdoc\n\n// http://www.etsi.org/deliver/etsi_ts/102900_102999/102918/01.02.01_60/ts_102918v010201p.pdf -\n// annex A.5\ntype xadesSignatures struct {\n\tXMLElement c14n      `xmlx:\"http://uri.etsi.org/02918/v1.2.1# XAdESSignatures\"`\n\tSignature  signature // Restrict to exactly one Signature.\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-Signature\ntype signature struct {\n\tXMLElement     c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# Signature\"`\n\tID             string `xmlx:\"Id,attr,unique\"`\n\tSignedInfo     signedInfo\n\tSignatureValue signatureValue\n\tKeyInfo        keyInfo // Restrict to exactly one entry: signer's X.509 certificate.\n\tObject         object  // Restrict to exactly one entry: XAdES QualifyingProperties.\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-SignedInfo\ntype signedInfo struct {\n\tXMLElement             c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# SignedInfo,c14nroot\"`\n\tID                     string `xmlx:\"Id,attr,optional,unique\"`\n\tCanonicalizationMethod canonicalizationMethod\n\tSignatureMethod        signatureMethod\n\tReference              []reference\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-CanonicalizationMethod\ntype canonicalizationMethod struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# CanonicalizationMethod\"`\n\tAlgorithm  string `xmlx:\",attr\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-SignatureMethod\ntype signatureMethod struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# SignatureMethod\"`\n\tAlgorithm  string `xmlx:\",attr\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-Reference\ntype reference struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# Reference\"`\n\tID         string `xmlx:\"Id,attr,unique\"`\n\tURI        string `xmlx:\",attr\"`\n\tType       string `xmlx:\",attr,optional\"`\n\n\tTransforms   transforms `xmlx:\",optional\"`\n\tDigestMethod digestMethod\n\tDigestValue  digestValue\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-Transforms\ntype transforms struct {\n\tXMLElement c14n `xmlx:\"http://www.w3.org/2000/09/xmldsig# Transforms\"`\n\tTransform  []transform\n}\n\ntype transform struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# Transform\"`\n\tAlgorithm  string `xmlx:\",attr\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-DigestMethod\ntype digestMethod struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# DigestMethod\"`\n\tAlgorithm  string `xmlx:\",attr\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-DigestValue\ntype digestValue struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# DigestValue\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-SignatureValue\ntype signatureValue struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# SignatureValue,c14nroot\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-KeyInfo\ntype keyInfo struct {\n\tXMLElement c14n     `xmlx:\"http://www.w3.org/2000/09/xmldsig# KeyInfo\"`\n\tID         string   `xmlx:\"Id,attr,optional,unique\"`\n\tX509Data   x509Data // Restrict to exactly one entry: signer's X.509 certificate.\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-X509Data\ntype x509Data struct {\n\tXMLElement      c14n            `xmlx:\"http://www.w3.org/2000/09/xmldsig# X509Data\"`\n\tX509Certificate x509Certificate // Restrict to exactly one entry: signer's X.509 certificate.\n}\n\ntype x509Certificate struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# X509Certificate\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-Object\ntype object struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# Object\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\t// MimeType not allowed.\n\t// Encoding not allowed.\n\n\tQualifyingProperties qualifyingProperties // Restrict to exactly one entry: XAdES QualifyingProperties.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2\ntype qualifyingProperties struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# QualifyingProperties\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\tTarget     string `xmlx:\",attr\"`\n\n\tSignedProperties   signedProperties\n\tUnsignedProperties unsignedProperties `xmlx:\",optional\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2.1\ntype signedProperties struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignedProperties,c14nroot\"`\n\tID         string `xmlx:\"Id,attr,unique\"`\n\n\tSignedSignatureProperties  signedSignatureProperties\n\tSignedDataObjectProperties signedDataObjectProperties\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2.3\ntype signedSignatureProperties struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignedSignatureProperties\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\n\tSigningTime               signingTime\n\tSigningCertificate        signingCertificate\n\tSignaturePolicyIdentifier signaturePolicyIdentifier `xmlx:\",optional\"`\n\n\t// SignatureProductionPlace and SignerRole are actually not allowed in\n\t// our use case, but empty elements are put here by DigiDocService: so\n\t// allow the elements, but no actual content.\n\tSignatureProductionPlace signatureProductionPlace `xmlx:\",optional\"`\n\tSignerRole               signerRole               `xmlx:\",optional\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.1\ntype signingTime struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigningTime\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.2\ntype signingCertificate struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigningCertificate\"`\n\tCert       cert // Restrict to exactly one entry: signer's Cert.\n}\n\ntype cert struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# Cert\"`\n\t// URI not allowed.\n\n\tCertDigest   certDigest\n\tIssuerSerial issuerSerial\n}\n\ntype certDigest struct {\n\tXMLElement   c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# CertDigest\"`\n\tDigestMethod digestMethod\n\tDigestValue  digestValue\n}\n\ntype issuerSerial struct {\n\tXMLElement       c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# IssuerSerial\"`\n\tX509IssuerName   x509IssuerName\n\tX509SerialNumber x509SerialNumber\n}\n\n// https://www.w3.org/TR/xmldsig-core/#sec-X509Data\ntype x509IssuerName struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# X509IssuerName\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\ntype x509SerialNumber struct {\n\tXMLElement c14n   `xmlx:\"http://www.w3.org/2000/09/xmldsig# X509SerialNumber\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.3\ntype signaturePolicyIdentifier struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignaturePolicyIdentifier\"`\n\n\tSignaturePolicyID signaturePolicyID\n\t// SignaturePolicyImplied not allowed.\n}\n\ntype signaturePolicyID struct {\n\tXMLElement  c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignaturePolicyId\"`\n\tSigPolicyID sigPolicyID\n\t// Transforms not allowed.\n\tSigPolicyHash       sigPolicyHash\n\tSigPolicyQualifiers sigPolicyQualifiers\n}\n\ntype sigPolicyID struct {\n\tXMLElement  c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigPolicyId\"`\n\tIdentifier  identifier\n\tDescription description `xmlx:\",optional\"`\n\t// DocumentationReferences not allowed.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.1.2\ntype identifier struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# Identifier\"`\n\tQualifier  string `xmlx:\",attr\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\ntype description struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# Description\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.3\ntype sigPolicyHash struct {\n\tXMLElement   c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigPolicyHash\"`\n\tDigestMethod digestMethod\n\tDigestValue  digestValue\n}\n\ntype sigPolicyQualifiers struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigPolicyQualifiers\"`\n\n\tSigPolicyQualifier sigPolicyQualifier // Restrict to exactly one entry: BDOC URI.\n}\n\ntype sigPolicyQualifier struct {\n\tXMLElement c14n  `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SigPolicyQualifier\"`\n\tSPURI      spURI // Restrict to exactly one type: BDOC URI.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.3.1\ntype spURI struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SPURI\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.7\ntype signatureProductionPlace struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignatureProductionPlace\"`\n\n\tCity            city            `xmlx:\",optional\"`\n\tStateOrProvince stateOrProvince `xmlx:\",optional\"`\n\tPostalCode      postalCode      `xmlx:\",optional\"`\n\tCountryName     countryName     `xmlx:\",optional\"`\n}\n\ntype city struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# City\"`\n\t// No content allowed: see signedSignatureProperties.\n}\n\ntype stateOrProvince struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# StateOrProvince\"`\n\t// No content allowed: see signedSignatureProperties.\n}\n\ntype postalCode struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# PostalCode\"`\n\t// No content allowed: see signedSignatureProperties.\n}\n\ntype countryName struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# CountryName\"`\n\t// No content allowed: see signedSignatureProperties.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.8\ntype signerRole struct {\n\tXMLElement   c14n         `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignerRole\"`\n\tClaimedRoles claimedRoles `xmlx:\",optional\"`\n\t// CertifiedRoles not allowed.\n}\n\ntype claimedRoles struct {\n\tXMLElement  c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# ClaimedRoles\"`\n\tClaimedRole []claimedRole\n}\n\ntype claimedRole struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# ClaimedRole\"`\n\t// No content allowed: see signedSignatureProperties.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2.4\ntype signedDataObjectProperties struct {\n\tXMLElement       c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignedDataObjectProperties\"`\n\tID               string `xmlx:\"Id,attr,optional,unique\"`\n\tDataObjectFormat []dataObjectFormat\n\t// CommitmentTypeIndication not allowed.\n\t// AllDataObjectsTimeStamp not allowed.\n\t// IndividualDataObjectsTimeStamp not allowed.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.2.5\ntype dataObjectFormat struct {\n\tXMLElement      c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# DataObjectFormat\"`\n\tObjectReference string `xmlx:\",attr\"`\n\n\t// Description not allowed.\n\t// ObjectIdentifier not allowed.\n\tMimeType mimeType\n\t// Encoding not allowed.\n}\n\ntype mimeType struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# MimeType\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2.2\ntype unsignedProperties struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# UnsignedProperties\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\n\tUnsignedSignatureProperties unsignedSignatureProperties\n\t// UnsignedDataObjectProperties not allowed.\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 6.2.5\ntype unsignedSignatureProperties struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# UnsignedSignatureProperties\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\n\t// Restrict to an optional SignatureTimeStamp and mandatory\n\t// CertificateValues and RevocationValues.\n\tSignatureTimeStamp xadesTimeStamp `xmlx:\",optional\"`\n\tCertificateValues  certificateValues\n\tRevocationValues   revocationValues\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.1.4.3\ntype xadesTimeStamp struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# SignatureTimeStamp\"`\n\tID         string `xmlx:\"Id,attr,unique\"`\n\n\tCanonicalizationMethod canonicalizationMethod `xmlx:\",optional\"`\n\tEncapsulatedTimeStamp  encapsulatedTimeStamp  // Restrict to exactly one timestamp.\n}\n\ntype encapsulatedTimeStamp struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# EncapsulatedTimeStamp\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.6.1\ntype certificateValues struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# CertificateValues\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\n\tEncapsulatedX509Certificate []encapsulatedX509Certificate\n\t// OtherCertificate not allowed.\n}\n\ntype encapsulatedX509Certificate struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# EncapsulatedX509Certificate\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\t// Encoding not allowed: always http://uri.etsi.org/01903/v1.2.2#DER.\n\n\tValue string `xmlx:\",chardata\"`\n}\n\n// http://uri.etsi.org/01903/v1.3.2/ts_101903v010302p.pdf - section 7.6.2\ntype revocationValues struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# RevocationValues\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\n\t// CRLValues not allowed.\n\tOCSPValues ocspValues\n\t// OtherValues not allowed.\n}\n\ntype ocspValues struct {\n\tXMLElement c14n `xmlx:\"http://uri.etsi.org/01903/v1.3.2# OCSPValues\"`\n\n\tEncapsulatedOCSPValue encapsulatedOCSPValue // Restrict to exactly one entry: signer's OCSP response.\n}\n\ntype encapsulatedOCSPValue struct {\n\tXMLElement c14n   `xmlx:\"http://uri.etsi.org/01903/v1.3.2# EncapsulatedOCSPValue\"`\n\tID         string `xmlx:\"Id,attr,optional,unique\"`\n\tValue      string `xmlx:\",chardata\"`\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/xml.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n)\n\nconst xmlns = \"xmlns\"\n\n// new DigiDoc Client uses Open Document Format for Office\n// Applications (OpenDocument) v1.2, in which extra Space\n// entry \"manifest\" added to the manifest.xml file of the\n// digitally signed container: \"manifest:version=1.2\"\nconst digidocManifestExtraSpace = \"manifest\"\n\n// new DigiDoc Client uses Open Document Format for Office\n// Applications (OpenDocument) v1.2, in which extra Local\n// entry \"version\" added to the manifest.xml file of the\n// digitally signed container: \"manifest:version=1.2\"\nconst digidocManifestExtraLocal = \"version\"\n\n// parser performs syntactic analysis on an XML document.\n//\n// We use encoding/xml.Decoder as the XML lexer, but it is too lenient for us\n// to use as the parser. E.g., if an element occurs multiple times, then only\n// the last element is reported. This is not acceptable for signature parsing.\n// Additionally, we want to use the parsed tokens to reconstruct canonical XML,\n// but the Decoder discards too much information for this to be possible.\n//\n// So we resort to parsing the raw stream of XML lexemes ourselves.\ntype parser struct {\n\td    *xml.Decoder\n\topen []xml.Name // Tracks open XML elements. Names are untranslated.\n\tns   mapstack\n\n\t// Lookahead values for readToken and unreadToken.\n\tnextToken xml.Token\n\tnextError error\n\n\t// Uniqueness set for parsed attribute values.\n\tunique map[string]struct{}\n}\n\nfunc newParser(r io.Reader) *parser {\n\treturn &parser{\n\t\td:      xml.NewDecoder(r),\n\t\tunique: make(map[string]struct{}),\n\t}\n}\n\n// token returns the next lexeme for syntactic analysis. This method is largely\n// a copy of encoding/xml.Decoder.Token with Strict = true, but it returns the\n// local startElement instead of xml.StartElement and ignores comments.\nfunc (p *parser) token() (xml.Token, error) {\n\tt, err := p.d.RawToken()\n\tif err != nil {\n\t\tif err == io.EOF {\n\t\t\tif len(p.open) > 0 {\n\t\t\t\terr = UnexpectedParseEOFError{\n\t\t\t\t\tUnclosed:    p.open[len(p.open)-1],\n\t\t\t\t\tDescription: _CONTAINER_XML_EOF,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, RawTokenError{Err: err,\n\t\t\tDescription: _CONTAINER_XML_TOKEN_PARSE}\n\t}\n\tswitch tt := t.(type) {\n\tcase xml.StartElement:\n\t\t// Push element and any new namespaces on the stack. Replace\n\t\t// with translated startElement.\n\t\tif err = p.push(&tt); err != nil {\n\t\t\treturn t, err\n\t\t}\n\t\tts := startElement{nsprefix: tt.Name.Space, name: tt.Name}\n\t\tif err = p.translate(&ts.name.Space, false); err != nil {\n\t\t\treturn t, err\n\t\t}\n\t\tfor _, tta := range tt.Attr {\n\t\t\ta := attr{nsprefix: tta.Name.Space, Attr: tta}\n\t\t\tif err = p.translate(&a.Name.Space, true); err != nil {\n\t\t\t\treturn t, err\n\t\t\t}\n\t\t\tif findAttr(ts.attr, a.Name) != nil {\n\t\t\t\treturn t, DuplicateAttributeError{\n\t\t\t\t\tElement:     ts.name,\n\t\t\t\t\tAttribute:   a.Name,\n\t\t\t\t\tDescription: _CONTAINER_XML_ATTR_DUP,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif a.Value == \"\" { // Allowed by XML, but not us.\n\t\t\t\treturn t, EmptyAttributeError{\n\t\t\t\t\tElement:     ts.name,\n\t\t\t\t\tAttribute:   a.Name,\n\t\t\t\t\tDescription: _CONTAINER_XML_ATTR_EMPTY,\n\t\t\t\t}\n\t\t\t}\n\t\t\tts.attr = append(ts.attr, a)\n\t\t}\n\t\tt = ts\n\tcase xml.EndElement:\n\t\t// Translate the end element using the current namespace. Check\n\t\t// that the untranslated name matches the top of the stack and\n\t\t// undo any namespace bindings.\n\t\tname := tt.Name\n\t\tp.translate(&tt.Name.Space, false) //nolint:errcheck // Succeeds if pop does.\n\t\tt, err = tt, p.pop(name)\n\tcase xml.Comment:\n\t\t// Skip any comments: they are irrelevant to parsing.\n\t\treturn p.token()\n\t}\n\treturn t, err\n}\n\n// attr and startElement are like their encoding/xml counterparts, but preserve\n// original namespace prefix required for canonical re-encoding.\ntype attr struct {\n\tnsprefix string\n\txml.Attr\n}\n\nfunc (a attr) namespace() bool {\n\t// Use nsprefix and not Name.Space which is translated and can cause\n\t// problems if \"xmlns\" is used as a namespace URI.\n\treturn a.nsprefix == xmlns ||\n\t\t(a.nsprefix == digidocManifestExtraSpace &&\n\t\t\ta.Name.Local == digidocManifestExtraLocal) ||\n\t\ta.nsprefix == \"\" && a.Name.Local == xmlns\n}\n\ntype startElement struct {\n\tnsprefix string\n\tname     xml.Name\n\tattr     []attr\n}\n\nfunc (p *parser) translate(space *string, attr bool) error {\n\t// Skip if attribute without a namespace or declares a new namespace.\n\tif attr && (*space == \"\" || *space == xmlns) {\n\t\treturn nil\n\t}\n\tif url, ok := p.ns.get(*space); ok || *space == \"\" {\n\t\t// Allow default namespace (\"\") to be undeclared in which case\n\t\t// it's value is \"\": also the value of uri if !ok.\n\t\t*space = url\n\t\treturn nil\n\t}\n\treturn UndeclaredNamespacePrefixError{Namespace: *space,\n\t\tDescription: _CONTAINER_XML_NS}\n}\n\nfunc findAttr(attrs []attr, name xml.Name) *attr {\n\tfor _, a := range attrs {\n\t\t// Only compare the name, because two attributes from the same\n\t\t// namespace but with different prefixes are still the same.\n\t\tif a.Name == name {\n\t\t\treturn &a\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *parser) push(start *xml.StartElement) error {\n\tp.open = append(p.open, start.Name)\n\tp.ns.push()\n\tfor _, a := range start.Attr {\n\t\tvar ns string\n\t\tswitch {\n\t\tcase a.Name.Space == xmlns ||\n\t\t\ta.Name.Space == digidocManifestExtraSpace:\n\t\t\tswitch a.Name.Local {\n\t\t\tcase \"\":\n\t\t\t\treturn EmptyNamespacePrefixError{\n\t\t\t\t\tElement:     start.Name,\n\t\t\t\t\tDescription: _CONTAINER_XML_NS_EMPTY,\n\t\t\t\t}\n\t\t\tcase xmlns:\n\t\t\t\treturn RedeclareXMLNSPrefixError{\n\t\t\t\t\tElement:     start.Name,\n\t\t\t\t\tDescription: _CONTAINER_XML_NS_INNER,\n\t\t\t\t}\n\t\t\t}\n\t\t\tns = a.Name.Local\n\t\t\tfallthrough\n\t\tcase a.Name.Space == \"\" &&\n\t\t\t(a.Name.Local == xmlns ||\n\t\t\t\ta.Name.Local == digidocManifestExtraLocal):\n\t\t\tif a.Value == \"\" {\n\t\t\t\treturn UndeclaringNamespaceError{\n\t\t\t\t\tElement:     start.Name,\n\t\t\t\t\tNamespace:   ns,\n\t\t\t\t\tDescription: _CONTAINER_XML_NS_UNDEC,\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.ns.set(ns, a.Value)\n\t\tcase a.Name.Space == \"\" && strings.Contains(a.Name.Local, \":\"):\n\t\t\treturn NamespaceOrPrefixContainSemicolonError{\n\t\t\t\tElement:     start.Name,\n\t\t\t\tDescription: _CONTAINER_XML_NS_COLON,\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *parser) pop(name xml.Name) error {\n\tif len(p.open) == 0 {\n\t\treturn UnexpectedEndElementError{Name: name,\n\t\t\tDescription: _CONTAINER_XML_END}\n\t}\n\topen := p.open[len(p.open)-1]\n\tp.open = p.open[:len(p.open)-1]\n\tif open != name {\n\t\treturn MismatchingTagsError{Start: open, End: name,\n\t\t\tDescription: _CONTAINER_XML_TAGS}\n\t}\n\tp.ns.pop()\n\treturn nil\n}\n\n// readToken and unreadToken provide lookahead for p.token.\nfunc (p *parser) readToken() (xml.Token, error) {\n\tif p.nextToken != nil || p.nextError != nil {\n\t\ttoken, err := p.nextToken, p.nextError\n\t\tp.nextToken, p.nextError = nil, nil\n\t\treturn token, err\n\t}\n\treturn p.token()\n}\n\nfunc (p *parser) unreadToken(token xml.Token, err error) {\n\tif p.nextToken != nil || p.nextError != nil {\n\t\tpanic(\"double unreadToken\")\n\t}\n\tif token == nil && err == nil {\n\t\tpanic(\"unreadToken(nil, nil)\")\n\t}\n\tp.nextToken = token\n\tp.nextError = err\n}\n\n// readCharacterData merges and returns all character data until the next\n// non-character data token. Note that since readCharacterData ends with an\n// unreadToken the returned data cannot be unread.\nfunc (p *parser) readCharacterData() (string, error) {\n\tbuf := buffer()\n\tdefer release(buf)\n\tfor {\n\t\ttoken, err := p.readToken()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tcdata, ok := token.(xml.CharData)\n\t\tif !ok {\n\t\t\tp.unreadToken(token, err)\n\t\t\treturn buf.String(), nil\n\t\t}\n\t\tbuf.Write(cdata)\n\t}\n}\n\n// readWhitespace merges and returns all whitespace-only character data until\n// the next non-whitespace token or error. Note that since readWhitespace ends\n// with an unreadToken the returned whitespace cannot be unread.\nfunc (p *parser) readWhitespace() string {\n\tbuf := buffer()\n\tdefer release(buf)\n\tfor {\n\t\ttoken, err := p.readToken()\n\t\tcdata, ok := token.(xml.CharData)\n\t\tindex := bytes.IndexFunc(cdata, func(r rune) bool {\n\t\t\t// Limit our set of supported whitespace.\n\t\t\treturn r != ' ' && r != '\\t' && r != '\\n'\n\t\t})\n\t\tif err != nil || !ok || index >= 0 {\n\t\t\tp.unreadToken(token, err)\n\t\t\treturn buf.String()\n\t\t}\n\t\tbuf.Write(cdata)\n\t}\n}\n\n// parse parses the token stream from p and stores the result in the struct\n// reflected in v. It works much like a very stripped down version of\n// encoding/xml.Decoder.Decode, only supporting structs and slices of structs.\n//\n// Because the XML tags used by this parser are incompatible with encoding/xml,\n// the key for field tags is \"xmlx\" instead of \"xml\" to avoid confusion.\n//\n// Parsing into structs uses the following rules.\n//\n//   - Only XML elements can be parsed into structs.\n//\n//   - The first field of all structs must be XMLElement of type c14n with a\n//     tag of the form \"namespace name\" specifying the name of the element\n//     being parsed. This replaces the field named XMLName of type xml.Name for\n//     structs used with encoding/xml. The tag serves the same purpose, but we\n//     are not interested in storing the name (since we know it already) and\n//     need canonicalization data instead: forcing both xml.Name and c14n\n//     fields would be redundant so they are merged into one.\n//\n//   - If the tag specified in the last rule is suffixed with \",c14nroot\", then\n//     it marks the element as a canonicalization root and the ns field of\n//     XMLElement will be filled. Structs without this tag suffix cannot be\n//     used as canonical XML root-elements.\n//\n//   - For every exported field in the struct with a tag of the form\n//     \"namespace name,attr\", \"name,attr\", or \",attr\", the field must be of\n//     type string and the XML element must contain an attribute with a\n//     matching name and non-empty value, unless the tag is suffixed with\n//     \",optional\". If no namespace is given, then the attribute must not have\n//     an explicit namespace prefix. If no name is given, then the name of the\n//     field is used. The value of the attribute is stored in the field. If the\n//     XML element has non-namespace declaring attributes which do not have\n//     corresponding struct fields, then parse returns an error. This differs\n//     from encoding/xml, which does not allow specifying the namespace, allows\n//     types other than string, allows empty values, has no \",optional\" tag\n//     (since all attributes are optional), and ignores extra or missing\n//     attributes.\n//\n//   - If the tag specified in the last rule is suffixed with \",unique\", then\n//     the value of the attribute must be unique compared to all other\n//     attributes tagged with \",unique\". This is intended to be used to ensure\n//     uniqueness of identity attributes in XML elements.\n//\n//   - All attribute fields must come before any non-attribute fields\n//     (excluding XMLElement). This differs from encoding/xml, which allows\n//     attribute fields to be interleaved with others.\n//\n//   - If the struct contains an exported field with a \",chardata\" tag, then\n//     the field must be of type string and character data within the XML\n//     element is stored in it. If such a field exists, then the XML element\n//     must not contain any sub-elements. At most one field with this tag is\n//     allowed. If no such field exists, then the element must not contain any\n//     non-whitespace character data. This differs from encoding/xml, which\n//     supports byte slice fields, allows sub-elements by accumulating all\n//     character data between them into the field, ignores additional chardata\n//     fields, and discards character data not stored anywhere.\n//\n//   - Every exported field in the struct without a \",attr\" or \",chardata\" tag\n//     must be of type struct or slice of structs (note specifically that\n//     pointers are not allowed). The XML element must contain sub-elements\n//     matching those fields in the exact same order, unless a field has an\n//     \",optional\" tag in which case the corresponding sub-element can be\n//     missing. If the XML element contains sub-elements which do not have\n//     corresponding struct fields, then parse returns an error. This differs\n//     from encoding/xml, which supports more types, allows non-matching\n//     ordering of the fields and sub-elements, has no \",optional\" tag (since\n//     all fields are optional), and ignores extra or missing sub-elements.\n//\n//   - If the field being matched to an XML sub-element is of type struct, then\n//     parsing is applied recursively to that struct.\n//\n//   - If the field being matched to an XML sub-element is of type slice of\n//     structs, then one or more XML sub-elements are matched to the struct\n//     type of the slice element, creating new entries in the slice.\n//\n// If v reflects a struct which does not satisfy the type rules set above, then\n// parse will panic, as that means there was a programmer error.\n//\n// If optional is true and the first token is not a start element matching the\n// XMLElement name in the struct, then v is not modified, the token is unread,\n// and parse returns false, nil.\nfunc (p *parser) parse(v reflect.Value, optional bool) (match bool, err error) {\n\theader := c14nHeader(v)\n\tname, c14nroot := xmlxElement(v.Type().Field(0)) // header ensured field 0 exists.\n\n\ttoken, err := p.readToken()\n\tif err != nil {\n\t\treturn false, ParseXMLStartTokenError{Element: name, Err: err,\n\t\t\tDescription: _CONTAINER_XML_START_TOKEN}\n\t}\n\tstart, ok := token.(startElement)\n\tif !ok {\n\t\tif optional {\n\t\t\tp.unreadToken(token, err)\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, ParseXMLNotStartElementError{Element: name,\n\t\t\tType:        fmt.Sprintf(\"%T\", token),\n\t\t\tDescription: _CONTAINER_XML_START_TOKEN_FAIL}\n\t}\n\n\tif start.name != name {\n\t\tif optional {\n\t\t\tp.unreadToken(token, err)\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, ParseXMLUnexpectedElementError{Element: name,\n\t\t\tName:        start.name,\n\t\t\tDescription: _CONTAINER_XML_TOKEN}\n\t}\n\tif c14nroot { // Copy current namespace bindings.\n\t\theader.ns = p.ns.flatten()\n\t}\n\theader.start = start\n\n\tattrn, err := p.parseAttributes(v, start)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tif err = p.parseSubelements(v, header, attrn); err != nil {\n\t\treturn true, err\n\t}\n\n\t// Start and end element matching already done by token(). Just\n\t// consume the end element, ensuring there are no extra tokens.\n\tif token, err = p.readToken(); err != nil {\n\t\treturn true, ParseXMLEndTokenError{Element: name, Err: err,\n\t\t\tDescription: _CONTAINER_XML_END_TOKEN}\n\t}\n\tif _, ok := token.(xml.EndElement); !ok {\n\t\tif more, ok := token.(startElement); ok {\n\t\t\treturn true, ParseXMLElementTrailingElementError{\n\t\t\t\tElement:     name,\n\t\t\t\tTrailing:    more.name,\n\t\t\t\tDescription: _CONTAINER_XML_TOKEN_TRAIL_EL,\n\t\t\t}\n\t\t}\n\t\treturn true, ParseXMLElementTrailingTokenError{\n\t\t\tElement:     name,\n\t\t\tType:        fmt.Sprintf(\"%T\", token),\n\t\t\tDescription: _CONTAINER_XML_TOKEN_TRAIL,\n\t\t}\n\t}\n\treturn true, nil\n}\n\nfunc (p *parser) parseAttributes(v reflect.Value, s startElement) (n int, err error) {\n\tvar vattr []attr\n\tfor i := 1; i < v.NumField(); i++ { // Skip first XMLElement c14n field.\n\t\tname, isattr, optional, unique := xmlxAttribute(v.Type().Field(i))\n\t\tif !isattr {\n\t\t\tbreak\n\t\t}\n\t\tvattr = append(vattr, attr{Attr: xml.Attr{Name: name}})\n\n\t\tfound := findAttr(s.attr, name)\n\t\tif found == nil {\n\t\t\tif optional {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn 0, ParseXMLMissingAttributeError{Element: s.name,\n\t\t\t\tAttribute:   name,\n\t\t\t\tDescription: _CONTAINER_XML_TOKEN_ATTR}\n\t\t}\n\t\tif unique {\n\t\t\tif _, ok := p.unique[found.Value]; ok {\n\t\t\t\treturn 0, ParseXMLNonUniqueAttributeError{\n\t\t\t\t\tElement:     s.name,\n\t\t\t\t\tAttribute:   name,\n\t\t\t\t\tValue:       found.Value,\n\t\t\t\t\tDescription: _CONTAINER_XML_TOKEN_ATTR_UNIQ,\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.unique[found.Value] = struct{}{}\n\t\t}\n\t\tv.Field(i).SetString(found.Value)\n\t}\n\tfor _, attr := range s.attr {\n\t\tif attr.namespace() {\n\t\t\tcontinue\n\t\t}\n\t\tif findAttr(vattr, attr.Name) == nil {\n\t\t\treturn 0, ParseXMLExtraAttributeError{\n\t\t\t\tElement:     s.name,\n\t\t\t\tAttribute:   attr.Name,\n\t\t\t\tDescription: _CONTAINER_XML_TOKEN_ATTR_EX,\n\t\t\t}\n\t\t}\n\t}\n\treturn len(vattr), nil\n}\n\nfunc (p *parser) parseSubelements(v reflect.Value, header *c14n, attrn int) error {\n\tt := v.Type()\n\tvar subfields bool\n\tfor i := 1 + attrn; i < v.NumField(); i++ { // Skip first field and attributes.\n\t\tfield := v.Field(i)\n\t\tftype := t.Field(i)\n\t\tif !subfields { // First field after attributes.\n\t\t\tsubfields = true\n\t\t\tif xmlxCharacterData(ftype) {\n\t\t\t\tcdata, err := p.readCharacterData()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn ParseXMLCharacterDataTokenError{\n\t\t\t\t\t\tElement:     header.start.name,\n\t\t\t\t\t\tErr:         err,\n\t\t\t\t\t\tDescription: _CONTAINER_XML_TOKEN_CHAR_DATA,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfield.SetString(cdata)\n\n\t\t\t\tif v.NumField() > i+1 {\n\t\t\t\t\tpanic(fmt.Sprint(t, \" must not have \",\n\t\t\t\t\t\t`sub-elements along with character data`))\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Whitespace after start token if no character data.\n\t\t\theader.whitespace = append(header.whitespace, p.readWhitespace())\n\t\t}\n\t\toptional := xmlxSubelement(ftype)\n\n\t\tif ftype.Type.Kind() == reflect.Struct {\n\t\t\tmatch, err := p.parse(field, optional)\n\t\t\tif err != nil {\n\t\t\t\treturn ParseXMLSubStructError{\n\t\t\t\t\tElement:     header.start.name,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _CONTAINER_XML_EL_STRUCT,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif match {\n\t\t\t\theader.whitespace = append(header.whitespace, p.readWhitespace())\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// []struct\n\t\tfor match := true; match; optional = true {\n\t\t\telem := reflect.New(ftype.Type.Elem()).Elem()\n\t\t\tvar err error\n\t\t\tif match, err = p.parse(elem, optional); err != nil {\n\t\t\t\treturn ParseXMLSubSliceError{\n\t\t\t\t\tElement:     header.start.name,\n\t\t\t\t\tIndex:       field.Len(),\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _CONTAINER_XML_EL_SLICE,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif match {\n\t\t\t\tfield.Set(reflect.Append(field, elem))\n\t\t\t\theader.whitespace = append(header.whitespace, p.readWhitespace())\n\t\t\t}\n\t\t}\n\t}\n\tif !subfields {\n\t\t// If there were no fields to parse, then we must still be\n\t\t// prepared for whitespace between the start and end token.\n\t\t// Must be done after the loop to not consume whitespace that\n\t\t// is part of stored character data.\n\t\theader.whitespace = append(header.whitespace, p.readWhitespace())\n\t}\n\treturn nil\n}\n\nconst xmlx = \"xmlx\"\n\nconst (\n\txmlxC14NRoot = 1 << iota\n\txmlxAttr\n\txmlxOptional\n\txmlxUnique\n\txmlxCharData\n)\n\nfunc xmlxElement(f reflect.StructField) (name xml.Name, c14nroot bool) {\n\tname, flags := xmlxTag(f, xmlxC14NRoot)\n\tif name.Space == \"\" || name.Local == \"\" {\n\t\tpanic(fmt.Sprintf(`XMLElement c14n \"xmlx\" tag %q must contain namespace and name`,\n\t\t\tf.Tag.Get(xmlx)))\n\t}\n\treturn name, flags&xmlxC14NRoot > 0\n}\n\nfunc xmlxAttribute(f reflect.StructField) (name xml.Name, attr, optional, unique bool) {\n\tlookahead := xmlxCharData // Additional flags allowed after attributes.\n\tname, flags := xmlxTag(f, xmlxAttr|xmlxOptional|xmlxUnique|lookahead)\n\tif name.Local == \"\" {\n\t\tname.Local = f.Name\n\t}\n\tif attr = flags&xmlxAttr > 0; attr {\n\t\tif flags&lookahead > 0 { // Not allowed for attr.\n\t\t\tpanic(fmt.Sprint(f.Name, \" \", f.Type,\n\t\t\t\t` tagged with unallowed \"xmlx\" flag \"chardata\"`))\n\t\t}\n\t\tif f.Type.Kind() != reflect.String {\n\t\t\tpanic(fmt.Sprint(f.Name, \" \", f.Type,\n\t\t\t\t\" tagged as XML attribute (must be string)\"))\n\t\t}\n\t}\n\treturn name, attr, flags&xmlxOptional > 0, flags&xmlxUnique > 0\n}\n\nfunc xmlxCharacterData(f reflect.StructField) bool {\n\tlookahead := xmlxOptional // Additional flags allowed instead of chardata.\n\tname, flags := xmlxTag(f, xmlxCharData|lookahead)\n\tif flags&xmlxCharData == 0 {\n\t\treturn false\n\t}\n\tif name.Space != \"\" || name.Local != \"\" {\n\t\tpanic(`XML character data \"xmlx\" tag must not contain name`)\n\t}\n\tif flags&lookahead > 0 { // Not allowed for chardata.\n\t\tpanic(fmt.Sprint(f.Name, \" \", f.Type,\n\t\t\t` tagged with unallowed \"xmlx\" flag \"optional\"`))\n\t}\n\tif kind := f.Type.Kind(); kind != reflect.String {\n\t\tpanic(fmt.Sprint(f.Name, \" \", f.Type,\n\t\t\t\" tagged as XML character data (must be string)\"))\n\t}\n\treturn true\n}\n\nfunc xmlxSubelement(f reflect.StructField) (optional bool) {\n\tname, flags := xmlxTag(f, xmlxOptional)\n\tif name.Space != \"\" || name.Local != \"\" {\n\t\tpanic(`XML sub-element \"xmlx\" tag must not contain name`)\n\t}\n\tif kind := f.Type.Kind(); kind != reflect.Struct &&\n\t\t(kind != reflect.Slice || f.Type.Elem().Kind() != reflect.Struct) {\n\n\t\tpanic(fmt.Sprint(f.Name, \" \", f.Type,\n\t\t\t\" tagged as XML sub-element (must be struct or []struct)\"))\n\t}\n\treturn flags&xmlxOptional > 0\n}\n\n// xmlxTag parses the \"xmlx\" tag of the struct field. Only flags enumerated in\n// allowed can be used. Returns the name in the tag and any specified flags.\nfunc xmlxTag(f reflect.StructField, allowed int) (name xml.Name, flags int) {\n\ttag, ok := f.Tag.Lookup(xmlx)\n\tif !ok {\n\t\treturn\n\t}\n\n\tsplit := strings.Split(tag, \" \")\n\tlocal := split[0]\n\tswitch len(split) {\n\tcase 1: // No namespace.\n\tcase 2:\n\t\tname.Space = split[0]\n\t\tlocal = split[1]\n\tdefault:\n\t\tpanic(fmt.Sprintf(`malformed \"xmlx\" tag %q`, tag))\n\t}\n\n\tsplit = strings.Split(local, \",\")\n\tname.Local = split[0]\n\tfor i := 1; i < len(split); i++ {\n\t\tvar flag int\n\t\tswitch split[i] {\n\t\tcase \"c14nroot\":\n\t\t\tflag = xmlxC14NRoot\n\t\tcase \"attr\":\n\t\t\tflag = xmlxAttr\n\t\tcase \"optional\":\n\t\t\tflag = xmlxOptional\n\t\tcase \"unique\":\n\t\t\tflag = xmlxUnique\n\t\tcase \"chardata\":\n\t\t\tflag = xmlxCharData\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(`unsupported \"xmlx\" flag %q`, split[i]))\n\t\t}\n\t\tif allowed&flag == 0 {\n\t\t\tpanic(fmt.Sprintf(`%s %s tagged with unallowed \"xmlx\" flag %q`,\n\t\t\t\tf.Name, f.Type, split[i]))\n\t\t}\n\t\tflags |= flag\n\t}\n\treturn\n}\n\nfunc parseXML(data []byte, v interface{}) error {\n\tp := newParser(bytes.NewReader(data))\n\n\t// Discard the leading XML declaration and any surrounding whitespace.\n\ttoken, err := p.readToken()\n\tpi, ok := token.(xml.ProcInst)\n\tif err != nil || !ok || pi.Target != \"xml\" {\n\t\t// Not a <?xml ...?> token, parse it as the root element.\n\t\tp.unreadToken(token, err)\n\t}\n\tp.readWhitespace()\n\n\t// Parse the root element.\n\tif _, err = p.parse(valueOfPtr(v).Elem(), false); err != nil {\n\t\treturn err\n\t}\n\n\t// Only allow whitespace until EOF.\n\tp.readWhitespace()\n\tswitch token, err = p.readToken(); {\n\tcase err == io.EOF:\n\t\treturn nil\n\tcase err == nil:\n\t\treturn ParseXMLTrailingTokenError{Type: fmt.Sprintf(\"%T\", token),\n\t\t\tDescription: _CONTAINER_XML_TOKEN_TRAIL}\n\tdefault:\n\t\treturn ParseXMLTrailingError{Err: err,\n\t\t\tDescription: _CONTAINER_XML_TOKEN_PARSE}\n\t}\n}\n\nfunc valueOfPtr(v interface{}) reflect.Value {\n\tr := reflect.ValueOf(v)\n\tvar t string\n\tswitch {\n\tcase v == nil:\n\t\tt = \"nil\"\n\tcase r.Kind() != reflect.Ptr:\n\t\tt = r.Type().String()\n\tcase r.IsNil():\n\t\tt = \"(\" + r.Type().String() + \")(nil)\"\n\t}\n\tif t != \"\" {\n\t\tpanic(fmt.Sprint(\"cannot process \", t, \" as XML root (must be non-nil pointer)\"))\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "common/collector/container/bdoc/xml_test.go",
    "content": "package bdoc\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"io\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nfunc TestParserToken(t *testing.T) {\n\t// encoding/xml.Decoder.RawToken handles most things for us: we only\n\t// need to test namespace translations and start/end tag matching.\n\ttests := []struct {\n\t\tname   string\n\t\tinput  string\n\t\tstream []xml.Token\n\t\terr    error // Error that comes after stream.\n\t}{\n\t\t// errors\n\t\t{\"empty\", \"\", nil, io.EOF},\n\t\t{\"raw token error\", \"<foo\", nil, new(RawTokenError)},\n\t\t{\"unclosed element\", \"<foo>\", []xml.Token{\n\t\t\tstartElement{name: xml.Name{Local: \"foo\"}},\n\t\t}, new(UnexpectedParseEOFError)},\n\t\t{\"unexpected close tag\", \"</foo>\", nil, new(UnexpectedEndElementError)},\n\t\t{\"mismatched element\", \"<foo></bar>\", []xml.Token{\n\t\t\tstartElement{name: xml.Name{Local: \"foo\"}},\n\t\t}, new(MismatchingTagsError)},\n\t\t{\"mismatched element different prefix\", `<foo></a:foo>`, []xml.Token{\n\t\t\tstartElement{name: xml.Name{Local: \"foo\"}},\n\t\t}, new(MismatchingTagsError)},\n\t\t{\"empty namespace prefix\", `<foo xmlns:=\"ns\"/>`, nil, new(NamespaceOrPrefixContainSemicolonError)},\n\t\t{\"redeclare xmlns prefix\", `<foo xmlns:xmlns=\"ns\"/>`, nil, new(RedeclareXMLNSPrefixError)},\n\t\t{\"undeclaring namespace\", `<foo xmlns=\"\"/>`, nil, new(UndeclaringNamespaceError)},\n\t\t{\"duplicate attribute\", `<foo bar=\"bar\" bar=\"baz\"/>`, nil, new(DuplicateAttributeError)},\n\t\t{\"duplicate attribute different prefix\",\n\t\t\t`<foo xmlns:a=\"ns\" xmlns:b=\"ns\" a:bar=\"bar\" b:bar=\"baz\"/>`,\n\t\t\tnil, new(DuplicateAttributeError)},\n\t\t{\"undeclared prefix\", \"<foo:bar/>\", nil, new(UndeclaredNamespacePrefixError)},\n\t\t{\"empty attribute\", `<foo bar=\"\"/>`, nil, new(EmptyAttributeError)},\n\n\t\t// translations\n\t\t{\"default\", `<foo xmlns=\"namespace\"/>`, []xml.Token{\n\t\t\tstartElement{\n\t\t\t\tname: xml.Name{Space: \"namespace\", Local: \"foo\"},\n\t\t\t\tattr: []attr{\n\t\t\t\t\t{Attr: xml.Attr{\n\t\t\t\t\t\tName:  xml.Name{Local: \"xmlns\"},\n\t\t\t\t\t\tValue: \"namespace\",\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\txml.EndElement{\n\t\t\t\tName: xml.Name{Space: \"namespace\", Local: \"foo\"},\n\t\t\t},\n\t\t}, io.EOF},\n\n\t\t{\"named\", `<foo:bar xmlns:foo=\"namespace\"/>`, []xml.Token{\n\t\t\tstartElement{\n\t\t\t\tnsprefix: \"foo\",\n\t\t\t\tname:     xml.Name{Space: \"namespace\", Local: \"bar\"},\n\t\t\t\tattr: []attr{\n\t\t\t\t\t{\n\t\t\t\t\t\tnsprefix: \"xmlns\",\n\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\tName:  xml.Name{Space: \"xmlns\", Local: \"foo\"},\n\t\t\t\t\t\t\tValue: \"namespace\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\txml.EndElement{\n\t\t\t\tName: xml.Name{Space: \"namespace\", Local: \"bar\"},\n\t\t\t},\n\t\t}, io.EOF},\n\n\t\t{\"multiple\",\n\t\t\t`<foo:bar xmlns:foo=\"namespace1\" xmlns:baz=\"namespace2\" baz:quux=\"attr\"/>`,\n\t\t\t[]xml.Token{\n\t\t\t\tstartElement{\n\t\t\t\t\tnsprefix: \"foo\",\n\t\t\t\t\tname:     xml.Name{Space: \"namespace1\", Local: \"bar\"},\n\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"xmlns\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"xmlns\",\n\t\t\t\t\t\t\t\t\tLocal: \"foo\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"namespace1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"xmlns\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"xmlns\",\n\t\t\t\t\t\t\t\t\tLocal: \"baz\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"namespace2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"baz\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"namespace2\",\n\t\t\t\t\t\t\t\t\tLocal: \"quux\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"attr\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\txml.EndElement{\n\t\t\t\t\tName: xml.Name{Space: \"namespace1\", Local: \"bar\"},\n\t\t\t\t},\n\t\t\t}, io.EOF},\n\n\t\t{\"nested\", `<foo xmlns:bar=\"namespace1\">\n\t\t\t\t<bar:baz xmlns:bar=\"namespace2\"/>\n\t\t\t\t<bar:quux/>\n\t\t\t</foo>`,\n\t\t\t[]xml.Token{\n\t\t\t\tstartElement{\n\t\t\t\t\tname: xml.Name{Local: \"foo\"},\n\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"xmlns\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"xmlns\",\n\t\t\t\t\t\t\t\t\tLocal: \"bar\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"namespace1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\txml.CharData(\"\\n\\t\\t\\t\\t\"),\n\t\t\t\tstartElement{\n\t\t\t\t\tnsprefix: \"bar\",\n\t\t\t\t\tname:     xml.Name{Space: \"namespace2\", Local: \"baz\"},\n\t\t\t\t\tattr: []attr{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tnsprefix: \"xmlns\",\n\t\t\t\t\t\t\tAttr: xml.Attr{\n\t\t\t\t\t\t\t\tName: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"xmlns\",\n\t\t\t\t\t\t\t\t\tLocal: \"bar\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tValue: \"namespace2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\txml.EndElement{\n\t\t\t\t\tName: xml.Name{Space: \"namespace2\", Local: \"baz\"},\n\t\t\t\t},\n\t\t\t\txml.CharData(\"\\n\\t\\t\\t\\t\"),\n\t\t\t\tstartElement{\n\t\t\t\t\tnsprefix: \"bar\",\n\t\t\t\t\tname:     xml.Name{Space: \"namespace1\", Local: \"quux\"},\n\t\t\t\t},\n\t\t\t\txml.EndElement{\n\t\t\t\t\tName: xml.Name{Space: \"namespace1\", Local: \"quux\"},\n\t\t\t\t},\n\t\t\t\txml.CharData(\"\\n\\t\\t\\t\"),\n\t\t\t\txml.EndElement{\n\t\t\t\t\tName: xml.Name{Local: \"foo\"},\n\t\t\t\t},\n\t\t\t}, io.EOF},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tp := newParser(bytes.NewReader([]byte(test.input)))\n\t\t\tfor _, expected := range test.stream {\n\t\t\t\ttoken, err := p.token()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"unexpected error:\", err)\n\t\t\t\t}\n\n\t\t\t\t// We cannot use != since startElement contains\n\t\t\t\t// a slice which is not comparable.\n\t\t\t\tif !reflect.DeepEqual(token, expected) {\n\t\t\t\t\tt.Errorf(\"unexpected token: %#v; want %#v\",\n\t\t\t\t\t\ttoken, expected)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_, err := p.token()\n\t\t\tif err != test.err && errors.CausedBy(err, test.err) == nil {\n\t\t\t\tt.Errorf(\"unexpected error: %+v; want %+v\",\n\t\t\t\t\terr, test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseXML(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected interface{}\n\t\tpanic    interface{}\n\t\terr      error\n\t}{\n\t\t// programmer errors (panic)\n\t\t{\"nil\", \"\", nil, \"cannot process nil as XML root (must be non-nil pointer)\", nil},\n\t\t{\"non-struct\", \"\", int(1), \"cannot process int as XML element (must be struct)\", nil},\n\t\t{\"empty struct\", \"\", struct{}{}, \"struct {} must have at least one field\", nil},\n\t\t{\n\t\t\t\"first field\", \"\", struct{ first int }{1},\n\t\t\t\"struct { first int } must have XMLElement of type c14n as first field\",\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"untagged\", \"\", struct{ XMLElement c14n }{},\n\t\t\t`XMLElement c14n \"xmlx\" tag \"\" must contain namespace and name`, nil,\n\t\t},\n\t\t{\n\t\t\t\"no name\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"\"`\n\t\t\t}{}, `XMLElement c14n \"xmlx\" tag \"\" must contain namespace and name`, nil,\n\t\t},\n\t\t{\n\t\t\t\"no namespace\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo\"`\n\t\t\t}{}, `XMLElement c14n \"xmlx\" tag \"foo\" must contain namespace and name`, nil,\n\t\t},\n\t\t// Open Document Format for Office Applications (OpenDocument) v1.2\n\t\t// uses extra entry, so two entries \"bar baz\" is OK,\n\t\t// but three \"bar baz biz\" is not OK\n\t\t{\n\t\t\t\"malformed\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar baz biz\"`\n\t\t\t}{}, `malformed \"xmlx\" tag \"foo bar baz biz\"`, nil,\n\t\t},\n\t\t{\n\t\t\t\"unsupported\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar,unsupported\"`\n\t\t\t}{}, `unsupported \"xmlx\" flag \"unsupported\"`, nil,\n\t\t},\n\t\t{\n\t\t\t\"unallowed flag\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar,attr\"`\n\t\t\t}{}, `XMLElement bdoc.c14n tagged with unallowed \"xmlx\" flag \"attr\"`, nil,\n\t\t},\n\t\t{\n\t\t\t\"attr chardata\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tAttribute  string `xmlx:\",attr,chardata\"`\n\t\t\t}{}, `Attribute string tagged with unallowed \"xmlx\" flag \"chardata\"`, nil,\n\t\t},\n\t\t{\n\t\t\t\"non-string attr\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tAttribute  int  `xmlx:\",attr\"`\n\t\t\t}{}, \"Attribute int tagged as XML attribute (must be string)\", nil,\n\t\t},\n\t\t{\n\t\t\t\"chardata name\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tValue      string `xmlx:\"foo baz,chardata\"`\n\t\t\t}{}, `XML character data \"xmlx\" tag must not contain name`, nil,\n\t\t},\n\t\t{\n\t\t\t\"chardata optional\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tValue      string `xmlx:\",chardata,optional\"`\n\t\t\t}{}, `Value string tagged with unallowed \"xmlx\" flag \"optional\"`, nil,\n\t\t},\n\t\t{\n\t\t\t\"non-string chardata\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tValue      int  `xmlx:\",chardata\"`\n\t\t\t}{}, \"Value int tagged as XML character data (must be string)\", nil,\n\t\t},\n\t\t{\n\t\t\t\"chardata subelements\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\tSubelement struct{}\n\t\t\t}{}, \"struct { \" +\n\t\t\t\t`XMLElement bdoc.c14n \"xmlx:\\\"foo bar\\\"\"; ` +\n\t\t\t\t`Value string \"xmlx:\\\",chardata\\\"\"; ` +\n\t\t\t\t`Subelement struct {} ` +\n\t\t\t\t\"} must not have sub-elements along with character data\", nil,\n\t\t},\n\t\t{\n\t\t\t\"subelement name\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n     `xmlx:\"foo bar\"`\n\t\t\t\tSubelement struct{} `xmlx:\"foo sub\"`\n\t\t\t}{}, `XML sub-element \"xmlx\" tag must not contain name`, nil,\n\t\t},\n\t\t{\n\t\t\t\"non-struct subelement\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tValue      string\n\t\t\t}{}, \"Value string tagged as XML sub-element (must be struct or []struct)\", nil,\n\t\t},\n\n\t\t// errors\n\t\t{\n\t\t\t\"EOF\", \"\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLStartTokenError),\n\t\t},\n\t\t{\n\t\t\t\"non-element\", \"character data\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLNotStartElementError),\n\t\t},\n\t\t{\n\t\t\t\"wrong name\", `<baz xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLUnexpectedElementError),\n\t\t},\n\t\t{\n\t\t\t\"missing attribute\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tAttribute  string `xmlx:\",attr\"`\n\t\t\t}{}, nil, new(ParseXMLMissingAttributeError),\n\t\t},\n\t\t{\n\t\t\t\"non-unique attribute\", `<bar xmlns=\"foo\">\n\t\t\t\t\t\t<first id=\"foo\"/>\n\t\t\t\t\t\t<second id=\"foo\"/>\n\t\t\t\t\t</bar>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tFirst      struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"foo first\"`\n\t\t\t\t\tID         string `xmlx:\"id,attr,unique\"`\n\t\t\t\t}\n\t\t\t\tSecond struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"foo second\"`\n\t\t\t\t\tID         string `xmlx:\"id,attr,unique\"`\n\t\t\t\t}\n\t\t\t}{}, nil, new(ParseXMLNonUniqueAttributeError),\n\t\t},\n\t\t{\n\t\t\t\"extra attribute\", `<bar xmlns=\"foo\" baz=\"baz\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLExtraAttributeError),\n\t\t},\n\t\t{\n\t\t\t\"slice at least one\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tSlice      []struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo slice\"`\n\t\t\t\t}\n\t\t\t}{}, nil, new(ParseXMLSubSliceError),\n\t\t},\n\t\t{\n\t\t\t\"element trailing element\", `<bar xmlns=\"foo\"><baz/></bar>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLElementTrailingElementError),\n\t\t},\n\t\t{\n\t\t\t\"element trailing token\", `<bar xmlns=\"foo\">trailing</bar>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLElementTrailingTokenError),\n\t\t},\n\t\t{\n\t\t\t\"trailing token\", `<bar xmlns=\"foo\"/><baz/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{}, nil, new(ParseXMLTrailingTokenError),\n\t\t},\n\n\t\t// parsing\n\t\t{\n\t\t\t\"simple\", `<bar xmlns=\"foo\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{c14n{whitespace: []string{\"\"}}}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"procinst and whitespace\", xml.Header + \" \\t<bar xmlns=\\\"foo\\\"/>\\r\\n\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{c14n{whitespace: []string{\"\"}}}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"internal whitespace\", \"<bar xmlns=\\\"foo\\\">\\n</bar>\", struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t}{c14n{whitespace: []string{\"\\n\"}}}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"c14nroot\", `<foo xmlns=\"default\" xmlns:ns=\"prefixed\"/>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"default foo,c14nroot\"`\n\t\t\t}{c14n{\n\t\t\t\tns: map[string]string{\n\t\t\t\t\t\"\":   \"default\",\n\t\t\t\t\t\"ns\": \"prefixed\",\n\t\t\t\t},\n\t\t\t\twhitespace: []string{\"\"},\n\t\t\t}}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"attributes\", `<foo ns:full=\"bar\" name=\"baz\" Field=\"quux\"\n\t\t\t\t\txmlns=\"default\" xmlns:ns=\"namespace\"/>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"default foo\"`\n\t\t\t\tFull       string `xmlx:\"namespace full,attr\"`\n\t\t\t\tName       string `xmlx:\"name,attr\"`\n\t\t\t\tField      string `xmlx:\",attr\"`\n\t\t\t\tOptional   string `xmlx:\",attr,optional\"`\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{whitespace: []string{\"\"}},\n\t\t\t\tFull:       \"bar\",\n\t\t\t\tName:       \"baz\",\n\t\t\t\tField:      \"quux\",\n\t\t\t}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"character data\", `<bar xmlns=\"foo\">character data</bar>`, struct {\n\t\t\t\tXMLElement c14n   `xmlx:\"foo bar\"`\n\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t}{\n\t\t\t\tValue: \"character data\",\n\t\t\t}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"sub-elements\", `<bar xmlns=\"foo\">\n\t\t\t\t\t\t<struct/>\n\t\t\t\t\t\t<slice>first</slice>\n\t\t\t\t\t\t<slice>second</slice>\n\t\t\t\t\t</bar>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tStruct     struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo struct\"`\n\t\t\t\t}\n\t\t\t\tSlice []struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"foo slice\"`\n\t\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\t}\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{whitespace: []string{\n\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\\t\",\n\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\\t\",\n\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\\t\",\n\t\t\t\t\t\"\\n\\t\\t\\t\\t\\t\",\n\t\t\t\t}},\n\t\t\t\tStruct: struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo struct\"`\n\t\t\t\t}{c14n{\n\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\tSpace: \"foo\",\n\t\t\t\t\t\t\tLocal: \"struct\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\twhitespace: []string{\"\"},\n\t\t\t\t}},\n\t\t\t\tSlice: []struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"foo slice\"`\n\t\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLElement: c14n{\n\t\t\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"foo\",\n\t\t\t\t\t\t\t\t\tLocal: \"slice\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tValue: \"first\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tXMLElement: c14n{\n\t\t\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\t\t\tSpace: \"foo\",\n\t\t\t\t\t\t\t\t\tLocal: \"slice\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tValue: \"second\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil, nil,\n\t\t},\n\t\t{\n\t\t\t\"optional\", `<bar xmlns=\"foo\"><after-optional/></bar>`, struct {\n\t\t\t\tXMLElement c14n `xmlx:\"foo bar\"`\n\t\t\t\tStruct     struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo struct\"`\n\t\t\t\t} `xmlx:\",optional\"`\n\t\t\t\tSlice []struct {\n\t\t\t\t\tXMLElement c14n   `xmlx:\"foo slice\"`\n\t\t\t\t\tValue      string `xmlx:\",chardata\"`\n\t\t\t\t} `xmlx:\",optional\"`\n\t\t\t\tAfterOptional struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo after-optional\"`\n\t\t\t\t}\n\t\t\t}{\n\t\t\t\tXMLElement: c14n{whitespace: []string{\"\", \"\"}},\n\t\t\t\tAfterOptional: struct {\n\t\t\t\t\tXMLElement c14n `xmlx:\"foo after-optional\"`\n\t\t\t\t}{c14n{\n\t\t\t\t\tstart: startElement{\n\t\t\t\t\t\tname: xml.Name{\n\t\t\t\t\t\t\tSpace: \"foo\",\n\t\t\t\t\t\t\tLocal: \"after-optional\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\twhitespace: []string{\"\"},\n\t\t\t\t}},\n\t\t\t}, nil, nil,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar v reflect.Value\n\t\t\tvar pv interface{}\n\t\t\tif test.expected != nil { // Allows for testing with test.expected == nil.\n\t\t\t\tv = reflect.New(reflect.TypeOf(test.expected))\n\t\t\t\tpv = v.Interface()\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tr := recover()\n\t\t\t\tif r != test.panic {\n\t\t\t\t\tt.Errorf(\"unexpected panic: %#v; want %#v\",\n\t\t\t\t\t\tr, test.panic)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terr := parseXML([]byte(test.input), pv)\n\t\t\tif err != nil || test.err != nil {\n\t\t\t\tif err != test.err && errors.CausedBy(err, test.err) == nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %+v; want %T\",\n\t\t\t\t\t\terr, test.err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Comparing c14n.start bloats the test table a lot and\n\t\t\t// does not give much extra value, since startElement\n\t\t\t// is already tested in TestParserToken: skip it.\n\t\t\tv.Elem().Field(0).Addr().Interface().(*c14n).start = startElement{}\n\t\t\tparsed := v.Elem().Interface()\n\t\t\tif !reflect.DeepEqual(parsed, test.expected) {\n\t\t\t\tt.Errorf(\"unexpected result: %#v; want %#v\",\n\t\t\t\t\tparsed, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/container.go",
    "content": "/*\nPackage container provides common code for handling signature containers.\n*/\npackage container // import \"ivxv.ee/container\"\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// Signature contains metadata about a signature.\ntype Signature struct {\n\tID          string // ID uniquely identifies this signature in the container.\n\tSigner      *x509.Certificate\n\tIssuer      *x509.Certificate\n\tSigningTime time.Time\n}\n\nfunc (s *Signature) CommonName() string {\n\tpattern := regexp.MustCompile(\"[0-9]+\")\n\tif pattern.FindString(s.Signer.Subject.CommonName) == \"\" {\n\t\tpersonalCode := strings.TrimPrefix(s.Signer.Subject.SerialNumber, \"PNOEE-\")\n\n\t\treturn fmt.Sprint(s.Signer.Subject.CommonName + \",\" + personalCode)\n\t}\n\n\treturn fmt.Sprint(s.Signer.Subject.CommonName)\n}\n\n// Container is a container that protects data with one or multiple signatures.\n// It is only necessary for Container to be able to read containers and not\n// create them.\ntype Container interface {\n\t// Close closes and frees any resources held by this Container. Any\n\t// byte slices returned by Data can no longer be used and can be reused\n\t// by Container implementations.\n\tio.Closer\n\n\t// Signatures returns metadata about the signatures.\n\tSignatures() []Signature\n\n\t// Data returns the signed data protected by this Container. The data\n\t// is expected to be a set of byte slices with unique string keys.\n\t//\n\t// Note! It might seem to make more sense to not return all the\n\t// documents as in-memory byte slices, but rather as file references or\n\t// open readers to them. However, since they need to be completely read\n\t// in order to check container signatures, then it does not make sense\n\t// to have to read them twice and it is easier to just remember the\n\t// read data.\n\t//\n\t// The returned data must no longer be referenced after the container\n\t// is closed: implementations are free to reclaim the byte slices for\n\t// reuse.\n\tData() map[string][]byte\n}\n\n// Conf is the container set configuration. It maps enabled container types to\n// their configurations. The latter is listed as an unspecified YAML Node,\n// which will be applied to the corresponding container type's configuration\n// structure.\ntype Conf map[Type]yaml.Node\n\n// Opener contains a configured set of container implementations.\ntype Opener map[Type]OpenFunc\n\n// Configure configures a set of container implementations specified in the\n// configuration.\nfunc Configure(c Conf) (o Opener, err error) {\n\to = make(Opener)\n\n\t// For each configured implementation, ...\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tfor t, y := range c {\n\t\t// ...check if it is linked, ...\n\t\tre, ok := registry[t]\n\t\tif !ok {\n\t\t\treturn nil, UnlinkedTypeError{Type: t,\n\t\t\t\tDescription: _CONTAINER_PARSE}\n\t\t}\n\n\t\t// ...is configured with the canonical name ...\n\t\tif t != re.canonical {\n\t\t\treturn nil, ConfiguredAliasError{Type: t, Canonical: re.canonical,\n\t\t\t\tDescription: _CONTAINER_CNAME}\n\t\t}\n\n\t\t// ...and if creating an opening function succeeds.\n\t\tf, err := re.newOpen(y)\n\t\tif err != nil {\n\t\t\treturn nil, ConfigureTypeError{Type: t, Err: err,\n\t\t\t\tDescription: _CONTAINER_PARSER}\n\t\t}\n\t\tfor _, alias := range re.aliases {\n\t\t\to[alias] = f\n\t\t}\n\t}\n\n\treturn\n}\n\n// Open dispatches the encoded container to the correct opening function and\n// returns the verified Container.\nfunc (o Opener) Open(t Type, container io.Reader) (c Container, err error) {\n\tf, ok := o[t]\n\tif !ok {\n\t\treturn nil, UnconfiguredTypeError{Type: t,\n\t\t\tDescription: _CONTAINER_PARSE}\n\t}\n\treturn f(container)\n}\n\n// OpenFile opens the file at path, and passes its contents to Open. The path\n// must have an extension corresponding to the container type to use.\nfunc (o Opener) OpenFile(path string) (c Container, err error) {\n\tfp, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, OpenFileError{Path: path, Err: err,\n\t\t\tDescription: _CONTAINER_OPEN}\n\t}\n\tdefer fp.Close()\n\n\text := strings.TrimPrefix(filepath.Ext(path), \".\")\n\tif len(ext) == 0 {\n\t\treturn nil, MissingExtensionError{Path: path,\n\t\t\tDescription: _CONTAINER_EXT}\n\t}\n\treturn o.Open(Type(ext), fp)\n}\n\n// UnverifiedOpen dispatches the encoded container to the correct unverified\n// opening function and returns the unverified Container.\n//\n// Note! This is a dangerous function, which provides access to signed data\n// without verifying the signatures and should only be used during\n// bootstrapping. Even then, the signatures should be retroactively verified.\nfunc UnverifiedOpen(t Type, container io.Reader) (c Container, err error) {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tre, ok := registry[t]\n\tif !ok {\n\t\treturn nil, OpenUnlinkedTypeError{Type: t,\n\t\t\tDescription: _CONTAINER_PARSE}\n\t}\n\treturn re.unverifiedOpen(container)\n}\n"
  },
  {
    "path": "common/collector/container/dummy/dummy.go",
    "content": "//ivxv:development\n//go:development\n/*\nPackage dummy implements a dummy container used for testing.\n\nThe dummy container is just a YAML-encoding of the values that should be\nreturned by the interface methods:\n\n\tsignatures:\n\t  - signer:      <PEM-encoding of signer certificate>\n\t    issuer:      <PEM-encoding of issuer certificate>\n\t    signingtime: <RFC3339-formatted signing time>\n\tdata:\n\t  <key>: <string>\n\nIf issuer is omitted, then it is assumed that signer is self-signed and will\nalso be used as the issuer.\n\nIf signingtime is omitted, then 1970-01-01T00:00:00Z will be used.\n\nThe string value of a data key can be wrapped with \"base64(\" and \")\" in which\ncase the contents will be Base64 decoded during parsing.\n*/\npackage dummy // import \"ivxv.ee/container/dummy\"\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tcontainer.Register(container.Dummy,\n\t\tfunc(n yaml.Node) (container.OpenFunc, error) {\n\t\t\tvar c Conf\n\t\t\tif err := yaml.Apply(n, &c); err != nil {\n\t\t\t\treturn nil, ConfigurationError{Err: err}\n\t\t\t}\n\t\t\treturn c.Open, nil\n\t\t},\n\t\tfunc(encoded io.Reader) (container.Container, error) {\n\t\t\treturn new(Conf).Open(encoded)\n\t\t})\n}\n\n// unverifiedOpen opens a dummy signature container and returns data from it\n// without verifying any signatures (which dummy does anyway).\n//\n// Unexported function, because nobody should use this except during\n// bootstrapping via container.UnverifiedOpen.\n\n// Conf is the dummy container opener configuration.\ntype Conf struct {\n\t// Trusted is a slice of Common Names. If not empty, then all signers\n\t// of dummy containers opened with this configuration must be included\n\t// in Trusted.\n\tTrusted []string\n}\n\n// Open opens a dummy signature container and returns a container.Container for\n// accessing it.\nfunc (c *Conf) Open(encoded io.Reader) (cnt container.Container, err error) {\n\t// Unmarshal the YAML.\n\tvar y struct {\n\t\tSignatures []struct {\n\t\t\tSigner      string\n\t\t\tIssuer      string\n\t\t\tSigningTime string\n\t\t}\n\t\tData map[string]string\n\t}\n\tif err = yaml.Unmarshal(encoded, nil, &y); err != nil {\n\t\treturn nil, UnmarshalContainerError{Err: err}\n\t}\n\n\t// Parse the signatures into container.Signatures.\n\td := &dummy{data: make(map[string][]byte)}\n\tfor i, s := range y.Signatures {\n\t\tid := strconv.Itoa(i)\n\n\t\tvar cert *x509.Certificate\n\t\tif cert, err = cryptoutil.PEMCertificate(s.Signer); err != nil {\n\t\t\treturn nil, ParseSignerError{ID: id, Certificate: s.Signer, Err: err}\n\t\t}\n\n\t\t// Check that the signer is trusted.\n\t\tif !c.trusted(cert.Subject.CommonName) {\n\t\t\treturn nil, UntrustedSignerError{ID: id, Signer: cert.Subject.CommonName}\n\t\t}\n\n\t\tissuer := cert\n\t\tif len(s.Issuer) > 0 {\n\t\t\tif issuer, err = cryptoutil.PEMCertificate(s.Issuer); err != nil {\n\t\t\t\treturn nil, ParseIssuerError{ID: id, Certificate: s.Issuer, Err: err}\n\t\t\t}\n\t\t}\n\n\t\tt := time.Date(1970, time.January, 01, 00, 00, 00, 00, time.UTC)\n\t\tif len(s.SigningTime) > 0 {\n\t\t\tt, err = time.Parse(time.RFC3339, s.SigningTime)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, ParseSigningTimeError{ID: id, Time: s.SigningTime, Err: err}\n\t\t\t}\n\t\t}\n\n\t\td.signatures = append(d.signatures, container.Signature{\n\t\t\tID:          id,\n\t\t\tSigner:      cert,\n\t\t\tIssuer:      issuer,\n\t\t\tSigningTime: t,\n\t\t})\n\t}\n\n\t// Decode the data.\n\tfor k, s := range y.Data {\n\t\tif d.data[k], err = decode(s); err != nil {\n\t\t\treturn nil, DecodeValueError{Key: k, Err: err}\n\t\t}\n\t}\n\n\treturn d, nil\n}\n\nfunc (c *Conf) trusted(cn string) bool {\n\tif len(c.Trusted) == 0 {\n\t\treturn true\n\t}\n\tfor _, t := range c.Trusted {\n\t\tif t == cn {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// decode checks if s has a \"base64(\" prefix and a \")\" suffix and Base64\n// decodes the inner content. If the prefix or suffix is not found, then s is\n// returned unmodified.\nfunc decode(s string) ([]byte, error) {\n\tconst prefix, suffix = \"base64(\", \")\"\n\tt := strings.TrimSpace(s)\n\tif strings.HasPrefix(t, prefix) && strings.HasSuffix(t, suffix) {\n\t\treturn base64.StdEncoding.DecodeString(t[len(prefix) : len(t)-len(suffix)])\n\t}\n\treturn []byte(s), nil\n}\n\n// dummy is a parsed dummy Container.\ntype dummy struct {\n\tsignatures []container.Signature\n\tdata       map[string][]byte\n}\n\nfunc (d *dummy) Signatures() []container.Signature {\n\treturn d.signatures\n}\n\nfunc (d *dummy) Data() map[string][]byte {\n\treturn d.data\n}\n\nfunc (d *dummy) Close() error {\n\treturn nil\n}\n\n// SignatureValue implements the ivxv.ee/q11n/ocsp.SignatureValuer interface.\n// It checks if the signature ID is valid, but then returns a constant since\n// dummy containers do not have actual signatures.\nfunc (d *dummy) SignatureValue(id string) ([]byte, error) {\n\treturn []byte(\"signature value\"), d.checkID(id)\n}\n\n// TimestampData implements the ivxv.ee/q11n/tsp.TimestampDataer interface. It\n// checks if the signature ID is valid, but then returns a constant since dummy\n// containers do not have actual signatures to timestamp.\nfunc (d *dummy) TimestampData(id string) ([]byte, error) {\n\treturn []byte(\"timestamp data\"), d.checkID(id)\n}\n\nfunc (d *dummy) checkID(id string) error {\n\t// id must be a valid index.\n\ti, err := strconv.Atoi(id)\n\tif err != nil {\n\t\treturn SignatureIDParseError{ID: id, Err: err}\n\t}\n\tif i < 0 || i >= len(d.signatures) {\n\t\treturn NoSuchIDError{ID: id}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/container/dummy/dummy_test.go",
    "content": "package dummy\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nconst (\n\tdataKey   = \"signed data\"\n\tdataValue = `this is some data that is \"signed\" in the container`\n\tsignerCN  = \"dummy container test certificate\"\n\tissuerCN  = \"dummy container test issuer\"\n\n\tencoded = `\nsignatures:\n  - signer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBnjCCAUOgAwIBAgIJAKztlcHSEoWWMAoGCCqGSM49BAMCMCsxKTAnBgNVBAMM\n      IGR1bW15IGNvbnRhaW5lciB0ZXN0IGNlcnRpZmljYXRlMB4XDTE2MTAwNzA4NTA1\n      MloXDTI2MDgxNjA4NTA1MlowKzEpMCcGA1UEAwwgZHVtbXkgY29udGFpbmVyIHRl\n      c3QgY2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAToQ4OuVhmz\n      69qimYxoyVNmgRzFd1LJJtsm6wWeWpaoFa5yl+GR0Lw53RK2I2WcZ8zwdKtusOmf\n      eJB50hNXJaBfo1AwTjAdBgNVHQ4EFgQU6FDwDvLpx5CiJwiJRLA8zLlMPGswHwYD\n      VR0jBBgwFoAU6FDwDvLpx5CiJwiJRLA8zLlMPGswDAYDVR0TBAUwAwEB/zAKBggq\n      hkjOPQQDAgNJADBGAiEA/gbSkL0VquQWMvlogaI/GEY2XT/lpXKIdakCf/Qjg24C\n      IQCsBZbv02YVVnVmGJ4wCw6sSzuQcBxjGlDULea3JrWMFA==\n      -----END CERTIFICATE-----\n    issuer: |\n      -----BEGIN CERTIFICATE-----\n      MIIBlzCCAT6gAwIBAgIJAKMVxT8JoMPDMAoGCCqGSM49BAMCMCYxJDAiBgNVBAMM\n      G2R1bW15IGNvbnRhaW5lciB0ZXN0IGlzc3VlcjAgFw0xNzA4MTUxNTEyMThaGA8y\n      MTE3MDcyMjE1MTIxOFowJjEkMCIGA1UEAwwbZHVtbXkgY29udGFpbmVyIHRlc3Qg\n      aXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExDNTim5vmEt3/I1Ii8lC\n      chSOYAQzQIY88U1cPcMEHqpcll5UZmoxBKuv0X8rRECUK3z6vXK65djcJF4IWUiV\n      dKNTMFEwHQYDVR0OBBYEFAQVpWuMNqCeLXVLlinwXftO0VmGMB8GA1UdIwQYMBaA\n      FAQVpWuMNqCeLXVLlinwXftO0VmGMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E\n      AwIDRwAwRAIgVQ4tzmXu8UX/0+9zgob+SvDroz7fvWPeh0iauBWMx5wCIGHIdyjL\n      2X0l/NsePltPkvwTOfqVUcW9hbDQnuvhGVSO\n      -----END CERTIFICATE-----\n    signingtime: 2017-02-28T11:30:20Z\ndata:\n  ` + dataKey + \": \" + dataValue\n)\n\nvar signingTime = time.Date(2017, time.February, 28, 11, 30, 20, 0, time.UTC)\n\nfunc TestDummy(t *testing.T) {\n\tc, err := new(Conf).Open(strings.NewReader(encoded))\n\tif err != nil {\n\t\tt.Fatal(\"parsing dummy container failed:\", err)\n\t}\n\n\tif s := c.Signatures(); len(s) != 1 {\n\t\tt.Error(\"unexpected signatures count:\", len(s))\n\t} else if cn := s[0].Signer.Subject.CommonName; cn != signerCN {\n\t\tt.Error(\"unexpected signer CN value:\", cn)\n\t} else if is := s[0].Issuer.Subject.CommonName; is != issuerCN {\n\t\tt.Error(\"unexpected issuer CN value:\", cn)\n\t} else if st := s[0].SigningTime; !st.Equal(signingTime) {\n\t\tt.Error(\"unexpected signing time value:\", st)\n\t}\n\n\td := c.Data()\n\tif len(d) != 1 {\n\t\tt.Error(\"unexpected data key count:\", len(d))\n\t}\n\tif v, ok := d[dataKey]; !ok {\n\t\tt.Errorf(\"missing data key %q\", dataKey)\n\t} else if string(v) != dataValue {\n\t\tt.Errorf(\"unexpected data value of key %q: %s\", dataKey, v)\n\t}\n}\n\nfunc TestTrusted(t *testing.T) {\n\tfor _, test := range []struct {\n\t\tname  string\n\t\tconf  *Conf\n\t\tcause error\n\t}{\n\t\t{\"unconfigured\", new(Conf), nil},\n\t\t{\"trusted\", &Conf{Trusted: []string{signerCN}}, nil},\n\t\t{\"untrusted\", &Conf{Trusted: []string{\"other signer\"}}, new(UntrustedSignerError)},\n\t} {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := test.conf.Open(strings.NewReader(encoded))\n\t\t\tif err != test.cause && errors.CausedBy(err, test.cause) == nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v, want cause %T\", err, test.cause)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/container/log_desc.go",
    "content": "package container\n\nconst (\n\t_CONTAINER_PARSE  = \"Unsupported .bdoc container parser\"\n\t_CONTAINER_CNAME  = \".bdoc container parser has no canonical name\"\n\t_CONTAINER_PARSER = \"Failed to configure .bdoc container parser\"\n\t_CONTAINER_OPEN   = \"Failed to open .bdoc container\"\n\t_CONTAINER_EXT    = \"Missing .bdoc container extension\"\n)\n"
  },
  {
    "path": "common/collector/container/registry.go",
    "content": "package container\n\nimport (\n\t\"io\"\n\t\"sync\"\n\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// Type identifies a signature container type. The actual implementations are\n// in other packages.\ntype Type string\n\n// Enumeration of container types.\nconst (\n\tDummy Type = \"dummy\" // import \"ivxv.ee/container/dummy\"\n\tBDOC  Type = \"bdoc\"  // import \"ivxv.ee/container/bdoc\"\n\tASiCE Type = \"asice\" // Alias for bdoc.\n)\n\n// OpenFunc is the type of functions that parse and verify an encoded container\n// into a Container.\ntype OpenFunc func(io.Reader) (Container, error)\n\n// NewFunc is the type of functions that create a container opening function\n// with a specified configuration.\ntype NewFunc func(yaml.Node) (OpenFunc, error)\n\n// UnverifiedOpenFunc is the type of functions that open a container without\n// needing any configuration and verifying any signatures.\ntype UnverifiedOpenFunc func(io.Reader) (Container, error)\n\ntype regentry struct {\n\tnewOpen        NewFunc\n\tunverifiedOpen UnverifiedOpenFunc\n\tcanonical      Type\n\taliases        []Type // Includes canonical.\n}\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = make(map[Type]regentry)\n)\n\n// Register registers a signature container implementation. It is intended to\n// be called from init functions of packages that implement container types.\n//\n// n is a constructor function used to create container opening functions with\n// a specified configuration. o is a container opening function which is used\n// during bootstrapping to open a container without needing any configuration\n// and verifying its signatures.\n//\n// If any aliases are given, then they will be registered as alternative valid\n// types that refer to the registered implementation. Container configuration\n// must still refer to the canonical name, but containers can be opened by\n// referring to the canonical name or any of the aliases.\nfunc Register(t Type, n NewFunc, o UnverifiedOpenFunc, aliases ...Type) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\taliases = append(aliases, t)\n\tfor _, alias := range aliases {\n\t\tregistry[alias] = regentry{n, o, t, aliases}\n\t}\n}\n"
  },
  {
    "path": "common/collector/cookie/cookie.go",
    "content": "/*\nPackage cookie creates and opens session cookies which are protected from\nreading and modification by a shared secret.\n\nNote! Session cookies are not inherently protected from replay attacks.\nAdditional steps must be taken if resubmitting or delaying a cookie can cause\nproblems.\n*/\npackage cookie\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"sync\"\n)\n\n// Key is the type of the shared secret used to create and access cookies.\ntype Key []byte\n\n// C is a cookie manager which can create or open cookies.\ntype C struct {\n\taead  cipher.AEAD // Authenticated encryption state.\n\tnonce []byte      // Next nonce: random initial value, incremented after each use.\n\tlock  sync.Mutex  // Synchronizes access to the nonce.\n}\n\n// New creates a new cookie manager with the provided key. The key must be\n// valid for the underlying cipher, currently AES.\nfunc New(key Key) (c *C, err error) {\n\tc = new(C)\n\n\t// Set up authenticated encryption.\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, KeyError{Err: err, Description: _COOKIE_AES}\n\t}\n\tif c.aead, err = cipher.NewGCM(block); err != nil {\n\t\treturn nil, CipherError{Err: err,\n\t\t\tDescription: _COOKIE_AES}\n\t}\n\n\t// Initialize the nonce.\n\tc.nonce = make([]byte, c.aead.NonceSize())\n\tif _, err = rand.Read(c.nonce); err != nil {\n\t\treturn nil, InitNonceError{Err: err,\n\t\t\tDescription: _COOKIE_NONCE}\n\t}\n\treturn\n}\n\n// Create creates a new cookie which contains the data, but cannot be read or\n// modified without knowing the shared secret passed to New.\nfunc (c *C) Create(data []byte) (cookie []byte) {\n\t// Cookies are a concatenation of the fixed length nonce, which we will\n\t// copy, and the ciphertext, which aead.Seal will append.\n\tcookie = make([]byte, len(c.nonce), len(c.nonce)+len(data)+c.aead.Overhead())\n\n\t// Copy the current nonce and increment it for next use.\n\tc.lock.Lock()\n\tcopy(cookie, c.nonce)\n\n\ti := c.aead.NonceSize() - 1\n\tc.nonce[i]++\n\tfor i > 0 && c.nonce[i] == 0 { // If a byte wraps, then increment the next one.\n\t\ti--\n\t\tc.nonce[i]++\n\t}\n\tc.lock.Unlock()\n\n\t// Encrypt data, append the result to cookie, and return.\n\treturn c.aead.Seal(cookie, cookie, data, nil)\n}\n\n// Open opens a cookie using the shared secret passed to New and returns the\n// data within.\nfunc (c *C) Open(cookie []byte) (data []byte, err error) {\n\tn := c.aead.NonceSize()\n\tif len(cookie) <= n+c.aead.Overhead() {\n\t\treturn nil, ShortCookieError{Len: len(cookie), Err: err,\n\t\t\tDescription: _COOKIE_SHORT}\n\t}\n\n\tnonce := cookie[:n]\n\tcipher := cookie[n:]\n\n\tif data, err = c.aead.Open(data, nonce, cipher, nil); err != nil {\n\t\treturn nil, OpenError{Err: err, Description: _COOKIE_READ}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/cookie/cookie_test.go",
    "content": "package cookie\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\n// zero creates a new cookie manager with a zero key and zero nonce: this\n// ensures predictable output.\nfunc zero(t *testing.T) *C {\n\tc, err := New(make([]byte, 16))\n\tif err != nil {\n\t\tt.Fatal(\"new failed:\", err)\n\t}\n\tc.nonce = make([]byte, len(c.nonce))\n\treturn c\n}\n\nvar (\n\tdata   = []byte(\"foobar\")\n\tcookie = []byte{\n\t\t0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t0x65, 0xe7, 0xb5, 0xac, 0x01, 0xc4, 0xe8, 0xc9, 0xd8, 0x17, 0x8d, 0x1a,\n\t\t0xfc, 0x1e, 0xe1, 0x19, 0xd8, 0x39, 0xfd, 0x94, 0x7c, 0x6d,\n\t}\n)\n\nfunc TestCreate(t *testing.T) {\n\tif got := zero(t).Create(data); !bytes.Equal(got, cookie) {\n\t\tt.Errorf(\"unexpected cookie value: %x\", got)\n\t}\n}\n\nfunc TestOpen(t *testing.T) {\n\tif got, err := zero(t).Open(cookie); err != nil {\n\t\tt.Error(\"open failed:\", err)\n\t} else if !bytes.Equal(got, data) {\n\t\tt.Errorf(\"unexpected data value: %q\", got)\n\t}\n}\n\nfunc TestIncrementNonce(t *testing.T) {\n\tc := zero(t)\n\tempty := make([]byte, len(c.nonce))\n\n\tone := make([]byte, len(c.nonce))\n\tone[len(one)-1] = 1\n\n\tff := make([]byte, len(c.nonce))\n\tff[len(ff)-1] = 0xff\n\n\tcarry := make([]byte, len(c.nonce))\n\tcarry[len(carry)-2] = 1\n\n\ttests := []struct {\n\t\tname   string\n\t\tbefore []byte\n\t\tafter  []byte\n\t}{\n\t\t{\"one\", empty, one},\n\t\t{\"carry\", ff, carry},\n\t\t{\"wrap\", bytes.Repeat([]byte{0xff}, len(c.nonce)), empty},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcopy(c.nonce, test.before)\n\t\t\tc.Create(nil)\n\t\t\tif !bytes.Equal(c.nonce, test.after) {\n\t\t\t\tt.Errorf(\"unexpected nonce: %x, want %x\", c.nonce, test.after)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/cookie/log_desc.go",
    "content": "package cookie\n\nconst (\n\t_COOKIE_AES   = \"Failed to create AES cipher\"\n\t_COOKIE_NONCE = \"Failed to create a nonce (random value)\"\n\t_COOKIE_SHORT = \"Cookie size is too short\"\n\t_COOKIE_READ  = \"Failed to verify a cookie\"\n)\n"
  },
  {
    "path": "common/collector/crypto/elgamal/ciphertext.go",
    "content": "package elgamal\n\nimport (\n\t\"encoding/asn1\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\tasn_11 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\n// UnmarshalCiphertext ASN.1 unmarshalls IVXV specific ASN.1 ElGamal ciphertext as\n//\n//\tciphertext ::= SEQUENCE {\n//\t\talgorithm\tAlgorithmIdentifier\n//\t\tdata\tIVXV ELGAMAL CIPHERTEXT\n//\t}\n//\n//\tIVXV ELGAMAL CIPHERTEXT ::= SEQUENCE {\n//\t\tA\tGROUP ELEMENT\n//\t\tB\tGROUP ELEMENT\n//\t}\n//\n//\tGROUP ELEMENT ::= CHOICE {\n//\t\tModPElement\tINTEGER\n//\t\tECpElement\tOCTET STRING\n//\t\tEdwards25519Element\tOCTET STRING\n//\t}\n//\n// and then performs all ciphertext correctness checks as\n// `processor squash --conf conf.bdoc --params params.bdoc` offline application\n// would do, i.e.\n//\n// 1. ElGamal ciphertext is correct ASN.1\n//\n// 2. OID of ElGamal ciphertext is supported by the group implementation\n//\n// 3.1. For ModP group checks that:\n//\n// a) Both ElGamal ciphertext A and B values belong to the group\n//\n// b) Both ElGamal ciphertext A and B values > 0\n//\n// c) Both ElGamal ciphertext A and B values' group order > 0\n//\n// d) Both ElGamal ciphertext A and B values are quadratic residue modulo P\n//\n// 3.2. For EC group check that:\n//\n// a) Both ElGamal ciphertext A and B values belong to the group\n//\n// b) Both ElGamal ciphertext A and B values are valid EC points and are on the curve\nfunc UnmarshalCiphertext(g group.Group, data asn_11.DER) (*elgamal.Ciphertext, error) {\n\tsequence := cryptobyte.String(data)\n\tvar ciphertext cryptobyte.String\n\tvar algorithm cryptobyte.String\n\tvar oid asn1.ObjectIdentifier\n\tvar ciphertextData cryptobyte.String\n\tvar A, B cryptobyte.String\n\n\tif !sequence.ReadASN1(&ciphertext, asn_1.SEQUENCE) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNotSequenceError{\n\t\t\tDescription: _CIP_SEQ,\n\t\t}\n\t}\n\n\tif !ciphertext.ReadASN1(&algorithm, asn_1.SEQUENCE) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNotAlgorithmIdentifierError{\n\t\t\tDescription: _CIP_AID,\n\t\t}\n\t}\n\n\tif !algorithm.ReadASN1ObjectIdentifier(&oid) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNotAlgorithmError{\n\t\t\tDescription: _CIP_AID_OID,\n\t\t}\n\t}\n\n\t// Trusted ElGamal oid\n\telGamalOIDs := []asn1.ObjectIdentifier{\n\t\tcrypto.ElGamalEncryptionOID(),\n\t\tecElGamalEncryptionOID(),\n\t}\n\n\tvar found bool\n\tfor _, elGamalOID := range elGamalOIDs {\n\t\tif strings.Compare(oid.String(), elGamalOID.String()) == 0 {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNotElGamalOIDError{\n\t\t\tOID:         oid.String(),\n\t\t\tDescription: _CIP_OID_MISMATCH,\n\t\t}\n\t}\n\n\tif !ciphertext.ReadASN1(&ciphertextData, asn_1.SEQUENCE) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierCiphertextDataError{\n\t\t\tDescription: _CIP_DATA,\n\t\t}\n\t}\n\n\tif !ciphertextData.ReadAnyASN1Element(&A, nil) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNoEphemeralError{\n\t\t\tDescription: _CIP_A,\n\t\t}\n\t}\n\n\tif !ciphertextData.ReadAnyASN1Element(&B, nil) {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierNoBlindedMsgError{\n\t\t\tDescription: _CIP_B,\n\t\t}\n\t}\n\n\t// Is there left any unread bytes?\n\tif !sequence.Empty() || !ciphertext.Empty() || !algorithm.Empty() || !ciphertextData.Empty() {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierTrailingBytesError{\n\t\t\tDescription: _CIP_TRAIL,\n\t\t}\n\t}\n\n\t// Prepare ElGamal ASN.1 ciphertext for crypto library supported format\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn_1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddBytes(A)\n\t\tbuilder.AddBytes(B)\n\t})\n\n\tder, err := builder.Bytes()\n\tif err != nil {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierASN1MarshalCiphertextError{Err: err,\n\t\t\tDescription: _CIP_MARSHAL}\n\t}\n\n\tct, err := elgamal.ASN1UnmarshalCiphertext(g, der)\n\tif err != nil {\n\t\treturn nil, UnmarshalCiphertextElGamalASN1CiphertextVerifierError{Err: err,\n\t\t\tDescription: _CIP_VERIFY,\n\t\t}\n\t}\n\n\treturn ct, nil\n}\n"
  },
  {
    "path": "common/collector/crypto/elgamal/ciphertext_test.go",
    "content": "package elgamal\n\nimport (\n\t\"encoding/base64\"\n\t\"testing\"\n\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nconst (\n\tmodP3072Group = \"RFC3526ModPGroup3072\"\n\tecP384Group   = \"NIST-P384\"\n\n\tbase64DERModP3072Ciphertext = \"MIIDGjALBgkrBgEEAZdVAgEwggMJAoIBgQCDkyKlAYLbY7VkpU2ayZkDQi3AKfXNKdQaUDnn+/jUB6W3Bn1jE2ijXtunt8g5fKeiDxuP59AcoI3pSKIZOqFFYOTKI+kDXe3bPz9RDziBWXtI6TawYj1C07dPCivdazxc6qUBUO1teeUZsQQV37q6FZ4KLc3r13gtCzQPCAykd/UIRxPSmp1hWZ4ytbg15YLt56E6wOLbHI6mbfqRgteWcsVM+YEicqPhVXDyLlkNMBk6OYXbjALjieX4B4K1sCPvB01ZIizjdHLZPzTas6juZ8Ga1izrS/tVaIRQVgQY2NXcgAYopZaW6K9uHIOApvTy+7ykOfhfrKmBoMy+54eOq5BN9rkcCkRhu+7L1qigmPM5SR1xiwgVAoN9lrp1lhZdfQAPmUVmchfTCyvGX05ZlqP2AxYgBbtiprbXqkr0K2SdCtee2ta1HLzs/oDHylzjdbQmqbXBaHEbJ90eFpm585Nq2jaQLW9V9wGJ39vzcQSZe4i1fdK546EBfNNCT2oCggGAEjdBWB8wPj+0Ag/lIxLWTbdSQViDNUIln/7QxZxwBrxIP5sRoVRnScxHu4TMskj8kQ9kwnmSeiP7yTnbUsHdko/bJPFhhMLeEe3sXMhA9J0WdRDGeWV1gtORFOIkD/jB8LAO6TpI3r9yZ8F+9SAxnHLyZ9KxDgfMk9/S0qCNYMTo/uFBlQiNl1cDxL92yoVfTkT6ufFa6T7VHhXKHgBNrR/r6VKVmcmpLseQHwmqHInjKAZ+S/PqxLY4yRmHNJXbun7bjaBvXMotvcKWenZbe+G2K+E0Q55V19PfivIrb2KrKumpSuS7wMSgLyuDdgC1AwKL9qeAwXfaDeHQ4DMK1WW/sg4NXow6/3e+WkgRI3ncFIfHVIxpcUsYqai+8XPomiomhhTNVLDAmTswdjnnsPgmDqAjIWLUyFvQup1iIYP/y3CJs4U0Cnc2MZ4CQIsaZ4i49dsR7kxaCLU+sPbUhDRaXK+LCRlDKc+vVTGi9O+r+Ztf+etwbr3zyRSqVK6v\"\n\tbase64DERECp384Ciphertext   = \"MIHWMAsGCSsGAQQBho0fATCBxgRhBI/cd6M0Dyjedj2fOF1IA6ikpa4cdwniqMU49KibgsOtffMvtglYJei8B7eHyjE7D8Wl9HFmeBoLgeUTre0Mfk4Lknd4v1c/GJu1bYZlAMvmg46ysrItQ1sXD/lOwPNeiARhBHGq8thwH1qCSWQae5McuJB5GM4MO4m4tz3YQ6qnuKomG3b+QTS9ToU9ZK3OQj1Jjf6RA08tmERk417DC+IC+hbH6JjCCrVs2pvTBx5H0OQ+2uZ2yEhMYKwyLsfRGzu3rA==\"\n)\n\nfunc TestASN1CiphertextVerify(t *testing.T) {\n\ttestSamples := []struct {\n\t\tgroupName        string\n\t\tbase64Ciphertext string\n\t}{\n\t\t{\n\t\t\tgroupName:        modP3072Group,\n\t\t\tbase64Ciphertext: base64DERModP3072Ciphertext,\n\t\t},\n\t\t{\n\t\t\tgroupName:        ecP384Group,\n\t\t\tbase64Ciphertext: base64DERECp384Ciphertext,\n\t\t},\n\t}\n\n\tfor _, testSample := range testSamples {\n\t\tt.Run(testSample.groupName, func(t *testing.T) {\n\t\t\tg, err := group.Get(testSample.groupName)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tciphertext, err := base64.StdEncoding.DecodeString(testSample.base64Ciphertext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t_, err = UnmarshalCiphertext(g, ciphertext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/crypto/elgamal/elgamal.go",
    "content": "package elgamal\n\nimport \"encoding/asn1\"\n\nvar (\n\tecElGamalEncryptionOID = func() asn1.ObjectIdentifier { return asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 99999, 1} } //nolint:lll\n)\n"
  },
  {
    "path": "common/collector/crypto/elgamal/log_desc.go",
    "content": "package elgamal\n\nconst (\n\t_CIP_SEQ          = \"Failed to read ElGamal ciphertext ASN.1 sequence\"\n\t_CIP_AID          = \"Failed to read ElGamal ciphertext AlgorithmIdentifier\"\n\t_CIP_AID_OID      = \"Failed to read ElGamal ciphertext AlgorithmIdentifier's Algorithm oid\"\n\t_CIP_OID_MISMATCH = \"Ciphertext oid is not ElGamal oid\"\n\t_CIP_DATA         = \"Failed to read ElGamal ciphertext data ASN.1 sequence\"\n\t_CIP_A            = \"Failed to read ElGamal ciphertext A ASN.1 element\"\n\t_CIP_B            = \"Failed to read ElGamal ciphertext B ASN.1 element\"\n\t_CIP_TRAIL        = \"Trailing bytes left while reading ASN.1 ElGamal ciphertext\"\n\t_CIP_MARSHAL      = \"Failed to ASN.1 marshal ElGamal ciphertext\"\n\t_CIP_VERIFY       = \"Failed to verify ASN.1 ElGamal ciphertext\"\n\t_PUB_MODP_PARAMS  = \"Failed to ASN.1 unmarshal x509 key ModP group parameters\"\n\t_PUB_ECP_PARAMS   = \"Failed to ASN.1 unmarshal x509 key ECp group parameters\"\n\t_PUB_ALGO         = \"Unknown ASN.1 unmarshalled x509 key parameters algorithm\"\n\t_PUB_PARAMS       = \"Failed to ASN.1 unmarshal x509 key parameters\"\n\t_PUB_DATA         = \"Failed to ASN.1 unmarshal x509 key data\"\n\t_PUB_TRAIL        = \"ASN.1 unmarshalling x509 key data left trailing bytes behind\"\n\t_PUB_EL           = \"Failed to ASN.1 unmarshal ElGamal public key element\"\n\t_MODP_LEN         = \"Unsupported ModP element length\"\n\t_MODP_G           = \"Unknown ModP group\"\n\t_ECP_LEN          = \"Unsupported ECp element length\"\n\t_ECP_G            = \"Unknown ECp group\"\n)\n"
  },
  {
    "path": "common/collector/crypto/elgamal/x509.go",
    "content": "package elgamal\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\tasn_11 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\ntype ECpGroupParameters struct {\n\tCurveID    string\n\tElectionID string\n}\n\ntype ModPGroupParameters struct {\n\tP          *big.Int\n\tG          *big.Int\n\tElectionID string\n}\n\ntype X509Unmarshaller struct {\n\tElectionID string\n}\n\nfunc NewX509Unmarshaller() *X509Unmarshaller {\n\treturn &X509Unmarshaller{}\n}\n\n// UnmarshalKeyParameters unmarshalls IVXV x509 public key parameters, but returns\n// crypto library compatible ElGamal oid.\nfunc (x509 *X509Unmarshaller) UnmarshalKeyParameters(algorithmIdentifier pkix.AlgorithmIdentifier) (oid asn1.ObjectIdentifier, parameters crypto.AlgorithmIdentifierParameters, err error) { //nolint:lll\n\tvar G group.Group\n\n\tswitch algorithmIdentifier.Algorithm.String() {\n\tcase crypto.ElGamalEncryptionOID().String():\n\t\tvar params ModPGroupParameters\n\n\t\t_, err = asn1.Unmarshal(algorithmIdentifier.Parameters.FullBytes, &params)\n\t\tif err != nil {\n\t\t\treturn nil, nil, UnmarshalKeyParametersX509UnmarshallerModPKeyParametersError{Err: err,\n\t\t\t\tDescription: _PUB_MODP_PARAMS}\n\t\t}\n\n\t\t// Crypto library compatible oid for ElGamal encryption\n\t\toid = crypto.ElGamalEncryptionOID()\n\t\tx509.ElectionID = params.ElectionID\n\t\tG, err = params.IVXVModPGroupParser()\n\tcase ecElGamalEncryptionOID().String():\n\t\tvar params ECpGroupParameters\n\n\t\t_, err = asn1.Unmarshal(algorithmIdentifier.Parameters.FullBytes, &params)\n\t\tif err != nil {\n\t\t\treturn nil, nil, UnmarshalKeyParametersX509UnmarshallerECpKeyParametersError{Err: err,\n\t\t\t\tDescription: _PUB_ECP_PARAMS}\n\t\t}\n\n\t\t// Crypto library compatible oid for ElGamal encryption\n\t\toid = crypto.ElGamalEncryptionOID()\n\t\tx509.ElectionID = params.ElectionID\n\t\tG, err = params.IVXVECpGroupParser()\n\tdefault:\n\t\treturn nil, nil, UnmarshalKeyParametersX509UnmarshallerUnknownAlgorithmError{\n\t\t\tOID:         algorithmIdentifier.Algorithm.String(),\n\t\t\tDescription: _PUB_ALGO}\n\t}\n\tif err != nil {\n\t\treturn nil, nil, UnmarshalKeyParametersX509UnmarshallerError{\n\t\t\tErr: err, Description: _PUB_PARAMS}\n\t}\n\n\treturn oid, elgamal.NewParameters(G), nil\n}\n\n// UnmarshalKeyElement unmarshalls IVXV x509 public key element.\nfunc (x509 *X509Unmarshaller) UnmarshalKeyElement(g group.Group, der asn_11.DER) (element group.Element, err error) {\n\tsequence := cryptobyte.String(der)\n\tvar data cryptobyte.String\n\n\tif !sequence.ReadASN1(&data, asn_1.SEQUENCE) {\n\t\treturn nil, UnmarshalKeyElementX509UnmarshallerNotSequenceError{\n\t\t\tDescription: _PUB_DATA}\n\t}\n\n\tif !sequence.Empty() {\n\t\treturn nil, UnmarshalKeyElementX509UnmarshallerTrailingBytesError{\n\t\t\tDescription: _PUB_TRAIL}\n\t}\n\n\telement, err = elgamal.ASN1UnmarshalPublicElement(g, asn_11.DER(data))\n\tif err != nil {\n\t\treturn nil, UnmarshalKeyElementX509UnmarshallerElGamalPublicError{\n\t\t\tErr: err, Description: _PUB_EL}\n\t}\n\n\treturn\n}\n\nfunc (p *ModPGroupParameters) IVXVModPGroupParser() (G group.Group, err error) { //nolint:gocritic\n\tswitch p.P.BitLen() {\n\tcase 2048:\n\t\tG, err = group.Get(\"RFC3526ModPGroup2048\")\n\tcase 3072:\n\t\tG, err = group.Get(\"RFC3526ModPGroup3072\")\n\tcase 4096:\n\t\tG, err = group.Get(\"RFC3526ModPGroup4096\")\n\tcase 8192:\n\t\tG, err = group.Get(\"RFC3526ModPGroup8192\")\n\tdefault:\n\t\treturn nil, IVXVModPGroupParserModPGroupParametersUnsupportedModPElementError{Err: err,\n\t\t\tDescription: _MODP_LEN}\n\t}\n\tif err != nil {\n\t\treturn nil, IVXVModPGroupParserModPGroupParametersUnknownModPGroupError{Err: err,\n\t\t\tDescription: _MODP_G}\n\t}\n\n\treturn\n}\n\nfunc (p *ECpGroupParameters) IVXVECpGroupParser() (G group.Group, err error) { //nolint:gocritic\n\tswitch p.CurveID {\n\tcase \"P-256\":\n\t\tG, err = group.Get(\"NIST-P256\")\n\tcase \"P-384\":\n\t\treturn group.Get(\"NIST-P384\")\n\tcase \"P-521\":\n\t\treturn group.Get(\"NIST-P521\")\n\tdefault:\n\t\treturn nil, IVXVECpGroupParserECpGroupParametersUnsupportedECpElementError{Err: err,\n\t\t\tDescription: _ECP_LEN}\n\t}\n\tif err != nil {\n\t\treturn nil, IVXVECpGroupParserECpGroupParametersUnknownGroupError{Err: err,\n\t\t\tDescription: _ECP_G}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "common/collector/crypto/elgamal/x509_test.go",
    "content": "package elgamal\n\nimport (\n\t\"testing\"\n\n\t\"tivi.io/core/crypto\"\n\tx_509 \"tivi.io/core/crypto/x509\"\n)\n\nconst (\n\tmodP3072IVXVPEMPublicKey = \"-----BEGIN PUBLIC KEY-----\\nMIIDMjCCAaEGCSsGAQQBl1UCATCCAZICggGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiK\\nZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY3\\n7WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STP\\nX4NlXSPco62WHGLzViCFUrue1SkHcJaWbWcMNU5KvJgE8XRsCMoYIXwykF5GLjbOO+OedywYDoYD\\nmyeDouwHoo+1xV3wb0xSyd4ry/aVWBcYOZVJfOqVauUV0iYYmPoFEBVyjlqKqsQtrTMXDQRQejOo\\nVSGr3xy6ZOz7hQRY2+8KiupxV10GDH2zlw+FpuHkx6v1rozbCTPXHoyU4EolYZ3O49ImGtLua/Ev\\n+gbZighk2HYCcz7IamRSHysYF3sgDLvhF1d6YV1sdwmIwLrZRuII4k+gdOWrMUPbW/zg/RCOS4LR\\nIKk60sr//////////wIBAhsISEEtU0VUVVADggGJADCCAYQCggGAeitj08u4VcaxSlj1vbnO8QKD\\nZAmFy72WeziY8otUuEHI5GQZVXZOCew/9EI/cSYTGf1tqDlg3m1wTIRkX5VY/zGUWPhiXEdLqiuK\\nOzDDHBlKWE1Sikq81qdMwtn+Jz4e83/Cnefj/q3C30GMhiWKRg3TTR0SmS7hnuboOEDXAd0NAHP2\\n5/eYj4UKiFVbRz0bmw80x3jCtGVZts/bdq5SO1jPAh9822HnxWU7jADRKFggItodgAgQFaFQQAad\\n49ae3G8tgk8pj+W9kc3aCZEdN2hUj4LRy7h8mWky2abi1HWPZcbPuldHnSyCYvXawHZyyhNn2pos\\nvpo6fkorkmBfBuxzudr0K8bw95iaRBVBa5Yug7hY3BHr/shwmYYMci10u8IfnHJqHCUweJr0pP31\\nFsO2OkHCPnwyFJ0vhbgky2O693owerdaucZm2bXYANN2T//sonU5JdFq2WYB9DK5e+6e0GeOJxhb\\nlOJeUpgz3vFpM6WPB3HMaY3+m/YE1JJO\\n-----END PUBLIC KEY-----\"\n\tecP384IVXVPEMPublicKey   = \"-----BEGIN PUBLIC KEY-----\\nMIGGMBwGCSsGAQQBho0fATAPGwVQLTM4NBsGUksyMDI0A2YAMGMEYQSJJIKZPcSGFK4FxmrLBPaY\\nV8meE4KNJxDUKk7cY6aBwckU+FqdY8C+MmSp3+MY42nxufI92C2EOBsaDfloRw3NM9x7FdXwhrgf\\nTOJR5J20rWN2jSJCV65GQA8uh+NnL7Y=\\n-----END PUBLIC KEY-----\"\n)\n\nfunc TestX509Unmarshaller(t *testing.T) {\n\tunmarshaller := x_509.NewUnmarshaller[crypto.EncryptionKey](NewX509Unmarshaller())\n\tpublicKeys := []struct {\n\t\tname string\n\t\tkey  []byte\n\t}{\n\t\t{\n\t\t\tname: \"RFC3526ModPGroup3072\",\n\t\t\tkey:  []byte(modP3072IVXVPEMPublicKey),\n\t\t},\n\t\t{\n\t\t\tname: \"NIST-P384\",\n\t\t\tkey:  []byte(ecP384IVXVPEMPublicKey),\n\t\t},\n\t}\n\n\tfor _, publicKey := range publicKeys {\n\t\tparams, pkey, err := unmarshaller.Unmarshal(publicKey.key)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif pkey == nil {\n\t\t\tt.Fatal(\"Public key is nil\")\n\t\t}\n\n\t\tif params.Group().Name() != publicKey.name {\n\t\t\tt.Fatalf(\"expected group name %s, but got %s\", publicKey.name, params.Group().Name())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/cryptoutil/cryptoutil.go",
    "content": "/*\nPackage cryptoutil provides utility functions that are used as building blocks\nwhen implementing cryptographic formats and algorithms.\n*/\npackage cryptoutil\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"math/big\"\n)\n\nconst entropy256bit uint64 = 32\n\n// hp is a map from hash function identifier to DigestInfo ASN.1 prefix.\nvar hp = map[crypto.Hash][]byte{\n\tcrypto.SHA224: {\n\t\t0x30, 0x2d, 0x30, 0x0d,\n\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04,\n\t\t0x04, 0x1c,\n\t},\n\tcrypto.SHA256: {\n\t\t0x30, 0x31, 0x30, 0x0d,\n\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,\n\t\t0x04, 0x20,\n\t},\n\tcrypto.SHA384: {\n\t\t0x30, 0x41, 0x30, 0x0d,\n\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02,\n\t\t0x04, 0x30,\n\t},\n\tcrypto.SHA512: {\n\t\t0x30, 0x51, 0x30, 0x0d,\n\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03,\n\t\t0x04, 0x40,\n\t},\n}\n\n// DigestInfo hashes data using the hash function h and returns the result in a\n// DER-encoded PKCS #1 DigestInfo structure. DigestInfo panics if h is not\n// available or a prefix for it has not been defined in x509util: this reduces\n// error checking code, as these problems can be statically detected.\nfunc DigestInfo(h crypto.Hash, data []byte) []byte {\n\t// Create a copy of the prefix, so that Sum can append to it directly.\n\t// Allocate space for both the prefix and the hash.\n\tp := hp[h]\n\tprefix := make([]byte, len(p), len(p)+h.Size())\n\tcopy(prefix, p)\n\n\thash := h.New()\n\thash.Write(data)\n\treturn hash.Sum(prefix)\n}\n\n// PEMDecode performs strict PEM decoding, i.e., the block type must match\n// exactly, no headers are allowed, and there cannot be any trailing data.\nfunc PEMDecode(encoded, blockType string) (decoded []byte, err error) {\n\tblock, rest := pem.Decode([]byte(encoded))\n\tif block == nil {\n\t\treturn nil, NotPEMEncodingError{\n\t\t\tDescription: _CUTIL_PEM,\n\t\t}\n\t}\n\tif len(rest) > 0 {\n\t\treturn nil, PEMTrailingDataError{Trailing: rest,\n\t\t\tDescription: _CUTIL_PEM}\n\t}\n\tif block.Type != blockType {\n\t\treturn nil, PEMBlockTypeError{Type: block.Type,\n\t\t\tExpected: blockType, Description: _CUTIL_PEM_H}\n\t}\n\tif len(block.Headers) > 0 {\n\t\treturn nil, PEMHeadersError{Headers: block.Headers,\n\t\t\tDescription: _CUTIL_PEM_H_BAD}\n\t}\n\treturn block.Bytes, nil\n}\n\n// PEMCertificates parses one or more PEM type certificates into a slice and\n// returns them.\nfunc PEMCertificates(pems ...string) (certs []*x509.Certificate, err error) {\n\tfor i, pem := range pems {\n\t\tcert, err := PEMCertificate(pem)\n\t\tif err != nil {\n\t\t\treturn nil, PEMCertificateError{\n\t\t\t\tIdx:         i,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _CUTIL_CERT_PEM,\n\t\t\t}\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\treturn\n}\n\n// PEMCertificate parses a PEM block of type CERTIFICATE and the X.509\n// certificate within.\nfunc PEMCertificate(p string) (cert *x509.Certificate, err error) {\n\tder, err := PEMDecode(p, \"CERTIFICATE\")\n\tif err != nil {\n\t\treturn nil, PEMCertificateDecodeError{Err: err,\n\t\t\tDescription: _CUTIL_PEM}\n\t}\n\tcert, err = x509.ParseCertificate(der)\n\tif err != nil {\n\t\treturn nil, PEMCertParseX509Error{Err: err,\n\t\t\tDescription: _CUTIL_CERT_PEM}\n\t}\n\treturn\n}\n\n// Base64Certificate parses a X.509 certificate from base64-encoded DER data.\nfunc Base64Certificate(b string) (cert *x509.Certificate, err error) {\n\tcertDER, err := base64.StdEncoding.DecodeString(b)\n\tif err != nil {\n\t\treturn nil, Base64CertificateDecodeError{Err: err,\n\t\t\tDescription: _CUTIL_CERT_B64}\n\t}\n\tcert, err = x509.ParseCertificate(certDER)\n\tif err != nil {\n\t\treturn nil, Base64CertParseX509Error{Err: err,\n\t\t\tDescription: _CUTIL_CERT_PARSE}\n\t}\n\treturn\n}\n\n// PEMCertificatePool parses a slice of PEM-encoded certificates and puts them\n// in a CertPool.\nfunc PEMCertificatePool(pems ...string) (pool *x509.CertPool, err error) {\n\tpool = x509.NewCertPool()\n\tfor i, pem := range pems {\n\t\tc, err := PEMCertificate(pem)\n\t\tif err != nil {\n\t\t\treturn nil, PEMCertError{Index: i, Err: err,\n\t\t\t\tDescription: _CUTIL_CERT_PEM}\n\t\t}\n\t\tpool.AddCert(c)\n\t}\n\treturn pool, nil\n}\n\n// CertificatePool puts all provided certificates in a CertPool. This is a\n// simple helper function to replace a for loop with a single expression.\nfunc CertificatePool(certs ...*x509.Certificate) (pool *x509.CertPool) {\n\tpool = x509.NewCertPool()\n\tfor _, c := range certs {\n\t\tpool.AddCert(c)\n\t}\n\treturn pool\n}\n\n// ReEncodeECDSASignature re-encodes an ECDSA XML digital signature to an ECDSA\n// X.509 PKI digital signature. The XML signature format is specified in\n// https://tools.ietf.org/html/rfc4050#section-3.3 and the X.509 PKI signature\n// format is specified in https://tools.ietf.org/html/rfc3279#section-2.2.3.\nfunc ReEncodeECDSASignature(signature []byte) (recode []byte, err error) {\n\tvar r, s *big.Int\n\tif r, s, err = ParseECDSAXMLSignature(signature); err != nil {\n\t\treturn nil, ECDSASignatureParseError{Signature: signature,\n\t\t\tErr: err, Description: _CUTIL_ECDSA2XML}\n\t}\n\tif recode, err = asn1.Marshal(struct {\n\t\tR *big.Int\n\t\tS *big.Int\n\t}{r, s}); err != nil {\n\t\treturn nil, ECDSASignatureASN1MarshalError{Err: err,\n\t\t\tDescription: _CUTIL_ASN1ECDSA}\n\t}\n\treturn\n}\n\nfunc IsECDSAASN1EncodedSignature(signature []byte) error {\n\ttype ecdsaRawSig struct {\n\t\tR *big.Int\n\t\tS *big.Int\n\t}\n\tsig := new(ecdsaRawSig)\n\n\tunmarshal, err := asn1.Unmarshal(signature, sig)\n\tif err != nil {\n\t\treturn SignatureIsNotASN1EncodedECDSAError{Err: err,\n\t\t\tDescription: _CUTIL_UASN1ECDSA}\n\t}\n\tif len(unmarshal) != 0 {\n\t\treturn SignatureIsASN1EncodedECDSAButHasRestAmountOfBytesError{\n\t\t\tAmount:      len(unmarshal),\n\t\t\tDescription: _CUTIL_UASN1ECDSA,\n\t\t}\n\t}\n\treturn nil\n}\n\n// ParseECDSAXMLSignature parses an ECDSA XML digital signature. The XML\n// signature is a concatenation of the R and S outputs of the ECDSA algorithm\n// as specified in https://tools.ietf.org/html/rfc4050#section-3.3.\n//\n// Note! This format differs from the one used in X.509 certificates specified\n// here https://tools.ietf.org/html/rfc3279#section-2.2.3.\nfunc ParseECDSAXMLSignature(signature []byte) (r, s *big.Int, err error) {\n\tif len(signature)%2 != 0 {\n\t\treturn nil, nil, ECDSASignatureLengthError{Description: _CUTIL_ECDSA_SIZE}\n\t}\n\tn := len(signature) / 2\n\tr = big.NewInt(0).SetBytes(signature[:n])\n\ts = big.NewInt(0).SetBytes(signature[n:])\n\treturn\n}\n\n// ParseECDSAASN1Signature parses an ECDSA ASN.1 digital signature. The ASN.1\n// signature is a DER-encoded SEQUENCE of the R and S INTEGERS of the ECDSA\n// algorithm as specified in https://tools.ietf.org/html/rfc3279#section-2.2.3.\n//\n// Note! This format differs from the one used in XML digital signatures\n// specified here https://tools.ietf.org/html/rfc4050#section-3.3.\nfunc ParseECDSAASN1Signature(der []byte) (r, s *big.Int, err error) {\n\tvar parsed struct {\n\t\tR *big.Int\n\t\tS *big.Int\n\t}\n\tif rest, err := asn1.Unmarshal(der, &parsed); err != nil {\n\t\treturn nil, nil, ECDSASignatureASN1UnmarshalError{Err: err,\n\t\t\tDescription: _CUTIL_UASN1ECDSA}\n\t} else if len(rest) > 0 {\n\t\treturn nil, nil, ECDSASignatureTrailingDataError{Err: err,\n\t\t\tDescription: _CUTIL_UASN1ECDSA}\n\t}\n\treturn parsed.R, parsed.S, nil\n}\n\n// AlgorithmIdentifierCmp compares two pkix.AlgorithmIdentifier structures\n// and returns true if they are equal, false otherwise.\nfunc AlgorithmIdentifierCmp(a, b pkix.AlgorithmIdentifier) bool {\n\tif !a.Algorithm.Equal(b.Algorithm) {\n\t\treturn false\n\t}\n\n\taParams := a.Parameters.FullBytes\n\tif len(aParams) == 0 {\n\t\t// If the optional Parameters field is empty encode it as a NULL tag with length 0\n\t\taParams = []byte{5, 0}\n\t}\n\tbParams := b.Parameters.FullBytes\n\tif len(bParams) == 0 {\n\t\t// If the optional Parameters field is empty encode it as a NULL tag with length 0\n\t\tbParams = []byte{5, 0}\n\t}\n\n\treturn bytes.Equal(aParams, bParams)\n}\n\n// Nonce44Bytes returns a base64(cryptographic nonce) with a length of 44 bytes.\nfunc Nonce44Bytes() (string, error) {\n\tnonce := make([]byte, entropy256bit)\n\t_, err := rand.Read(nonce)\n\tif err != nil {\n\t\treturn \"\", NonceGenerationError{Err: err,\n\t\t\tDescription: _CUTIL_44B_RAND}\n\t}\n\treturn base64.StdEncoding.EncodeToString(nonce), nil\n}\n"
  },
  {
    "path": "common/collector/cryptoutil/cryptoutil_test.go",
    "content": "package cryptoutil\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"encoding/base64\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n\n\t_ \"crypto/sha256\"\n\t_ \"crypto/sha512\"\n)\n\nfunc TestDigestInfo(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\thash crypto.Hash\n\t\twant []byte\n\t}{\n\t\t{\"SHA-224\", crypto.SHA224, []byte{\n\t\t\t0x30, 0x2d, 0x30, 0x0d,\n\t\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04,\n\t\t\t0x04, 0x1c,\n\t\t\t0xd1, 0x4a, 0x02, 0x8c, 0x2a, 0x3a, 0x2b, 0xc9,\n\t\t\t0x47, 0x61, 0x02, 0xbb, 0x28, 0x82, 0x34, 0xc4,\n\t\t\t0x15, 0xa2, 0xb0, 0x1f, 0x82, 0x8e, 0xa6, 0x2a,\n\t\t\t0xc5, 0xb3, 0xe4, 0x2f,\n\t\t}},\n\t\t{\"SHA-256\", crypto.SHA256, []byte{\n\t\t\t0x30, 0x31, 0x30, 0x0d,\n\t\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,\n\t\t\t0x04, 0x20,\n\t\t\t0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,\n\t\t\t0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,\n\t\t\t0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,\n\t\t\t0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,\n\t\t}},\n\t\t{\"SHA-384\", crypto.SHA384, []byte{\n\t\t\t0x30, 0x41, 0x30, 0x0d,\n\t\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02,\n\t\t\t0x04, 0x30,\n\t\t\t0x38, 0xb0, 0x60, 0xa7, 0x51, 0xac, 0x96, 0x38,\n\t\t\t0x4c, 0xd9, 0x32, 0x7e, 0xb1, 0xb1, 0xe3, 0x6a,\n\t\t\t0x21, 0xfd, 0xb7, 0x11, 0x14, 0xbe, 0x07, 0x43,\n\t\t\t0x4c, 0x0c, 0xc7, 0xbf, 0x63, 0xf6, 0xe1, 0xda,\n\t\t\t0x27, 0x4e, 0xde, 0xbf, 0xe7, 0x6f, 0x65, 0xfb,\n\t\t\t0xd5, 0x1a, 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b,\n\t\t}},\n\t\t{\"SHA-512\", crypto.SHA512, []byte{\n\t\t\t0x30, 0x51, 0x30, 0x0d,\n\t\t\t0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03,\n\t\t\t0x04, 0x40,\n\t\t\t0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd,\n\t\t\t0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07,\n\t\t\t0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc,\n\t\t\t0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce,\n\t\t\t0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0,\n\t\t\t0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f,\n\t\t\t0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81,\n\t\t\t0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e,\n\t\t}},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif got := DigestInfo(test.hash, nil); !bytes.Equal(got, test.want) {\n\t\t\t\tt.Errorf(\"unexpected results: got %x, want %x\", got, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPEMDecode(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tfile  string\n\t\tcause error\n\t}{\n\t\t{\"PEM\", \"certificate.pem\", nil},\n\t\t{\"DER\", \"certificate.der\", new(NotPEMEncodingError)},\n\t\t{\"trailing data\", \"certificate-trailing-data.pem\", new(PEMTrailingDataError)},\n\t\t{\"wrong type\", \"certificate-wrong-type.pem\", new(PEMBlockTypeError)},\n\t\t{\"with headers\", \"certificate-with-header.pem\", new(PEMHeadersError)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tdata, err := os.ReadFile(filepath.Join(\"testdata\", test.file))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to read %s: %v\\n\", test.file, err)\n\t\t\t}\n\t\t\t_, err = PEMDecode(string(data), \"CERTIFICATE\")\n\t\t\tif err != test.cause && errors.CausedBy(err, test.cause) == nil {\n\t\t\t\tt.Errorf(\"unexpected error: got %v, want cause %T\", err, test.cause)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNonce44Bytes(t *testing.T) {\n\t// Generate a nonce\n\tnonce, err := Nonce44Bytes()\n\tif err != nil {\n\t\tmsg := \"Expected no errors, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n\n\t// Test that nonce is base64 encoded string\n\t_, err = base64.StdEncoding.DecodeString(nonce)\n\tif err != nil {\n\t\tmsg := \"Expected nonce to be base64, but got %v\"\n\t\tt.Errorf(msg, err)\n\t}\n\n\t// Test that nonce is base64 encoded string with a length of 44 bytes\n\tif len(nonce) != 44 {\n\t\tmsg := \"Expected nonce to be %d bytes, but got %d\"\n\t\tt.Errorf(msg, 44, len(nonce))\n\t}\n\n\t// Test that base64 decoded nonce is 32 bytes long\n\tb, _ := base64.StdEncoding.DecodeString(nonce)\n\tif len(b) != 32 {\n\t\tmsg := \"Expected base64 decoded nonce to be %d bytes, but got %d\"\n\t\tt.Errorf(msg, 32, len(b))\n\t}\n}\n"
  },
  {
    "path": "common/collector/cryptoutil/log_desc.go",
    "content": "package cryptoutil\n\nconst (\n\t_CUTIL_PEM               = \"Failed to PEM decode\"\n\t_CUTIL_PEM_H             = \"Unexpected PEM header\"\n\t_CUTIL_PEM_H_BAD         = \"Invalid PEM header\"\n\t_CUTIL_CERT_PEM          = \"Failed to parse PEM certificate\"\n\t_CUTIL_CERT_B64          = \"base-64 decoding of a certificate content failed\"\n\t_CUTIL_CERT_PARSE        = \"Failed to parse certificate content\"\n\t_CUTIL_ECDSA2XML         = \"Failed to convert ECDSA signature to XML format\"\n\t_CUTIL_ASN1ECDSA         = \"ASN.1 marshalling of ECDSA signature failed\"\n\t_CUTIL_UASN1ECDSA        = \"ASN.1 unmarshalling of ECDSA signature failed\"\n\t_CUTIL_ECDSA_SIZE        = \"ECDSA signature has invalid size\"\n\t_CUTIL_44B_RAND          = \"Cannot generate 44-byte random value\"\n\t_CUTIL_ATTR_ASN1         = \"Attribute value ASN.1 marshalling failed\"\n\t_CUTIL_RDN_PREFIX        = \"Missing ',' prefix in relative distinguish name string\"\n\t_CUTIL_RDN_NO            = \"Missing relative distinguish name string\"\n\t_CUTIL_RDN_ATTR_NO       = \"Missing relative distinguish name attribute\"\n\t_CUTIL_RDN_EQ            = \"Missing '=' in relative distinguish name string\"\n\t_CUTIL_ATTR_UNKNOWN      = \"Unknown relative distinguish name attribute\"\n\t_CUTIL_ATTR_SHORT        = \"Relative distinguish name attribute's Object Identifier is too short\"\n\t_CUTIL_ATTR_HEX          = \"Missing relative distinguish name attribute base-16 value\"\n\t_CUTIL_ATTR_HEX_DEC      = \"Failed to base-16 decode relative distinguish name attribute value\"\n\t_CUTIL_ATTR_UASN1        = \"Failed to ASN.1 unmarshal relative distinguish name attribute value\"\n\t_CUTIL_ATTR_UTF8         = \"Relative distinguish name attribute value is invalid UTF-8\"\n\t_CUTIL_ATTR_SLASH        = \"Relative distinguish name attribute value has unescaped trailing slash\"\n\t_CUTIL_ATTR_1_CHAR       = \"Relative distinguish name attribute value's first char is incorrectly escaped\"\n\t_CUTIL_ATTR_HEX_PAIR     = \"Invalid relative distinguish name attribute value base-16 pair\"\n\t_CUTIL_ATTR_UNKNOWN_CHAR = \"Relative distinguish name attribute value contains unsupported escape chars\"\n\t_CUTIL_ATTR_CHAR_EMPTY   = \"Relative distinguish name attribute value has ' ' in leading or trailing character, which is not allowed\"\n\t_CUTIL_ATTR_SPACE        = \"Relative distinguish name attribute value has unescaped trailing space\"\n)\n"
  },
  {
    "path": "common/collector/cryptoutil/rdn.go",
    "content": "package cryptoutil\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/hex\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// RDNSequenceEqual checks if two relative distinguished name sequences contain\n// the same names in the same order and if names contain the same attributes,\n// ignoring order in case of multiple values.\n//\n// This does not enforce any restrictions on attributes: it only checks if the\n// sequences match even if they contain multiple attributes of the same type,\n// values not valid for a type, etc.\n//\n// Only attribute values which are comparable using the equality operator can\n// be compared (https://golang.org/ref/spec#Comparison_operators). So if there\n// is an attribute with matching types and values, but the values are not\n// comparable, then RDNSequenceEqual will return false.\nfunc RDNSequenceEqual(a, b pkix.RDNSequence) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\tfor i, set := range a {\n\t\tif !rdnEqual(set, b[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc rdnEqual(a, b pkix.RelativeDistinguishedNameSET) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\t// Create a copy of b that we can manipulate.\n\tc := make(pkix.RelativeDistinguishedNameSET, len(b))\n\tcopy(c, b)\n\n\t// Comparing non-comparable attribute values panics, so defer recover.\n\t// We can safely say that a and b are not equal if a panic was caused.\n\tdefer func() { // defer recover() does not work.\n\t\trecover() //nolint:errcheck // Discard the panic.\n\t}()\nnext:\n\tfor _, aatv := range a {\n\t\tfor i, catv := range c {\n\t\t\t// FIXME: Comparison of different numeric types, e.g., int and uint.\n\t\t\tif aatv.Type.Equal(catv.Type) && aatv.Value == catv.Value {\n\t\t\t\t// Delete c[i] so we do not match it twice.\n\t\t\t\t// Minimize copying by not preserving order.\n\t\t\t\tc[i] = c[len(c)-1]\n\t\t\t\tc = c[:len(c)-1]\n\t\t\t\tcontinue next\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\nvar (\n\t// shortnames is a map from short names of relative distinguished name\n\t// attribute types to their corresponding object identifiers.\n\t//\n\t// Keep these sorted by OID for easier tracking.\n\tshortnames = map[string]asn1.ObjectIdentifier{\n\t\t\"emailAddress\":           {1, 2, 840, 113549, 1, 9, 1},\n\t\t\"CN\":                     {2, 5, 4, 3},\n\t\t\"SN\":                     {2, 5, 4, 4},\n\t\t\"serialNumber\":           {2, 5, 4, 5},\n\t\t\"C\":                      {2, 5, 4, 6},\n\t\t\"L\":                      {2, 5, 4, 7},\n\t\t\"ST\":                     {2, 5, 4, 8},\n\t\t\"O\":                      {2, 5, 4, 10},\n\t\t\"OU\":                     {2, 5, 4, 11},\n\t\t\"GN\":                     {2, 5, 4, 42},\n\t\t\"organizationIdentifier\": {2, 5, 4, 97},\n\t}\n\n\t// shortToOID is a version of the shortnames map with lowercase keys\n\t// necessary for case-insensitive lookup. Filled from shortnames on\n\t// init.\n\tshortToOID = make(map[string]asn1.ObjectIdentifier)\n\n\t// oidToShort maps relative distinguished name attribute type object\n\t// identifiers (in string form) to short names. Filled from shortnames\n\t// on init.\n\toidToShort = make(map[string]string)\n)\n\nfunc init() {\n\tfor short, oid := range shortnames {\n\t\tshortToOID[strings.ToLower(short)] = oid\n\t\toidToShort[oid.String()] = short\n\t}\n}\n\n// EncodeRDNSequence encodes a sequence of relative distinguished names (i.e.,\n// a distinguished name) into a string according to RFC 4514 section 2.\nfunc EncodeRDNSequence(dn pkix.RDNSequence) (string, error) {\n\tif len(dn) == 0 {\n\t\treturn \"\", nil\n\t}\n\tvar buf bytes.Buffer\n\tfor i := len(dn) - 1; i >= 0; i-- {\n\t\tif buf.Len() > 0 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t\tfor i, atv := range dn[i] {\n\t\t\tif i > 0 {\n\t\t\t\tbuf.WriteByte('+')\n\t\t\t}\n\t\t\tif err := encodeATV(&buf, atv); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\t}\n\treturn buf.String(), nil\n}\n\nvar bsEscapeRE = regexp.MustCompile(`(^#|^ |[\"+,;<=>\\\\]| $)`)\n\nfunc encodeATV(buf *bytes.Buffer, atv pkix.AttributeTypeAndValue) error {\n\tat := atv.Type.String()\n\tshort, haveShort := oidToShort[at]\n\tif haveShort {\n\t\tat = short\n\t}\n\tbuf.WriteString(at)\n\n\tbuf.WriteByte('=')\n\n\ts, ok := atv.Value.(string)\n\tif !haveShort || !ok {\n\t\t// Use hexadecimal encoding of the DER encoding of the value.\n\t\tbuf.WriteByte('#')\n\t\tder, err := asn1.Marshal(atv.Value)\n\t\tif err != nil {\n\t\t\treturn EncodeAttributeValueError{Type: at, Err: err,\n\t\t\t\tDescription: _CUTIL_ATTR_ASN1}\n\t\t}\n\t\tbuf.WriteString(hex.EncodeToString(der))\n\t\treturn nil\n\t}\n\ts = bsEscapeRE.ReplaceAllString(s, `\\$1`)\n\ts = strings.ReplaceAll(s, \"\\x00\", \"\\\\00\")\n\tbuf.WriteString(s)\n\treturn nil\n}\n\n// DecodeRDNSequence decodes a string into a sequence of relative distinguished\n// names (i.e., a distinguished name) according to RFC 4514 Section 3.\nfunc DecodeRDNSequence(encoded string) (pkix.RDNSequence, error) {\n\tvar dn pkix.RDNSequence\n\tfor len(encoded) > 0 {\n\t\tif len(dn) > 0 {\n\t\t\tif !strings.HasPrefix(encoded, \",\") {\n\t\t\t\treturn nil, MissingRDNSeparatorError{\n\t\t\t\t\tEncoded:     encoded,\n\t\t\t\t\tDescription: _CUTIL_RDN_PREFIX,\n\t\t\t\t}\n\t\t\t}\n\t\t\tencoded = encoded[1:]\n\t\t\tif len(encoded) == 0 {\n\t\t\t\treturn nil, EmptyRDNError{\n\t\t\t\t\tDescription: _CUTIL_RDN_NO,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar rdn pkix.RelativeDistinguishedNameSET\n\t\tfor len(encoded) > 0 {\n\t\t\tif len(rdn) > 0 {\n\t\t\t\tif !strings.HasPrefix(encoded, \"+\") {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tencoded = encoded[1:]\n\t\t\t\tif len(encoded) == 0 {\n\t\t\t\t\treturn nil, EmptyAttributeError{\n\t\t\t\t\t\tDescription: _CUTIL_RDN_ATTR_NO,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tatv, rest, err := decodeATV(encoded)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trdn = append(rdn, atv)\n\t\t\tencoded = rest\n\t\t}\n\n\t\t// Insert the new RDN as first.\n\t\tdn = append(dn, nil)\n\t\tcopy(dn[1:], dn)\n\t\tdn[0] = rdn\n\t}\n\treturn dn, nil\n}\n\nfunc decodeATV(encoded string) (atv pkix.AttributeTypeAndValue, rest string, err error) {\n\t// Find the = that succeeds the type.\n\teq := strings.IndexByte(encoded, '=')\n\tif eq < 0 {\n\t\treturn atv, \"\", DecodeATVMissingEqualsError{Encoded: encoded,\n\t\t\tDescription: _CUTIL_RDN_EQ}\n\t}\n\n\t// Get the type OID. Short names are case-insensitive\n\tatype := encoded[:eq]\n\tvar ok bool\n\tif atv.Type, ok = shortToOID[strings.ToLower(atype)]; !ok {\n\t\t// Attempt to decode it as a raw OID.\n\t\tfor _, s := range strings.Split(atype, \".\") {\n\t\t\tvar n int\n\t\t\tn, err = strconv.Atoi(s)\n\t\t\tif err != nil {\n\t\t\t\treturn atv, \"\", UnknownAttributeTypeError{Type: atype,\n\t\t\t\t\tDescription: _CUTIL_ATTR_UNKNOWN}\n\t\t\t}\n\t\t\tatv.Type = append(atv.Type, n)\n\t\t}\n\t\tif len(atv.Type) < 2 {\n\t\t\treturn atv, \"\", AttributeTypeOIDTooShortError{OID: atv.Type,\n\t\t\t\tDescription: _CUTIL_ATTR_SHORT}\n\t\t}\n\t}\n\n\t// If the value starts with #, then it is a hexstring.\n\tencoded = encoded[eq+1:]\n\tif strings.HasPrefix(encoded, \"#\") {\n\t\trest, err = decodeHexString(encoded[1:], &atv.Value)\n\t\treturn\n\t}\n\n\t// A hexstring must be used if the attribute type was a\n\t// numericoid, but we allow it to be a string anyway. This\n\t// should be OK.\n\n\t// Otherwise it is an escaped UTF-8 string.\n\tatv.Value, rest, err = decodeEscaped(encoded)\n\treturn\n}\n\nfunc decodeHexString(encoded string, value *interface{}) (rest string, err error) {\n\t// Find the comma or plus which terminates the hexstring.\n\tend := strings.IndexAny(encoded, \",+\")\n\tif end < 0 {\n\t\tend = len(encoded) // Last value of the DN.\n\t}\n\tencoded, rest = encoded[:end], encoded[end:]\n\n\t// Decode the hexadecimal string.\n\tif len(encoded) == 0 {\n\t\treturn \"\", EmptyAttributeValueHexStringError{\n\t\t\tDescription: _CUTIL_ATTR_HEX,\n\t\t}\n\t}\n\tder, err := hex.DecodeString(encoded)\n\tif err != nil {\n\t\treturn \"\", AttributeValueHexStringError{\n\t\t\tValue:       encoded,\n\t\t\tErr:         err,\n\t\t\tDescription: _CUTIL_ATTR_HEX_DEC,\n\t\t}\n\t}\n\n\t*value = new(interface{})\n\ttrailing, err := asn1.Unmarshal(der, value)\n\tif err != nil {\n\t\treturn \"\", InvalidAttributeValueDERError{\n\t\t\tValue:       der,\n\t\t\tErr:         err,\n\t\t\tDescription: _CUTIL_ATTR_UASN1,\n\t\t}\n\t}\n\tif len(trailing) > 0 {\n\t\treturn \"\", AttributeValueDERTrailingDataError{\n\t\t\tValue:       der,\n\t\t\tTrailing:    trailing,\n\t\t\tDescription: _CUTIL_ATTR_UASN1,\n\t\t}\n\t}\n\treturn\n}\n\nfunc decodeEscaped(encoded string) (value, rest string, err error) {\n\tvar buf bytes.Buffer\n\tvar i int\nloop:\n\tfor i < len(encoded) {\n\t\tr, size := utf8.DecodeRuneInString(encoded[i:])\n\t\tswitch r {\n\t\tcase utf8.RuneError:\n\t\t\treturn \"\", \"\", AttributeValueInvalidUTF8Error{\n\t\t\t\tEncoded:     encoded[i:],\n\t\t\t\tDescription: _CUTIL_ATTR_UTF8,\n\t\t\t}\n\n\t\tcase '\\\\': // Escaped byte.\n\t\t\t// Check for special escaped char first.\n\t\t\tif len(encoded[i+size:]) == 0 {\n\t\t\t\treturn \"\", \"\", AttributeValueUnescapedTrailingSlashError{\n\t\t\t\t\tEncoded:     encoded,\n\t\t\t\t\tDescription: _CUTIL_ATTR_SLASH,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif special := encoded[i+size]; strings.IndexByte(` \"#+,;<=>\\`, special) >= 0 {\n\t\t\t\tbuf.WriteByte(special)\n\t\t\t\ti += size + 1\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// Otherwise it must be a hex pair.\n\t\t\tif len(encoded[i+size:]) < 2 {\n\t\t\t\treturn \"\", \"\", AttributeValueBadEscapedCharError{\n\t\t\t\t\tEscaped:     string(encoded[i+size]),\n\t\t\t\t\tDescription: _CUTIL_ATTR_1_CHAR,\n\t\t\t\t}\n\t\t\t}\n\t\t\tb, err := hex.DecodeString(encoded[i+size : i+size+2])\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", \"\", AttributeValueHexPairError{\n\t\t\t\t\tPair:        encoded[i+size : i+size+2],\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _CUTIL_ATTR_HEX_PAIR,\n\t\t\t\t}\n\t\t\t}\n\t\t\tbuf.WriteByte(b[0])\n\t\t\ti += size + 2\n\n\t\tcase '+', ',': // End of value.\n\t\t\tbreak loop\n\n\t\tcase 0, '\"', ';', '<', '>': // Not allowed.\n\t\t\treturn \"\", \"\", AttributeValueUnescapedSpecialError{\n\t\t\t\tSpecial:     string(r),\n\t\t\t\tDescription: _CUTIL_ATTR_UNKNOWN_CHAR,\n\t\t\t}\n\n\t\tcase ' ': // Not allowed in leading nor trailing character.\n\t\t\tif i == 0 {\n\t\t\t\treturn \"\", \"\", AttributeValueUnescapedLeadingSpaceError{\n\t\t\t\t\tEncoded:     encoded,\n\t\t\t\t\tDescription: _CUTIL_ATTR_CHAR_EMPTY,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif tail := encoded[i+size:]; len(tail) == 0 ||\n\t\t\t\ttail[0] == ',' || tail[0] == '+' {\n\n\t\t\t\treturn \"\", \"\", AttributeValueUnescapedTrailingSpaceError{\n\t\t\t\t\tEncoded:     encoded,\n\t\t\t\t\tDescription: _CUTIL_ATTR_SPACE,\n\t\t\t\t}\n\t\t\t}\n\t\t\tfallthrough\n\n\t\tdefault:\n\t\t\tbuf.WriteRune(r)\n\t\t\ti += size\n\t\t}\n\t}\n\tvalue = buf.String()\n\trest = encoded[i:]\n\treturn\n}\n"
  },
  {
    "path": "common/collector/cryptoutil/rdn_test.go",
    "content": "package cryptoutil\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"testing\"\n)\n\nfunc TestRDNSequenceEqual(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\ta, b  pkix.RDNSequence\n\t\tequal bool\n\t}{\n\t\t{\"nil\", nil, nil, true},\n\t\t{\"empty\", pkix.RDNSequence{}, pkix.RDNSequence{}, true},\n\t\t{\"empty nil\", pkix.RDNSequence{}, nil, true},\n\t\t{\"non-empty nil\", pkix.RDNSequence{{}}, nil, false},\n\n\t\t{\"equal\", pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, true},\n\n\t\t{\"types\", pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2},\n\t\t\t\tValue: \"foobar\",\n\t\t\t}},\n\t\t}, pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\t\tValue: \"foobar\",\n\t\t\t}},\n\t\t}, false},\n\n\t\t{\"values\", pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\t\tValue: \"foobar\",\n\t\t\t}},\n\t\t}, pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\t\tValue: \"not foobar\",\n\t\t\t}},\n\t\t}, false},\n\n\t\t{\"not comparable\", pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\t\tValue: []byte{0xde, 0xad, 0xbe, 0xef},\n\t\t\t}},\n\t\t}, pkix.RDNSequence{\n\t\t\t{{\n\t\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\t\tValue: []byte{0xde, 0xad, 0xbe, 0xef},\n\t\t\t}},\n\t\t}, false},\n\n\t\t{\"reordered sequence\", pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}, {{\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}}}, pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}}, {{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, false},\n\n\t\t{\"reordered set\", pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}, {\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}}}, pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}, {\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, true},\n\n\t\t{\"missing RDN\", pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}, {{\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}}}, pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, false},\n\n\t\t{\"missing attribute\", pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}, {\n\t\t\tType:  asn1.ObjectIdentifier{4, 5, 6},\n\t\t\tValue: 123,\n\t\t}}}, pkix.RDNSequence{{{\n\t\t\tType:  asn1.ObjectIdentifier{1, 2, 3},\n\t\t\tValue: \"foobar\",\n\t\t}}}, false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif RDNSequenceEqual(test.a, test.b) != test.equal {\n\t\t\t\tt.Errorf(\"assertion failed\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncodeDecodeRDNSequence(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdecoded pkix.RDNSequence\n\t\tencoded string\n\t}{\n\t\t{\"nil\", nil, \"\"},\n\t\t{\"empty\", pkix.RDNSequence{}, \"\"},\n\t\t{\"single\", pkix.Name{CommonName: \"foobar\"}.ToRDNSequence(), \"CN=foobar\"},\n\n\t\t{\"multiple\", pkix.Name{\n\t\t\tCommonName:   \"foobar\",\n\t\t\tSerialNumber: \"1234\",\n\t\t}.ToRDNSequence(), \"serialNumber=1234,CN=foobar\"},\n\n\t\t{\"multi-value\", pkix.RDNSequence{{\n\t\t\t{Type: asn1.ObjectIdentifier{2, 5, 4, 42}, Value: \"foo\"},\n\t\t\t{Type: asn1.ObjectIdentifier{2, 5, 4, 4}, Value: \"bar\"},\n\t\t}}, \"GN=foo+SN=bar\"},\n\n\t\t{\"escape\", pkix.Name{CommonName: \"#\\\"+,;<=>\\\\\\x00 \"}.ToRDNSequence(),\n\t\t\t`CN=\\#\\\"\\+\\,\\;\\<\\=\\>\\\\\\00\\ `},\n\n\t\t{\"spaces\", pkix.Name{CommonName: \"   \"}.ToRDNSequence(), `CN=\\  \\ `},\n\n\t\t{\"unknown\", pkix.RDNSequence{{\n\t\t\t{Type: asn1.ObjectIdentifier{1, 2, 3}, Value: \"foobar\"},\n\t\t}}, \"1.2.3=#1306666f6f626172\"},\n\n\t\t{\"non-string\", pkix.RDNSequence{{\n\t\t\t{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: int64(1)},\n\t\t}}, \"CN=#020101\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tencoded, err := EncodeRDNSequence(test.decoded)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to encode DN:\", err)\n\t\t\t}\n\t\t\tif encoded != test.encoded {\n\t\t\t\tt.Errorf(\"unexpected encoding: got %q, want %q\", encoded, test.encoded)\n\t\t\t}\n\n\t\t\tdecoded, err := DecodeRDNSequence(test.encoded)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to decode DN:\", err)\n\t\t\t}\n\t\t\tif !RDNSequenceEqual(decoded, test.decoded) {\n\t\t\t\tt.Errorf(\"unexpected decoding: got %v, want %v\", decoded, test.decoded)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/cryptoutil/testdata/certificate-trailing-data.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBkzCCATmgAwIBAgIJAIDiKWCedlpcMAoGCCqGSM49BAMCMCYxJDAiBgNVBAMM\nG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0xNjEwMjgwOTE5MjNaFw0y\nNjA5MDYwOTE5MjNaMCYxJDAiBgNVBAMMG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZp\nY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIiz+HuSEt5ryu0A0aX2hHMn\nheAgvYx1UmYEaTjFuiswEyRWQnYpmik2IZhlBBgkEktkkPYpEdMXtQG+tIyF3mOj\nUDBOMB0GA1UdDgQWBBTgi8P8MuVOox7mzeukQohYjrHljzAfBgNVHSMEGDAWgBTg\ni8P8MuVOox7mzeukQohYjrHljzAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA\nMEUCIFtkQ0MtdYd6nU6cMpVpcJ3bPGZz5yKi5vLT6jAIR9R0AiEA1iOjE9LA0eDV\n4DYSvHPr4NbQ0tdS2XiBjLO/Wl5h+bM=\n-----END CERTIFICATE-----\ntrailing\n"
  },
  {
    "path": "common/collector/cryptoutil/testdata/certificate-with-header.pem",
    "content": "-----BEGIN CERTIFICATE-----\nname: certificate with header\n\nMIIBkzCCATmgAwIBAgIJAIDiKWCedlpcMAoGCCqGSM49BAMCMCYxJDAiBgNVBAMM\nG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0xNjEwMjgwOTE5MjNaFw0y\nNjA5MDYwOTE5MjNaMCYxJDAiBgNVBAMMG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZp\nY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIiz+HuSEt5ryu0A0aX2hHMn\nheAgvYx1UmYEaTjFuiswEyRWQnYpmik2IZhlBBgkEktkkPYpEdMXtQG+tIyF3mOj\nUDBOMB0GA1UdDgQWBBTgi8P8MuVOox7mzeukQohYjrHljzAfBgNVHSMEGDAWgBTg\ni8P8MuVOox7mzeukQohYjrHljzAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA\nMEUCIFtkQ0MtdYd6nU6cMpVpcJ3bPGZz5yKi5vLT6jAIR9R0AiEA1iOjE9LA0eDV\n4DYSvHPr4NbQ0tdS2XiBjLO/Wl5h+bM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/cryptoutil/testdata/certificate-wrong-type.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIBkzCCATmgAwIBAgIJAIDiKWCedlpcMAoGCCqGSM49BAMCMCYxJDAiBgNVBAMM\nG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0xNjEwMjgwOTE5MjNaFw0y\nNjA5MDYwOTE5MjNaMCYxJDAiBgNVBAMMG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZp\nY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIiz+HuSEt5ryu0A0aX2hHMn\nheAgvYx1UmYEaTjFuiswEyRWQnYpmik2IZhlBBgkEktkkPYpEdMXtQG+tIyF3mOj\nUDBOMB0GA1UdDgQWBBTgi8P8MuVOox7mzeukQohYjrHljzAfBgNVHSMEGDAWgBTg\ni8P8MuVOox7mzeukQohYjrHljzAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA\nMEUCIFtkQ0MtdYd6nU6cMpVpcJ3bPGZz5yKi5vLT6jAIR9R0AiEA1iOjE9LA0eDV\n4DYSvHPr4NbQ0tdS2XiBjLO/Wl5h+bM=\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "common/collector/cryptoutil/testdata/certificate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBkzCCATmgAwIBAgIJAIDiKWCedlpcMAoGCCqGSM49BAMCMCYxJDAiBgNVBAMM\nG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZpY2F0ZTAeFw0xNjEwMjgwOTE5MjNaFw0y\nNjA5MDYwOTE5MjNaMCYxJDAiBgNVBAMMG2NyeXB0b3V0aWwgdGVzdCBjZXJ0aWZp\nY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIiz+HuSEt5ryu0A0aX2hHMn\nheAgvYx1UmYEaTjFuiswEyRWQnYpmik2IZhlBBgkEktkkPYpEdMXtQG+tIyF3mOj\nUDBOMB0GA1UdDgQWBBTgi8P8MuVOox7mzeukQohYjrHljzAfBgNVHSMEGDAWgBTg\ni8P8MuVOox7mzeukQohYjrHljzAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA\nMEUCIFtkQ0MtdYd6nU6cMpVpcJ3bPGZz5yKi5vLT6jAIR9R0AiEA1iOjE9LA0eDV\n4DYSvHPr4NbQ0tdS2XiBjLO/Wl5h+bM=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/errors/errors.go",
    "content": "/*\nPackage errors provides interfaces and methods for working with hierarchical\nerrors.\n*/\npackage errors\n\nimport \"reflect\"\n\n// Causer wraps the basic Cause method.\n//\n// All errors which were caused by another error should implement this\n// interface and the Cause method should return the causing error.\n//\n// The returned error must be non-nil. If the implementing error was not caused\n// by another error, then it should not implement Causer at all.\ntype Causer interface {\n\tCause() error\n}\n\n// Walk walks the causes of err, calling f on each error, including err itself.\n// It stops once it reaches an error which does not implement Causer. If f\n// returns a non-nil error, then Walk cancels and returns that error, otherwise\n// nil. Walk does nothing if err is nil.\nfunc Walk(err error, f func(error) error) error {\n\tvar ret error\n\tfor ret == nil && err != nil {\n\t\tret = f(err)\n\t\tc, ok := err.(Causer)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\terr = c.Cause()\n\t}\n\treturn ret\n}\n\n// CausedBy checks if the error itself is of the given type or if it was caused\n// by an error of that type. It returns the top-most error that is of the\n// requested type or nil. As a conveinience, pointers to error types also match\n// the pointed error types so that CausedBy(Error{}, new(Error)) returns a\n// non-nil result.\nfunc CausedBy(err error, v interface{}) (found error) {\n\tif v == nil {\n\t\treturn nil\n\t}\n\tneedle := reflect.TypeOf(v)\n\tfor needle.Kind() == reflect.Ptr {\n\t\tneedle = needle.Elem()\n\t}\n\n\tWalk(err, func(err error) error { //nolint:errcheck // Only found or nil is returned.\n\t\thay := reflect.TypeOf(err)\n\t\tfor hay.Kind() == reflect.Ptr {\n\t\t\thay = hay.Elem()\n\t\t}\n\t\tif hay == needle {\n\t\t\tfound = err\n\t\t\treturn found // Just to cancel the walk.\n\t\t}\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "common/collector/errors/errors_test.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\nfunc ExampleWalk() {\n\terr := WalkTopError{\n\t\tErr: WalkIntermediateError{\n\t\t\tErr: WalkRootError{}}}\n\t_ = Walk(err, func(err error) error {\n\t\tfmt.Printf(\"called f(%s)\\n\", reflect.TypeOf(err).Name())\n\t\treturn nil\n\t})\n\t// Output:\n\t// called f(WalkTopError)\n\t// called f(WalkIntermediateError)\n\t// called f(WalkRootError)\n}\n\nfunc ExampleCausedBy() {\n\tvar err error = CausedByTopError{\n\t\tErr: CausedByIntermediateError{\n\t\t\tErr: CausedByRootError{}}}\n\tfmt.Println(\"caused by top:\", CausedBy(err, new(CausedByTopError)) != nil)\n\tfmt.Println(\"caused by intermediate:\", CausedBy(err, new(CausedByIntermediateError)) != nil)\n\tfmt.Println(\"caused by root:\", CausedBy(err, new(CausedByRootError)) != nil)\n\tfmt.Println(\"caused by other:\", CausedBy(err, OtherError{}) != nil)\n\t// Output:\n\t// caused by top: true\n\t// caused by intermediate: true\n\t// caused by root: true\n\t// caused by other: false\n}\n"
  },
  {
    "path": "common/collector/go.mod",
    "content": "module ivxv.ee/common/collector\n\ngo 1.23\n\nrequire (\n\tgo.etcd.io/etcd/api/v3 v3.5.17\n\tgo.etcd.io/etcd/client/v3 v3.5.17\n\tgolang.org/x/crypto v0.27.0\n\tgoogle.golang.org/grpc v1.68.0\n\ttivi.io/core v0.2.2-0.20241127235149-149435f9e4f4\n)\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/gtank/ristretto255 v0.1.2 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n"
  },
  {
    "path": "common/collector/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=\ngithub.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=\ngolang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ntivi.io/core v0.2.2-0.20241127235149-149435f9e4f4 h1:NgIyqKugpgiIia6wiRM6I9E1XStcitd1x8wIs0z75f8=\ntivi.io/core v0.2.2-0.20241127235149-149435f9e4f4/go.mod h1:iVb6IvZMOrl1JPCwSkeVv7sV7Cd9cDjmyCg4xOvRL+w=\n"
  },
  {
    "path": "common/collector/identity/identity.go",
    "content": "/*\nPackage identity provides an abstraction for extracting unique identifiers from\nclients' Distinguished Name.\n*/\npackage identity\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// Type identifies an identity extraction method.\ntype Type string\n\n// Enumeration of identity types.\nconst (\n\tCommonName   Type = \"commonname\"   // Built-in.\n\tSerialNumber Type = \"serialnumber\" // Built-in.\n\tPNOEE        Type = \"pnoee\"        // Built-in.\n)\n\n// Identifier is the type of functions that extract unique identifiers from\n// Distinguished Names. Identifier must be safe for concurrent use.\ntype Identifier func(*pkix.Name) (string, error)\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = map[Type]Identifier{\n\t\tCommonName:   commonName,\n\t\tSerialNumber: serialNumber,\n\t\tPNOEE:        pnoee,\n\t}\n)\n\n// Register registers an identity extraction method. It is intended to be\n// called from init functions of packages that implement identity types.\nfunc Register(t Type, i Identifier) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\tregistry[t] = i\n}\n\n// Get returns the Identifier corresponding to the requested Type.\nfunc Get(t Type) (i Identifier, err error) {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\ti, ok := registry[t]\n\tif !ok {\n\t\treturn nil, UnlinkedTypeError{Type: t, Description: _IDENTITY_UNKNOWN}\n\t}\n\treturn i, nil\n}\n\n// commonName returns the CommonName from a Distinguished Name.\nfunc commonName(name *pkix.Name) (id string, err error) {\n\tif name == nil {\n\t\treturn \"\", CNEmptyDNError{\n\t\t\tDescription: _IDENTITY_NO_DN,\n\t\t}\n\t}\n\tid = name.CommonName\n\tif len(id) == 0 {\n\t\terr = EmptyCNError{Description: _IDENTITY_NO_CN}\n\t}\n\treturn\n}\n\n// serialNumber returns the SerialNumber from a Distinguished Name.\nfunc serialNumber(name *pkix.Name) (id string, err error) {\n\tif name == nil {\n\t\treturn \"\", SerialEmptyDNError{\n\t\t\tDescription: _IDENTITY_NO_DN_SERIAL,\n\t\t}\n\t}\n\tid = name.SerialNumber\n\tif len(id) == 0 {\n\t\terr = EmptySerialError{\n\t\t\tDescription: _IDENTITY_NO_CN_SERIAL,\n\t\t}\n\t}\n\treturn\n}\n\n// pnoee returns the SerialNumber from a Distinguished Name after removing an\n// optional PNOEE- prefix. The prefix denotes the Estonian national personal\n// number in accordance with ETSI EN 319 412-1 section 5.1.3.\nfunc pnoee(name *pkix.Name) (id string, err error) {\n\tif name == nil {\n\t\treturn \"\", PNOEEEmptyDNError{\n\t\t\tDescription: _IDENTITY_NO_VOTER_ID,\n\t\t}\n\t}\n\tid, err = serialNumber(name)\n\treturn strings.TrimPrefix(id, \"PNOEE-\"), err\n}\n"
  },
  {
    "path": "common/collector/identity/log_desc.go",
    "content": "package identity\n\nconst (\n\t_IDENTITY_UNKNOWN      = \"Unknown identity checking utility type\"\n\t_IDENTITY_NO_DN        = \"Empty x509 certificate distinguished name (DN)\"\n\t_IDENTITY_NO_CN        = \"Empty x509 certificate common name (CN)\"\n\t_IDENTITY_NO_DN_SERIAL = \"Empty x509 certificate distinguished name (DN) serial nr.\"\n\t_IDENTITY_NO_CN_SERIAL = \"Empty x509 certificate common name (CN) serial nr.\"\n\t_IDENTITY_NO_VOTER_ID  = \"Empty x509 certificate PKIX voter name\"\n)\n"
  },
  {
    "path": "common/collector/log/log.go",
    "content": "/*\nPackage log is the logging framework used by collector services.\n*/\npackage log\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/syslog\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nconst timefmt = \"2006-01-02T15:04:05.000000000Z07:00\"\n\n// maxlen is the maximum byte-length of a formatted log entries field. If a\n// field exceeds this, then it will be truncated.\n//\n// maxlen is a variable and not a constant so that it can be overrriden\n// by tests.\nvar maxlen = 4096\n\n// key is the key type of values stored in context by this package.\ntype key int\n\nconst (\n\tloggerKey key = iota // Context key for logger instance.\n\tconnIDKey            // Context key for connection ID.\n\tsessIDKey            // Context key for session ID.\n)\n\ntype logger struct {\n\tdebug uint32           // Is debug logging enabled?\n\tw     writer           // Destination to log formatted entries to.\n\treq   writer           // Separate destination (facility) for requests.\n\tnow   func() time.Time // Function used to get current time.\n}\n\n// writer provides an interface in front of syslog.Writer, so that it can be\n// switched out for testing.\ntype writer interface {\n\tDebug(m string) error\n\tInfo(m string) error\n\tErr(m string) error\n\tAlert(m string) error\n\tClose() error\n}\n\ntype noopWriter struct{}\n\nfunc (w noopWriter) Debug(string) error { return nil }\nfunc (w noopWriter) Info(string) error  { return nil }\nfunc (w noopWriter) Err(string) error   { return nil }\nfunc (w noopWriter) Alert(string) error { return nil }\nfunc (w noopWriter) Close() error       { return nil }\n\n// NewContext adds a new logger into the context. The returned context can then\n// be later passed to one of the logging functions, which use the logger from\n// it.\nfunc NewContext(ctx context.Context, tag string) (context.Context, error) {\n\t// No need to set default severity level, as we will not be using it.\n\tw, err := syslog.New(syslog.LOG_LOCAL0, tag)\n\tif err != nil {\n\t\treturn ctx, err\n\t}\n\treq, err := syslog.New(syslog.LOG_LOCAL1, tag)\n\tif err != nil {\n\t\treturn ctx, err\n\t}\n\treturn context.WithValue(ctx, loggerKey, &logger{0, w, req, time.Now}), nil\n}\n\n// TestContext adds a logger to the context, which does not log anything. This\n// is meant to be used in tests, which call logging functions, but where we do\n// not want to log anything.\nfunc TestContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, loggerKey, &logger{0, noopWriter{}, noopWriter{}, time.Now})\n}\n\n// SetDebug sets debugging in the logger in the context.\nfunc SetDebug(ctx context.Context, debug bool) {\n\tvar v uint32\n\tif debug {\n\t\tv = 1\n\t}\n\tatomic.StoreUint32(&fromctx(ctx).debug, v)\n}\n\n// Close closes the logger in the context.\nfunc Close(ctx context.Context) error {\n\tl := fromctx(ctx)\n\terr := l.w.Close()\n\tif rerr := l.req.Close(); err == nil {\n\t\terr = rerr\n\t}\n\treturn err\n}\n\n// WithConnectionID sets the connection ID used when logging with the returned\n// context.\nfunc WithConnectionID(ctx context.Context, cid string) context.Context {\n\treturn context.WithValue(ctx, connIDKey, cid)\n}\n\n// WithSessionID sets the session ID used when logging with the returned\n// context.\nfunc WithSessionID(ctx context.Context, sid string) context.Context {\n\treturn context.WithValue(ctx, sessIDKey, sid)\n}\n\nfunc fromctx(ctx context.Context) *logger {\n\t// fromctx will return nil if used on a context which was not acquired\n\t// through NewContext, but this is a programmer error which will panic\n\t// on the first logging function call and will always be detected, so\n\t// we are okay with it.\n\treturn ctx.Value(loggerKey).(*logger)\n}\n\n// Entry declares the method used to identify a log entry.\ntype Entry interface {\n\tEntryID() string\n}\n\n// ErrorEntry is an Entry which is also an error.\ntype ErrorEntry interface {\n\tEntry\n\terror\n}\n\n// Sensitive is used for logging sensitive byte slices. The value of Sensitive\n// is never logged directly, only its length and hash.\n//\n// NB! Logging sensitive data from a limited message space will not protect the\n// data: only use Sensitive for data whose hashes cannot be brute-forced or\n// precomputed.\ntype Sensitive []byte\n\n// Debug logs a debug entry, which will not be sent to external monitoring and\n// can be disabled. See Log for details about the format of the logged data.\nfunc Debug(ctx context.Context, entry Entry) {\n\tl := fromctx(ctx)\n\tif atomic.LoadUint32(&l.debug) == 1 {\n\t\tl.w.Debug(format(ctx, entry)) //nolint:errcheck // Ignore logging errors.\n\t}\n}\n\n// Log logs an entry. The entry will be formatted as a JSON object with five\n// members.\n//\n//   - Timestamp    an ISO8601 timestamp of the time the entry was logged,\n//   - ConnectionID an unique network connection identifier, only used in\n//     request handlers,\n//   - SessionID    an unique session identifier, only used in request\n//     handlers after a session ID has been established,\n//   - ID           an unique log entry identifier, each ID must be used only\n//     once in the codebase, and\n//   - Entry        a JSON encoding of the log entry.\n//\n// The log entry will be encoded to JSON according to the following rules:\n//\n//   - nested values which implement the Entry interface will be encoded as a\n//     JSON object with two members: \"ID\", the unique identifier of the\n//     nested entry, and \"Entry\", the encoding of the nested entry according\n//     to these rules, excluding any interface checking,\n//\n//   - values of type Sensitive will be encoded as a JSON object with two\n//     members: \"Hash\", a SHA-256 hash of the value, and \"Length\", the length\n//     of the original value,\n//\n//   - values of type time.Time will be formatted according to RFC 3339 with\n//     either second (if the nanosecond part is zero) or nanosecond precision,\n//\n//   - values of type pkix.Name and pkix.RDNSequence will be encoded to a\n//     string according to RFC 4514,\n//\n//   - values of type *x509.Certificate will be encoded as a JSON object with\n//     members containing the certificate's subject name, issuer name,\n//     hexadecimal serial number, and SHA-256 hash of its DER encoding,\n//\n//   - values which implement the error interface will be converted to a\n//     string using Error() and processed as a string value,\n//\n//   - values which implement the fmt.Stringer interface will be converted to\n//     a string using String() and processed as a string value.\n//\n//   - byte slices will be Base64-encoded to a string and processed as a\n//     string value,\n//\n//   - if a string value exceeds the maximum length, a JSON object will be\n//     encoded in its place with two members: \"Truncated\", the string value\n//     truncated to the maximum length, and \"FullLength\", the byte length of\n//     the original value,\n//\n//   - string values (including truncated ones) will be URL-encoded according\n//     to RFC 3986,\n//\n//   - values of kind bool, int, int8, int16, int32, int64, uint, uint8,\n//     uint16, uint32, uint64, float32, and float64 will be encoded as JSON\n//     booleans or numbers,\n//\n//   - slices and arrays will be encoded as JSON arrays with these rules\n//     applied to each value,\n//\n//   - maps with key strings will be encoded as JSON objects with these rules\n//     applied to each name and value, with the exception that if a name\n//     exceeds the maximum length it will not be truncated, but cause an\n//     error,\n//\n//   - structs will be encoded as JSON objects with field names as member\n//     names and these rules applied to each value, and\n//\n//   - all other values are unsupported and will cause an error.\n//\n// As noted, when encoding a nested entry, interface checks are skipped. First,\n// this avoids recursing into the first rule, and second, we wish to ignore the\n// Error and String methods of log entries to get the actual structure of the\n// entry and not just a string representation.\n//\n// In case an error occurs during log entry encoding a plaintext string will be\n// logged instead, containing the time, connection ID, session ID, entry ID,\n// and the error that occurred. The unencoded entry will not be included.\nfunc Log(ctx context.Context, entry Entry) {\n\tfromctx(ctx).w.Info(format(ctx, entry)) //nolint:errcheck // Ignore logging errors.\n}\n\n// Error logs an error entry. By default, the entry is logged with severity\n// \"error\", which indicates an error in the normal flow of the application.\n// However, if the entry was caused by an error wrapped with Alert, then\n// severity \"alert\" is used instead, indicating an error not directly tied to\n// the application, but of a higher-level, e.g., when an external service is\n// not reachable or writing to disk fails.\n//\n// See Log for details about the format of the logged data.\nfunc Error(ctx context.Context, entry ErrorEntry) {\n\tl := fromctx(ctx)\n\tf := l.w.Err\n\tif errors.CausedBy(entry, new(alert)) != nil {\n\t\tf = l.w.Alert\n\t}\n\tf(format(ctx, entry)) //nolint:errcheck // Ignore logging errors.\n}\n\ntype alert struct {\n\terr error\n}\n\nfunc (a alert) Cause() error  { return a.err }\nfunc (a alert) Error() string { return a.err.Error() }\n\n// Alert wraps err, marking that it should be logged with severity \"alert\".\nfunc Alert(err error) error {\n\treturn alert{err}\n}\n\ntype message struct {\n\tTimestamp    string\n\tConnectionID string `json:\",omitempty\"`\n\tSessionID    string `json:\",omitempty\"`\n\tID           string\n\tEntry        *json.RawMessage `json:\",omitempty\"`\n}\n\n// format formats the entry to a string as described by Log.\nfunc format(ctx context.Context, entry Entry) string {\n\t// Use time.RFC3339Nano with trailing zeroes.\n\tnow := fromctx(ctx).now().Format(timefmt)\n\n\tvar cid, sid string\n\tif val := ctx.Value(connIDKey); val != nil {\n\t\tcid = val.(string)\n\t}\n\tif val := ctx.Value(sessIDKey); val != nil {\n\t\tsid = val.(string)\n\t}\n\n\t// Get an encoder from the pool and encode the log entry.\n\te := encPool.Get().(*encoder)\n\terr := e.encode(reflect.ValueOf(entry), false)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to encode: %v\", err)\n\t}\n\n\tvar b []byte\n\tif err == nil {\n\t\t// On success, marshal the entire log entry.\n\t\tencoded := e.Bytes()\n\t\tm := (*json.RawMessage)(&encoded)\n\n\t\t// Omit empty entries for readability.\n\t\tif empty(encoded) {\n\t\t\tm = nil\n\t\t}\n\t\tb, err = json.Marshal(message{now, cid, sid, entry.EntryID(), m})\n\t}\n\n\t// Put the encoder back into the pool regardless of success and only\n\t// after the entry has been marshaled. Otherwise we introduce a race\n\t// condition where another goroutine can get the same encoder from the\n\t// pool and overwrite the data in its buffer.\n\te.Reset()\n\tencPool.Put(e)\n\n\tif err != nil {\n\t\t// This should never happen, but we do not want to panic here\n\t\t// if it does: log a plain error. Avoid logging the the entry\n\t\t// since it may contain unencoded information.\n\t\treturn fmt.Sprintf(\"failed to format log message: \"+\n\t\t\t\"time=%s, cid=%s, sid=%s, id=%s, err=%v\",\n\t\t\tnow, cid, sid, entry.EntryID(), err)\n\t}\n\treturn string(b)\n}\n\ntype encoder struct {\n\tbytes.Buffer\n\t*json.Encoder\n}\n\nvar encPool = sync.Pool{New: func() interface{} {\n\te := new(encoder)\n\te.Encoder = json.NewEncoder(e)\n\te.SetEscapeHTML(false)\n\treturn e\n}}\n\n// encode traverses v and encodes it to JSON according to the rules described\n// in Log. If nest is false, then checking if v implements Entry will be\n// skipped: used when we are already nesting v and now want to encode its\n// value.\nfunc (e *encoder) encode(v reflect.Value, nest bool) (err error) {\n\t// Check for specific types.\n\tswitch t := v.Interface().(type) {\n\tcase alert:\n\t\t// Do not log alert, which is only used for tagging entries.\n\t\treturn e.encode(reflect.ValueOf(t.Cause()), nest)\n\tcase Entry:\n\t\tif nest {\n\t\t\treturn e.encodeNested(t)\n\t\t}\n\tcase Sensitive:\n\t\treturn e.encodeSensitive(t)\n\tcase time.Time:\n\t\tlayout := time.RFC3339\n\t\tif t.Nanosecond() > 0 {\n\t\t\tlayout = time.RFC3339Nano\n\t\t}\n\t\treturn e.encodeString(t.Format(layout))\n\tcase pkix.RDNSequence:\n\t\treturn e.encodeRDNSequence(t)\n\tcase pkix.Name:\n\t\tt.ExtraNames = t.Names // Include unrecognized names.\n\t\treturn e.encodeRDNSequence(t.ToRDNSequence())\n\tcase *x509.Certificate:\n\t\treturn e.encodeCertificate(t)\n\tcase error:\n\t\treturn e.encodeString(t.Error())\n\tcase fmt.Stringer:\n\t\treturn e.encodeString(t.String())\n\t}\n\n\t// Check for supported kinds.\n\tswitch v.Kind() {\n\tcase reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,\n\t\treflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8,\n\t\treflect.Uint16, reflect.Uint32, reflect.Uint64,\n\t\treflect.Float32, reflect.Float64:\n\n\t\t// Numbers have a pre-defined format and limited length, so\n\t\t// encode unmodified.\n\t\treturn e.Encode(v.Interface())\n\n\tcase reflect.String:\n\t\treturn e.encodeString(v.String())\n\n\tcase reflect.Slice:\n\t\t// Check if we are encoding a byte slice: encode to Base64.\n\t\tif v.Type().Elem().Kind() == reflect.Uint8 {\n\t\t\treturn e.encodeString(base64.StdEncoding.EncodeToString(v.Bytes()))\n\t\t}\n\t\tfallthrough\n\tcase reflect.Array:\n\t\treturn e.encodeArray(v)\n\n\tcase reflect.Map:\n\t\treturn e.encodeMap(v)\n\n\tcase reflect.Struct:\n\t\treturn e.encodeStruct(v)\n\n\tcase reflect.Ptr, reflect.Interface:\n\t\tif v.IsNil() {\n\t\t\treturn e.Encode(nil)\n\t\t}\n\t\treturn e.encode(v.Elem(), nest)\n\n\tdefault:\n\t\t// Although the encoding/json package can handle more cases\n\t\t// than listed here (e.g., any type that implements\n\t\t// json.Marshaler), we do not support them as log entries.\n\t\treturn fmt.Errorf(\"unsupported log entry type: %s\", v.Type())\n\t}\n}\n\n// encodeSensitive encodes a value of type Sensitive into a JSON object as\n// described in Log.\nfunc (e *encoder) encodeSensitive(sensitive Sensitive) (err error) {\n\th := sha256.Sum256(sensitive)\n\treturn e.Encode(struct {\n\t\tHash   []byte\n\t\tLength int\n\t}{\n\t\th[:],\n\t\tlen(sensitive),\n\t})\n}\n\n// encodeRDNSequence encodes a value of type pkix.RDNSequence into a string.\nfunc (e *encoder) encodeRDNSequence(rdns pkix.RDNSequence) (err error) {\n\ts, err := cryptoutil.EncodeRDNSequence(rdns)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode RDN sequence: %v\", err)\n\t}\n\treturn e.encodeString(s)\n}\n\n// encodeCertificate encodes a value of type *x509.Certificate into a JSON\n// object as described in Log.\nfunc (e *encoder) encodeCertificate(c *x509.Certificate) (err error) {\n\tif c == nil {\n\t\treturn e.Encode(nil)\n\t}\n\n\thash := sha256.Sum256(c.Raw)\n\treturn e.encodeStruct(reflect.ValueOf(struct {\n\t\tSubject pkix.Name\n\t\tIssuer  pkix.Name\n\t\tSerial  string\n\t\tHash    []byte\n\t}{\n\t\tSubject: c.Subject,\n\t\tIssuer:  c.Issuer,\n\t\tSerial:  c.SerialNumber.Text(16),\n\t\tHash:    hash[:],\n\t}))\n}\n\n// encodeString encodes a string into a JSON value as described in Log.\nfunc (e *encoder) encodeString(s string) (err error) {\n\tif len(s) > maxlen {\n\t\treturn e.Encode(struct {\n\t\t\tTruncated  string\n\t\t\tFullLength int\n\t\t}{\n\t\t\turl.QueryEscape(s[:maxlen]),\n\t\t\tlen(s),\n\t\t})\n\t}\n\n\treturn e.Encode(url.QueryEscape(s))\n}\n\n// encodeArray encodes an array or slice into a JSON array with encoded values.\nfunc (e *encoder) encodeArray(v reflect.Value) (err error) {\n\te.WriteByte('[')\n\tfor i := 0; i < v.Len(); i++ {\n\t\tif i > 0 {\n\t\t\te.WriteByte(',')\n\t\t}\n\t\tif err = e.encode(v.Index(i), true); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\te.WriteByte(']')\n\treturn\n}\n\n// encodeMap encodes a map with string keys into a JSON object with encoded\n// names and values.\nfunc (e *encoder) encodeMap(v reflect.Value) (err error) {\n\t// Allow only string keys. encoding/json package also allows numbers\n\t// and encoding.TextMarshalers, but we have no need for that.\n\tif v.Type().Key().Kind() != reflect.String {\n\t\treturn fmt.Errorf(\"unsupported log entry map key type: %s\", v.Type().Key())\n\t}\n\n\te.WriteByte('{')\n\tfor i, key := range v.MapKeys() {\n\t\tif i > 0 {\n\t\t\te.WriteByte(',')\n\t\t}\n\t\tname := key.String()\n\t\tif len(name) > maxlen {\n\t\t\treturn fmt.Errorf(\"map key too long: %d\", len(name))\n\t\t}\n\t\tif err = e.encodeString(name); err != nil {\n\t\t\treturn\n\t\t}\n\t\te.WriteByte(':')\n\t\tif err = e.encode(v.MapIndex(key), true); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\te.WriteByte('}')\n\treturn\n}\n\n// encodeStruct encodes a struct into a JSON object with encoded values.\nfunc (e *encoder) encodeStruct(v reflect.Value) (err error) {\n\te.WriteByte('{')\n\tfor i := 0; i < v.NumField(); i++ {\n\t\tif strings.EqualFold(v.Type().Field(i).Name, \"Description\") {\n\t\t\tcontinue\n\t\t}\n\t\tif i > 0 {\n\t\t\te.WriteByte(',')\n\t\t}\n\t\tif err = e.Encode(v.Type().Field(i).Name); err != nil {\n\t\t\treturn\n\t\t}\n\t\te.WriteByte(':')\n\t\tif err = e.encode(v.Field(i), true); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\te.WriteByte('}')\n\treturn\n}\n\n// encodeNested encodes a nested log entry into a JSON object as described in\n// Log.\nfunc (e *encoder) encodeNested(n Entry) (err error) {\n\te.WriteString(`{\"ID\":`)\n\tif err = e.Encode(n.EntryID()); err != nil {\n\t\treturn\n\t}\n\tafterID := e.Len()\n\n\te.WriteString(`,\"Entry\":`)\n\tbeforeEntry := e.Len()\n\n\tif err = e.encode(reflect.ValueOf(n), false); err != nil {\n\t\treturn\n\t}\n\n\t// In case we encoded an empty entry, then just output the ID.\n\tencoded := e.Bytes()[beforeEntry:]\n\tif empty(encoded) {\n\t\te.Truncate(afterID)\n\t}\n\n\te.WriteByte('}')\n\treturn\n}\n\n// empty checks if the JSON encoding in b corresponds to a null value or empty\n// object.\nfunc empty(b []byte) bool {\n\t// json.Encoder.Encode adds a newline after each encode, which is why\n\t// null will have a newline at the end.\n\ts := string(b)\n\treturn s == \"null\\n\" || s == \"{}\"\n}\n\ntype reqMessage struct {\n\tTimestamp    string\n\tConnectionID interface{}\n\tRequest      string\n}\n\n// Request logs an incoming request into the separate request log destination.\n// The log message will be formatted as a JSON object with three members.\n//\n//   - Timestamp    an ISO8601 timestamp of the time the entry was logged,\n//   - ConnectionID the unique network connection identifier which received\n//     the request, and\n//   - Request      the incoming request, URL-encoded according to RFC 3986.\nfunc Request(ctx context.Context, request []byte) error {\n\tl := fromctx(ctx)\n\tencoder := encPool.Get().(*encoder)\n\tdefer encPool.Put(encoder)\n\tdefer encoder.Reset()\n\tif err := encoder.Encode(reqMessage{\n\t\tTimestamp:    l.now().Format(timefmt),\n\t\tConnectionID: ctx.Value(connIDKey),\n\t\tRequest:      url.QueryEscape(string(request)), // Ignore maxlen.\n\t}); err != nil {\n\t\treturn err\n\t}\n\treturn l.req.Info(encoder.String())\n}\n"
  },
  {
    "path": "common/collector/log/log_test.go",
    "content": "package log\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar now = time.Date(2016, time.November, 3, 13, 42, 24, 123000000, time.UTC)\n\nconst nowstr = \"2016-11-03T13:42:24.123000000Z\"\n\n// testWriter implements writer by writing into a buffer.\ntype testWriter struct {\n\t*bytes.Buffer\n}\n\nfunc (t testWriter) Debug(m string) error {\n\t_, err := fmt.Fprint(t, \"debug: \", m)\n\treturn err\n}\n\nfunc (t testWriter) Info(m string) error {\n\t_, err := fmt.Fprint(t, \"info: \", m)\n\treturn err\n}\n\nfunc (t testWriter) Err(m string) error {\n\t_, err := fmt.Fprint(t, \"err: \", m)\n\treturn err\n}\n\nfunc (t testWriter) Alert(m string) error {\n\t_, err := fmt.Fprint(t, \"alert: \", m)\n\treturn err\n}\n\nfunc (t testWriter) Close() error {\n\treturn nil\n}\n\n// testContext adds a new test logger into the context.\nfunc testContext(ctx context.Context, b *bytes.Buffer, now time.Time, cid, sid string, debug bool) context.Context {\n\tctx = context.WithValue(ctx, loggerKey, &logger{\n\t\tw:   testWriter{b},\n\t\tnow: func() time.Time { return now },\n\t})\n\tSetDebug(ctx, debug)\n\tif len(cid) > 0 {\n\t\tctx = WithConnectionID(ctx, cid)\n\t}\n\tif len(sid) > 0 {\n\t\tctx = WithSessionID(ctx, sid)\n\t}\n\treturn ctx\n}\n\n// Message types for testing.\ntype testEmpty struct{}\n\nfunc (t *testEmpty) EntryID() string { return \"TestEmpty\" }\n\ntype testString string\n\nfunc (t testString) EntryID() string { return \"TestString\" }\n\ntype testNumber float64\n\nfunc (t testNumber) EntryID() string { return \"TestNumber\" }\n\ntype testArray [2]string\n\nfunc (t testArray) EntryID() string { return \"TestArray\" }\n\ntype testSlice []string\n\nfunc (t testSlice) EntryID() string { return \"TestSlice\" }\n\ntype testBase64 []byte\n\nfunc (t testBase64) EntryID() string { return \"TestBase64\" }\n\ntype testMap map[string]string\n\nfunc (t testMap) EntryID() string { return \"TestMap\" }\n\ntype testStruct struct {\n\tField interface{}\n}\n\nfunc (t testStruct) EntryID() string { return \"TestStruct\" }\n\ntype testStringer string\n\nfunc (t testStringer) String() string { return string(t) + \".String\" }\n\ntype testError string\n\nfunc (t testError) Error() string { return string(t) + \".Error\" }\n\ntype testNested struct {\n\tNested Entry\n}\n\nfunc (t testNested) EntryID() string { return \"TestNested\" }\n\ntype testSensitive struct {\n\tSensitive Sensitive\n}\n\nfunc (t testSensitive) EntryID() string { return \"TestSensitive\" }\n\ntype testErrorEntry struct {\n\tErr error\n}\n\nfunc (t testErrorEntry) EntryID() string { return \"TestErrorEntry\" }\nfunc (t testErrorEntry) Cause() error    { return t.Err }\nfunc (t testErrorEntry) Error() string   { return t.Err.Error() }\n\ntype testTime struct {\n\tTime time.Time\n}\n\nfunc (t testTime) EntryID() string { return \"TestTime\" }\n\ntype testCertificate struct {\n\tCertificate *x509.Certificate\n}\n\nfunc (t testCertificate) EntryID() string { return \"TestCertificate\" }\n\nfunc TestSeverity(t *testing.T) {\n\tvar b bytes.Buffer\n\tctx := testContext(context.Background(), &b, now, \"\", \"\", true)\n\n\ttests := []struct {\n\t\tseverity string\n\t\tfn       func(context.Context)\n\t}{\n\t\t{\"debug\", func(ctx context.Context) { Debug(ctx, (*testEmpty)(nil)) }},\n\t\t{\"info\", func(ctx context.Context) { Log(ctx, (*testEmpty)(nil)) }},\n\t\t{\"err\", func(ctx context.Context) { Error(ctx, testErrorEntry{testError(\"\")}) }},\n\t\t{\"alert\", func(ctx context.Context) { Error(ctx, testErrorEntry{Alert(testError(\"\"))}) }},\n\t}\n\n\tfor _, test := range tests {\n\t\tb.Reset()\n\t\tt.Run(test.severity, func(t *testing.T) {\n\t\t\ttest.fn(ctx)\n\t\t\tlogged := b.String()\n\t\t\t//nolint: gocritic\n\t\t\t// It says that `strings.Index(logged, \":\")` can return -1\n\t\t\tif sev := logged[:strings.Index(logged, \":\")]; sev != test.severity {\n\t\t\t\tt.Errorf(\"unexpected severity: got %q, want %q\", sev, test.severity)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDebug(t *testing.T) {\n\tvar b bytes.Buffer\n\tvar m *testEmpty\n\n\tt.Run(\"enabled\", func(t *testing.T) {\n\t\tDebug(testContext(context.Background(), &b, now, \"\", \"\", true), m)\n\t\tif b.Len() == 0 {\n\t\t\tt.Error(\"no output when debugging enabled\")\n\t\t}\n\t})\n\n\tb.Reset()\n\n\tt.Run(\"disabled\", func(t *testing.T) {\n\t\tDebug(testContext(context.Background(), &b, now, \"\", \"\", false), m)\n\t\tif b.Len() > 0 {\n\t\t\tt.Error(\"unexpected output when debugging disabled\")\n\t\t}\n\t})\n}\n\nfunc TestFormat(t *testing.T) {\n\tvar b bytes.Buffer\n\tconst prefix = `info: {\"Timestamp\":\"` + nowstr + `\",`\n\n\t// Override maxlen for testing.\n\tmaxlen = 36\n\n\t// Parse test certificate.\n\tder, err := os.ReadFile(\"testdata/certificate.der\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to read test certificate:\", err)\n\t}\n\tcert, err := x509.ParseCertificate(der)\n\tif err != nil {\n\t\tt.Fatal(\"failed to parse test certificate:\", err)\n\t}\n\tfmt.Println(\"caused by other:\", OtherError{})\n\ttests := []struct {\n\t\tname   string\n\t\tcid    string\n\t\tsid    string\n\t\tentry  Entry\n\t\toutput string\n\t}{\n\t\t{\"minimal\", \"\", \"\", &testEmpty{}, prefix + `\"ID\":\"TestEmpty\"}`},\n\n\t\t{\"minimal pointer\", \"\", \"\", (*testEmpty)(nil), prefix + `\"ID\":\"TestEmpty\"}`},\n\n\t\t{\"connection ID\", \"cid\", \"\", (*testEmpty)(nil),\n\t\t\tprefix + `\"ConnectionID\":\"cid\",\"ID\":\"TestEmpty\"}`},\n\n\t\t{\"session ID\", \"cid\", \"sid\", (*testEmpty)(nil),\n\t\t\tprefix + `\"ConnectionID\":\"cid\",\"SessionID\":\"sid\",\"ID\":\"TestEmpty\"}`},\n\n\t\t{\"string\", \"\", \"\", testString(\"message\"),\n\t\t\tprefix + `\"ID\":\"TestString\",\"Entry\":\"message\"}`},\n\n\t\t{\"url-encoded\", \"\", \"\", testString(\" \\xff\\\"\\\\\"),\n\t\t\tprefix + `\"ID\":\"TestString\",\"Entry\":\"+%FF%22%5C\"}`},\n\n\t\t{\"truncate\", \"\", \"\", testString(\"a really long string that gets truncated\"),\n\t\t\tprefix + `\"ID\":\"TestString\",\"Entry\":{` +\n\t\t\t\t`\"Truncated\":\"a+really+long+string+that+gets+trunc\",\"FullLength\":40}}`},\n\n\t\t{\"number\", \"\", \"\", testNumber(123.45),\n\t\t\tprefix + `\"ID\":\"TestNumber\",\"Entry\":123.45}`},\n\n\t\t{\"array\", \"\", \"\", testArray{\"message\", \" \\xff\\\"\\\\\"},\n\t\t\tprefix + `\"ID\":\"TestArray\",\"Entry\":[\"message\",\"+%FF%22%5C\"]}`},\n\n\t\t{\"slice\", \"\", \"\", testSlice{\"message\", \" \\xff\\\"\\\\\"},\n\t\t\tprefix + `\"ID\":\"TestSlice\",\"Entry\":[\"message\",\"+%FF%22%5C\"]}`},\n\n\t\t{\"base64\", \"\", \"\", testBase64(\"byte slice\"),\n\t\t\tprefix + `\"ID\":\"TestBase64\",\"Entry\":\"Ynl0ZSBzbGljZQ%3D%3D\"}`},\n\n\t\t{\"sensitive\", \"\", \"\", testSensitive{Sensitive: Sensitive(\"sensitive data\")},\n\t\t\tprefix + `\"ID\":\"TestSensitive\",\"Entry\":{\"Sensitive\":{` +\n\t\t\t\t`\"Hash\":\"HQjFmEMNLY2Y+j0/SejcozHaqBiVjYvw2/oqo4TYp/0=\",\"Length\":14}}}`},\n\n\t\t{\"map\", \"\", \"\", testMap{\" \\xff\\\"\\\\\": \" \\xff\\\"\\\\\"},\n\t\t\tprefix + `\"ID\":\"TestMap\",\"Entry\":{\"+%FF%22%5C\":\"+%FF%22%5C\"}}`},\n\n\t\t{\"struct\", \"\", \"\", testStruct{Field: \" \\xff\\\"\\\\\"},\n\t\t\tprefix + `\"ID\":\"TestStruct\",\"Entry\":{\"Field\":\"+%FF%22%5C\"}}`},\n\n\t\t{\"stringer\", \"\", \"\", testStruct{Field: testStringer(\"message\")},\n\t\t\tprefix + `\"ID\":\"TestStruct\",\"Entry\":{\"Field\":\"message.String\"}}`},\n\n\t\t{\"error\", \"\", \"\", testErrorEntry{Err: testError(\"message\")},\n\t\t\tprefix + `\"ID\":\"TestErrorEntry\",\"Entry\":{\"Err\":\"message.Error\"}}`},\n\n\t\t{\"nested\", \"\", \"\", testNested{Nested: testString(\" \\xff\\\"\\\\\")},\n\t\t\tprefix + `\"ID\":\"TestNested\",\"Entry\":{\"Nested\":{\"ID\":\"TestString\",\"Entry\":\"+%FF%22%5C\"}}}`},\n\n\t\t{\"nested empty\", \"\", \"\", testNested{Nested: &testEmpty{}},\n\t\t\tprefix + `\"ID\":\"TestNested\",\"Entry\":{\"Nested\":{\"ID\":\"TestEmpty\"}}}`},\n\n\t\t{\"nested empty pointer\", \"\", \"\", testNested{Nested: (*testEmpty)(nil)},\n\t\t\tprefix + `\"ID\":\"TestNested\",\"Entry\":{\"Nested\":{\"ID\":\"TestEmpty\"}}}`},\n\t\t{\"gen\", \"\", \"\", TestGenerated{}, prefix + `\"ID\":\"ivxv.ee/common/collector/log.TestGenerated\"}`},\n\n\t\t{\"alert\", \"\", \"\", testErrorEntry{Err: Alert(testErrorEntry{Err: testError(\"message\")})},\n\t\t\tprefix + `\"ID\":\"TestErrorEntry\",\"Entry\":{\"Err\":{` +\n\t\t\t\t`\"ID\":\"TestErrorEntry\",\"Entry\":{\"Err\":\"message.Error\"}}}}`},\n\n\t\t{\"time\", \"\", \"\", testTime{\n\t\t\tTime: time.Date(2017, time.February, 28, 16, 42, 35, 00, time.UTC),\n\t\t}, prefix + `\"ID\":\"TestTime\",\"Entry\":{\"Time\":\"2017-02-28T16%3A42%3A35Z\"}}`},\n\n\t\t{\"time nano\", \"\", \"\", testTime{\n\t\t\tTime: time.Date(2017, time.February, 28, 16, 42, 35, 123456789, time.UTC),\n\t\t}, prefix + `\"ID\":\"TestTime\",\"Entry\":{\"Time\":\"2017-02-28T16%3A42%3A35.123456789Z\"}}`},\n\n\t\t{\"certificate\", \"\", \"\", testCertificate{Certificate: cert},\n\t\t\tprefix + `\"ID\":\"TestCertificate\",\"Entry\":{\"Certificate\":{` +\n\t\t\t\t`\"Subject\":\"CN%3DConfiguration+Signer\",` +\n\t\t\t\t`\"Issuer\":\"CN%3DConfiguration+Signer\",` +\n\t\t\t\t`\"Serial\":\"995d4b1336861e79\",` +\n\t\t\t\t`\"Hash\":{\"Truncated\":\"49B434Z%2FbrFreLtC7fTop2vIKiImMMTQwK%2BA\",` +\n\t\t\t\t`\"FullLength\":44}}}}`},\n\t}\n\n\tfor _, test := range tests {\n\t\tb.Reset()\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tctx := testContext(context.Background(), &b, now, test.cid, test.sid, true)\n\t\t\tLog(ctx, test.entry)\n\t\t\tif got := b.String(); got != test.output {\n\t\t\t\tt.Errorf(\"unexpected output,\\ngot:  %s\\nwant: %s\", got, test.output)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/mid/authenticate.go",
    "content": "package mid\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n)\n\n// MobileAuthenticate starts a Mobile-ID authentication session.\nfunc (c *Client) MobileAuthenticate(ctx context.Context, idCode, phone string) (\n\tsesscode string, challengeRnd []byte, challenge []byte, err error) {\n\n\t// Generate random authentication challenge to sign. Although we could\n\t// use the challenge directly, we pass it through the hash function to\n\t// simplify VerifyAuthenticationSignature which requires the pre-image\n\t// of the signed data.\n\tchallengeRnd = make([]byte, c.authHashFunction.Size())\n\tif _, err = rand.Read(challengeRnd); err != nil {\n\t\terr = GenerateAuthenticationChallengeError{Err: err,\n\t\t\tDescription: _MID_RAND}\n\t\treturn\n\t}\n\td := c.authHashFunction.New()\n\td.Write(challengeRnd)\n\tchallenge = d.Sum(nil)\n\n\thashType := hashFunctionNames[c.authHashFunction]\n\n\tsesscode, err = c.startSession(ctx, sessAuth, idCode, phone, challenge, hashType)\n\tif err != nil {\n\t\terr = MobileAuthenticateError{Err: err, Description: _MID_SESS}\n\t\treturn\n\t}\n\n\treturn\n}\n\n// GetMobileAuthenticateStatus queries the status of a Mobile-ID authentication\n// session. If err is nil and signature is empty, then the transaction is still\n// outstanding. If err is nil and signature is non-nil, then the user is\n// authenticated, although callers should use VerifyAuthenticationSignature to\n// double-check.\nfunc (c *Client) GetMobileAuthenticateStatus(ctx context.Context, sesscode string) (\n\tcert *x509.Certificate, algorithm string, signature []byte, err error) {\n\n\tvar certDER []byte\n\talgorithm, signature, certDER, err = c.getSessionStatus(ctx, sessAuth, sesscode)\n\tif err != nil {\n\t\terr = GetMobileAuthenticateStatusError{Err: err,\n\t\t\tDescription: _MID_SESS_STAT}\n\t\treturn\n\t}\n\n\tif certDER != nil {\n\t\tcert, err = c.parseAndVerify(ctx, certDER)\n\t}\n\n\treturn\n}\n\n// parseAndVerify is a helper function to parse and verify the authentication\n// certificate.\nfunc (c *Client) parseAndVerify(ctx context.Context, certDER []byte) (\n\tcert *x509.Certificate, err error) {\n\t// Parse the authentication certificate.\n\tif cert, err = x509.ParseCertificate(certDER); err != nil {\n\t\terr = ParseAuthenticationCertificateError{\n\t\t\tCertificate: certDER,\n\t\t\tErr:         err,\n\t\t\tDescription: _MID_CERT,\n\t\t}\n\t\treturn\n\t}\n\n\t// Verify the authentication certificate and get the issuer.\n\topts := x509.VerifyOptions{\n\t\tRoots:         c.rpool,\n\t\tIntermediates: c.ipool,\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t}\n\tchains, err := cert.Verify(opts)\n\tif err != nil {\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = AuthenticationCertificateVerificationError{\n\t\t\tCertificate: cert,\n\t\t\tErr:         err,\n\t\t\tDescription: _MID_CERT_VERIFY,\n\t\t}\n\t\terr = certerr\n\t\treturn\n\t}\n\tissuer := cert\n\tif len(chains[0]) > 1 { // At least one chain is guaranteed.\n\t\tissuer = chains[0][1]\n\t}\n\n\t// Check OCSP status.\n\tstatus, err := c.ocsp.Check(ctx, cert, issuer, nil)\n\tif err != nil {\n\t\terr = CheckAuthenticationCertOCSPResponsError{\n\t\t\tResponse:    status,\n\t\t\tErr:         err,\n\t\t\tDescription: _MID_OCSP,\n\t\t}\n\t\treturn\n\t}\n\tif !status.Good {\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = AuthenticationCertificateRevokedError{\n\t\t\tReason:      status.RevocationReason,\n\t\t\tDescription: _MID_OCSP_N_GOOD,\n\t\t}\n\t\terr = certerr\n\t\treturn\n\t}\n\n\treturn\n}\n\n// VerifyAuthenticationSignature verifies the certificate signature on the\n// authentication challenge.\nfunc VerifyAuthenticationSignature(cert *x509.Certificate, algorithm string,\n\tsigned, signature []byte) (err error) {\n\n\tsigalg, ok := signatureAlgs[algorithm]\n\tif !ok {\n\t\treturn SigAlgorithmNotSupportedError{\n\t\t\tAlgorithm:   algorithm,\n\t\t\tDescription: _MID_ALG,\n\t\t}\n\t}\n\n\t// We need to re-encode ECDSA signatures for the Go standard library.\n\tswitch sigalg {\n\tcase x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:\n\t\tif signature, err = cryptoutil.ReEncodeECDSASignature(signature); err != nil {\n\t\t\treturn ReEncodeAuthenticationSignatureError{\n\t\t\t\tSignature:   signature,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _MID_XML2ASN1,\n\t\t\t}\n\t\t}\n\t}\n\n\tif err = cert.CheckSignature(sigalg, signed, signature); err != nil {\n\t\treturn VerifyAuthenticationSignatureError{Err: err,\n\t\t\tDescription: _MID_SIG}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/mid/certificate.go",
    "content": "package mid\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n)\n\n// https://github.com/SK-EID/MID#313-request-parameters\ntype getMobileCertificate struct {\n\tRelyingPartyUUID       string `json:\"relyingPartyUUID\"`\n\tRelyingPartyName       string `json:\"relyingPartyName\"`\n\tPhoneNumber            string `json:\"phoneNumber\"`\n\tNationalIdentityNumber string `json:\"nationalIdentityNumber\"`\n}\n\n// https://github.com/SK-EID/MID#316-response-structure\ntype getMobileCertificateResponse struct {\n\tResult  string `json:\"result\"`\n\tCert    []byte `json:\"cert\"`\n\tTime    string `json:\"time\"`\n\tTraceID string `json:\"traceId\"`\n}\n\n// GetMobileCertificate queries for a Mobile-ID ECC signing certificate.\nfunc (c *Client) GetMobileCertificate(ctx context.Context, id, phone string) (\n\tcert *x509.Certificate, err error) {\n\n\t// We cannot use a struct literal, because gen would report it\n\t// as a duplicate error type.\n\tvar input InputError\n\tswitch {\n\tcase len(id) == 0:\n\t\tinput.Err = GetCertificateNoIDCodeError{Description: _MID_N_ID}\n\t\terr = input\n\tcase len(phone) == 0:\n\t\tinput.Err = GetCertificateNoPhoneError{Description: _MID_N_PNO}\n\t\terr = input\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar resp getMobileCertificateResponse\n\tif err = httpPost(ctx, c.url+\"certificate\", getMobileCertificate{\n\t\tRelyingPartyUUID:       c.conf.RelyingPartyUUID,\n\t\tRelyingPartyName:       c.conf.RelyingPartyName,\n\t\tNationalIdentityNumber: id,\n\t\tPhoneNumber:            phone,\n\t}, &resp); err != nil {\n\t\treturn nil, GetMobileCertificateError{Err: err,\n\t\t\tDescription: _MID_HTTP_CERT}\n\t}\n\n\tswitch resp.Result {\n\tcase \"OK\":\n\tcase \"NOT_FOUND\":\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = MobileCertificateNotFoundError{Description: _MID_RESP_NFOUND}\n\t\treturn nil, certerr\n\tcase \"NOT_ACTIVE\":\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = MobileCertificateNotActiveError{Description: _MID_RESP_NACTIVE}\n\t\treturn nil, certerr\n\tdefault:\n\t\tvar status StatusError\n\t\tstatus.Err = UnknownMobileCertificateResultError{Status: resp.Result,\n\t\t\tDescription: _MID_RESP_UNKNOWN_ERR}\n\t\treturn nil, status\n\t}\n\n\tif cert, err = x509.ParseCertificate(resp.Cert); err != nil {\n\t\treturn nil, ParseMobileCertificateError{\n\t\t\tCertificate: resp.Cert,\n\t\t\tErr:         err,\n\t\t\tDescription: _MID_CERT,\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/mid/http.go",
    "content": "package mid\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\nconst maxResponseSize = 10240 // 10 KiB.\n\n// https://github.com/SK-EID/MID#327-error-response-contents\ntype errorResponse struct {\n\tError   string `json:\"error\"`\n\tTime    string `json:\"time\"`\n\tTraceID string `json:\"traceId\"`\n}\n\nfunc httpGet(ctx context.Context, url string, resp interface{}) error {\n\thttpReq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn CreateHTTPGetRequestError{URL: url, Err: err,\n\t\t\tDescription: _MID_URI}\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\treturn httpDo(ctx, \"\", httpReq, resp)\n}\n\nfunc httpPost(ctx context.Context, url string, req interface{}, resp interface{}) error {\n\tjsonReq, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn MarshalJSONRequestError{Err: err,\n\t\t\tDescription: _MID_JSON}\n\t}\n\n\thttpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonReq))\n\tif err != nil {\n\t\treturn CreateHTTPPostRequestError{URL: url, Err: err,\n\t\t\tDescription: _MID_HTTP}\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\treturn httpDo(ctx, fmt.Sprintf(\"%T\", req), httpReq, resp)\n}\n\nfunc httpDo(ctx context.Context, tag string, httpReq *http.Request, resp interface{}) error {\n\treqDump, err := httputil.DumpRequestOut(httpReq, true)\n\tif err != nil {\n\t\treturn DumpHTTPRequestError{Err: err,\n\t\t\tDescription: _MID_HTTP}\n\t}\n\tlog.Debug(ctx, HTTPRequest{Request: string(reqDump),\n\t\tDescription: _MID_HTTP_READY})\n\n\tlog.Log(ctx, SendingRequest{URL: httpReq.URL, Method: httpReq.Method,\n\t\tBodyType: tag, Description: _MID_HTTP_SEND})\n\thttpResp, err := http.DefaultClient.Do(httpReq)\n\tif err != nil {\n\t\treturn log.Alert(SendRequestError{Err: err,\n\t\t\tDescription: _MID_RESP_ERR})\n\t}\n\tdefer func() {\n\t\tif cerr := httpResp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = ResponseBodyCloseError{Err: cerr,\n\t\t\t\tDescription: _MID_CLOSE}\n\t\t}\n\t}()\n\tlog.Log(ctx, ReceivedResponse{Description: _MID_RESP})\n\n\trespDump, err := httputil.DumpResponse(httpResp, false)\n\tif err != nil {\n\t\treturn DumpHTTPResponseError{Err: err,\n\t\t\tDescription: _MID_RESP_PARSE}\n\t}\n\tlog.Debug(ctx, HTTPResponse{Response: string(respDump),\n\t\tDescription: _MID_RESP_PARSE_OK})\n\n\t// Does encoding/json.Unmarshal retain any references to the\n\t// original byte slice in the unmarshaled structure? If not, then\n\t// instead of allocating a new byte slice here we could reuse pooled\n\t// buffers for temporarily storing the JSON between reading and\n\t// decoding.\n\tbody, err := io.ReadAll(safereader.New(httpResp.Body, maxResponseSize))\n\tif err != nil {\n\t\treturn ReadHTTPResponseBodyError{Err: err,\n\t\t\tDescription: _MID_RESP_READ}\n\t}\n\tlog.Debug(ctx, HTTPResponseBody{Body: string(body),\n\t\tDescription: _MID_RESP_READ_OK})\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\tif len(body) > 0 {\n\t\t\tvar jsonErr errorResponse\n\t\t\tif err = json.Unmarshal(body, &jsonErr); err != nil {\n\t\t\t\treturn UnmarshalJSONErrorResponseError{Err: err,\n\t\t\t\t\tDescription: _MID_RESP_JSONCODE}\n\t\t\t}\n\n\t\t\terr = ErrorResponseError{\n\t\t\t\tHTTPStatus:  httpResp.Status,\n\t\t\t\tMessage:     jsonErr.Error,\n\t\t\t\tTime:        jsonErr.Time,\n\t\t\t\tTraceID:     jsonErr.TraceID,\n\t\t\t\tDescription: _MID_RESP_ERR,\n\t\t\t}\n\t\t\treturn err\n\n\t\t}\n\t\treturn HTTPStatusError{Status: httpResp.Status, Description: _MID_ERRCODE}\n\t}\n\n\tif err = json.Unmarshal(body, &resp); err != nil {\n\t\treturn UnmarshalJSONResponseError{Err: err,\n\t\t\tDescription: _MID_RESP_JSON}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/mid/log_desc.go",
    "content": "package mid\n\nconst (\n\t_MID_RAND             = \"Generate random value for Mobile-ID challenge\"\n\t_MID_SESS             = \"Cannot create Mobile-ID session via Mobile-ID provider\"\n\t_MID_SESS_STAT        = \"HTTP get session status from Mobile-ID provider failed\"\n\t_MID_CERT             = \"Parsing client Mobile-ID certificate failed\"\n\t_MID_CERT_VERIFY      = \"Failed to verify client Mobile-ID certificate\"\n\t_MID_OCSP             = \"Failed to check client Mobile-ID certificate against OCSP provider\"\n\t_MID_OCSP_N_GOOD      = \"OCSP status of client Mobile-ID certificate is not good\"\n\t_MID_ALG              = \"Unsupported Mobile-ID signature algorithm\"\n\t_MID_XML2ASN1         = \"Mobile-ID signature is failed to be converted from XML to ASN.1 format\"\n\t_MID_SIG              = \"Failed to verify signature of a Mobile-ID client\"\n\t_MID_N_ID             = \"No voter ID\"\n\t_MID_N_PNO            = \"No voter phone nr.\"\n\t_MID_HTTP_CERT        = \"Failed to get Mobile-ID certificate from Mobile-ID provider\"\n\t_MID_RESP_NFOUND      = \"Mobile-ID provider responded with NOT_FOUND\"\n\t_MID_RESP_NACTIVE     = \"Mobile-ID provider responded with NOT_ACTIVE\"\n\t_MID_RESP_UNKNOWN_ERR = \"Mobile-ID provider responded with unknown error code\"\n\t_MID_URI              = \"Failed to prepare request URI for Mobile-ID provider\"\n\t_MID_JSON             = \"JSON marshalling request for Mobile-ID provider failed\"\n\t_MID_HTTP             = \"Failed to prepare HTTP request URI for Mobile-ID provider\"\n\t_MID_HTTP_READY       = \"HTTP request for Mobile-ID provider has been prepared successfully\"\n\t_MID_HTTP_SEND        = \"Sending HTTP request to Mobile-ID provider\"\n\t_MID_RESP_ERR         = \"Mobile-ID provider responded with error\"\n\t_MID_CLOSE            = \"Cannot gracefully close HTTP connection with Mobile-ID provider\"\n\t_MID_RESP             = \"Successfully received Mobile-ID provider response\"\n\t_MID_RESP_PARSE       = \"Cannot parse HTTP Mobile-ID provider response\"\n\t_MID_RESP_PARSE_OK    = \"Successfully parsed HTTP Mobile-ID provider response\"\n\t_MID_RESP_READ        = \"Failed to read Mobile-ID response\"\n\t_MID_RESP_READ_OK     = \"Successfully read Mobile-ID response\"\n\t_MID_RESP_JSONCODE    = \"JSON unmarshalling error code from Mobile-ID provider failed\"\n\t_MID_ERRCODE          = \"Error status code from Mobile-ID provider\"\n\t_MID_RESP_JSON        = \"JSON unmarshalling response for Mobile-ID provider failed\"\n\t_MID_IN               = \"This error wraps errors which are caused by bad input to mid functions\"\n\t_MID_USER             = \"Error returned if the submitted phone number does not belong to a Mobile-ID use\"\n\t_MID_SIM              = \"Error returned if there is a problem with user's phone SIM card and the user should contact the service provider\"\n\t_MID_ABS              = \"Error returned if the voter's phone is absent (switched off or out of coverage)\"\n\t_MID_CANCEL           = \"Error returned if the voter canceled the operation\"\n\t_MID_EXP              = \"Error returned if the session expired before the voter entered their PIN\"\n\t_MID_CERT_ERR         = \"Wraps errors which are caused by errors with the voter's certificate (revoked, suspended, not activated, etc)\"\n\t_MID_STAT_ERR         = \"Wraps errors which are caused by an unexpected session status: this is a catch-all for other types of Mobile-ID problems\"\n\t_MID_N_HASH           = \"No hashing algorithm provided\"\n\t_MID_N_HASH_T         = \"No hashing type provided\"\n\t_MID_SESS_STATE       = \"Unknown Mobile-ID session state\"\n\t_MID_SESS_RES         = \"Unknown Mobile-ID session result\"\n\t_MID_CA               = \"No CA certificates provided to verify Mobile-ID provider certificate\"\n\t_MID_CA_PARSE         = \"Failed to parse Mobile-ID CA certificate\"\n\t_MID_ICA_PARSE        = \"Failed to parse Mobile-ID intermediate CA certificate\"\n\t_MID_OCSP_CFG         = \"Failed to configure Mobile-ID OCSP client\"\n\t_MID_HASH             = \"Unsupported hash algorithm\"\n)\n"
  },
  {
    "path": "common/collector/mid/mid.go",
    "content": "/*\nPackage mid provides client for the MID-REST service.\n\nhttps://github.com/SK-EID/MID\n*/\npackage mid\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"strings\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/ocsp\"\n)\n\nvar (\n\t// InputError wraps errors which are caused by bad input to mid functions.\n\t_ = InputError{Err: nil, Description: _MID_IN}\n\n\t// NotMIDUserError is the error returned if the submitted phone number\n\t// does not belong to a Mobile-ID user.\n\t_ = NotMIDUserError{Description: _MID_USER}\n\n\t// SIMError is the error returned if there is a problem with user's\n\t// phone SIM card and the user should contact the service provider.\n\t_ = SIMError{Result: \"\", Description: _MID_SIM}\n\n\t// AbsentError is the error returned if the voter's phone is absent\n\t// (switched off or out of coverage).\n\t_ = AbsentError{Description: _MID_ABS}\n\n\t// CanceledError is the error returned if the voter canceled the\n\t// operation.\n\t_ = CanceledError{Description: _MID_CANCEL}\n\n\t// ExpiredError is the error returned if the session expired\n\t// before the voter entered their PIN.\n\t_ = ExpiredError{Description: _MID_EXP}\n\n\t// CertificateError wraps errors which are caused by errors with the\n\t// voter's certificate (revoked, suspended, not activated, etc).\n\t_ = CertificateError{Err: nil, Description: _MID_CERT_ERR}\n\n\t// StatusError wraps errors which are caused by an unexpected session\n\t// status: this is a catch-all for other types of Mobile-ID problems.\n\t_ = StatusError{Err: nil, Description: _MID_STAT_ERR}\n\n\t// allowedAuthHashFunctions is list of hash functions allowed for\n\t// authentication. Since the hash function is determined by it's hash\n\t// size (Conf.AuthChallengeSize), the functions should have hash sizes\n\t// unique in the list. If Conf.AuthChallengeSize is zero, then the\n\t// first one from this list is used.\n\tallowedAuthHashFunctions = []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512}\n)\n\n// Conf contains the configurable options for the MID REST API client. It\n// only contains serialized values such that it can easily be unmarshaled from\n// a file.\ntype Conf struct {\n\tURL              string // URL of MID REST API.\n\tRelyingPartyUUID string // The UUID of the relying party, i.e service consumer.\n\tRelyingPartyName string // The name of the relying party, i.e service consumer.\n\n\tLanguage    string // Language for user dialog in mobile phone.\n\tAuthMessage string // Message to display during authentication.\n\tSignMessage string // Message to display during signing.\n\n\t// MessageFormat is the message format name sent to MID service.\n\t// Possible values are \"GSM-7\" (default by MID) and \"UCS-2”. GSM-7\n\t// allows AuthMessage and SignMessage to contain up to 40 characters\n\t// from standard GSM 7-bit alpabet including up to 5 characters from\n\t// extension table (€[]^|{}\\). UCS-2 allows up to 20 characters from\n\t// UCS-2 alphabet. More info in MID documentation:\n\t// https://github.com/SK-EID/MID#323-request-parameters.\n\t// More info about encoding: https://en.wikipedia.org/wiki/GSM_03.38.\n\tMessageFormat     string\n\tAuthChallengeSize int64 // The authentication challenge size, either 32, 48 or 64 bytes.\n\tStatusTimeoutMS   int64 // The long-polling timeout for authentication/signing status request.\n\n\tRoots         []string  // PEM-encoded authentication certificate verification roots.\n\tIntermediates []string  // PEM-encoded authentication certificate verification intermediates.\n\tOCSP          ocsp.Conf // OCSP configuration for checking authentication certificate revocation.\n}\n\n// Client implements MID REST API authentication and signing.\ntype Client struct {\n\tconf  Conf\n\trpool *x509.CertPool\n\tipool *x509.CertPool\n\tocsp  *ocsp.Client\n\t// authHashFunction is the hash function to use for authentication.\n\t// Determined by Conf.AuthChallengeSize.\n\tauthHashFunction crypto.Hash\n\t// url is the same as 'conf.URL', but guaranteed to end with slash.\n\turl string\n}\n\n// New returns a new MID REST API client with the provided configuration.\nfunc New(conf *Conf) (c *Client, err error) {\n\tif len(conf.Roots) == 0 {\n\t\treturn nil, UnconfiguredRootsError{Description: _MID_CA}\n\t}\n\n\tc = &Client{conf: *conf} // Save a copy of conf so it cannot be changed.\n\tif c.rpool, err = cryptoutil.PEMCertificatePool(c.conf.Roots...); err != nil {\n\t\treturn nil, RootsParsingError{Err: err,\n\t\t\tDescription: _MID_CA_PARSE}\n\t}\n\tif c.ipool, err = cryptoutil.PEMCertificatePool(c.conf.Intermediates...); err != nil {\n\t\treturn nil, IntermediatesParsingError{Err: err,\n\t\t\tDescription: _MID_ICA_PARSE}\n\t}\n\tif c.ocsp, err = ocsp.New(&c.conf.OCSP); err != nil {\n\t\treturn nil, OCSPClientError{Err: err, Description: _MID_OCSP_CFG}\n\t}\n\tif c.authHashFunction, err = findAuthHashFunction(c.conf.AuthChallengeSize); err != nil {\n\t\treturn nil, err\n\t}\n\tc.url = conf.URL\n\tif !strings.HasSuffix(c.url, \"/\") {\n\t\tc.url += \"/\"\n\t}\n\treturn\n}\n\n// findAuthHashFunction is helper function to find allowed authentication hash\n// algorithm by it's hash size. If the size is not configured, the first value\n// in the allowedAuthHashFunctions is used.\nfunc findAuthHashFunction(size int64) (crypto.Hash, error) {\n\tif size == 0 {\n\t\treturn allowedAuthHashFunctions[0], nil\n\t}\n\tvar sizes []int\n\tfor _, hf := range allowedAuthHashFunctions {\n\t\tif hf.Size() == int(size) {\n\t\t\treturn hf, nil\n\t\t}\n\t\tsizes = append(sizes, hf.Size())\n\t}\n\treturn 0, AuthChallengeSizeError{Size: size, AllowedSizes: sizes,\n\t\tDescription: _MID_HASH}\n}\n"
  },
  {
    "path": "common/collector/mid/mid_test.go",
    "content": "package mid\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/ocsp\"\n)\n\nconst (\n\ttestID     = \"60001018800\"\n\ttestSerial = \"PNOEE-60001018800\"\n\ttestPhone  = \"+37200000566\"\n)\n\nvar (\n\thashFun    = crypto.SHA384\n\ttestClient *Client\n)\n\nfunc TestMain(m *testing.M) {\n\tread := func(path string) string {\n\t\tb, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"failed to read file:\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\treturn string(b)\n\t}\n\n\tvar err error\n\tif testClient, err = New(&Conf{\n\t\tURL:               \"https://tsp.demo.sk.ee/mid-api/\",\n\t\tRelyingPartyUUID:  \"00000000-0000-0000-0000-000000000000\",\n\t\tRelyingPartyName:  \"DEMO\",\n\t\tLanguage:          \"EST\",\n\t\tAuthMessage:       \"Test authentication message.\",\n\t\tSignMessage:       \"Test signing message.\",\n\t\tMessageFormat:     \"GSM-7\",\n\t\tAuthChallengeSize: 64,\n\t\tRoots:             []string{read(\"testdata/TEST_of_EE_Certification_Centre_Root_CA.pem\")},\n\t\tIntermediates:     []string{read(\"testdata/TEST_of_ESTEID-SK_2015.pem\")},\n\t\tOCSP: ocsp.Conf{\n\t\t\tURL:        \"http://demo.sk.ee/ocsp\",\n\t\t\tResponders: []string{read(\"testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem\")},\n\t\t\tMaxSkew:    300,\n\t\t\tMaxAge:     1,\n\t\t},\n\t}); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"failed to create test client:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc TestAuthentication(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-MID-REST service\")\n\t}\n\tt.Parallel()\n\n\tctx := log.TestContext(context.Background())\n\tcode, challengeRnd, _, err := testClient.MobileAuthenticate(ctx, testID, testPhone)\n\tif err != nil {\n\t\tt.Fatal(\"failed to start authentication session:\", err)\n\t}\n\n\tvar cert *x509.Certificate\n\tvar algo string\n\tvar signature []byte\n\tfor len(signature) == 0 {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling authentication\") // Use fmt instead of t.Log to get running output.\n\t\tif cert, algo, signature, err = testClient.GetMobileAuthenticateStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check authentication status:\", err)\n\t\t}\n\t}\n\n\tif cert.Subject.SerialNumber != testSerial {\n\t\tt.Error(\"unexpected subject serial number:\", cert.Subject.SerialNumber)\n\t}\n\n\tif err = VerifyAuthenticationSignature(cert, algo, challengeRnd, signature); err != nil {\n\t\tt.Error(\"failed to verify authentication challenge signature:\", err)\n\t}\n}\n\nfunc TestCertificate(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-MID-REST service\")\n\t}\n\tt.Parallel()\n\n\tctx := log.TestContext(context.Background())\n\tcert, err := testClient.GetMobileCertificate(ctx, testID, testPhone)\n\tif err != nil {\n\t\tt.Fatal(\"failed to get signing certificate:\", err)\n\t}\n\n\tif cert.Subject.SerialNumber != testSerial {\n\t\tt.Error(\"unexpected subject serial number:\", cert.Subject.SerialNumber)\n\t}\n}\n\nfunc TestSigning(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-MID-REST service\")\n\t}\n\tt.Parallel()\n\n\tpem, err := os.ReadFile(\"testdata/signer.pem\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to read signer certificate:\", err)\n\t}\n\tcert, err := cryptoutil.PEMCertificate(string(pem))\n\tif err != nil {\n\t\tt.Fatal(\"failed to parse signer certificate:\", err)\n\t}\n\n\trnd := make([]byte, 100)\n\tif _, err = rand.Read(rnd); err != nil {\n\t\tt.Fatal(\"failed to generate data to sign:\", err)\n\t}\n\th := hashFun.New()\n\tif _, err = h.Write(rnd); err != nil {\n\t\tt.Fatal(\"failed to hash data to sign:\", err)\n\t}\n\thash := h.Sum(nil)\n\n\tctx := log.TestContext(context.Background())\n\tcode, err := testClient.MobileSignHash(ctx, testID, testPhone, hash, hashFunctionNames[hashFun])\n\tif err != nil {\n\t\tt.Fatal(\"failed to start signing session:\", err)\n\t}\n\n\tvar algo string\n\tvar signature []byte\n\tfor len(signature) == 0 {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling signing\") // Use fmt instead of t.Log to get running output.\n\t\tif algo, signature, err = testClient.GetMobileSignHashStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check signing status:\", err)\n\t\t}\n\t}\n\n\t// Although VerifyAuthenticationSignature is meant for authentication\n\t// (as the name suggests), we can reuse it in this test case.\n\tif err = VerifyAuthenticationSignature(cert, algo, rnd, signature); err != nil {\n\t\tt.Error(\"failed to verify authentication challenge signature:\", err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/mid/session.go",
    "content": "package mid\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"fmt\"\n)\n\ntype sessType string\n\nconst (\n\tsessAuth sessType = \"authentication\"\n\tsessSign sessType = \"signature\"\n)\n\nvar (\n\t// hashFunctionNames is map from hash algorithm to it's name for MID\n\t// REST API.\n\thashFunctionNames = map[crypto.Hash]string{\n\t\tcrypto.SHA256: \"SHA256\",\n\t\tcrypto.SHA384: \"SHA384\",\n\t\tcrypto.SHA512: \"SHA512\",\n\t}\n\n\t// signatureAlgs is map from signature algorithm name in MID REST API\n\t// to algorithm type.\n\tsignatureAlgs = map[string]x509.SignatureAlgorithm{\n\t\t\"SHA256WithRSAEncryption\": x509.SHA256WithRSA,\n\t\t\"SHA384WithRSAEncryption\": x509.SHA384WithRSA,\n\t\t\"SHA512WithRSAEncryption\": x509.SHA512WithRSA,\n\n\t\t\"SHA256WithECEncryption\": x509.ECDSAWithSHA256,\n\t\t\"SHA384WithECEncryption\": x509.ECDSAWithSHA384,\n\t\t\"SHA512WithECEncryption\": x509.ECDSAWithSHA512,\n\t}\n)\n\n// https://github.com/SK-EID/MID#323-request-parameters\ntype startSessionRequest struct {\n\tRelyingPartyUUID       string `json:\"relyingPartyUUID\"`\n\tRelyingPartyName       string `json:\"relyingPartyName\"`\n\tPhoneNumber            string `json:\"phoneNumber\"`\n\tNationalIdentityNumber string `json:\"nationalIdentityNumber\"`\n\tHash                   []byte `json:\"hash\"`\n\tHashType               string `json:\"hashType\"`\n\tLanguage               string `json:\"language\"`\n\tDisplayText            string `json:\"displayText,omitempty\"`\n\tDisplayTextFormat      string `json:\"displayTextFormat,omitempty\"`\n}\n\n// https://github.com/SK-EID/MID#325-example-response\ntype startSessionResponse struct {\n\tSessionID string `json:\"sessionID\"`\n}\n\n// startSession is helper function to start either authentication or signing\n// dialog with the user.\nfunc (c *Client) startSession(ctx context.Context, t sessType, idCode, phone string,\n\thash []byte, hashType string) (sesscode string, err error) {\n\n\t// We cannot use a struct literal, because gen would report it\n\t// as a duplicate error type.\n\tvar input InputError\n\tswitch {\n\tcase len(idCode) == 0:\n\t\tinput.Err = StartSessionNoIDCodeError{Description: _MID_N_ID}\n\t\terr = input\n\tcase len(phone) == 0:\n\t\tinput.Err = StartSessionNoPhoneError{Description: _MID_N_PNO}\n\t\terr = input\n\tcase len(hash) == 0:\n\t\tinput.Err = StartSessionNoHashError{Description: _MID_N_HASH}\n\t\terr = input\n\tcase len(hashType) == 0:\n\t\tinput.Err = StartSessionNoHashTypeError{Description: _MID_N_HASH_T}\n\t\terr = input\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tmessage := c.conf.AuthMessage\n\tif t == sessSign {\n\t\tmessage = c.conf.SignMessage\n\t}\n\n\tvar resp startSessionResponse\n\tif err = httpPost(ctx, c.url+string(t), startSessionRequest{\n\t\tRelyingPartyUUID:       c.conf.RelyingPartyUUID,\n\t\tRelyingPartyName:       c.conf.RelyingPartyName,\n\t\tNationalIdentityNumber: idCode,\n\t\tPhoneNumber:            phone,\n\t\tHash:                   hash,\n\t\tHashType:               hashType,\n\t\tLanguage:               c.conf.Language,\n\t\tDisplayText:            message,\n\t\tDisplayTextFormat:      c.conf.MessageFormat,\n\t}, &resp); err != nil {\n\t\terr = StartSessionError{Err: err, Description: _MID_RESP_ERR}\n\t\treturn\n\t}\n\n\tsesscode = resp.SessionID\n\n\treturn\n}\n\n// https://github.com/SK-EID/MID#335-response-structure\ntype sessionStatusResponse struct {\n\tState     string            `json:\"state\"`\n\tResult    string            `json:\"result\"`\n\tSignature signatureResponse `json:\"signature\"`\n\tCert      []byte            `json:\"cert\"`\n\tTime      string            `json:\"time\"`\n\tTraceID   string            `json:\"traceId\"`\n}\n\ntype signatureResponse struct {\n\tValue     []byte `json:\"value\"`\n\tAlgorithm string `json:\"algorithm\"`\n}\n\n// getSessionStatus is helper function to get the status of either\n// authentication or signing dialog with the user.\nfunc (c *Client) getSessionStatus(ctx context.Context, t sessType, sesscode string) (\n\talgorithm string, signature []byte, certDER []byte, err error) {\n\n\tvar resp sessionStatusResponse\n\turl := fmt.Sprintf(\"%s%s/session/%s?timeoutMs=%d\", c.url, t, sesscode, c.conf.StatusTimeoutMS)\n\tif err = httpGet(ctx, url, &resp); err != nil {\n\t\treturn \"\", nil, nil, GetSessionStatusError{\n\t\t\tErr: err, Description: _MID_SESS_STAT}\n\t}\n\n\tswitch resp.State {\n\tcase \"RUNNING\":\n\t\treturn\n\tcase \"COMPLETE\":\n\tdefault:\n\t\tvar status StatusError\n\t\tstatus.Err = UnexpectedSessionStateError{State: resp.State,\n\t\t\tDescription: _MID_SESS_STATE}\n\t\treturn \"\", nil, nil, status\n\t}\n\n\tswitch resp.Result {\n\tcase \"OK\":\n\t\treturn resp.Signature.Algorithm, resp.Signature.Value, resp.Cert, nil\n\tcase \"TIMEOUT\":\n\t\tvar expired ExpiredError\n\t\treturn \"\", nil, nil, expired\n\tcase \"NOT_MID_CLIENT\":\n\t\tvar notMID NotMIDUserError\n\t\treturn \"\", nil, nil, notMID\n\tcase \"USER_CANCELLED\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", nil, nil, canceled\n\tcase \"SIM_ERROR\", \"SIGNATURE_HASH_MISMATCH\":\n\t\tvar sim SIMError\n\t\tsim.Result = resp.Result\n\t\treturn \"\", nil, nil, sim\n\tcase \"PHONE_ABSENT\":\n\t\tvar absent AbsentError\n\t\treturn \"\", nil, nil, absent\n\tdefault:\n\t\tvar status StatusError\n\t\tstatus.Err = UnexpectedSessionResultError{Result: resp.Result,\n\t\t\tDescription: _MID_SESS_RES}\n\t\treturn \"\", nil, nil, status\n\t}\n}\n"
  },
  {
    "path": "common/collector/mid/sign.go",
    "content": "package mid\n\nimport (\n\t\"context\"\n)\n\n// MobileSignHash starts a Mobile-ID signing session to sigh hash.\nfunc (c *Client) MobileSignHash(ctx context.Context, idCode, phone string, hash []byte,\n\thashType string) (sesscode string, err error) {\n\n\tsesscode, err = c.startSession(ctx, sessSign, idCode, phone, hash, hashType)\n\tif err != nil {\n\t\terr = MobileSignHashError{Err: err, Description: _MID_SESS}\n\t\treturn\n\t}\n\n\treturn\n}\n\n// GetMobileSignHashStatus queries the status of a Mobile-ID signing session.\n// If err is nil and signature is empty, then the transaction is still\n// outstanding.\nfunc (c *Client) GetMobileSignHashStatus(ctx context.Context, sesscode string) (\n\talgorithm string, signature []byte, err error) {\n\n\talgorithm, signature, _, err = c.getSessionStatus(ctx, sessSign, sesscode)\n\tif err != nil {\n\t\terr = GetMobileSignHashStatusError{Err: err, Description: _MID_SESS_STAT}\n\t\treturn\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/mid/testdata/TEST_of_EE_Certification_Centre_Root_CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\nGA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\ncnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\naWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\nxRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\nywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\nMmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\nVNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\nkV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\npwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\nAgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\nBwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\nI+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\nBmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\nSnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\ntH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\nSfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/mid/testdata/TEST_of_ESTEID-SK_2015.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\nMjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\naWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\nA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\nz8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\nzs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\nJ7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\nh5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\not3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\nAAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\nHQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\nIARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\nay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\nAQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\nAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\nBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\nfTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\nAQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\naWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\nMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\nY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\nAH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\nDgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\nyPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\nWFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\nPKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/mid/testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\nMjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\nb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\nLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\ngOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\nr1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\nz00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\nOM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\nwB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\nRrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\nFIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\nAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\naQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\nJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\nGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\nczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\nBgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\naC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\nMHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\nnH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\nmawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\ndh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/mid/testdata/signer.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIQUsz4AdR7FcpcrHiyXaldkjANBgkqhkiG9w0BAQsFADBr\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNU\nRUlELVNLIDIwMTUwHhcNMTkwNDA5MTA0OTIyWhcNMzAxMjExMjE1OTU5WjCBpTEL\nMAkGA1UEBhMCRUUxPTA7BgNVBAMMNE/igJlDT05ORcW9LcWgVVNMSUsgVEVTVE5V\nTUJFUixNQVJZIMOETk4sNjAwMDEwMTg4MDAxJzAlBgNVBAQMHk/igJlDT05ORcW9\nLcWgVVNMSUsgVEVTVE5VTUJFUjESMBAGA1UEKgwJTUFSWSDDhE5OMRowGAYDVQQF\nExFQTk9FRS02MDAwMTAxODgwMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEG8\nMxwPLh5qmCfkkAPMw+8nKf4cqDETMoWiFiVOGu3cdI61ARLdRQUfa9wpzFDQGtmK\nuScHrLE25ZPZWEozK72jggIfMIICGzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIG\nQDB1BgNVHSAEbjBsMF8GCisGAQQBzh8DAQMwUTAeBggrBgEFBQcCAjASDBBPbmx5\nIGZvciBURVNUSU5HMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBv\nc2l0b29yaXVtL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBRYUZMh7LjBd2OpIXrj\n0YnUK1hPJzCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwUQYG\nBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlv\nbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjATBgYEAI5GAQYwCQYHBACO\nRgEGATAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUH\nAQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlk\nMjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVT\nVF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MDQGA1UdHwQtMCswKaAnoCWGI2h0\ndHBzOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTUuY3JsMA0GCSqGSIb3DQEBCwUA\nA4ICAQAWPebd0D8hssTj7Cdzp6zCFtHsZjmcgn21hLzsVDYqSde+/M7aLJ9WrCNl\nSvjldScWRXBBhwH7SmePxVvmi061fmlb2Mg22XCOqWOp+Eyt9LtTtHlSi21v5VNh\nnVX0RRPlCGseXHAyYjLtIGx744LCsZ/nblYtfAYrh2fJnYBOJddiUctKfb96M/sZ\nNFlfAXbejBpoi0a+wXmL8fTY/PNAWT2UNhsUWA3XgQpsca0TBb6m4rVc9VfnjH0v\nV9gaboH8jJL0M5bfPa4oE676Uw4YhtbRp2gXFdMKjb/5KpdAdfb5EhGSk8+rWZnW\nBgfsRfHw8YnrLO3sOhPYWlXdIkJ4TPsxK/StJVIlIye5UBESxR1J4wZ0iI4wvwQU\nTQ7xuIS89XgjlLm9/R7Qy86GN4lj6J0lD89dnFckduN/Hk5vMA+sGvPtIsV9q/fi\nWl6+SCYMmH6D+FpVpxYhq/VQoabwSWlhsgnjE+RddP6H3pWdICwJ6r7Iyx48SUc4\nj8/lh6dxg6NR3TPQSyPQg1bJjaNO3L79pgxevX3LFOJ0eLhLmY2gvT5MvjkOjULc\nXspKSjyNqyg8uJgT33SSBjArppWKMHpxybsaiY6X5jwdyO9qV32We94snptiqOrZ\naJ/LJ0RgOE7t7yBlDCdaUHohuUGB8+UD6efOBnP48L+FQz0D+w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/log_desc.go",
    "content": "package ocsp\n\nconst (\n\t_OCSP_RESPONDER        = \"Failed to parse OCSP response intermediate certificates from an election configuration\"\n\t_OCSP_URI              = \"No OCSP provider URI\"\n\t_OCSP_REQ              = \"Failed to create OCSP client request\"\n\t_OCSP_CONN             = \"Could not contact OCSP provider, trying again\"\n\t_OCSP_FAIL             = \"OCSP provider responded with error\"\n\t_OCSP_RESP             = \"Failed to verify OCSP provider response\"\n\t_OCSP_PARSE            = \"Could not parse OCSP response\"\n\t_OCSP_RAW2BS           = \"Failed to parse OCSP response to basic OCSP response (formatting)\"\n\t_OCSP_ASN1             = \"ASN.1 marshalling OCSP client request failed\"\n\t_OCSP_REQ_PREP         = \"Failed to prepare OCSP client request\"\n\t_OCSP_REQ_HTTP         = \"Failed to create HTTP request to be sent to OCSP provider\"\n\t_OCSP_REQ_INFO         = \"Info about HTTP request to OCSP provider\"\n\t_OCSP_REQ_SEND         = \"Sending OCSP request to OCSP provider over HTTP\"\n\t_OCSP_REQ_FAIL         = \"OCSP provider responded with error\"\n\t_OCSP_REQ_CLOSE        = \"Failed to close HTTP connection between OCSP client and provider\"\n\t_OCSP_REQ_RESP         = \"Successfully received OCSP response from OCSP provider\"\n\t_OCSP_RESP_DUMP        = \"Failed to parse HTTP OCSP response\"\n\t_OCSP_RESP_OK          = \"Successfully parsed OCSP response\"\n\t_OCSP_RESP_STAT        = \"OCSP response has non-OK status\"\n\t_OCSP_RESP_HTTP_HEADER = \"OCSP response 'Content-Type' HTTP header is not a 'application/ocsp-response'\"\n\t_OCSP_RESP_READ        = \"Failed to read OCSP response\"\n\t_OCSP_RESP_READ_OK     = \"Successfully read OCSP response\"\n\t_OCSP_RESP_BASIC       = \"Failed to parse OCSP response as basic OCSP response\"\n\t_OCSP_AUTHKID          = \"Missing 'AuthorityKeyId' field in a x509 certificate\"\n\t_OCSP_RESP_ASN1        = \"ASN.1 unmarshalling OCSP response failed\"\n\t_OCSP_RESP_TYPE        = \"OCSP response type is not basic\"\n\t_OCSP_RESP_B_ASN1      = \"ASN.1 unmarshalling basic OCSP response failed\"\n\t_OCSP_COMMON_CHECKS    = \"Failed to perform common OCSP checks\"\n\t_OCSP_OFF              = \"OCSP thisUpdate value > now + (OCSP allowed offset)\"\n\t_OCSP_EXPIRED          = \"OCSP is response is expired\"\n\t_OCSP_BETWEEN          = \"OCSP response producedAt is not between thisUpdate and (now + OCSP allowed offset) timestamps\"\n\t_OCSP_STORED           = \"Check stored in .bdoc container OCSP response failed\"\n\t_OCSP_PAT              = \"OCSP producedAt >= thisUpdate\"\n\t_OCSP_SUB              = \"(producedAt - thisUpdate) > OCSP allowed offset\"\n\t_OCSP_UNIQ             = \"Only 1 OCSP response is allowed\"\n\t_OCSP_CERT             = \"User certificate from OCSP response doesn't match provided user certificate\"\n\t_OCSP_NONCE            = \"Nonce calculated not equal to nonce from OCSP response\"\n\t_OCSP_RESPONDER_GET    = \"Fetching all responder certificates from an election configuration failed\"\n\t_OCSP_ALG_UNKNOWN      = \"Unsupported signature algorithm from OCSP response\"\n\t_OCSP_SIG_VERIFY       = \"Checking signature from OCSP response failed\"\n\t_OCSP_CERT_PARSE       = \"Parsing OCSP responder certificate from an OCSP response failed\"\n\t_OCSP_CERT_VERIFY      = \"Verify OCSP responder certificate against OCSP intermediate CA certificates\"\n\t_OCSP_N_ISSUER         = \"No OCSP CA certificate provided to verify OCSP responder certificate\"\n)\n"
  },
  {
    "path": "common/collector/ocsp/ocsp.go",
    "content": "/*\nPackage ocsp implements an OCSP client\nhttps://tools.ietf.org/html/rfc6960\n*/\npackage ocsp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\nconst (\n\t// The maximum amount the response thisUpdate can be set in the future\n\t// allowing for correction for system clock inconsistencies\n\tmaxSkew = 300 * time.Millisecond\n\n\t// The maximum amount the response thisUpdate can differ from\n\t// the current time at the time of response validation\n\tmaxAge = 1 * time.Minute\n\n\t// Maximum size for the ocsp server response.\n\tmaxResponseSize = 10240 // 10 KiB.\n)\n\nvar (\n\t// https://tools.ietf.org/html/rfc6960#section-4.4.1\n\tidPKIXOCSPNonce = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 2}\n\n\t// https://tools.ietf.org/html/rfc6960#section-4.2.1\n\tidPKIXOCSPBasic = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 1}\n\n\t// Map of signature algorithms\n\tsigMap = map[string]x509.SignatureAlgorithm{\n\t\t\"1.2.840.113549.1.1.11\": x509.SHA256WithRSA,\n\t\t\"1.2.840.113549.1.1.12\": x509.SHA384WithRSA,\n\t\t\"1.2.840.113549.1.1.13\": x509.SHA512WithRSA,\n\t}\n)\n\n// Conf contains the configurable options for the OCSP client. It only contains\n// serialized values such that it can easily be unmarshaled from a file.\ntype Conf struct {\n\t// URL is OCSP url. This parameter is optional, but if set then all\n\t// .bdoc votes' certificates are verified against that URL.\n\t// If not set, then URL is parsed from a .bdoc vote's certificate.\n\tURL string\n\n\t// Responders is a list of all trusted OCSP responders. This is used\n\t// when parsing OCSP response, the OCSP response's responder certificate\n\t// is matched against this list, and if found, then confirms this\n\t// OCSP response to be received from trusted authority.\n\t//\n\t// NB! Use this only for testing purposes with custom certificates.\n\t// Leave this list empty on production, as responder certificates\n\t// SHOULD BE verified against user certificate's issuer.\n\tResponders []string\n\n\t// Retry is the amount of times an OCSP request is retried in case of\n\t// network or responder errors.\n\tRetry uint64\n\n\t// MaxSkew in milliseconds, if 0 then defaults to 300 milliseconds.\n\tMaxSkew uint64\n\n\t// MaxAge in minutes, if 0 then defaults to 1 minute.\n\tMaxAge uint64\n}\n\n// Client is used for performing OCSP requests and checking responses.\ntype Client struct {\n\turl        string\n\tresponders []*x509.Certificate\n\tretry      uint64\n\tmaxSkew    time.Duration\n\tmaxAge     time.Duration\n}\n\n// New returns a new OCSP client with the provided configuration.\nfunc New(conf *Conf) (c *Client, err error) {\n\tc = &Client{\n\t\turl:     conf.URL,\n\t\tretry:   conf.Retry,\n\t\tmaxSkew: time.Duration(conf.MaxSkew) * time.Millisecond, //nolint:gosec\n\t\tmaxAge:  time.Duration(conf.MaxAge) * time.Minute,       //nolint:gosec\n\t}\n\tif conf.MaxSkew <= 0 {\n\t\tc.maxSkew = maxSkew\n\t}\n\tif conf.MaxAge <= 0 {\n\t\tc.maxAge = maxAge\n\t}\n\tif c.responders, err = cryptoutil.PEMCertificates(conf.Responders...); err != nil {\n\t\treturn nil, ResponderParsingError{Err: err,\n\t\t\tDescription: _OCSP_RESPONDER}\n\t}\n\treturn\n}\n\n// CertStatus is the return value for OCSP commands containing all the\n// relevant information about the OCSP response.\ntype CertStatus struct {\n\tProducedAt       time.Time // The time this response was produced at.\n\tNonce            []byte    // The nonce used in the response\n\tGood             bool      // Is the status of the requested certificate good?\n\tUnknown          bool      // Is the status of the certificate unknown\n\tRevocationReason int       // If the certificate is revoked, the reason of revocation.\n}\n\n// LiveCertStatus is an extension of CertStatus returned from live OCSP queries\n// which also contains the raw response received from the server.\ntype LiveCertStatus struct {\n\tCertStatus\n\tRawResponse []byte // The raw ASN.1 DER-encoded response from the server.\n}\n\n// Check checks the status of the certificate against the configured OCSP\n// server. If nonce is not nil, then that value will be used as the nonce in\n// the request, otherwise no nonce is used.\nfunc (c *Client) Check(ctx context.Context, cert, issuer *x509.Certificate, nonce []byte) (\n\tstatus *LiveCertStatus, err error) {\n\t// Create request\n\treqCert, err := newCertID(cert)\n\tif err != nil {\n\t\treturn nil, RequestCertIDCreateError{Err: err,\n\t\t\tDescription: _OCSP_REQ}\n\t}\n\n\t// Submit the request to url and read the response.\n\tvar resp []byte\n\tvar basic *basicOCSPResponse\nretry:\n\tfor attempt := uint64(0); ; attempt++ {\n\t\tswitch resp, basic, err = c.submitRequest(ctx, cert, reqCert, nonce); {\n\t\tcase err == nil:\n\t\t\tbreak retry\n\t\tcase attempt < c.retry && shouldRetry(err):\n\t\t\tlog.Log(ctx, RetryingSubmitRequest{Attempt: attempt + 1, Err: err,\n\t\t\t\tDescription: _OCSP_CONN})\n\t\t\ttime.Sleep(1 * time.Second)\n\t\tdefault:\n\t\t\treturn nil, SubmitRequestError{Err: err,\n\t\t\t\tDescription: _OCSP_FAIL}\n\t\t}\n\t}\n\n\t// Check response.\n\trespNonce, err := c.checkResponse(basic, reqCert, issuer, nonce)\n\tif err != nil {\n\t\treturn nil, CheckResponseError{Err: err,\n\t\t\tDescription: _OCSP_RESP}\n\t}\n\n\t// Return status.\n\tsingle := basic.TBSResponseData.Responses[0]\n\n\treturn &LiveCertStatus{\n\t\tRawResponse: resp,\n\t\tCertStatus: CertStatus{\n\t\t\tProducedAt:       basic.TBSResponseData.ProducedAt,\n\t\t\tGood:             bool(single.CertStatusGood),\n\t\t\tUnknown:          bool(single.CertStatusUnknown),\n\t\t\tNonce:            respNonce,\n\t\t\tRevocationReason: int(single.CertStatusRevoked.RevocationReason),\n\t\t},\n\t}, nil\n}\n\nfunc shouldRetry(err error) bool {\n\treturn errors.Walk(err, func(err error) error {\n\t\tswitch t := err.(type) {\n\n\t\t// Sending the HTTP request or reading the response body failed.\n\t\tcase SendRequestError, ResponseBodyReadError:\n\t\t\treturn err\n\n\t\t// The HTTP status was a 5xx code.\n\t\tcase UnexpectedResponseStatusError:\n\t\t\tif strings.HasPrefix(t.Status.(string), \"5\") {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t// The OCSP response status was internalError or tryLater.\n\t\tcase UnexpectedOCSPResponseStatusError:\n\t\t\tif status := t.Status.(string); status == ocspResponseStatus[2] ||\n\t\t\t\tstatus == ocspResponseStatus[3] {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}) != nil\n}\n\n// CheckFullResponse checks a DER-encoded full OCSP response. A full OCSP\n// response envelopes a basic OCSP response with the status of the server\n// response and the OID of the response type. CheckFullResponse unpacks the\n// basic response and calls CheckResponse.\nfunc (c *Client) CheckFullResponse(response []byte, cert, issuer *x509.Certificate,\n\tnonce []byte, sigTime time.Time) (\n\tstatus *CertStatus, err error) {\n\n\tresp, err := unpackResponse(response)\n\tif err != nil {\n\t\treturn nil, UnpackResponseError{Err: err,\n\t\t\tDescription: _OCSP_PARSE}\n\t}\n\n\treturn c.CheckResponse(resp, cert, issuer, nonce, sigTime)\n}\n\n// CheckResponse checks a stored DER-encoded basic OCSP response. If nonce is\n// not nil, then the nonce in the response must match that value.\nfunc (c *Client) CheckResponse(response []byte, cert, issuer *x509.Certificate,\n\tnonce []byte, sigTime time.Time) (\n\tstatus *CertStatus, err error) {\n\n\t// Unmarshal the basic response.\n\tvar basic basicOCSPResponse\n\tif err = unmarshalResponse(response, &basic); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Generate the CertID.\n\treqCert, err := newCertID(cert)\n\tif err != nil {\n\t\treturn nil, ResponseCertIDCreateError{Err: err,\n\t\t\tDescription: _OCSP_REQ}\n\t}\n\n\t// Check response.\n\trespNonce, err := c.checkStoredResponse(&basic, reqCert, issuer, nonce, sigTime)\n\tif err != nil {\n\t\treturn nil, CheckStoredResponseError{Err: err,\n\t\t\tDescription: _OCSP_RESP}\n\t}\n\n\t// Return status.\n\tsingle := basic.TBSResponseData.Responses[0]\n\n\treturn &CertStatus{\n\t\tProducedAt:       basic.TBSResponseData.ProducedAt,\n\t\tGood:             bool(single.CertStatusGood),\n\t\tUnknown:          bool(single.CertStatusUnknown),\n\t\tNonce:            respNonce,\n\t\tRevocationReason: int(single.CertStatusRevoked.RevocationReason),\n\t}, nil\n}\n\n// ParseTime parses a DER-encoded basic OCSP response and returns the time it\n// was produced at.\n//\n// Warning! ParseTime does not check the validity of the response, it only\n// returns the produced at time value.\nfunc ParseTime(response []byte) (time.Time, error) {\n\tvar basic basicOCSPResponse\n\tif err := unmarshalResponse(response, &basic); err != nil {\n\t\treturn time.Time{}, err\n\t}\n\treturn basic.TBSResponseData.ProducedAt, nil\n}\n\n// ExtractProducedAtTimeFromRawOcspResponse extracts ProducedAt from a raw OCSP\n// response sent by OCSP authority server.\nfunc ExtractProducedAtTimeFromRawOcspResponse(rawOcspResponse []byte) (time.Time, error) {\n\t// Unpack raw ASN.1 DER-encoded ocspResponse\n\tresp, err := unpackResponse(rawOcspResponse)\n\tif err != nil {\n\t\treturn time.Time{}, UnpackRawOCSPResponseError{Err: err,\n\t\t\tDescription: _OCSP_PARSE}\n\t}\n\n\tvar basic basicOCSPResponse\n\n\t// Parse DER-encoded OCSP resp to basicOCSPResponse\n\tif err := unmarshalResponse(resp, &basic); err != nil {\n\t\treturn time.Time{}, UnmarshalOCSPResponseToBasicOCSPResponseError{\n\t\t\tErr: err, Description: _OCSP_RAW2BS}\n\t}\n\n\treturn basic.TBSResponseData.ProducedAt, nil\n}\n\nfunc (c *Client) submitRequest(ctx context.Context, cert *x509.Certificate, reqCert *certID, nonce []byte) (\n\tresponse []byte, basic *basicOCSPResponse, err error) {\n\n\tr := ocspRequest{\n\t\tTBSRequest: tbsRequest{\n\t\t\tRequestList: []request{{ReqCert: *reqCert}},\n\t\t},\n\t}\n\tif nonce != nil {\n\t\tr.TBSRequest.RequestExtensions = []pkix.Extension{{\n\t\t\tId:    idPKIXOCSPNonce,\n\t\t\tValue: nonce,\n\t\t}}\n\t}\n\treq, err := asn1.Marshal(r)\n\tif err != nil {\n\t\terr = RequestMarshalError{Err: err, Description: _OCSP_ASN1}\n\t\treturn\n\t}\n\n\tvar client http.Client\n\n\t// Try to fetch OCSP URL from conf, as it is takes precedence\n\tif c.url != \"\" { //nolint:gocritic,revive\n\t\t// Do nothing and use OCSP URL from conf\n\t} else if len(cert.OCSPServer) != 0 {\n\t\t// If OCSP URL not set in conf, use one from cert.\n\t\t// First found OCSP URL is assumed to be the correct one\n\t\tc.url = cert.OCSPServer[0]\n\t} else {\n\t\t// By this time, OCSP URL should be known\n\t\treturn nil, nil, UnconfiguredURLError{\n\t\t\tDescription: _OCSP_URI,\n\t\t}\n\t}\n\n\thttpReq, err := http.NewRequest(http.MethodPost, c.url, bytes.NewBuffer(req))\n\tif err != nil {\n\t\terr = NewRequestError{Err: err, Description: _OCSP_REQ_PREP}\n\t\treturn\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\thttpReq.Header.Set(\"Content-Type\", \"application/ocsp-request\")\n\n\treqDump, err := httputil.DumpRequestOut(httpReq, true)\n\tif err != nil {\n\t\terr = ReqDumpError{Err: err, Description: _OCSP_REQ_HTTP}\n\t\treturn\n\t}\n\tlog.Debug(ctx, RequestDebugDump{Request: string(reqDump),\n\t\tDescription: _OCSP_REQ_INFO})\n\n\tlog.Log(ctx, SendingRequest{\n\t\tURL:            c.url,\n\t\tSerial:         reqCert.SerialNumber,\n\t\tIssuerNameHash: reqCert.IssuerNameHash,\n\t\tDescription:    _OCSP_REQ_SEND,\n\t})\n\n\thttpResp, err := client.Do(httpReq)\n\tif err != nil {\n\t\terr = log.Alert(SendRequestError{Err: err, Description: _OCSP_REQ_FAIL})\n\t\treturn\n\t}\n\tdefer func() {\n\t\tif cerr := httpResp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = ResponseBodyCloseError{Err: cerr,\n\t\t\t\tDescription: _OCSP_REQ_CLOSE}\n\t\t}\n\t}()\n\n\tlog.Log(ctx, ReceivedResponse{Description: _OCSP_REQ_RESP})\n\n\trespDump, err := httputil.DumpResponse(httpResp, false)\n\tif err != nil {\n\t\terr = RespDumpError{Err: err, Description: _OCSP_RESP_DUMP}\n\t\treturn\n\t}\n\tlog.Debug(ctx, ResponseDebugDump{Response: string(respDump),\n\t\tDescription: _OCSP_RESP_OK})\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\terr = UnexpectedResponseStatusError{Status: httpResp.Status,\n\t\t\tDescription: _OCSP_RESP_STAT}\n\t\treturn\n\t}\n\n\tif ctype := httpResp.Header.Get(\"Content-Type\"); ctype != \"application/ocsp-response\" {\n\t\terr = UnexpectedContentTypeError{ContentType: ctype,\n\t\t\tDescription: _OCSP_RESP_HTTP_HEADER}\n\t\treturn\n\t}\n\n\t// The entire response will be returned so we need to allocate a new\n\t// byte slice using ioutil.ReadAll.\n\tresponse, err = io.ReadAll(safereader.New(httpResp.Body, maxResponseSize))\n\tif err != nil {\n\t\terr = ResponseBodyReadError{Err: err, Description: _OCSP_RESP_READ}\n\t\treturn\n\t}\n\tlog.Debug(ctx, BodyDump{Body: response, Description: _OCSP_RESP_READ_OK})\n\n\t// Unpack basicOCSPResponse\n\tresp, err := unpackResponse(response)\n\tif err != nil {\n\t\terr = ResponseUnpackError{Err: err, Description: _OCSP_RESP_BASIC}\n\t\treturn\n\t}\n\n\t// Unmarshal the basic response.\n\tbasic = new(basicOCSPResponse)\n\terr = unmarshalResponse(resp, basic)\n\treturn\n}\n\nfunc unpackResponse(der []byte) (response []byte, err error) {\n\tvar resp ocspResponse\n\trest, err := asn1.Unmarshal(der, &resp)\n\tif err != nil {\n\t\treturn nil, ResponseUnmarshalError{Err: err,\n\t\t\tDescription: _OCSP_RESP_ASN1}\n\t} else if len(rest) > 0 {\n\t\treturn nil, ResponseUnmarshalExcessBytesError{Bytes: rest,\n\t\t\tDescription: _OCSP_RESP_ASN1}\n\t}\n\n\tif resp.ResponseStatus != ocspResponseStatusSuccessful {\n\t\tstatus, ok := ocspResponseStatus[int(resp.ResponseStatus)]\n\t\tif !ok {\n\t\t\tstatus = fmt.Sprint(resp.ResponseStatus)\n\t\t}\n\t\treturn nil, UnexpectedOCSPResponseStatusError{Status: status,\n\t\t\tDescription: _OCSP_RESP_STAT}\n\t}\n\n\tif !resp.ResponseBytes.ResponseType.Equal(idPKIXOCSPBasic) {\n\t\treturn nil, UnexpectedResponseTypeError{\n\t\t\tResponseType: resp.ResponseBytes.ResponseType,\n\t\t\tDescription:  _OCSP_RESP_TYPE,\n\t\t}\n\t}\n\n\treturn resp.ResponseBytes.Response, nil\n}\n\nfunc unmarshalResponse(der []byte, basic *basicOCSPResponse) (err error) {\n\trest, err := asn1.Unmarshal(der, basic)\n\tif err != nil {\n\t\treturn BasicOCSPResponseUnmarshalError{Err: err,\n\t\t\tDescription: _OCSP_RESP_B_ASN1}\n\n\t} else if len(rest) > 0 {\n\t\treturn BasicOCSPResponseUnmarshalExcessBytesError{Bytes: rest,\n\t\t\tDescription: _OCSP_RESP_B_ASN1}\n\t}\n\treturn\n}\n\n// checkResponse checks if the given response should be accepted.\nfunc (c *Client) checkResponse(resp *basicOCSPResponse,\n\tcert *certID, issuer *x509.Certificate, nonce []byte) (\n\trespNonce []byte, err error) {\n\tvar defaultTime time.Time\n\n\t// Perform common checks.\n\tif respNonce, err = c.checkResponseCommon(resp, cert, issuer, nonce, defaultTime); err != nil {\n\t\treturn nil, CheckResponseCommonError{Err: err,\n\t\t\tDescription: _OCSP_COMMON_CHECKS}\n\t}\n\n\t// Check thisUpdate skew and age.\n\tthisUpdate := resp.TBSResponseData.Responses[0].ThisUpdate\n\tcurrent := time.Now()\n\tskewed := current.Add(c.maxSkew)\n\tif thisUpdate.After(skewed) {\n\t\treturn nil, ThisUpdateSetInFutureError{\n\t\t\tThisUpdate:  thisUpdate,\n\t\t\tMaxSkewed:   skewed,\n\t\t\tDescription: _OCSP_OFF,\n\t\t}\n\t}\n\tif age := current.Sub(thisUpdate); age > c.maxAge {\n\t\treturn nil, ThisUpdateExceedsMaxAgeError{\n\t\t\tCurrent:     current,\n\t\t\tThisUpdate:  thisUpdate,\n\t\t\tDescription: _OCSP_EXPIRED,\n\t\t}\n\t}\n\n\t// Make sure producedAt is between thisUpdate and now + skew.\n\tif pat := resp.TBSResponseData.ProducedAt; pat.Before(thisUpdate) || pat.After(skewed) {\n\t\treturn nil, ProducedAtWrongTimeError{\n\t\t\tProducedAt:  pat,\n\t\t\tThisUpdate:  thisUpdate,\n\t\t\tSkewed:      skewed,\n\t\t\tDescription: _OCSP_BETWEEN,\n\t\t}\n\t}\n\treturn\n}\n\n// checkStoredResponse checks if the given stored response should be accepted\n// and returns the response nonce on success\nfunc (c *Client) checkStoredResponse(resp *basicOCSPResponse,\n\tcert *certID, issuer *x509.Certificate, nonce []byte, sigTime time.Time) (\n\trespNonce []byte, err error) {\n\n\t// Perform common checks.\n\tif respNonce, err = c.checkResponseCommon(resp, cert, issuer, nonce, sigTime); err != nil {\n\t\treturn nil, CheckStoredResponseCommonError{Err: err,\n\t\t\tDescription: _OCSP_STORED}\n\t}\n\n\t// Check time between thisUpdate and producedAt.\n\tthisUpdate := resp.TBSResponseData.Responses[0].ThisUpdate\n\tpat := resp.TBSResponseData.ProducedAt\n\tif pat.Before(thisUpdate) {\n\t\treturn nil, ProducedAtBeforeThisUpdateError{\n\t\t\tProducedAt:  pat,\n\t\t\tThisUpdate:  thisUpdate,\n\t\t\tDescription: _OCSP_PAT,\n\t\t}\n\t}\n\tif age := pat.Sub(thisUpdate); age > c.maxAge {\n\t\treturn nil, StoredThisUpdateExceedsMaxAgeError{\n\t\t\tProducedAt:  pat,\n\t\t\tThisUpdate:  thisUpdate,\n\t\t\tDescription: _OCSP_SUB,\n\t\t}\n\t}\n\treturn\n}\n\n// checkResponseCommon performs checks on the basic response that are common\n// for fresh and stored responses.\nfunc (c *Client) checkResponseCommon(resp *basicOCSPResponse,\n\tcert *certID, issuer *x509.Certificate, nonce []byte, sigTime time.Time) (\n\trespNonce []byte, err error) {\n\n\t// Compare certificate requested and in the response.\n\tif n := len(resp.TBSResponseData.Responses); n != 1 {\n\t\treturn nil, UnexpectedTBSResponseCountError{Count: n,\n\t\t\tDescription: _OCSP_UNIQ}\n\t}\n\tsingle := resp.TBSResponseData.Responses[0]\n\tif !cert.equal(&single.CertID) {\n\t\treturn nil, CertIDMismatchError{\n\t\t\tStored:      single.CertID,\n\t\t\tControl:     cert,\n\t\t\tDescription: _OCSP_CERT,\n\t\t}\n\t}\n\n\t// Get the nonce from the response, if present.\n\tfor _, extension := range resp.TBSResponseData.ResponseExtensions {\n\t\tif extension.Id.Equal(idPKIXOCSPNonce) {\n\t\t\trespNonce = extension.Value\n\t\t}\n\t}\n\n\t// If nonce is not nil, then compare to respNonce.\n\tif nonce != nil && !bytes.Equal(nonce, respNonce) {\n\t\treturn nil, ResponseNonceMismatchError{\n\t\t\tRequest:     nonce,\n\t\t\tResponse:    respNonce,\n\t\t\tDescription: _OCSP_NONCE,\n\t\t}\n\t}\n\n\t// Find responder certificate and check signature on the response.\n\tresponder, err := c.responder(resp, issuer, sigTime)\n\tif err != nil {\n\t\treturn nil, ResponderCertificateError{Err: err,\n\t\t\tDescription: _OCSP_RESPONDER_GET}\n\t}\n\n\talg, ok := sigMap[resp.SignatureAlgorithm.Algorithm.String()]\n\tif !ok {\n\t\treturn nil, SignatureAlgorithmNotSupportedError{\n\t\t\tAlgorithm:   resp.SignatureAlgorithm.Algorithm,\n\t\t\tDescription: _OCSP_ALG_UNKNOWN,\n\t\t}\n\t}\n\tif err = responder.CheckSignature(alg, resp.TBSResponseData.Raw, resp.Signature.RightAlign()); err != nil {\n\t\treturn nil, CheckSignatureError{Err: err,\n\t\t\tDescription: _OCSP_SIG_VERIFY}\n\t}\n\n\treturn\n}\n\nfunc (c *Client) responder(resp *basicOCSPResponse, issuer *x509.Certificate,\n\tsigTime time.Time) (\n\t*x509.Certificate, error) {\n\n\tname := resp.TBSResponseData.ResponderIDByName\n\n\t// First check if the response is signed by a configured responder.\n\tfor _, responder := range c.responders {\n\n\t\tresponder.Subject.ExtraNames = responder.Subject.Names\n\t\tif cryptoutil.RDNSequenceEqual(responder.Subject.ToRDNSequence(), name) {\n\t\t\treturn responder, nil\n\t\t}\n\t}\n\n\t// Fail fast\n\tif issuer == nil {\n\t\treturn nil, ResponderCertificateNotFoundError{Responder: name,\n\t\t\tDescription: _OCSP_N_ISSUER}\n\t}\n\n\t// Check if its certificate is in the response, is issued by\n\t// the same issuer, and is allowed for OCSP signing.\n\tvar responder *x509.Certificate\n\tvar err error\n\topts := x509.VerifyOptions{\n\t\tRoots:     cryptoutil.CertificatePool(issuer),\n\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},\n\t}\n\n\t// 0001-01-01 00:00:00 +0000 UTC (time.Time default value)\n\tvar defaultTime time.Time\n\tif sigTime != defaultTime {\n\t\topts.CurrentTime = sigTime\n\t}\n\n\t// First found\n\tfor _, der := range resp.Certs {\n\t\tresponder, err = x509.ParseCertificate(der.FullBytes)\n\t\tif err != nil {\n\t\t\treturn nil, ParseResponseCertificateError{\n\t\t\t\tDER:         der.FullBytes,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _OCSP_CERT_PARSE,\n\t\t\t}\n\t\t}\n\n\t\tresponder.Subject.ExtraNames = responder.Subject.Names\n\t\tif !cryptoutil.RDNSequenceEqual(responder.Subject.ToRDNSequence(), name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err = responder.Verify(opts); err != nil {\n\t\t\treturn nil, VerifyResponderCertificateError{\n\t\t\t\tSubject:     responder.Subject,\n\t\t\t\tIssuer:      responder.Issuer,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _OCSP_CERT_VERIFY,\n\t\t\t}\n\t\t}\n\n\t\tbreak\n\t}\n\n\treturn responder, nil\n}\n"
  },
  {
    "path": "common/collector/ocsp/ocsp_test.go",
    "content": "package ocsp\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\nconst (\n\tdemoSkEeOCSPResponderIssuer = \"TEST_of_KLASS3-SK_2016.pem\"\n\tdemoSkEeOCSPResponder       = \"DEMO_of_KLASS3-SK_2016_SSL_OCSP_RESPONDER_2018.pem\"\n\tdemoSkEeOcspURL             = \"http://demo.sk.ee/ocsp\"\n\t// > 2022 year ID-cards' issuer\n\testeid2018 = \"esteid2018.pem\"\n\t// <= 2022 year ID-cards'/Mobile-IDs' issuer\n\ttestOfEsteidsk2015 = \"TEST_of_ESTEID-SK_2015.pem\"\n\testeidsk2015       = \"esteidsk2015.pem\"\n\t// Smart-IDs'/Mobile-IDs' issuer\n\teidsk2016 = \"eidsk2016.pem\"\n)\n\nfunc TestCheckOCSPOfIDCardWithParsingOCSPURLFromConf(t *testing.T) {\n\tresponderPath := demoSkEeOCSPResponder\n\tresponderBytes, err := os.ReadFile(filepath.Join(\"testdata\", responderPath))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tclient, err := New(&Conf{\n\t\tURL:        demoSkEeOcspURL,\n\t\tResponders: []string{string(responderBytes)},\n\t\tRetry:      2,\n\t\tMaxSkew:    300,\n\t\tMaxAge:     1,\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"Create new OCSP config error: %v\\n\", err)\n\t}\n\n\tctx := log.TestContext(context.Background())\n\tcertPath := \"good.pem\"\n\tcert, err := testCert(certPath)\n\tif err != nil {\n\t\tt.Errorf(\"Obtain %v\\n\", err)\n\t}\n\n\t// Since issuer is nil, then we assume that responders will\n\t// verify the OCSP response\n\tcheck, err := client.Check(ctx, cert, nil, nil)\n\tif err != nil {\n\t\tt.Errorf(\"Error %v\\n\", err)\n\t}\n\n\tif !check.Good {\n\t\tt.Errorf(\"Certificate status is not good: %v\\n\", check.CertStatus)\n\t}\n}\n\nfunc TestCheckOCSPWithParsingOCSPURLFromCert(t *testing.T) {\n\ttestName := fmt.Sprintf(\"Issuer-%s-cert-%s\", esteid2018, \"ID-PNOEE-39605244244.pem\")\n\tt.Run(testName, func(t *testing.T) {\n\t\tresponderIssuerBytes, err := os.ReadFile(filepath.Join(\"testdata\", esteid2018))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresponderIssuer, err := cryptoutil.PEMCertificate(string(responderIssuerBytes))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tclient, err := New(&Conf{\n\t\t\t// No URL defined forces to parse it from client cert\n\t\t\tRetry:   2,\n\t\t\tMaxSkew: 300,\n\t\t\tMaxAge:  1,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Create new OCSP config error: %v\\n\", err)\n\t\t}\n\n\t\tctx := log.TestContext(context.Background())\n\t\tcert2, err := testCert(\"ID-PNOEE-39605244244.pem\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Obtain %v\\n\", err)\n\t\t}\n\n\t\tcheck, err := client.Check(ctx, cert2, responderIssuer, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error %v\\n\", err)\n\t\t}\n\n\t\tif !check.Good {\n\t\t\tt.Errorf(\"Certificate status is not good: %v\\n\", check.CertStatus)\n\t\t}\n\t})\n}\n\nfunc TestCheck(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping OCSP test against live responder\")\n\t}\n\n\t// Enumeration of certificate statuses to test for.\n\tconst (\n\t\tgood = iota\n\t\trevoked\n\t\tunknown\n\t)\n\n\t// Structure for single certificate test case.\n\ttype cert struct {\n\t\tname   string\n\t\tissuer string\n\t\tstatus int\n\t}\n\n\ttests := []struct {\n\t\turl        string\n\t\tresponders []string\n\t\tcerts      []cert\n\t}{\n\t\t{\"http://demo.sk.ee/ocsp\", []string{demoSkEeOCSPResponder}, []cert{\n\t\t\t{\"good.pem\", \"\", good},\n\t\t\t{\"revoked.pem\", \"\", revoked},\n\t\t\t{\"unknown.pem\", \"\", unknown},\n\t\t}},\n\t}\n\n\t// Define test functions for a single certificate and for a client\n\t// beforehand, so that our test loop in the end does not nest too deep.\n\n\tctx := log.TestContext(context.Background())\n\ttestSingle := func(t *testing.T, client *Client, cert, issuer string, status int) {\n\t\tt.Parallel()\n\n\t\tc, err := testCert(cert)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"failed to parse certificate:\", err)\n\t\t}\n\n\t\tvar i *x509.Certificate\n\t\tif len(issuer) > 0 {\n\t\t\tif i, err = testCert(issuer); err != nil {\n\t\t\t\tt.Fatal(\"failed to parse certificate issuer:\", err)\n\t\t\t}\n\t\t}\n\n\t\tresp, err := client.Check(ctx, c, i, nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"failed to check certificate status:\", err)\n\t\t}\n\n\t\tswitch {\n\t\tcase status == good && !resp.Good:\n\t\t\tfallthrough\n\t\tcase status == revoked && (resp.Good || resp.Unknown):\n\t\t\tfallthrough\n\t\tcase status == unknown && !resp.Unknown:\n\t\t\tt.Errorf(\"unexpected status, good: %t, reason: %d, unknown: %t\",\n\t\t\t\tresp.Good, resp.RevocationReason, resp.Unknown)\n\t\t}\n\t}\n\n\ttestClient := func(t *testing.T, url string, responders []string, certs []cert) {\n\t\tt.Parallel()\n\n\t\tvar pems []string\n\t\tfor _, r := range responders {\n\t\t\tpem, err := os.ReadFile(filepath.Join(\"testdata\", r))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to read responder certificate:\", err)\n\t\t\t}\n\t\t\tpems = append(pems, string(pem))\n\t\t}\n\n\t\tclient, err := New(&Conf{\n\t\t\tURL:        url,\n\t\t\tResponders: pems,\n\t\t\tRetry:      2,\n\t\t\tMaxSkew:    300,\n\t\t\tMaxAge:     1,\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(\"failed to create client:\", err)\n\t\t}\n\n\t\tfor _, cert := range certs {\n\t\t\tt.Run(cert.name, func(t *testing.T) {\n\t\t\t\ttestSingle(t, client, cert.name, cert.issuer, cert.status)\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.url, func(t *testing.T) {\n\t\t\ttestClient(t, test.url, test.responders, test.certs)\n\t\t})\n\t}\n}\n\nfunc TestCheckResponse(t *testing.T) {\n\tresponder, err := os.ReadFile(\n\t\tfilepath.Join(\"testdata\", demoSkEeOCSPResponder))\n\tif err != nil {\n\t\tt.Fatal(\"failed to read responder certificate:\", err)\n\t}\n\n\tclient, err := New(&Conf{\n\t\tResponders: []string{string(responder)}})\n\tif err != nil {\n\t\tt.Fatal(\"failed to create client:\", err)\n\t}\n\n\tcert, err := testCert(\"good.pem\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to parse certificate:\", err)\n\t}\n\n\tresp, err := os.ReadFile(\"testdata/test_response\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to read stored response:\", err)\n\t}\n\n\tnonce, err := os.ReadFile(\"testdata/test_nonce\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to read stored nonce:\", err)\n\t}\n\n\tstatus, err := client.CheckResponse(resp, cert, nil, nonce, time.Time{})\n\tif err != nil {\n\t\tt.Fatal(\"failed to check response:\", err)\n\t}\n\n\tswitch {\n\tcase status.Unknown:\n\t\tt.Error(\"certificate status unknown\")\n\tcase !status.Good:\n\t\tt.Error(\"certificate revoked, reason:\", status.RevocationReason)\n\t}\n}\n\nfunc testCert(cert string) (*x509.Certificate, error) {\n\tdata, err := os.ReadFile(filepath.Join(\"testdata\", cert))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cryptoutil.PEMCertificate(string(data))\n}\n"
  },
  {
    "path": "common/collector/ocsp/testdata/DEMO_of_KLASS3-SK_2016_SSL_OCSP_RESPONDER_2018.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFbTCCA1WgAwIBAgIQIaIsJ/GMLtFa6ZQy7hSXLjANBgkqhkiG9w0BAQsFADCB\njjELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNr\ndXMxITAfBgNVBAsMGFNlcnRpZml0c2VlcmltaXN0ZWVudXNlZDEXMBUGA1UEYQwO\nTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgS0xBU1MzLVNLIDIwMTYw\nHhcNMTgwNTAyMTAzMDMyWhcNMzAxMjE3MDUzMDMyWjByMTcwNQYDVQQDDC5ERU1P\nIG9mIEtMQVNTMy1TSyAyMDE2IFNTTCBPQ1NQIFJFU1BPTkRFUiAyMDE4MQ0wCwYD\nVQQLDARPQ1NQMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNVBAYT\nAkVFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9sMAp99uoSGfMeGw\nng8+P9DlX13iaR11afLQOgI2h+lx1D+3SJ0z7x+4Uw6LzEzdsUcieJU8v/wbV9k0\nYfRJg0Po7Gmb8/upv4FFvalG9G60r6+2tvA144nsqywQRMtypOxKsMbQBJ9xSDHG\nFTcD5v111N0I4SGx8hid94/NFXQnj/j6KZeZeZdFwGViE/ZE434k5ICac/IEIjPU\nv2Kds4P5dWAXFky+WiJWNtHt6NUjVSWXzZhsH2J0YZlK2DRGyZYy3zGGGFEY01Kx\nxor32e/429hc3Znkpt8ZrzyC6Uz2H8iOfT8YrQ3MBABPFsD50VeTuRkIK75LD31+\nw1pt0wIDAQABo4HhMIHeMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggr\nBgEFBQcDCTAdBgNVHQ4EFgQUeCs6VaH4k7g/gF5G0YNa4SKVr0UwHwYDVR0jBBgw\nFoAULhuPuwEvNPjaMASLXMHyXCXj2PcwVQYIKwYBBQUHAQEESTBHMEUGCCsGAQUF\nBzAChjlodHRwczovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0tMQVNTMy1T\nS18yMDEwLmRlci5jcnQwDAYDVR0TAQH/BAIwADAPBgkrBgEFBQcwAQUEAgUAMA0G\nCSqGSIb3DQEBCwUAA4ICAQBhoT+lKRrK2jBkza9kwLigtY3rkmvR+VgQqohmmEb3\nBn8p6zRSk71piUdf5GBbLXXghTjlqZqkcLfX1lhJ1l9/3chOXq8+L4XjE7V938N0\n3N6Qg8KjNLA6zTqdNWCj97UmPbF31E5/TpTnxBZEZ6fYv076saovvfpuGZtSCQdw\nDIO3MTCbSdrL0eCiOJOHzswWr8EPhdiRx6wsyxOEL6Y5qJMTWdokv5J8EtTSy4Rq\nNTn+SvItvXfMfL6ZpzqjkfYDJjxaFK0iXLv8akUPakQuTJm8rGxmCaOc3z46o7lH\nh1lUbpemtDzaJL/QI5jZcv6XZyXHxQgTNEgy/1KPaMV5QuQRl09ej8SooBtfaxER\nhMHnIRhy3S2TWelgOm2oJkuZKD1HnKYQP9kTUDeFD8CwZpx3sLYeiKbZObTblJPS\neTTn8RgSVSnCNWSMTpIoZIWXQ8svUei3kz40owtKQOJL7tdh48zL22WNVZjuKmzX\nrKJhm8DLdKbqd/PDF5nN5rfFqiwOV/IAJ0VN2/6FKLwd0BlJkcbdTY4CoRj9jJyc\nd+5vnbC4FqFVErAKtwt6oTbcZf6r2kqCujZ7seaGbWp1+8YXuRFPf1/WXicSOUdD\nhqYOyop6RM/6JVB1WAcmoLdQZknDnn+UtqjqLph5QoJmV2z0e5guh33u+XPTEQic\nzw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/ESTEID-SK_2011.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFBTCCA+2gAwIBAgIQKVKTqv2MxtRNgzCjwmRRDTANBgkqhkiG9w0BAQUFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMB4XDTExMDMxODEwMTQ1OVoXDTI0MDMxODEw\nMTQ1OVowZDELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2Vlcmlt\naXNrZXNrdXMxFzAVBgNVBAMMDkVTVEVJRC1TSyAyMDExMRgwFgYJKoZIhvcNAQkB\nFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCz6Xxs\nZh6r/aXcNe3kSpNMOqmQoAXUpzzcr4ZSaGZh/7JHIiplvNi6tbW/lK7sAiRsb65K\nzMWROEauld66ggbDPga6kU97C+AXGu7+DROXstjUOv6VlrHZVAnLmIOkycpWaxjM\n+EfQPZuDxEbkw96B3/fG69Zbp3s9y6WEhwU5Y9IiQl8YTkGnNUxidQbON1BGQm+H\nVEsgTf22J6r6G3FsE07rnMNskNC3DjuLSCUKF4kH0rVGVK9BdiCdFaZjHEykjwjI\nGzqnyxyRKe4YbJ6B9ABm95eSFgMBHtZEYU+q0VUIQGhAGAurOTXjWi1TssA42mnL\nGQZEI5GXMXtabp51AgMBAAGjggGgMIIBnDASBgNVHRMBAf8ECDAGAQH/AgEAMA4G\nA1UdDwEB/wQEAwIBBjCB9gYDVR0gBIHuMIHrMIHoBgsrBgEEAc4fZAEBATCB2DCB\nsgYIKwYBBQUHAgIwgaUegaIASwBhAHMAdQB0AGEAdABhAGsAcwBlACAAaQBzAGkA\nawB1AHQAdAD1AGUAbgBkAGEAdgBhAGwAZQAgAGQAbwBrAHUAbQBlAG4AZABpAGwA\nZQAgAGsAYQBuAHQAYQB2AGEAdABlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0AGkA\nZABlACAAdgDkAGwAagBhAHMAdABhAG0AaQBzAGUAawBzAC4wIQYIKwYBBQUHAgEW\nFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzAdBgNVHQ4EFgQUe2ryVVBcuNl6CIdBrvqi\nKz1bV3YwHwYDVR0jBBgwFoAUEvJaPupWHL/NBqzx8SXJqUvUFJkwPQYDVR0fBDYw\nNDAyoDCgLoYsaHR0cDovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3Jj\nYS5jcmwwDQYJKoZIhvcNAQEFBQADggEBAKC4IN3FC2gVDIH05TNMgFrQOCGSnXhz\noJclRLoQ81BCOXTZI4qn7N74FHEnrAy6uNG7SS5qANqSaPIL8dp63jg/L4qn4iWa\nB5q5GGJOV07SnTHS7gUrqChGClnUeHxiZbL13PkP37Lnc+TKl1SKfgtn5FbH5cqr\nhvbA/VF3Yzlimu+L7EVohW9HKxZ//z8kDn6ieiPFfZdTOov/0eXVLlxqklybUuS6\nLYRRDiqQupgBKQBTwNbC8x0UHX00HokW+dCVcQvsUbv4xLhRq/MvyTthE+RdbkrV\n0JuzbfZvADfj75nA3+ZAzFYS5ZpMOjZ9p4rQVKpzQTklrF0m6mkdcEo=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/ESTEID-SK_2015.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGcDCCBVigAwIBAgIQRUgJC4ec7yFWcqzT3mwbWzANBgkqhkiG9w0BAQwFADB1\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG\nCSqGSIb3DQEJARYJcGtpQHNrLmVlMCAXDTE1MTIxNzEyMzg0M1oYDzIwMzAxMjE3\nMjM1OTU5WjBjMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVy\naW1pc2tlc2t1czEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxFzAVBgNVBAMMDkVT\nVEVJRC1TSyAyMDE1MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0oH6\n1NDxbdW9k8nLA1qGaL4B7vydod2Ewp/STBZB3wEtIJCLdkpEsS8pXfFiRqwDVsgG\nGbu+Q99trlb5LI7yi7rIkRov5NftBdSNPSU5rAhYPQhvZZQgOwRaHa5Ey+BaLJHm\nLqYQS9hQvQsCYyws+xVvNFUpK0pGD64iycqdMuBl/nWq3fLuZppwBh0VFltm4nhr\n/1S0R9TRJpqFUGbGr4OK/DwebQ5PjhdS40gCUNwmC7fPQ4vIH+x+TCk2aG+u3MoA\nz0IrpVWqiwzG/vxreuPPAkgXeFCeYf6fXLsGz4WivsZFbph2pMjELu6sltlBXfAG\n3fGv43t91VXicyzR/eT5dsB+zFsW1sHV+1ONPr+qzgDxCH2cmuqoZNfIIq+buob3\neA8ee+XpJKJQr+1qGrmhggjvAhc7m6cU4x/QfxwRYhIVNhJf+sKVThkQhbJ9XxuK\nk3c18wymwL1mpDD0PIGJqlssMeiuJ4IzagFbgESGNDUd4icm0hQT8CmQeUm1GbWe\nBYseqPhMQX97QFBLXJLVy2SCyoAz7Bq1qA43++EcibN+yBc1nQs2Zoq8ck9MK0bC\nxDMeUkQUz6VeQGp69ImOQrsw46qTz0mtdQrMSbnkXCuLan5dPm284J9HmaqiYi6j\n6KLcZ2NkUnDQFesBVlMEm+fHa2iR6lnAFYZ06UECAwEAAaOCAgowggIGMB8GA1Ud\nIwQYMBaAFBLyWj7qVhy/zQas8fElyalL1BSZMB0GA1UdDgQWBBSzq4i8mdVipIUq\nCM20HXI7g3JHUTAOBgNVHQ8BAf8EBAMCAQYwdwYDVR0gBHAwbjAIBgYEAI96AQIw\nCQYHBACL7EABAjAwBgkrBgEEAc4fAQEwIzAhBggrBgEFBQcCARYVaHR0cHM6Ly93\nd3cuc2suZWUvQ1BTMAsGCSsGAQQBzh8BAjALBgkrBgEEAc4fAQMwCwYJKwYBBAHO\nHwEEMBIGA1UdEwEB/wQIMAYBAf8CAQAwQQYDVR0eBDowOKE2MASCAiIiMAqHCAAA\nAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMCcGA1Ud\nJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcDAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEE\ncDBuMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcC5zay5lZS9DQTBKBggrBgEFBQcw\nAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0cy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRy\nZV9Sb290X0NBLmRlci5jcnQwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL3d3dy5z\nay5lZS9yZXBvc2l0b3J5L2NybHMvZWVjY3JjYS5jcmwwDQYJKoZIhvcNAQEMBQAD\nggEBAHRWDGI3P00r2sOnlvLHKk9eE7X93eT+4e5TeaQsOpE5zQRUTtshxN8Bnx2T\noQ9rgi18q+MwXm2f0mrGakYYG0bix7ZgDQvCMD/kuRYmwLGdfsTXwh8KuL6uSHF+\nU/ZTss6qG7mxCHG9YvebkN5Yj/rYRvZ9/uJ9rieByxw4wo7b19p22PXkAkXP5y3+\nqK/Oet98lqwI97kJhiS2zxFYRk+dXbazmoVHnozYKmsZaSUvoYNNH19tpS7BLdsg\ni9KpbvQLb5ywIMq9ut3+b2Xvzq8yzmHMFtLIJ6Afu1jJpqD82BUAFcvi5vhnP8M7\nb974R18WCOpgNQvXDI+2/8ZINeU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/ID-PNOEE-39605244244.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDyzCCAyygAwIBAgIQGT09E9iu5GphNcYlbYhQ2zAKBggqhkjOPQQDBDBYMQsw\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\nDA5OVFJFRS0xMDc0NzAxMzETMBEGA1UEAwwKRVNURUlEMjAxODAeFw0yMTA5MDYw\nNzQxMjVaFw0yNjA5MDIyMTU5NTlaMHMxCzAJBgNVBAYTAkVFMSQwIgYDVQQDDBta\nSU5HRkVMRCxBUlRKT00sMzk2MDUyNDQyNDQxETAPBgNVBAQMCFpJTkdGRUxEMQ8w\nDQYDVQQqDAZBUlRKT00xGjAYBgNVBAUTEVBOT0VFLTM5NjA1MjQ0MjQ0MHYwEAYH\nKoZIzj0CAQYFK4EEACIDYgAEoEWsqF5Z3dF+B5Lah/cIIbS9aaEfFzgeJDe4xsV5\nsOyUGOkHdZCjxkfFiym3NB5R3tTxjTJxIz38IWqVvRUegIe73w5dRYC6iTEbFYl0\n5SuI2B5L/ZJumTh/biPHz+5Uo4IBnjCCAZowCQYDVR0TBAIwADAOBgNVHQ8BAf8E\nBAMCBkAwSAYDVR0gBEEwPzAyBgsrBgEEAYORIQEBATAjMCEGCCsGAQUFBwIBFhVo\ndHRwczovL3d3dy5zay5lZS9DUFMwCQYHBACL7EABAjAdBgNVHQ4EFgQU3aQyXrDK\nEg+Kr4ZnWjs6RpEJL4MwgYoGCCsGAQUFBwEDBH4wfDAIBgYEAI5GAQEwCAYGBACO\nRgEEMBMGBgQAjkYBBjAJBgcEAI5GAQYBMFEGBgQAjkYBBTBHMEUWP2h0dHBzOi8v\nc2suZWUvZW4vcmVwb3NpdG9yeS9jb25kaXRpb25zLWZvci11c2Utb2YtY2VydGlm\naWNhdGVzLxMCRU4wHwYDVR0jBBgwFoAU2axw219+vpT4oOS+R6LQNK2aKhIwZgYI\nKwYBBQUHAQEEWjBYMCcGCCsGAQUFBzABhhtodHRwOi8vYWlhLnNrLmVlL2VzdGVp\nZDIwMTgwLQYIKwYBBQUHMAKGIWh0dHA6Ly9jLnNrLmVlL2VzdGVpZDIwMTguZGVy\nLmNydDAKBggqhkjOPQQDBAOBjAAwgYgCQgDOEu5Gn0BbkZ6hK7CeJucVSN3zwYpc\n5zD4+Ynr5JiLLqhVqLUMSVCTvZZA44q5atLVbDWdPYIw7MkhCGf1Z8JEmwJCAT2V\nKdguGz8r+SsPoaBncloP8bX6SgUjuDD+oHbm1xEheZRfK1TnvOoANI96+RDpNIUn\nKFfGNm0erMRXWucLD5Zg\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/ID-PNOEE-41602290078.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIw\nEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0\nZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQy\nNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpL\nT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYD\nVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqG\nSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vC\ntEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2CO\nX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mn\nq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQE\nAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0E\nAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDl\nTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBS\nEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/MID-PNOEE-60001018800.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIF8TCCA9mgAwIBAgIQUsz4AdR7FcpcrHiyXaldkjANBgkqhkiG9w0BAQsFADBr\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgRVNU\nRUlELVNLIDIwMTUwHhcNMTkwNDA5MTA0OTIyWhcNMzAxMjExMjE1OTU5WjCBpTEL\nMAkGA1UEBhMCRUUxPTA7BgNVBAMMNE/igJlDT05ORcW9LcWgVVNMSUsgVEVTVE5V\nTUJFUixNQVJZIMOETk4sNjAwMDEwMTg4MDAxJzAlBgNVBAQMHk/igJlDT05ORcW9\nLcWgVVNMSUsgVEVTVE5VTUJFUjESMBAGA1UEKgwJTUFSWSDDhE5OMRowGAYDVQQF\nExFQTk9FRS02MDAwMTAxODgwMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEG8\nMxwPLh5qmCfkkAPMw+8nKf4cqDETMoWiFiVOGu3cdI61ARLdRQUfa9wpzFDQGtmK\nuScHrLE25ZPZWEozK72jggIfMIICGzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIG\nQDB1BgNVHSAEbjBsMF8GCisGAQQBzh8DAQMwUTAeBggrBgEFBQcCAjASDBBPbmx5\nIGZvciBURVNUSU5HMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBv\nc2l0b29yaXVtL0NQUzAJBgcEAIvsQAECMB0GA1UdDgQWBBRYUZMh7LjBd2OpIXrj\n0YnUK1hPJzCBigYIKwYBBQUHAQMEfjB8MAgGBgQAjkYBATAIBgYEAI5GAQQwUQYG\nBACORgEFMEcwRRY/aHR0cHM6Ly9zay5lZS9lbi9yZXBvc2l0b3J5L2NvbmRpdGlv\nbnMtZm9yLXVzZS1vZi1jZXJ0aWZpY2F0ZXMvEwJFTjATBgYEAI5GAQYwCQYHBACO\nRgEGATAfBgNVHSMEGDAWgBRJwPJEOWXVm0Y7DThgg7HWLSiGpjCBgwYIKwYBBQUH\nAQEEdzB1MCwGCCsGAQUFBzABhiBodHRwOi8vYWlhLmRlbW8uc2suZWUvZXN0ZWlk\nMjAxNTBFBggrBgEFBQcwAoY5aHR0cHM6Ly9zay5lZS91cGxvYWQvZmlsZXMvVEVT\nVF9vZl9FU1RFSUQtU0tfMjAxNS5kZXIuY3J0MDQGA1UdHwQtMCswKaAnoCWGI2h0\ndHBzOi8vYy5zay5lZS90ZXN0X2VzdGVpZDIwMTUuY3JsMA0GCSqGSIb3DQEBCwUA\nA4ICAQAWPebd0D8hssTj7Cdzp6zCFtHsZjmcgn21hLzsVDYqSde+/M7aLJ9WrCNl\nSvjldScWRXBBhwH7SmePxVvmi061fmlb2Mg22XCOqWOp+Eyt9LtTtHlSi21v5VNh\nnVX0RRPlCGseXHAyYjLtIGx744LCsZ/nblYtfAYrh2fJnYBOJddiUctKfb96M/sZ\nNFlfAXbejBpoi0a+wXmL8fTY/PNAWT2UNhsUWA3XgQpsca0TBb6m4rVc9VfnjH0v\nV9gaboH8jJL0M5bfPa4oE676Uw4YhtbRp2gXFdMKjb/5KpdAdfb5EhGSk8+rWZnW\nBgfsRfHw8YnrLO3sOhPYWlXdIkJ4TPsxK/StJVIlIye5UBESxR1J4wZ0iI4wvwQU\nTQ7xuIS89XgjlLm9/R7Qy86GN4lj6J0lD89dnFckduN/Hk5vMA+sGvPtIsV9q/fi\nWl6+SCYMmH6D+FpVpxYhq/VQoabwSWlhsgnjE+RddP6H3pWdICwJ6r7Iyx48SUc4\nj8/lh6dxg6NR3TPQSyPQg1bJjaNO3L79pgxevX3LFOJ0eLhLmY2gvT5MvjkOjULc\nXspKSjyNqyg8uJgT33SSBjArppWKMHpxybsaiY6X5jwdyO9qV32We94snptiqOrZ\naJ/LJ0RgOE7t7yBlDCdaUHohuUGB8+UD6efOBnP48L+FQz0D+w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/TEST_of_ESTEID-SK_2015.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGgzCCBWugAwIBAgIQEDb9gCZi4PdWc7IoNVIbsTANBgkqhkiG9w0BAQwFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTUxMjE4MDcxMzQ0WhgP\nMjAzMDEyMTcyMzU5NTlaMGsxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\naWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEfMB0G\nA1UEAwwWVEVTVCBvZiBFU1RFSUQtU0sgMjAxNTCCAiIwDQYJKoZIhvcNAQEBBQAD\nggIPADCCAgoCggIBAMTeAFvLxmAeaOsRKaf+hlkOhW+CdEilmUIKWs+qCWVq+w8E\n8PA/TohAZdUcO4KFXothmPDmfOCb0ExXcnOPCr2NndavzB39htlyYKYxkOkZi3pL\nz8bZg/HvpBoy8KIg0sYdbhVPYHf6i7fuJjDac4zN1vKdVQXA6Tv5wS/e90/ZyF95\n5vycxdNLticdozm5yCDMNgsEji6QNA1zIi3+C2YmnDXx6VyxhuC2R3q0xNkwtJ4e\nzs1RZGxWokTNPzQc3ilGhEJlVsS8vP624hUHwufQnwrKWpc3+D+plMIO0j3E+hmh\n46gIadDRweFR/dzb+CIBHRaFh0LEBjd/cDFQlBI+E8vpkhqeWp6rp1xwnhCL201M\n3E1E1Mw+51Xqj7WOfY0TzjOmQJy8WJPEwU2m44KxW1SnpeEBVkgb4XYFeQHAllc7\nJ7JDv50BoIPpecgaqn1vKR7l//wDsL0MN1tDlBhl3x7TJ/fwMnwB1E3zVZR74TUZ\nh5J49CAcFrfM4RmP/0hcDW8+4wNWMg2Qgst2qmPZmHCI/OJt5yMt0Ud5yPF8AWxV\not3TxOBGjMiM8m6WsksFsQxp5WtA0DANGXIIfydTaTV16Mg+KpYVqFKxkvFBmfVp\n6xApMaFl3dY/m56O9JHEqFpBDF+uDQIMjFJxJ4Pt7Mdk40zfL4PSw9Qco2T3AgMB\nAAGjggINMIICCTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNV\nHQ4EFgQUScDyRDll1ZtGOw04YIOx1i0ohqYwDgYDVR0PAQH/BAQDAgEGMGYGA1Ud\nIARfMF0wMQYKKwYBBAHOHwMBATAjMCEGCCsGAQUFBwIBFhVodHRwczovL3d3dy5z\nay5lZS9DUFMwDAYKKwYBBAHOHwMBAjAMBgorBgEEAc4fAwEDMAwGCisGAQQBzh8D\nAQQwEgYDVR0TAQH/BAgwBgEB/wIBADBBBgNVHR4EOjA4oTYwBIICIiIwCocIAAAA\nAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwJwYDVR0l\nBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMCBggrBgEFBQcDBDCBiQYIKwYBBQUHAQEE\nfTB7MCUGCCsGAQUFBzABhhlodHRwOi8vZGVtby5zay5lZS9jYV9vY3NwMFIGCCsG\nAQUFBzAChkZodHRwOi8vd3d3LnNrLmVlL2NlcnRzL1RFU1Rfb2ZfRUVfQ2VydGlm\naWNhdGlvbl9DZW50cmVfUm9vdF9DQS5kZXIuY3J0MEMGA1UdHwQ8MDowOKA2oDSG\nMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvcnkvY3Jscy90ZXN0X2VlY2NyY2Eu\nY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQDBOYTpbbQuoJKAmtDPpAomDd9mKZCarIPx\nAH8UXphSndMqOmIUA4oQMrLcZ6a0rMyCFR8x4NX7abc8T81cvgUAWjfNFn8+bi6+\nDgbjhYY+wZ010MHHdUo2xPajfog8cDWJPkmz+9PAdyjzhb1eYoEnm5D6o4hZQCiR\nyPnOKp7LZcpsVz1IFXsqP7M5WgHk0SqY1vs+Yhu7zWPSNYFIzNNXGoUtfKhhkHiR\nWFX/wdzr3fqeaQ3gs/PyD53YuJXRzFrktgJJoJWnHEYIhEwbai9+OeKr4L4kTkxv\nPKTyjjpLKcjUk0Y0cxg7BuzwevonyBtL72b/FVs6XsXJJqCa3W4T\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/TEST_of_KLASS3-SK_2016.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGyTCCBbGgAwIBAgIQWwxFeuMr159YRRcFxuRXvzANBgkqhkiG9w0BAQwFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMTYxMjA1MDcyODA1WhcN\nMzAxMjE3MDcyODA1WjCBjjELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxITAfBgNVBAsMGFNlcnRpZml0c2VlcmltaXN0ZWVu\ndXNlZDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2Yg\nS0xBU1MzLVNLIDIwMTYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDL\nBBWlTGtbsgjmRQHKjUz7xsc4BI5Lts/l9t7seXPK5OzZiXomPK2y8y8QxslfDI9K\nEA0X7VAF2UPwgEDpBLXAOcD1cbijMl23Gz815TH7Lfg+ZpHDh375F1m2vlEoWX0U\nG7FioRM+xAsBK3EaL3HpJkN8fCSzUwgP7tCl+ivmQwTA0dAoXCib/XtTrYjYa57v\nHG8xMzKZvc9B4pK9DLvBe+fHbkXHEh1y1EU4AX2VE+eIcieqSHh1PtsB6/YCoSHD\na8/uffWcrurbbl++QHgQC8yE6jTQyEfHcIB7ZCy1RbH4l4QtdmsuG7YpxDyBcjFj\ndz1h1a82GFA67ZHlZ5ogDn7xXyu2fbUBcJlj9wM2wxiyrUt6HCFky6CJhL60zEGV\nur4nWL/2KYedOxa4CowTv52ceGHBMuWcBHQK2egs5l8ti8oN5jLIhHe2k0dCYJa5\n9fOf6QQv7jKeA/yfY6CmW8BWtY8knmFSOnsqEIBSbWNsYMB1+cidxyr3sTd+haTI\nZs1JVhS1+gFAe4xVJYDMdnU+nqIvZvNwP9fANC+Nl3h8rfdHLYIexRg8+m2lsrgV\nT2QOf73EJ+JS10vUI2SinVZikxltxHkA96jDWzs1kpdjKuPdei7fI1roKBhKLyUA\n6n7tB2XEp0E5K5v4HNXWnY7+skuyvSZxMShNgSmegQIDAQABo4ICMTCCAi0wEgYD\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAcYwgfYGA1UdIASB7jCB6zCB\ngwYJKwYBBAHOHwcDMHYwIAYIKwYBBQUHAgEWFGh0dHA6Ly93d3cuc2suZWUvY3Bz\nMFIGCCsGAQUFBwICMEYeRABBAHMAdQB0AHUAcwBlACAAcwBlAHIAdABpAGYAaQBr\nAGEAYQB0AC4AIABDAG8AcgBwAG8AcgBhAHQAZQAgAEkARAAuMAgGBmeBDAECAjAv\nBgkrBgEEAc4fBwIwIjAgBggrBgEFBQcCARYUaHR0cDovL3d3dy5zay5lZS9jcHMw\nCAYGBACPegEBMAkGBwQAi+xAAQEwCAYGBACPegEHMAkGBwQAi+xAAQMwHQYDVR0O\nBBYEFC4bj7sBLzT42jAEi1zB8lwl49j3MB8GA1UdIwQYMBaAFLU0Cp2lLxDF5yEO\nvsSxZUcbA3b+MIGIBggrBgEFBQcBAQR8MHowIAYIKwYBBQUHMAGGFGh0dHA6Ly9v\nY3NwLnNrLmVlL0NBMFYGCCsGAQUFBzAChkpodHRwczovL3NrLmVlL3VwbG9hZC9m\naWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2VudHJlX1Jvb3RfQ0EuZGVy\nLmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwczovL3d3dy5zay5lZS9yZXBvc2l0\nb3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDANBgkqhkiG9w0BAQwFAAOCAQEAWwdr\nvtnroOs0VHJWGJTKBclIZhxqmMWmy0Wt/cmblAT4NV0mcWGKwGj4F2S1tUnsw9x4\nzbpPy0GX0knKVeC76MnwbgjjjjQDs9DPc+CmLz3RMGqiw1ZIXvvXnoUiaB9vH6Xe\nkm7McvIUTZtRjKIgFK6IzU2P5YGl5OMJ8TFhhtH0Jwo/0pyJqM9W0hmMdSMOHTwt\nqHnBQGjn0KQ5+zDtgVy6rt8bn44yuKXUIdYOFMrhK9nE/D8c7sOEbcDbr1n+rLFS\n0ov2xMcySIa99Rk5HaCsfvQAz5RQx38gJ9Hu/Ah9AVuMBfNyyrgYzRu93dc/+XX2\nh/Oe2s4LuJUDvm1aFw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\nMjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\nb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\nLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\ngOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\nr1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\nz00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\nOM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\nwB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\nRrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\nFIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\nAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\naQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\nJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\nGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\nczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\nBgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\naC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\nMHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\nnH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\nmawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\ndh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/demo-of-klass3-sk-2016-ssl-ocsp-responder-2018.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFbTCCA1WgAwIBAgIQIaIsJ/GMLtFa6ZQy7hSXLjANBgkqhkiG9w0BAQsFADCB\njjELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRpZml0c2VlcmltaXNrZXNr\ndXMxITAfBgNVBAsMGFNlcnRpZml0c2VlcmltaXN0ZWVudXNlZDEXMBUGA1UEYQwO\nTlRSRUUtMTA3NDcwMTMxHzAdBgNVBAMMFlRFU1Qgb2YgS0xBU1MzLVNLIDIwMTYw\nHhcNMTgwNTAyMTAzMDMyWhcNMzAxMjE3MDUzMDMyWjByMTcwNQYDVQQDDC5ERU1P\nIG9mIEtMQVNTMy1TSyAyMDE2IFNTTCBPQ1NQIFJFU1BPTkRFUiAyMDE4MQ0wCwYD\nVQQLDARPQ1NQMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNVBAYT\nAkVFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9sMAp99uoSGfMeGw\nng8+P9DlX13iaR11afLQOgI2h+lx1D+3SJ0z7x+4Uw6LzEzdsUcieJU8v/wbV9k0\nYfRJg0Po7Gmb8/upv4FFvalG9G60r6+2tvA144nsqywQRMtypOxKsMbQBJ9xSDHG\nFTcD5v111N0I4SGx8hid94/NFXQnj/j6KZeZeZdFwGViE/ZE434k5ICac/IEIjPU\nv2Kds4P5dWAXFky+WiJWNtHt6NUjVSWXzZhsH2J0YZlK2DRGyZYy3zGGGFEY01Kx\nxor32e/429hc3Znkpt8ZrzyC6Uz2H8iOfT8YrQ3MBABPFsD50VeTuRkIK75LD31+\nw1pt0wIDAQABo4HhMIHeMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggr\nBgEFBQcDCTAdBgNVHQ4EFgQUeCs6VaH4k7g/gF5G0YNa4SKVr0UwHwYDVR0jBBgw\nFoAULhuPuwEvNPjaMASLXMHyXCXj2PcwVQYIKwYBBQUHAQEESTBHMEUGCCsGAQUF\nBzAChjlodHRwczovL3NrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0tMQVNTMy1T\nS18yMDEwLmRlci5jcnQwDAYDVR0TAQH/BAIwADAPBgkrBgEFBQcwAQUEAgUAMA0G\nCSqGSIb3DQEBCwUAA4ICAQBhoT+lKRrK2jBkza9kwLigtY3rkmvR+VgQqohmmEb3\nBn8p6zRSk71piUdf5GBbLXXghTjlqZqkcLfX1lhJ1l9/3chOXq8+L4XjE7V938N0\n3N6Qg8KjNLA6zTqdNWCj97UmPbF31E5/TpTnxBZEZ6fYv076saovvfpuGZtSCQdw\nDIO3MTCbSdrL0eCiOJOHzswWr8EPhdiRx6wsyxOEL6Y5qJMTWdokv5J8EtTSy4Rq\nNTn+SvItvXfMfL6ZpzqjkfYDJjxaFK0iXLv8akUPakQuTJm8rGxmCaOc3z46o7lH\nh1lUbpemtDzaJL/QI5jZcv6XZyXHxQgTNEgy/1KPaMV5QuQRl09ej8SooBtfaxER\nhMHnIRhy3S2TWelgOm2oJkuZKD1HnKYQP9kTUDeFD8CwZpx3sLYeiKbZObTblJPS\neTTn8RgSVSnCNWSMTpIoZIWXQ8svUei3kz40owtKQOJL7tdh48zL22WNVZjuKmzX\nrKJhm8DLdKbqd/PDF5nN5rfFqiwOV/IAJ0VN2/6FKLwd0BlJkcbdTY4CoRj9jJyc\nd+5vnbC4FqFVErAKtwt6oTbcZf6r2kqCujZ7seaGbWp1+8YXuRFPf1/WXicSOUdD\nhqYOyop6RM/6JVB1WAcmoLdQZknDnn+UtqjqLph5QoJmV2z0e5guh33u+XPTEQic\nzw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/esteid2018.pem",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIFVzCCBLigAwIBAgIQdUf6rBR0S4tbo2bU/mZV7TAKBggqhkjOPQQDBDBaMQsw\r\nCQYDVQQGEwJFRTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMRcwFQYDVQRh\r\nDA5OVFJFRS0xMDc0NzAxMzEVMBMGA1UEAwwMRUUtR292Q0EyMDE4MB4XDTE4MDky\r\nMDA5MjIyOFoXDTMzMDkwNTA5MTEwM1owWDELMAkGA1UEBhMCRUUxGzAZBgNVBAoM\r\nElNLIElEIFNvbHV0aW9ucyBBUzEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxEzAR\r\nBgNVBAMMCkVTVEVJRDIwMTgwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAHHOBlv\r\n7UrRPYP1yHhOb7RA/YBDbtgynSVMqYdxnFrKHUXh6tFkghvHuA1k2DSom1hE5kqh\r\nB5VspDembwWDJBOQWQGOI/0t3EtccLYjeM7F9xOPdzUbZaIbpNRHpQgVBpFX0xpL\r\nTgW27MpIMhU8DHBWFpeAaNX3eUpD4gC5cvhsK0RFEqOCAx0wggMZMB8GA1UdIwQY\r\nMBaAFH4pVuc0knhOd+FvLjMqmHHB/TSfMB0GA1UdDgQWBBTZrHDbX36+lPig5L5H\r\notA0rZoqEjAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADCCAc0G\r\nA1UdIASCAcQwggHAMAgGBgQAj3oBAjAJBgcEAIvsQAECMDIGCysGAQQBg5EhAQEB\r\nMCMwIQYIKwYBBQUHAgEWFWh0dHBzOi8vd3d3LnNrLmVlL0NQUzANBgsrBgEEAYOR\r\nIQEBAjANBgsrBgEEAYORfwEBATANBgsrBgEEAYORIQEBBTANBgsrBgEEAYORIQEB\r\nBjANBgsrBgEEAYORIQEBBzANBgsrBgEEAYORIQEBAzANBgsrBgEEAYORIQEBBDAN\r\nBgsrBgEEAYORIQEBCDANBgsrBgEEAYORIQEBCTANBgsrBgEEAYORIQEBCjANBgsr\r\nBgEEAYORIQEBCzANBgsrBgEEAYORIQEBDDANBgsrBgEEAYORIQEBDTANBgsrBgEE\r\nAYORIQEBDjANBgsrBgEEAYORIQEBDzANBgsrBgEEAYORIQEBEDANBgsrBgEEAYOR\r\nIQEBETANBgsrBgEEAYORIQEBEjANBgsrBgEEAYORIQEBEzANBgsrBgEEAYORIQEB\r\nFDANBgsrBgEEAYORfwEBAjANBgsrBgEEAYORfwEBAzANBgsrBgEEAYORfwEBBDAN\r\nBgsrBgEEAYORfwEBBTANBgsrBgEEAYORfwEBBjAqBgNVHSUBAf8EIDAeBggrBgEF\r\nBQcDCQYIKwYBBQUHAwIGCCsGAQUFBwMEMGoGCCsGAQUFBwEBBF4wXDApBggrBgEF\r\nBQcwAYYdaHR0cDovL2FpYS5zay5lZS9lZS1nb3ZjYTIwMTgwLwYIKwYBBQUHMAKG\r\nI2h0dHA6Ly9jLnNrLmVlL0VFLUdvdkNBMjAxOC5kZXIuY3J0MBgGCCsGAQUFBwED\r\nBAwwCjAIBgYEAI5GAQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2Muc2suZWUv\r\nRUUtR292Q0EyMDE4LmNybDAKBggqhkjOPQQDBAOBjAAwgYgCQgDeuUY4HczUbFKS\r\n002HZ88gclgYdztHqglENyTMtXE6dMBRnCbgUmhBCAA0mJSHbyFJ8W9ikLiSyurm\r\nkJM0hDE9KgJCASOqA405Ia5nKjTJPNsHQlMi7KZsIcTHOoBccx+54N8ZX1MgBozJ\r\nmT59rZY/2/OeE163BAwD0UdUQAnMPP6+W3Vd\r\n-----END CERTIFICATE-----\r\n"
  },
  {
    "path": "common/collector/ocsp/testdata/good.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDZzCCAk+gAwIBAgIJAK3FSpU4C1Z7MA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNV\nBAYTAkVFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQKDARUZXN0MR0wGwYDVQQDDBRU\nZXN0LUNlcnQgR29vZCA0Nzc3MDAeFw0xNzA0MDQxMTQyMTVaFw00NDA4MjAxMTQy\nMTVaMEoxCzAJBgNVBAYTAkVFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQKDARUZXN0\nMR0wGwYDVQQDDBRUZXN0LUNlcnQgR29vZCA0Nzc3MDCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAMw84JS7flrdU1k87ACOpXPOu6cLInYagYm0nD/6cu2n\nwpJhKb8IwuIEO8rKCDnOmDLiKDkUHZ3vsySKHogdvC6r6fsisrhaGdFxBNIHe+TD\n1g5tsf17ieD8VsL+56yaTSPf7ILSu0GMAyeso1V8mVid4OGCaILoOHkDU1tJnB9L\n/tQD3AwlsOiRIAoZjPc+yMvgcaIuyPOj0ZOuJ/r/uhSBOtuS6h8ljoaZUwxrd8sb\nDZEWqyYHRlSb4VT/yj5YdUzrMcXOUqpsXc5w74mk4+odjUHitNSgxqkyK+ZKzuOY\nw9CwdcFnKhWCWef/YPqPHItJflJ2+FQbgHbY6SYDE6MCAwEAAaNQME4wHQYDVR0O\nBBYEFGD/qgOZG297my3Qcw5UYrii9bVLMB8GA1UdIwQYMBaAFGD/qgOZG297my3Q\ncw5UYrii9bVLMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKddsFl5\n3JuS4XC9dOnW7u/Nc6G9WfgXO51xt97qC0HLrxVmerLJ+Gp8Ay1cBb++pjfnf71z\nehtUGwKYGzJEmIt8Id8FKbXZYYzCd+ioTaDqJ4xbTYmr8AJEO4AOIPbrp0e9B42i\n3s9EkPwsKBEEQLg8BxmUJ+SH/chJ8U55ZxmuSuL4mCEYsGpv6WOShor31K0NrjOv\nf7Dc43jjlf0Q3oQ0UpKYEZ00oZ1UzJGtI0GHYOlP0GvB7qsRE6Txqp0zgfkhDAbm\nugRQge3xjfvQ+1TIjFnc6QLIUJMHb9opSixo0eTxUZkWZ0/yhvz7ZP4ykJdvfOgT\n1xzgE0AHMHOnf8c=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/revoked.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDZTCCAk2gAwIBAgIJAJFvY0CjI3roMA0GCSqGSIb3DQEBCwUAMEkxCzAJBgNV\nBAYTAkVFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQKDARUZXN0MRwwGgYDVQQDDBNU\nZXN0LUNlcnQgQmFkIDQ3NzcwMB4XDTE3MDQwNDExNDI0MFoXDTQ0MDgyMDExNDI0\nMFowSTELMAkGA1UEBhMCRUUxDTALBgNVBAgMBFRlc3QxDTALBgNVBAoMBFRlc3Qx\nHDAaBgNVBAMME1Rlc3QtQ2VydCBCYWQgNDc3NzAwggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQDTQfn4M0ruxHUUCwkXGaL40AFpg2xaGsmi3W/VgBtrscZS\nWXymkBFBjIoVBWs7SUG0Hb35Lk7gOeg5lEnjVf0J1KatqcsrqN8OOlxfk3uH3TUV\n4IoiUT/x+nh9VriEKRKv3uhp5rlIE1OvLsYzJBzsf8BIovJbf7dKxcWlSfOZ9Tyw\ntM+eWkciT21Q1n78OZmPvb4mbA1dXnVHbe1Ymfmf2SPlnLLHj/l8JqCSJCTWb+GC\nezKTnFnDO80Blqxdiwoys0ZJWkHLOU2QCCI9cIFJd1zs0KmJFgG7rXzmrU5L1E7l\niYGdOiJftTdPGz3aWmM4CZCu6gyYJMEQE43Z82UrAgMBAAGjUDBOMB0GA1UdDgQW\nBBQSmtZNdyheLBdHTQjMQQMSSasUizAfBgNVHSMEGDAWgBQSmtZNdyheLBdHTQjM\nQQMSSasUizAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBnOh5IwRfl\nbtQcWCLRFpw6IuRZxt+x+ZhCgLyW0TZo5fugqvHV7fBxt+1K8XmaalLBm9wzDHVs\nCFPiCxav7pSPFoBubIJ1mgUGnZjJfLdQgKZ2Q1a7Rvs0CnG42BQtlmxOQcy1xWkT\nMVWMhCXuoiuPUMHBeeZqytAxYN98bKZXsWY98RyQtqfPh0GmCXkN5KDwP9UmhUxC\n2KgjOZ6CLTL+gQqFCYGY2r7R3sxBrSy+FzTfZAH6FIOFIQzPfH2nJEEwL+70Gyth\nrKV5Wcq8kxvR5IkuKULKZp6VH6qWFnBmwUgXh3+KSb6VWipNJg7nB5OIPdTdbvxs\nmW0WUovwG7cu\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/testdata/test_nonce",
    "content": "\u0004\u0010\"y(}\u001e'\n\u0001-"
  },
  {
    "path": "common/collector/ocsp/testdata/unknown.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDbTCCAlWgAwIBAgIJALCK2VW0x/ibMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNV\nBAYTAkVFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQKDARUZXN0MSAwHgYDVQQDDBdU\nZXN0LUNlcnQgVW5rbm93biA0Nzc3MDAeFw0xNzA0MDQxMTQzMTFaFw00NDA4MjAx\nMTQzMTFaME0xCzAJBgNVBAYTAkVFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQKDARU\nZXN0MSAwHgYDVQQDDBdUZXN0LUNlcnQgVW5rbm93biA0Nzc3MDCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBALUVrRID0txo7PWy//yiyKzIsFHLxOQsd+AX\naBvZiVui0FCoTrcVMqWhboVyl1Ezp5ucjPQprCjzStU0GLRKjSHfZQRhMR9eZd8Q\nl3TYWyz+CF9qYkGUedDYFXyRYiOs58Nm/Ih9D2i36JDdBLyY7/V0DuJZbDPi5mT+\nyIsYqjZgzMPW554+DNYcNBlU93Rqt2xM7Byy7yrZ8bhyyMqKgPuMU3rgmP7aFL+f\nkum8N5mMWPVbJVGr8EbK3MB2ZJVzyAEa8Pg7kxYQKAb3BNSpyk4h4ZZdAGODNVRk\n9OaG4BoEhZpjXEQrZ0/YXfZ2JYeklX6ZPJXiS/6pQjuCX0j0N9MCAwEAAaNQME4w\nHQYDVR0OBBYEFGvvqkyq7MWP2NC7w5abMV8jqOG6MB8GA1UdIwQYMBaAFGvvqkyq\n7MWP2NC7w5abMV8jqOG6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\nAGTpxNdrpN3FUFsSyrtZhb4E5NoomleogTfmw6mKIYUS6WeOPAY8Yc3j/WOdQwfv\nj3cV9g1otiDWfrihi/g+w5EfqOyuKSHpdJn4pGXCTjqVqXv49yV/7ikdA8EmFWRD\nUSHl5K4+yx+gyCmdYeRX54dvVGVWoEIzroC1tE4I5hgKCjDZrD+f/lq+yeOlQY/J\n0hlxlLCbEtayaUi12R+NfAMTcgAweo0Hw2uLZQze3E9Gihf4DQFAu9zbsn0NAlJX\nns2d7+lpA8ektYWSuFppOjp1G3wEy72Aj7PyFxq9VQu9+Boc9KeV61lQpO1/Dd3B\nZEpuX5vTQuplQ4zjw8Rad4Q=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/ocsp/types.go",
    "content": "package ocsp\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha1\" //nolint:gosec // See certIDHash.\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n)\n\nvar (\n\t// Hash function used to calculate CertID fields.\n\t//\n\t// We would like to use a stronger hash function here, but at the\n\t// moment of writing, the SK OCSP responders used by Estonian IVXV\n\t// deployments do not support anything else (and give very unexpected\n\t// responses when used).\n\tcertIDHash = sha1.Sum\n\n\t// OID for certIDHash.\n\tcertIDHashOID = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}\n)\n\n// REQUEST\n\n// https://tools.ietf.org/html/rfc6960#section-4.1.1\ntype ocspRequest struct {\n\tTBSRequest tbsRequest\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.1.1\ntype tbsRequest struct {\n\tVersion           int `asn1:\"explicit,tag:0,optional,default:0\"`\n\tRequestList       []request\n\tRequestExtensions []pkix.Extension `asn1:\"explicit,tag:2,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.1.1\ntype request struct {\n\tReqCert certID\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.1.1\ntype certID struct {\n\tHashAlgorithm  pkix.AlgorithmIdentifier\n\tIssuerNameHash []byte\n\tIssuerKeyHash  []byte\n\tSerialNumber   *big.Int\n}\n\nfunc newCertID(cert *x509.Certificate) (id *certID, err error) {\n\t// Since certIDHash is SHA-1 then skip calculating the IssuerKeyHash\n\t// and simply use AuthorityKeyId.\n\tif len(cert.AuthorityKeyId) == 0 {\n\t\treturn nil, AuthKeyIDMissingError{Description: _OCSP_AUTHKID}\n\t}\n\n\tnameHash := certIDHash(cert.RawIssuer)\n\treturn &certID{\n\t\tHashAlgorithm: pkix.AlgorithmIdentifier{\n\t\t\tAlgorithm: certIDHashOID,\n\t\t},\n\t\tIssuerNameHash: nameHash[:],\n\t\tIssuerKeyHash:  cert.AuthorityKeyId,\n\t\tSerialNumber:   cert.SerialNumber,\n\t}, nil\n}\n\nfunc (c *certID) equal(other *certID) bool {\n\treturn cryptoutil.AlgorithmIdentifierCmp(c.HashAlgorithm, other.HashAlgorithm) &&\n\t\tbytes.Equal(c.IssuerNameHash, other.IssuerNameHash) &&\n\t\tbytes.Equal(c.IssuerKeyHash, other.IssuerKeyHash) &&\n\t\tc.SerialNumber.Cmp(other.SerialNumber) == 0\n}\n\n// RESPONSE\n\n// https://tools.ietf.org/html/rfc6960#section-4.2.1\ntype ocspResponse struct {\n\tResponseStatus asn1.Enumerated\n\tResponseBytes  responseBytes `asn1:\"explicit,tag:0,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.2.1\nconst ocspResponseStatusSuccessful = 0\n\n// https://tools.ietf.org/html/rfc6960#section-4.2.1\nvar ocspResponseStatus = map[int]string{\n\t0: \"successful\",\n\t1: \"malformedRequest\",\n\t2: \"internalError\",\n\t3: \"tryLater\",\n\t// 4: unused,\n\t5: \"sigRequired\",\n\t6: \"unauthorized\",\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.2.1\ntype responseBytes struct {\n\tResponseType asn1.ObjectIdentifier\n\tResponse     []byte\n}\n\n// https://tools.ietf.org/html/rfc6960#page-15\ntype basicOCSPResponse struct {\n\tRaw                asn1.RawContent\n\tTBSResponseData    responseData\n\tSignatureAlgorithm pkix.AlgorithmIdentifier\n\tSignature          asn1.BitString\n\tCerts              []asn1.RawValue `asn1:\"explicit,tag:0,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc6960#page-15\ntype responseData struct {\n\tRaw     asn1.RawContent\n\tVersion int `asn1:\"explicit,tag:0,optional,default:0\"`\n\n\t// The asn1 module does not support choices, so use 2 optional fields\n\t// instead of ResponderID.\n\tResponderIDByName pkix.RDNSequence `asn1:\"explicit,tag:1,optional\"`\n\tResponderIDByKey  []byte           `asn1:\"explicit,tag:2,optional\"`\n\n\tProducedAt         time.Time\n\tResponses          []singleResponse\n\tResponseExtensions []pkix.Extension `asn1:\"explicit,tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc6960#page-15\ntype singleResponse struct {\n\tCertID certID\n\n\t// The asn1 module does not support choices, so use 3 optional fields\n\t// instead of CertStatus.\n\tCertStatusGood    asn1.Flag   `asn1:\"tag:0,optional\"`\n\tCertStatusRevoked revokedInfo `asn1:\"tag:1,optional\"`\n\tCertStatusUnknown asn1.Flag   `asn1:\"tag:2,optional\"`\n\n\tThisUpdate       time.Time\n\tNextUpdate       time.Time        `asn1:\"explicit,tag:0,optional\"`\n\tSingleExtensions []pkix.Extension `asn1:\"explicit,tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc6960#page-15\ntype revokedInfo struct {\n\tRevocationTime   time.Time\n\tRevocationReason asn1.Enumerated `asn1:\"explicit,tag:0,optional\"`\n}\n\n// The following constants define the many possible reasons a certificate may\n// have been revoked. These constants can be used to check the revocation reason\n// returned inside the CertStatus structure\n// https://tools.ietf.org/html/rfc5280#section-5.3.1\nconst (\n\tReasonUnspecified = iota\n\tReasonKeyCompromise\n\tReasonCACompromise\n\tReasonAffiliationChanged\n\tReasonSuperseded\n\tReasonCessationOfOperation\n\tReasonCertificateHold\n\t_\n\tReasonRemoveFromCRL\n\tReasonPrivilegeWithdrawn\n\tReasonAACompromise\n)\n"
  },
  {
    "path": "common/collector/q11n/log_desc.go",
    "content": "package q11n\n\nconst (\n\t_Q11N_BAD           = \"Bad certificate\"\n\t_Q11N_NO            = \"'qualification:' section in election configuration is empty\"\n\t_Q11N_PROT          = \"Unknown qualification properties protocol\"\n\t_Q11N_Q             = \"Failed to configure qualification properties verifier\"\n\t_Q11N_CTIME         = \"Unknown qualification properties canonical time verifier\"\n\t_Q11N_PARSE_CTIME   = \"Failed to parse a canonical time of a qualification property\"\n\t_Q11N_TIME_CMP      = \"Unknown qualification properties' time comparator\"\n\t_Q11N_PARSE_TIME    = \"Failed to parse a time of a qualification property\"\n\t_Q11N_TIME_CMP_FAIL = \"Qualification time of previous protocol > qualification time of current protocol\"\n)\n"
  },
  {
    "path": "common/collector/q11n/ocsp/log_desc.go",
    "content": "package ocsp\n\nconst (\n\t_OCSP_CFG     = \"Reading YAMl configuration of OCSP client failed\"\n\t_OCPS_NEW     = \"Failed to create new OCSP client\"\n\t_OCSP_NO_SIG  = \"No signatures in a .bdoc container\"\n\t_OCSP_VERIFY  = \"Failed to verify OCSP response of a .bdoc container\"\n\t_OCSP_UNKNOWN = \"Unknown OCSP response status\"\n\t_OCSP_REVOKED = \"OCSP response status is 'revoked'\"\n)\n"
  },
  {
    "path": "common/collector/q11n/ocsp/ocsp.go",
    "content": "//nolint:fmt\n/*\nPackage ocsp contains qualification protocols which perform OCSP requests.\n\nocsp registers the following qualifiers:\n\n    - ocsp, which checks the status of the signing certificate using OCSP\n            (there must be exactly one signature on the signed container) and\n            returns the OCSP response as the qualifying property, and\n*/\npackage ocsp\n\nimport (\n\t\"context\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/ocsp\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tq11n.Register(q11n.OCSP, newreg(), ocsp.ExtractProducedAtTimeFromRawOcspResponse)\n}\n\ntype client struct {\n\tocsp *ocsp.Client\n}\n\nfunc newreg() func(yaml.Node, string) (q11n.Qualifier, error) {\n\treturn func(n yaml.Node, _ string) (q q11n.Qualifier, err error) {\n\t\tvar conf ocsp.Conf\n\t\tif err = yaml.Apply(n, &conf); err != nil {\n\t\t\treturn nil, YAMLApplyError{Err: err,\n\t\t\t\tDescription: _OCSP_CFG}\n\t\t}\n\n\t\tc := new(client)\n\t\tc.ocsp, err = ocsp.New(&conf)\n\t\tif err != nil {\n\t\t\treturn nil, ClientError{Err: err,\n\t\t\t\tDescription: _OCPS_NEW}\n\t\t}\n\n\t\treturn c, nil\n\t}\n}\n\nfunc (c *client) Qualify(ctx context.Context, container container.Container) ([]byte, error) {\n\tsigs := container.Signatures()\n\tif len(sigs) != 1 {\n\t\treturn nil, NoSingleSignatureError{Count: len(sigs),\n\t\t\tDescription: _OCSP_NO_SIG}\n\t}\n\tcert := sigs[0].Signer\n\tissuer := sigs[0].Issuer\n\n\tvar nonce []byte\n\n\tstatus, err := c.ocsp.Check(ctx, cert, issuer, nonce)\n\tif err != nil {\n\t\treturn nil, CheckOCSPError{Err: err,\n\t\t\tDescription: _OCSP_VERIFY}\n\t}\n\tif !status.Good {\n\t\tif status.Unknown {\n\t\t\treturn nil, q11n.BadCertificateStatusError{\n\t\t\t\tErr: StatusUnknownError{Description: _OCSP_UNKNOWN},\n\t\t\t}\n\t\t}\n\t\treturn nil, q11n.BadCertificateStatusError{\n\t\t\tErr: RevokedError{Reason: status.RevocationReason, Description: _OCSP_REVOKED},\n\t\t}\n\t}\n\treturn status.RawResponse, nil\n}\n\n// SignatureValuer returns the raw signature value of the signature with the\n// given ID.\ntype SignatureValuer interface {\n\tSignatureValue(id string) ([]byte, error)\n}\n"
  },
  {
    "path": "common/collector/q11n/q11n.go",
    "content": "/*\nPackage q11n provides common code for requesting qualifying properties for\nsignature containers.\n\nQualifying properties can be certificate statuses, timemarks and -stamps,\nregistration confirmations, etc.\n*/\npackage q11n\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// Protocol identifies a signature container qualification protocol. The actual\n// qualifier implementations are in other packages.\ntype Protocol string\n\n// Enumeration of qualification protocols.\nconst (\n\tOCSP   Protocol = \"ocsp\"\n\tTSP    Protocol = \"tsp\"\n\tTSPREG Protocol = \"tspreg\"\n)\n\n// CanonicalOrder is the order of qualification protocols that is used for\n// determining the canonical time of a set of qualifying properties. It lists\n// protocols with properties that embed a qualification time in descending\n// order of priority.\nvar CanonicalOrder = []Protocol{TSPREG, TSP, OCSP}\n\n// Qualifier is used for requesting qualifying properties for signature\n// containers.\ntype Qualifier interface {\n\t// Qualify requests a qualifying property for a signed container and\n\t// returns it encoded as a byte slice.\n\t//\n\t// Implementations are free to require a specific container type, e.g.,\n\t// for querying extra properties not available through the Container\n\t// interface, but must check for it themselves.\n\t//\n\t// Implementations must obey cancellation signals from ctx.Done().\n\tQualify(context.Context, container.Container) ([]byte, error)\n}\n\n// Here we \"declare\" error types, but instead of defining them ourselves, we\n// want them to be generated so that they implement all the extra interfaces of\n// generated errors.\n//\n// Although this is an error, it still nests an error to uniquely specify where\n// the nesting error came from. So you would use these like\n//\n//\treturn q11n.BadCertificateStatusError{Err: UnderlyingError{}}\n//\n// where UnderlyingError will specify the package that returned the error.\nvar (\n\t// BadCertificateStatusError wraps errors which are caused by a\n\t// container signing certificate with bad status, e.g., revoked. This\n\t// can be returned by any qualifier which checks the status of the\n\t// signing certificate.\n\t_ = BadCertificateStatusError{Err: nil, Description: _Q11N_BAD}\n\n\t// NoPreconfiguredQualifiersError raises when \"qualification:\" section in\n\t// election.yml is empty\n\t_ = NoPreconfiguredQualifiersError{Description: _Q11N_NO}\n)\n\n// NewFunc is the type of functions that create a signature container qualifier\n// with the specified configuration and service directory. The latter can be\n// used to pass private keys and other sensitive information to the qualifier.\ntype NewFunc func(yaml.Node, string) (Qualifier, error)\n\n// ParseTimeFunc is the type of functions that parse a qualifying property and\n// return the embedded qualification time. A ParseTimeFunc only parses\n// qualifying properties of a specific qualification protocol.\ntype ParseTimeFunc func([]byte) (time.Time, error)\n\ntype regentry struct {\n\tnewQualifier NewFunc\n\tparseTime    ParseTimeFunc\n}\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = make(map[Protocol]regentry)\n)\n\n// Register registers a signature container qualifier implementation. It is\n// intended to be called from init functions of packages that implement\n// qualifiers.\n//\n// newFunc is a constructor function used to create qualifiers with a specified\n// configuration. parseTimeFunc is a time parsing function for reading the\n// qualification times of properties issued by qualifers. If the protocol does\n// not support such a function, then parseTimeFunc must be nil.\nfunc Register(p Protocol, newFunc NewFunc, parseTimeFunc ParseTimeFunc) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\tregistry[p] = regentry{newQualifier: newFunc, parseTime: parseTimeFunc}\n}\n\n// Conf is the qualifier set configuration. It contains an ordered list of\n// qualifier protocols to use and their configurations. The latter is an\n// unspecified YAML Node, which will be applied to the corresponding qualifier\n// protocol's configuration structure.\ntype Conf []struct {\n\tProtocol Protocol\n\tConf     yaml.Node\n}\n\n// Qualifiers is a list of qualifiers in the same order they were presented in\n// the configuration. This is also the order in which the qualification\n// requests should be made.\ntype Qualifiers []struct {\n\tProtocol  Protocol\n\tQualifier Qualifier\n}\n\n// Configure configures a list of qualifier implementations specified in the\n// configuration. sensitive is the path to the service instance directory which\n// can contain sensitive information, e.g., request signing keys.\nfunc Configure(c Conf, sensitive string) (qs Qualifiers, err error) {\n\tqs = make(Qualifiers, len(c))\n\n\t// For each configured implementation, ...\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tfor i, p := range c {\n\t\t// ...check if it is linked ...\n\t\tentry, ok := registry[p.Protocol]\n\t\tif !ok {\n\t\t\treturn nil, UnlinkedProtocolError{Protocol: p.Protocol,\n\t\t\t\tDescription: _Q11N_PROT}\n\t\t}\n\t\tqs[i].Protocol = p.Protocol\n\n\t\t// ...and if creating the qualifier succeeds.\n\t\tqs[i].Qualifier, err = entry.newQualifier(p.Conf, sensitive)\n\t\tif err != nil {\n\t\t\treturn nil, ConfigureProtocolError{Protocol: p.Protocol, Err: err,\n\t\t\t\tDescription: _Q11N_Q}\n\t\t}\n\t}\n\treturn\n}\n\n// Properties is a map from qualifier protocols to qualifying properties. It is\n// a convenience type to be used outside of q11n to store the results of\n// qualification.\ntype Properties map[Protocol][]byte\n\n// CanonicalTime returns the canonical qualification time of a set of\n// qualifying properties. The canonical time is determined by checking for\n// properties in CanonicalOrder and returning the qualification time of the\n// first property that is present. If none of the properties are present, then\n// the zero time is returned instead.\nfunc CanonicalTime(properties Properties) (time.Time, error) {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tfor _, protocol := range CanonicalOrder {\n\t\tproperty, ok := properties[protocol]\n\t\tif !ok {\n\t\t\tcontinue // Check next protocol.\n\t\t}\n\n\t\tentry, ok := registry[protocol]\n\t\tif !ok {\n\t\t\treturn time.Time{}, CanonicalTimeUnlinkedProtocolError{\n\t\t\t\tProtocol: protocol, Description: _Q11N_CTIME}\n\t\t}\n\t\tif entry.parseTime == nil {\n\t\t\tpanic(protocol + \" in CanonicalOrder without ParseTimeFunc\")\n\t\t}\n\n\t\tctime, err := entry.parseTime(property)\n\t\tif err != nil {\n\t\t\treturn time.Time{}, CanonicalTimeParseError{Protocol: protocol, Err: err,\n\t\t\t\tDescription: _Q11N_PARSE_CTIME}\n\t\t}\n\t\treturn ctime, nil\n\t}\n\treturn time.Time{}, nil // No canonical time protocol in properties.\n}\n\n// CompareQualificationTimes loops over all qualifiers, preconfigured via election.yml,\n// and compares that each previous properties[qualifier.Protocol] time is <= subsequent\n// properties[qualifier.Protocol] time.\n//\n// For example qualifers={tspreg, ocsp}, then ensures that tspreg.Time <= ocsp.Time.\n//\n// Another example qualifers={tspreg, tsp, ocsp}, then ensures that tspreg.Time <= tsp.Time,\n// tsp.Time <= ocsp.Time.\nfunc CompareQualificationTimes(qualifiers Qualifiers, properties Properties) error {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\n\t// Default value is 0001-01-01 00:00:00 +0000 UTC\n\tvar qualificationTime time.Time\n\n\tvar qualificationProtocol Protocol\n\n\t// Order of qualifiers does matter and is configurable in election.yml's\n\t// section \"qualification:\", i.e\n\t// qualification:\n\t//   - protocol: tspreg\n\t//     ...\n\t//   - protocol: ocsp\n\t//     ...\n\t// Means that first qualifier is tspreg\n\tfor _, qualifier := range qualifiers {\n\n\t\tentry, ok := registry[qualifier.Protocol]\n\t\tif !ok {\n\t\t\treturn CompareQualificationTimesNoRegistryForProtocolError{\n\t\t\t\tProtocol:    qualifier.Protocol,\n\t\t\t\tDescription: _Q11N_TIME_CMP}\n\t\t}\n\t\tif entry.parseTime == nil {\n\t\t\tpanic(qualifier.Protocol + \" in CanonicalOrder without ParseTimeFunc\")\n\t\t}\n\n\t\tctime, err := entry.parseTime(properties[qualifier.Protocol])\n\t\tif err != nil {\n\t\t\treturn CompareQualificationTimesCannotParseQualificationTimeError{\n\t\t\t\tProtocol:    qualifier.Protocol,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _Q11N_PARSE_TIME}\n\t\t}\n\n\t\t// Ensure that previous qualification time is <= ctime\n\t\tif qualificationTime.Before(ctime) || qualificationTime.Equal(ctime) {\n\t\t\tqualificationTime = ctime\n\t\t\tqualificationProtocol = qualifier.Protocol\n\t\t\tcontinue\n\t\t}\n\t\treturn CompareQualificationTimesQualificationTimeError{\n\t\t\tCurrentProtocol:                   qualifier.Protocol,\n\t\t\tCurrentProtocolQualificationTime:  ctime,\n\t\t\tPreviousProtocol:                  qualificationProtocol,\n\t\t\tPreviousProtocolQualificationTime: qualificationTime,\n\t\t\tDescription:                       _Q11N_TIME_CMP_FAIL}\n\t}\n\n\t// \"qualification:\" in election.yml is empty\n\tif qualificationProtocol == \"\" {\n\t\tvar noQualifiersErr NoPreconfiguredQualifiersError\n\t\treturn noQualifiersErr\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/q11n/tsp/log_desc.go",
    "content": "package tsp\n\nconst (\n\t_TSA_CFG           = \"Reading YAMl configuration of OCSP client failed\"\n\t_TSA_NEW           = \"Failed to create new OCSP client\"\n\t_TSA_KEY           = \"Failed to read TSA client private key from file system\"\n\t_TSA_KEY_PARSE     = \"Failed to parse PEM TSA client private key\"\n\t_TSA_KEY_PARSE_RSA = \"Failed to parse RSA TSA client private key\"\n\t_TSA_NO_SIG        = \"No signatures in a .bdoc container\"\n\t_TSA_DATAER        = \".bdoc container cannot be casted to TimestampDataer interface\"\n\t_TSA_SIG_VERIFY    = \"Failed to verify .bdoc signature timestamp\"\n\t_TSA_SIG_TS        = \"Failed to sign .bdoc signature timestamp\"\n\t_TSA_REQ           = \"Failed to create client timestamp request to TSA provider\"\n\t_TSA_RSA_SIG       = \"Failed to sign data using RSA algorithm\"\n\t_TSA_VERIFY        = \"Failed to verify OCSP response of a .bdoc container\"\n\t_TSA_UNKNOWN       = \"Unknown OCSP response status\"\n\t_TSA_REVOKED       = \"OCSP response status is 'revoked'\"\n)\n"
  },
  {
    "path": "common/collector/q11n/tsp/tsp.go",
    "content": "/*\nPackage tsp contains qualification protocols which perform PKIX timestamping\nrequests.\n\ntsp registers the following qualifiers:\n\n  - tsp, which requests a timestamp on the signature (there must be exactly\n    one signature on the signed container and containers must implement\n    TimestampDataer) and returns the timestamp token as the qualifying\n    property, and\n\n  - tspreg, which does the same as tsp, but uses a signature on the message\n    imprint of the request as the nonce. This is an ad-hoc solution\n    for signing timestamp protocol requests so that they can be used\n    as registration requests. The signing key is a PEM-encoded RSA\n    private key in the service directory with the name \"tspreg.key\".\n*/\npackage tsp\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/tsp\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tq11n.Register(q11n.TSP, newreg(false), tsp.ParseTime)\n\tq11n.Register(q11n.TSPREG, newreg(true), tsp.ParseTime)\n}\n\ntype client struct {\n\ttsp *tsp.Client\n\tkey *rsa.PrivateKey\n}\n\nfunc newreg(reg bool) func(yaml.Node, string) (q11n.Qualifier, error) {\n\treturn func(n yaml.Node, sensitive string) (q q11n.Qualifier, err error) {\n\t\tvar conf tsp.Conf\n\t\tif err = yaml.Apply(n, &conf); err != nil {\n\t\t\treturn nil, YamlApplyError{Err: err,\n\t\t\t\tDescription: _TSA_CFG}\n\t\t}\n\n\t\tc := new(client)\n\t\tif c.tsp, err = tsp.New(&conf); err != nil {\n\t\t\treturn nil, ClientError{Err: err,\n\t\t\t\tDescription: _TSA_NEW}\n\t\t}\n\n\t\tif reg {\n\t\t\tpem, err := os.ReadFile(filepath.Join(sensitive, \"tspreg.key\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, ReadPrivateKeyError{Err: err,\n\t\t\t\t\tDescription: _TSA_KEY}\n\t\t\t}\n\t\t\tder, err := cryptoutil.PEMDecode(string(pem), \"RSA PRIVATE KEY\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, DecodePrivateKeyError{Err: err,\n\t\t\t\t\tDescription: _TSA_KEY_PARSE}\n\t\t\t}\n\t\t\tif c.key, err = x509.ParsePKCS1PrivateKey(der); err != nil {\n\t\t\t\treturn nil, ParsePrivateKeyError{Err: err,\n\t\t\t\t\tDescription: _TSA_KEY_PARSE_RSA}\n\t\t\t}\n\t\t}\n\n\t\treturn c, nil\n\t}\n}\n\nfunc (c *client) Qualify(ctx context.Context, container container.Container) ([]byte, error) {\n\tsigs := container.Signatures()\n\tif len(sigs) != 1 {\n\t\treturn nil, NoSingleSignatureError{Count: len(sigs),\n\t\t\tDescription: _TSA_NO_SIG}\n\t}\n\tid := sigs[0].ID\n\n\tdataer, ok := container.(TimestampDataer)\n\tif !ok {\n\t\treturn nil, ContainerNotTimestampDataerError{\n\t\t\tDescription: _TSA_DATAER,\n\t\t}\n\t}\n\tdata, err := dataer.TimestampData(id)\n\tif err != nil {\n\t\treturn nil, TimestampDataError{ID: id, Err: err,\n\t\t\tDescription: _TSA_SIG_VERIFY}\n\t}\n\n\tvar nonce []byte\n\tif c.key != nil {\n\t\tnonce, err = c.sign(data)\n\t\tif err != nil {\n\t\t\treturn nil, SignTimestampDataError{Err: err,\n\t\t\t\tDescription: _TSA_SIG_TS}\n\t\t}\n\t}\n\n\tresp, err := c.tsp.Create(ctx, data, nonce)\n\tif err != nil {\n\t\treturn nil, CreateTimestampError{Err: err,\n\t\t\tDescription: _TSA_REQ}\n\t}\n\treturn resp, nil\n}\n\nfunc (c *client) sign(data []byte) (signature []byte, err error) {\n\t// The hash function used here must match the one used in ivxv.ee/common/collector/tsp\n\t// to create the message imprint.\n\thash := sha256.Sum256(data)\n\tsignature, err = rsa.SignPKCS1v15(nil, c.key, crypto.SHA256, hash[:])\n\tif err != nil {\n\t\treturn nil, SignHashError{Err: err,\n\t\t\tDescription: _TSA_RSA_SIG}\n\t}\n\n\treturn asn1.Marshal(struct {\n\t\tSignatureAlgorithm pkix.AlgorithmIdentifier\n\t\tSignature          []byte\n\t}{\n\t\tpkix.AlgorithmIdentifier{\n\t\t\t// sha256WithRSAEncryption\n\t\t\tAlgorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11},\n\t\t},\n\t\tsignature,\n\t})\n}\n\n// TimestampDataer returns the data to be timestamped for the signature with\n// the given ID.\ntype TimestampDataer interface {\n\tTimestampData(id string) ([]byte, error)\n}\n"
  },
  {
    "path": "common/collector/safereader/example_test.go",
    "content": "package safereader_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\n// ParseBigEndian parses a big-endian encoded unsigned integer from r. Returns\n// an error if r has more bytes than an uint64 can hold.\nfunc ParseBigEndian(r io.Reader) (u uint64, err error) {\n\tb, err := io.ReadAll(safereader.New(r, 8))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, o := range b {\n\t\tu = u<<8 | uint64(o)\n\t}\n\treturn\n}\n\nfunc Example() {\n\tu, err := ParseBigEndian(bytes.NewReader([]byte{0xde, 0xad, 0xbe, 0xef}))\n\tif err != nil {\n\t\tfmt.Println(\"error parsing 0xdeadbeef:\", err)\n\t\treturn\n\t}\n\tfmt.Println(u)\n\n\t_, err = ParseBigEndian(bytes.NewReader(make([]byte, 9)))\n\tif err != nil {\n\t\tfmt.Println(\"error parsing nine bytes:\", err)\n\t\treturn\n\t}\n\n\t// Output:\n\t// 3735928559\n\t// error parsing nine bytes: ivxv.ee/common/collector/safereader.LimitExceededError{Limit:8, }\n}\n"
  },
  {
    "path": "common/collector/safereader/log_desc.go",
    "content": "package safereader\n\nconst (\n\t_SAFEREADER_LIMIT = \"Reading more data from a stream that has been specified\"\n)\n"
  },
  {
    "path": "common/collector/safereader/reader.go",
    "content": "/*\nPackage safereader implements safely reading from streams up to a maximum size.\n*/\npackage safereader\n\nimport (\n\t\"io\"\n)\n\n// LimitExceededError is returned when reading from a stream with more data\n// than the specified limit.\nvar _ = LimitExceededError{Limit: 0, Description: _SAFEREADER_LIMIT}\n\ntype safereader struct {\n\tr io.Reader\n\tn int64\n\n\t// The LimitExceededError returned from Read will be identical for an\n\t// instance of safereader so create it once and reuse.\n\terr LimitExceededError\n}\n\n// New returns a Reader that reads from r but returns a LimitExceededError\n// after n bytes are read. This behavior is extremely similar to\n// io.LimitedReader which does the same but returns io.EOF. With safereader it\n// is possible to distinguish if there were more bytes in r or it returned\n// io.EOF after exactly n bytes.\n//\n// Due to the implementation, if more than n bytes are requested, the returned\n// Reader reads n+1 bytes from r. Callers must keep this in mind if they wish\n// to continue using r after having depleted the returned Reader.\nfunc New(r io.Reader, n int64) io.Reader {\n\ts := &safereader{r: r, n: n}\n\ts.err.Limit = n\n\treturn s\n}\n\nfunc (s *safereader) Read(p []byte) (n int, err error) {\n\tif s.n < 0 {\n\t\t// Limit was exceeded by previous calls to Read.\n\t\treturn 0, s.err\n\t}\n\n\t// If more than n bytes are requested, then attempt to read one extra\n\t// byte. This is allowed by io.Reader which says that implementations\n\t// can use all of p as scratch space.\n\tif int64(len(p)) > s.n {\n\t\tp = p[:s.n+1]\n\t}\n\tn, err = s.r.Read(p)\n\n\tif int64(n) > s.n {\n\t\t// Limit has been exceeded.\n\t\tn = int(s.n) // Only report s.n bytes as safe to use. Downcast is safe since n > s.n.\n\t\terr = s.err  // Replace underlying error with a LimitExceededError.\n\t\ts.n = -1     // Mark n as exceeded.\n\t\treturn\n\t}\n\n\ts.n -= int64(n)\n\treturn\n}\n"
  },
  {
    "path": "common/collector/safereader/reader_test.go",
    "content": "package safereader\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\t\"testing/iotest\"\n\t\"testing/quick\"\n)\n\ntype infinite struct{}\n\nfunc (i *infinite) Read(p []byte) (n int, err error) {\n\treturn len(p), nil\n}\n\nfunc TestRead(t *testing.T) {\n\t// testfn attempts to read bytes from an io.Reader of length wrapped in\n\t// a safereader with limit.\n\ttestfn := func(t *testing.T, read, length, limit int64) {\n\t\tinput := io.LimitReader(new(infinite), length)\n\t\tlimited := New(input, limit)\n\t\tn, err := io.CopyN(io.Discard, limited, read)\n\n\t\t// The number of bytes read must be equal to the smallest value\n\t\t// of read, length and limit.\n\t\texpn := read\n\t\tif length < expn {\n\t\t\texpn = length\n\t\t}\n\t\tif limit < expn {\n\t\t\texpn = limit\n\t\t}\n\t\tif n != expn {\n\t\t\tt.Error(\"expected to read\", expn, \"bytes, but got\", n)\n\t\t}\n\n\t\tswitch {\n\t\tcase read <= limit && read <= length:\n\t\t\t// If at most limit bytes were read and there was\n\t\t\t// enough data available, then err must be nil.\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"unexpected error:\", err)\n\t\t\t}\n\t\tcase read > length && length <= limit:\n\t\t\t// If more than length bytes were read, but less than\n\t\t\t// limit, then err must be io.EOF.\n\t\t\tif err != io.EOF {\n\t\t\t\tt.Error(\"unexpected error, want EOF:\", err)\n\t\t\t}\n\t\tcase read > limit && length > limit:\n\t\t\t// If more than limit bytes were both available and\n\t\t\t// read, then err must be a LimitExceededError.\n\t\t\tif err != limited.(*safereader).err {\n\t\t\t\tt.Error(\"unexpected error, want LimitExceededError:\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\tt.Error(\"unhandled test case\")\n\t\t}\n\t}\n\n\t// First run some manual test cases.\n\ttests := []struct {\n\t\tread   int64 // Number of bytes to read.\n\t\tlength int64 // Length of input stream.\n\t\tlimit  int64 // Safereader limit.\n\t}{\n\t\t{0, 0, 0},    // Do nothing.\n\t\t{8, 16, 32},  // Read less than length and limit.\n\t\t{16, 16, 32}, // Read all, length less than limit.\n\t\t{16, 16, 16}, // Read all, length equal to limit.\n\t\t{32, 32, 16}, // Read all, length more than limit.\n\t\t{16, 32, 16}, // Read exactly limit, more data available.\n\t\t{32, 16, 16}, // Read more than length, length exactly limit.\n\t}\n\n\tfor _, test := range tests {\n\t\tname := fmt.Sprintf(\"%d of %d with limit %d\", test.read, test.length, test.limit)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttestfn(t, test.read, test.length, test.limit)\n\t\t})\n\t}\n\n\t// Use testing/quick to run random black-box tests. Hopefully covers\n\t// any cases unhandled by the manual tests.\n\tt.Run(\"quick\", func(t *testing.T) {\n\t\t// Use uint8 in testfn wrapper to keep values generated by\n\t\t// quick.Check small and non-negative.\n\t\tif err := quick.Check(func(read, length, limit uint8) bool {\n\t\t\ttestfn(t, int64(read), int64(length), int64(limit))\n\t\t\treturn !t.Failed()\n\t\t}, nil); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t})\n}\n\nfunc TestReadError(t *testing.T) {\n\t// Ad-hoc solution for a reader which returns an error. The first read\n\t// will return a single byte and the second one ErrTimeout.\n\tr := iotest.TimeoutReader(iotest.OneByteReader(new(infinite)))\n\n\t// Create safereader with limit 1 and ensure that the underlying error\n\t// does not get replaced with a LimitExceededError.\n\tif _, err := io.Copy(io.Discard, New(r, 1)); err != iotest.ErrTimeout {\n\t\tt.Errorf(\"unexpected Read error: got %v, want %v\", err, iotest.ErrTimeout)\n\t}\n}\n"
  },
  {
    "path": "common/collector/scripts/Makefile",
    "content": "BIN := ivxv-admin-helper.sh ivxv-admin-sudo.sh\n\ninstall:\n\tmkdir -p $(DESTDIR)/usr/bin && cp $(BIN) $(DESTDIR)/usr/bin\n"
  },
  {
    "path": "common/collector/scripts/ivxv-admin-helper.sh",
    "content": "#!/bin/sh\n\n# IVXV Internet voting framework\n\n# Helper script for ivxv-admin account\n\n# Usage: ivxv-admin-helper <action> [<arg> ...]\n\nset -e\n\n\nusage() {\n  echo \"Usage:\"\n  echo \"    ivxv-admin-helper check-service-config <service-type> <service-id>\"\n  echo \"        Check service configuration\"\n  echo\n  echo \"    ivxv-admin-helper copy-logs-to-logmon <hostname> <logmonitor-address>\"\n  echo \"        Copy IVXV service log files to Log Monitor\"\n  echo\n  echo \"    ivxv-admin-helper restart-service <service-type> <service-id>\"\n  echo \"                                      <systemctl-service-id>\"\n  echo \"        Restart service\"\n}\n\n# ACTIONS {{{\n\n# check_config {{{\n# Check service configuration\ncheck_config() {\n  SERVICE_TYPE=\"$1\"\n  SERVICE_ID=\"$2\"\n\n  # check service config\n  test -e /etc/default/ivxv && . /etc/default/ivxv\n  \"/usr/bin/ivxv-${SERVICE_TYPE}\" ${EXTRAOPTS} -instance \"${SERVICE_ID}\" -check input\n}\n# }}}\n# copy_logs_to_logmon {{{\n# Copy IVXV service log files to Log Monitor\ncopy_logs_to_logmon() {\n  HOST_NAME=\"$1\"\n  LOGMON_ADDR=\"$2\"\n  LOGFILE_PATTERN='ivxv-????-??-??-??.log'\n\n  cd /var/log/ivxv\n\n  # check if log file exists\n  if [ \"$(echo ${LOGFILE_PATTERN})\" = \"${LOGFILE_PATTERN}\" ]; then\n    echo \"Log files not found in pattern ${LOGFILE_PATTERN}\"\n    exit 1\n  fi\n\n  # copy log files to Log Monitor\n  for SRC_FILE in ${LOGFILE_PATTERN}\n  do\n    TGT_FILE=\"$(\n      echo ${SRC_FILE} |\n        sed --regexp-extended --expression=\"s/^ivxv-(.+).log/${HOST_NAME}-\\1-ivxv.log/\"\n    )\"\n\n    rsync ${SRC_FILE} ${LOGMON_ADDR}:/var/log/ivxv/${TGT_FILE}\n  done\n}\n# }}}\n# restart_service {{{\n# Restart service\nrestart_service() {\n  SERVICE_TYPE=\"$1\"\n  SERVICE_ID=\"$2\"\n  SYSTEMCTL_SERVICE_ID=\"$3\"\n\n  ivxv-admin-helper check-service-config \"${SERVICE_TYPE}\" \"${SERVICE_ID}\"\n  systemctl restart --user \"${SYSTEMCTL_SERVICE_ID}\"\n}\n# }}}\n\n# }}}\n\n# parse CLI arguments {{{\nACTION=\"$1\"\ncase \"${ACTION}\" in\n  check-service-config)\n    check_config \"$2\" \"$3\"\n  ;;\n  copy-logs-to-logmon)\n    copy_logs_to_logmon \"$2\" \"$3\"\n  ;;\n  restart-service)\n    restart_service \"$2\" \"$3\" \"$4\"\n  ;;\n  --help)\n    usage\n    exit 0\n  ;;\n  *)\n    echo \"Unknown action: ${ACTION}\"\n    echo\n    usage\n    exit 1\n  ;;\nesac\n# }}}\n\nexit 0\n\n# vim:foldmethod=marker:\n"
  },
  {
    "path": "common/collector/scripts/ivxv-admin-sudo.sh",
    "content": "#!/bin/sh\n\n# IVXV Internet voting framework\n\n# Helper script for ivxv-admin account\n# for actions that require privilege escalation\n\n# Usage: ivxv-admin-sudo <action> [<arg> ...]\n\nset -e\n\nusage() {\n  echo \"Usage:\"\n  echo \"    ivxv-admin-sudo backup-ballot-box <voting-host> <service-id> \"\n  echo \"                                      <backup-filename>\"\n  echo \"        Backup ballot box (in backup service)\"\n  echo\n  echo \"    ivxv-admin-sudo backup-log <log-host> <backup-timestamp>\"\n  echo \"        Backup log file (in backup service)\"\n  echo\n  echo \"    ivxv-admin-sudo create-ssh-access <account-name>\"\n  echo \"        Create management service access to account in service host\"\n  echo\n  echo \"    ivxv-admin-sudo init-host\"\n  echo \"        Initialize service host\"\n  echo\n  echo \"    ivxv-admin-sudo init-service <service-id>\"\n  echo \"        Initialize service data directory. \"\n  echo \"        Value 'backup' is used for all backup services\"\n  echo\n  echo \"    ivxv-admin-sudo install-pkg <package-filename>\"\n  echo \"        Install IVXV package with dependencies\"\n  echo\n  echo \"    ivxv-admin-sudo prepare-ballot-box-backup <service-id> <backup-filename>\"\n  echo \"        Prepare votes backup file in voting service\"\n  echo\n  echo \"    ivxv-admin-sudo remove-admin-root-access\"\n  echo \"        Remove management service access to service host root account\"\n  echo\n  echo \"    ivxv-admin-sudo rsyslog-config-apply\"\n  echo \"        Apply rsyslog config file for IVXV logging\"\n}\n\n# ACTIONS {{{\n\n# backup_ballot_box {{{\n# Backup current ballot box\nbackup_ballot_box() {\n  VOTING_HOST=\"$1\"\n  SERVICE_ID=\"$2\"\n  BACKUP_FILENAME=\"$3\"\n  BACKUP_DIR=\"/var/backups/ivxv/ballot-box\"\n  echo \"# Preparing ballot box backup file in voting service ${SERVICE_ID}\"\n  ssh \"ivxv-voting@${VOTING_HOST}\" ivxv-admin-sudo prepare-ballot-box-backup \"${SERVICE_ID}\" \"${BACKUP_FILENAME}\"\n  echo \"# Copying backup file ${BACKUP_FILENAME} to backup service\"\n  scp \"ivxv-voting@${VOTING_HOST}\":\"${BACKUP_FILENAME}\" \"${BACKUP_DIR}/tmp-${BACKUP_FILENAME}\"\n  mv \"${BACKUP_DIR}/tmp-${BACKUP_FILENAME}\" \"${BACKUP_DIR}/${BACKUP_FILENAME}\"\n  echo \"# Removing backup file ${BACKUP_FILENAME} from voting service\"\n  ssh \"${VOTING_HOST}\" rm -f \"${BACKUP_FILENAME}\"\n}\n# }}}\n\n# backup_log {{{\n# Backup log\nbackup_log() {\n  LOG_HOST=\"$1\"\n  BACKUP_TIMESTAMP=\"$2\"\n  BACKUP_TARGET_FILEPATH=\"/var/backups/ivxv/log/${LOG_HOST}-${BACKUP_TIMESTAMP}.tar.gz\"\n  echo \"# Copying log files from log collector to backup service\"\n  RET=0\n  (\n    ssh ${LOG_HOST} \\\n      tar --create --gzip --verbose '/var/log/ivxv/ivxv*.log' \\\n      > \"${BACKUP_TARGET_FILEPATH}\"\n  ) || RET=$?\n  # ignore exit code 1 (file in grown during archiving process)\n  if [ \"${RET}\" = 1 ]; then\n    RET=0\n  fi\n  return \"${RET}\"\n}\n# }}}\n\n# create_ssh_access {{{\ncreate_ssh_access() {\n  ACCOUNT_NAME=\"$1\"\n  echo \"# Validating account '${ACCOUNT_NAME}'\"\n  echo \"${ACCOUNT_NAME}\" | grep --quiet --extended-regexp \"^(ivxv-|haproxy)\"\n\n  echo \"# Installing SSH authorized_keys file for '${ACCOUNT_NAME}'\"\n  ACCOUNT_HOME=\"$(getent passwd \"${ACCOUNT_NAME}\" | cut -d: -f 6)\"\n  test -d \"${ACCOUNT_HOME}/.ssh\" ||\n    mkdir --verbose \"${ACCOUNT_HOME}/.ssh\"\n  chown --changes \"${ACCOUNT_NAME}:ivxv\" \"${ACCOUNT_HOME}/.ssh\"\n  chmod --changes 700 \"${ACCOUNT_HOME}/.ssh\"\n\n  touch \"${ACCOUNT_HOME}/.ssh/authorized_keys\"\n  chown --changes \"${ACCOUNT_NAME}:ivxv\" \"${ACCOUNT_HOME}/.ssh/authorized_keys\"\n  chmod --changes 600 \"${ACCOUNT_HOME}/.ssh/authorized_keys\"\n  grep ivxv-admin-account ~ivxv-admin/.ssh/authorized_keys \\\n    > \"${ACCOUNT_HOME}/.ssh/authorized_keys\"\n}\n# }}}\n\n# init_host {{{\n# initialize service host\ninit_host() {\n  echo \"# Removing IVXV service packages\"\n  dpkg --purge \\\n    ivxv-choices \\\n    ivxv-log \\\n    ivxv-mid \\\n    ivxv-proxy \\\n    ivxv-sessionstatus \\\n    ivxv-smartid \\\n    ivxv-storage \\\n    ivxv-verification \\\n    ivxv-votesorder \\\n    ivxv-voting \\\n    ivxv-webeid\n  echo \"# Removing IVXV config files\"\n  rm --force --verbose \\\n    /etc/ivxv/choices.bdoc \\\n    /etc/ivxv/election.bdoc \\\n    /etc/ivxv/technical.bdoc \\\n    /etc/ivxv/trust.bdoc \\\n    /etc/ivxv/voters-??.bdoc\n\n  RSYSLOG_CFG_FILE=\"/etc/rsyslog.d/ivxv-logging.conf\"\n  if [ -e \"${RSYSLOG_CFG_FILE}\" ]; then\n    echo \"# Removing rsyslog config file\"\n    rm -fv \"${RSYSLOG_CFG_FILE}\"\n    echo \"# Restarting rsyslog service\"\n    systemctl restart rsyslog\n  fi\n}\n# }}}\n\n# init_service {{{\n# initialize service data directory\ninit_service() {\n  SERVICE_ID=\"$1\"\n  if [ \"${SERVICE_ID}\" = backup ]; then\n    echo \"# Removing backup service files\"\n    rm --recursive --force --verbose /var/backups/ivxv/management-conf/*\n    rm --recursive --force --verbose /var/backups/ivxv/log/*\n    rm --recursive --force --verbose /var/backups/ivxv/ballot-box/*\n  else\n    echo \"# Removing service directory\"\n    rm --recursive --force --verbose \"/var/lib/ivxv/service/${SERVICE_ID}\"\n  fi\n}\n# }}}\n\n# install_package {{{\n# install debian package dependencies\ninstall_package() {\n  # only extract file name, i.e. \"../file\" will resolve to \"file\", or \"/var/lib/file\" to, similarly, \"file\"\n  DEB_FILENAME=$(basename \"$1\")\n\n  # operate in dumb terminal\n  TERM=dumb\n  export TERM\n  DEBIAN_FRONTEND=noninteractive\n  export DEBIAN_FRONTEND\n\n  echo \"# Performing dpkg database audit\"\n  dpkg --audit\n\n  # install dependencies\n  DEB_FILEPATH=\"/etc/ivxv/debs/${DEB_FILENAME}\"\n  DEPS_ORIG=\"$(dpkg -I \"${DEB_FILEPATH}\" | grep 'Depends:' | cut -d: -f2)\"\n  echo \"# Installing package ${DEB_FILENAME} dependencies\"\n  DEPS=\"$(echo \"${DEPS_ORIG}\" | sed --regexp-extended --expression='s/\\([^\\)]+\\)//g' --expression='s/,//g')\"\n  apt-get --yes install ${DEPS}\n\n  # install package\n  echo \"# Installing package ${DEB_FILENAME}\"\n  dpkg -i \"${DEB_FILEPATH}\"\n}\n# }}}\n\n# prepare_ballot_box_backup {{{\n# Prepare votes backup file\nprepare_ballot_box_backup() {\n  SERVICE_ID=\"$1\"\n  BACKUP_FILENAME=\"$2\"\n  echo \"# Creating ballot box backup file ${BACKUP_FILENAME}\"\n  rm -fv \"${BACKUP_FILENAME}\"\n  RETVAL=\"\"\n  ivxv-voteexp -instance \"${SERVICE_ID}\" \"${BACKUP_FILENAME}\" || RETVAL=\"$?\"\n  if [ \"${RETVAL}\" = 2 ]; then\n    echo \"NOTE: The ivxv-voteexp utility exited with non-fatal errors\"\n  elif [ \"${RETVAL}\" ]; then\n    echo \"ERROR: ivxv-voteexp utility exited with error code ${RETVAL}\"\n    exit \"${RETVAL}\"\n  fi\n}\n# }}}\n\n# remove_admin_root_access {{{\n# remove management service access to service host root account\nremove_admin_root_access() {\n  echo \"# Removing ivxv-admin key from root SSH authorized_keys file\"\n  sed --in-place \\\n      --regexp-extended \\\n      --expression=\"s/^(ssh-rsa .+ ivxv-admin-account)\\$/# \\\\1/\" \\\n      /root/.ssh/authorized_keys\n}\n# }}}\n\n# rsyslog_config_apply {{{\n# apply IVXV logging config to rsyslog daemon\nrsyslog_config_apply() {\n  echo \"# Installing IVXV logging config for rsyslog\"\n  cp --verbose /etc/ivxv/ivxv-logging.conf /etc/rsyslog.d/ivxv-logging.conf\n  chmod --changes 644 /etc/rsyslog.d/ivxv-logging.conf\n  chown --changes root:root /etc/rsyslog.d/ivxv-logging.conf\n  echo \"# Restarting rsyslog service\"\n  systemctl restart rsyslog\n}\n# rsyslog_config_apply }}}\n\n# }}}\n\n# parse CLI arguments {{{\nACTION=\"$1\"\ncase \"${ACTION}\" in\n  backup-ballot-box)\n    backup_ballot_box \"$2\" \"$3\" \"$4\"\n  ;;\n  backup-log)\n    backup_log \"$2\" \"$3\"\n  ;;\n  create-ssh-access)\n    create_ssh_access \"$2\"\n  ;;\n  init-host)\n    init_host\n  ;;\n  init-service)\n    init_service \"$2\"\n  ;;\n  install-pkg)\n    install_package \"$2\"\n  ;;\n  prepare-ballot-box-backup)\n    prepare_ballot_box_backup \"$2\" \"$3\"\n  ;;\n  remove-admin-root-access)\n    remove_admin_root_access\n  ;;\n  rsyslog-config-apply)\n    rsyslog_config_apply\n  ;;\n  --help)\n    usage\n    exit 0\n  ;;\n  *)\n    echo \"Unknown action: ${ACTION}\"\n    echo\n    usage\n    exit 1\n  ;;\nesac\n# }}}\n\nexit 0\n\n# vim:foldmethod=marker:\n"
  },
  {
    "path": "common/collector/server/.gitignore",
    "content": "tlsciphersuites.go\n"
  },
  {
    "path": "common/collector/server/codec.go",
    "content": "package server\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/rpc\"\n\t\"net/rpc/jsonrpc\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\n// wrappedConn wraps net.Conn so its Read method can be replaced with another\n// io.Reader.\ntype wrappedConn struct {\n\tnet.Conn\n\tr io.Reader\n}\n\nfunc (w wrappedConn) Read(p []byte) (n int, err error) {\n\treturn w.r.Read(p)\n}\n\n// serverCodec is a rpc.ServerCodec which wraps a jsonrpc codec with additional\n// behavior:\n//\n//   - sets read and write deadlines,\n//   - sets a maximum request size limit (if configured),\n//   - logs requests to the request log (if configured),\n//   - checks the size of all request fields tagged with \"size\",\n//   - injects a context into the header of the request,\n//   - applies filters to the header of the request, and\n//   - sets the filtered header of the request as the header of the response.\n//\n//nolint:fmt\ntype serverCodec struct {\n\trpc.ServerCodec // Inner jsonrpc server codec.\n\n\tconn    wrappedConn // Wrapped connection served by this codec.\n\ttimeout time.Duration\n\tlogreq  bool\n\n\theader  *Header // Server header of the request and response.\n\tfilters headerFilters\n}\n\nfunc newCodec(ctx context.Context, conf *CodecConf, conn net.Conn,\n\tfilters headerFilters) *serverCodec {\n\n\tcodec := &serverCodec{\n\t\tconn:    wrappedConn{Conn: conn, r: conn},\n\t\ttimeout: time.Duration(conf.RWTimeout) * time.Second,\n\t\tlogreq:  conf.LogRequests,\n\t\theader:  &Header{Ctx: ctx},\n\t\tfilters: filters,\n\t}\n\tif conf.RequestSize > 0 {\n\t\tcodec.conn.r = safereader.New(conn, conf.RequestSize)\n\t}\n\t// Must be reference so that changes to codec.conn affect inner codec.\n\tcodec.ServerCodec = jsonrpc.NewServerCodec(&codec.conn)\n\treturn codec\n}\n\nvar errIgnored = errors.New(\"ignored\")\n\nvar buffers = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}\n\n// ReadRequestHeader sets a read deadline for the connection and then calls\n// ReadRequestHeader of the underlying codec, logging the request if\n// configured.\n//\n// Note: err will be ignored by the codecFilter, so any error information must\n// be logged by ReadRequestHeader. err will not be sent to the client. However,\n// a non-nil error must still be returned to stop processing the request any\n// further.\nfunc (s *serverCodec) ReadRequestHeader(req *rpc.Request) error {\n\tif s.logreq {\n\t\ttee := buffers.Get().(*bytes.Buffer)\n\t\tdefer buffers.Put(tee)\n\t\tdefer tee.Reset()\n\t\ts.conn.r = io.TeeReader(s.conn.r, tee)\n\t\tdefer func() {\n\t\t\tif err := log.Request(s.header.Ctx, tee.Bytes()); err != nil {\n\t\t\t\tlog.Error(s.header.Ctx, LogRequestError{Err: log.Alert(err),\n\t\t\t\t\tDescription: _SERVER_LOGGER})\n\t\t\t\t// Do not block handling of request.\n\t\t\t}\n\t\t}()\n\t}\n\n\tif err := s.conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil {\n\t\tlog.Error(s.header.Ctx, SetReadDeadlineError{Err: log.Alert(err),\n\t\t\tDescription: _SERVER_SET_TIMEOUT})\n\t\treturn errIgnored\n\t}\n\tif err := s.ServerCodec.ReadRequestHeader(req); err != nil {\n\t\tlog.Error(s.header.Ctx, ReadJSONRequestError{Err: err,\n\t\t\tDescription: _SERVER_HEADER})\n\t\treturn errIgnored\n\t}\n\treturn nil\n}\n\n// ReadRequestBody calls ReadRequestBody of the underlying codec, checks the\n// size of all of the request's fields, injects the connection context into the\n// embedded server.Header field, and passes the header through filters.\n// ReadRequestBody will panic if x does not satisfy the header interface: this\n// is by design to avoid developers forgetting to embed the server header in\n// request types.\n//\n// No timeout is required for ReadRequestBody since with jsonrpc the whole\n// request was actually read in ReadRequestHeader.\n//\n// Note: err will be ignored by the codecFilter and sent to the client by the\n// rpc package, so it should be as generic as possible. Any error information\n// should be logged by ReadRequestBody.\nfunc (s *serverCodec) ReadRequestBody(x interface{}) error {\n\tif err := s.ServerCodec.ReadRequestBody(x); err != nil {\n\t\tlog.Error(s.header.Ctx, UnmarshalRequestParamsError{\n\t\t\tRequestType: reflect.TypeOf(x),\n\t\t\tErr:         err,\n\t\t\tDescription: _SERVER_BODY,\n\t\t})\n\t\treturn ErrBadRequest\n\t}\n\n\t// ReadRequestBody may be called with a nil argument to discard the\n\t// body if a correct header was read, but bad method was requested.\n\tif x == nil {\n\t\treturn nil\n\t}\n\n\tif err := checkSize(reflect.ValueOf(x)); err != nil {\n\t\tlog.Error(s.header.Ctx, RequestSizeError{\n\t\t\tRequestType: reflect.TypeOf(x),\n\t\t\tErr:         err,\n\t\t\tDescription: _SERVER_REQ_SIZE,\n\t\t})\n\t\treturn ErrBadRequest\n\t}\n\n\t// Inject the context into x's header, pass the latter through filters,\n\t// and store it for the response. The filters already log any relevant\n\t// error information and return a generic error, so just pass it\n\t// through without doing anything.\n\th := x.(header).header()\n\th.Ctx = s.header.Ctx\n\terr := s.filters.next(h)\n\ts.header = h\n\treturn err\n}\n\n// checkSize walks over v looking for struct fields of type string or []byte\n// and with the tag `size:\"<int>\"`. For each matching field, the field value's\n// length is checked to be lesser than or equal to <int>.\nfunc checkSize(v reflect.Value) error {\n\tswitch v.Kind() {\n\tcase reflect.Struct:\n\t\tfor i := 0; i < v.NumField(); i++ {\n\t\t\tt := v.Type().Field(i)\n\t\t\tf := v.Field(i)\n\n\t\tfield:\n\t\t\tvar n int\n\t\t\tswitch t.Type.Kind() {\n\t\t\tcase reflect.String:\n\t\t\t\tn = f.Len()\n\t\t\tcase reflect.Slice:\n\t\t\t\tif t.Type.Elem().Kind() != reflect.Uint8 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tn = f.Len()\n\t\t\tcase reflect.Struct:\n\t\t\t\tif err := checkSize(f); err != nil {\n\t\t\t\t\treturn NestedFieldSizeError{Field: t.Name, Err: err,\n\t\t\t\t\t\tDescription: _SERVER_REQ_FIELD_STRUCT_SIZE}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\tcase reflect.Ptr, reflect.Interface:\n\t\t\t\tif !f.IsNil() {\n\t\t\t\t\tt.Type = t.Type.Elem()\n\t\t\t\t\tf = f.Elem()\n\t\t\t\t\tgoto field\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif tag := t.Tag.Get(\"size\"); len(tag) > 0 {\n\t\t\t\tmax, err := strconv.ParseUint(tag, 10, 64) //nolint:revive\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"invalid %s field size: %v\", t.Name, err))\n\t\t\t\t}\n\t\t\t\tif uint64(n) > max { //nolint:gosec\n\t\t\t\t\treturn FieldSizeError{\n\t\t\t\t\t\tField:       t.Name,\n\t\t\t\t\t\tSize:        n,\n\t\t\t\t\t\tMax:         max,\n\t\t\t\t\t\tDescription: _SERVER_REQ_FIELD_SIZE,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase reflect.Ptr, reflect.Interface:\n\t\tif !v.IsNil() {\n\t\t\treturn checkSize(v.Elem())\n\t\t}\n\t}\n\treturn nil\n}\n\n// WriteResponse injects the request's server header into the response, sets a\n// write deadline for the connection, and then calls WriteResponse of the\n// underlying codec. WriteResponse will panic if x does not satisfy the header\n// interface: this is by design to avoid developers forgetting to embed the\n// server header in response types.\n//\n// Note: err will be ignored by the codecFilter and will not be sent to the\n// client (as writing failed): it is meant for debugging the rpc package. Any\n// errors from reading or processing the request were already logged by other\n// components. Therefore WriteResponse is left with logging any errors related\n// to writing or generated by the rpc package.\nfunc (s *serverCodec) WriteResponse(resp *rpc.Response, x interface{}) error {\n\t// Log any rpc package errors and replace with generic ones.\n\tif strings.HasPrefix(resp.Error, \"rpc: \") {\n\t\tlog.Error(s.header.Ctx, RPCMethodError{ErrString: resp.Error,\n\t\t\tDescription: _SERVER_RESP_ERR})\n\t\tresp.Error = ErrBadRequest.Error()\n\t}\n\n\t// Set the header of the response unless we are returning an error.\n\tif len(resp.Error) == 0 {\n\t\t*x.(header).header() = *s.header\n\t}\n\n\tif err := s.conn.SetWriteDeadline(time.Now().Add(s.timeout)); err != nil {\n\t\tlog.Error(s.header.Ctx, SetWriteDeadlineError{Err: log.Alert(err),\n\t\t\tDescription: _SERVER_SET_RESP_TIMEOUT})\n\t\treturn errIgnored\n\t}\n\tif err := s.ServerCodec.WriteResponse(resp, x); err != nil {\n\t\tlog.Error(s.header.Ctx, WriteResponseError{Err: err,\n\t\t\tDescription: _SERVER_RESP})\n\t\treturn errIgnored\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/server/codec_test.go",
    "content": "package server\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nfunc TestCheckSize(t *testing.T) {\n\ttype Nested struct {\n\t\tString string `size:\"6\"`\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tx        interface{}\n\t\texpected error\n\t}{\n\t\t{\"simple\", struct {\n\t\t\tString  string `size:\"6\"`\n\t\t\tBytes   []byte `size:\"4\"`\n\t\t\tIgnored int64  `size:\"1\"`\n\t\t}{\n\t\t\t\"foobar\",\n\t\t\t[]byte{0xde, 0xad, 0xbe, 0xef},\n\t\t\t256,\n\t\t}, nil},\n\n\t\t{\"nested\", struct {\n\t\t\tNested Nested\n\t\t}{\n\t\t\tNested{\"foobar\"},\n\t\t}, nil},\n\n\t\t{\"simple too big\", struct {\n\t\t\tString string `size:\"4\"`\n\t\t}{\n\t\t\t\"foobar\",\n\t\t}, new(FieldSizeError)},\n\n\t\t{\"simple pointer too big\", struct {\n\t\t\tBytes *[]byte `size:\"2\"`\n\t\t}{\n\t\t\t&[]byte{0xde, 0xad, 0xbe, 0xef},\n\t\t}, new(FieldSizeError)},\n\n\t\t{\"nested too big\", struct {\n\t\t\tNested Nested\n\t\t}{\n\t\t\tNested{\"foobarbaz\"},\n\t\t}, new(NestedFieldSizeError)},\n\n\t\t{\"nested pointer too big\", struct {\n\t\t\tNested *Nested\n\t\t}{\n\t\t\t&Nested{\"foobarbaz\"},\n\t\t}, new(NestedFieldSizeError)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := checkSize(reflect.ValueOf(test.x))\n\t\t\tif err != test.expected && errors.CausedBy(err, test.expected) == nil {\n\t\t\t\tt.Errorf(\"unexpected error: got %v, want cause %T\", err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "common/collector/server/context.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n)\n\n// Header is a common protocol message header, which should be embedded in the\n// beginning of all requests and responses.\ntype Header struct {\n\t// Ctx gets injected into requests, but is not part of the transmitted\n\t// message.\n\t//\n\t// Although the context package specifically advises against storing\n\t// contexts inside struct types, the net/rpc package expects all\n\t// handler methods to only take two arguments, which means we cannot\n\t// pass context as the first argument. Instead of doing some complex\n\t// reflection workaround, we will ignore the context package advice and\n\t// put the context here. This may change if the net/rpc package starts\n\t// natively supporting contexts.\n\tCtx context.Context `json:\"-\"`\n\n\t// SessionID is a unique session identifier which is generated when the\n\t// client first connects and should be included in all following\n\t// requests to help tie connections of a single session together.\n\tSessionID string `size:\"32\"`\n\n\t// OS is the client-provided information about the operating system\n\t// that they are using.\n\tOS string `json:\",omitempty\" size:\"100\"`\n\n\t// AuthMethod is the client authentication method used. Not included in\n\t// the response.\n\tAuthMethod string `json:\",omitempty\" size:\"10\"`\n\n\t// AuthToken is an authentication token used by authentication\n\t// verifiers to authenticate the client. It may be omitted, depending\n\t// on the authentication method. Not included in the response.\n\tAuthToken []byte `json:\",omitempty\" size:\"16000\"`\n\n\t// DataToken is a data token used for keeping authentication\n\t// data. It may be omitted, depending on the authentication\n\t// method. Not included in the response.\n\tDataToken []byte `json:\",omitempty\" size:\"16000\"`\n\n\t// XSmartIDAuth is a Smart-ID authentication cookie that is used during\n\t// authentication process to generate a verification code for a client.\n\tXSmartIDAuth []byte `json:\",omitempty\" size:\"16000\"`\n}\n\n// header is an unexported interface to check if a message contains a Header.\ntype header interface {\n\theader() *Header\n}\n\nfunc (h *Header) header() *Header {\n\treturn h\n}\n\n// key is the key type of values stored in context by this package.\ntype key int\n\nconst (\n\ttlsClientKey  key = iota // Context key for TLS client certificates.\n\tauthClientKey            // Context key for authenticated client's distinguished name.\n\tvoteIDKey                // Context key for vote identifier from authentication token.\n\tvoterIDKey               // Context key for authenticated client's unique identifier.\n\tvoterIDNumber            // Context key for authenticated client's unique number.\n\n\t// Keys only used internally.\n\taddrKey // Context key for connection's remote address.\n\n\tauthMethod\n)\n\n// TLSClient returns the list of certificates presented by the TLS client.\n//\n// NB! The server package only requests the client's certificate and, if one is\n// provided, checks if the client is in possession of the private key: it does\n// NOT verify the certificate. If client authentication is required, then it is\n// up to the authentication module to verify the certificate.\nfunc TLSClient(ctx context.Context) []*x509.Certificate {\n\tif val := ctx.Value(tlsClientKey); val != nil {\n\t\treturn val.([]*x509.Certificate)\n\t}\n\treturn nil\n}\n\n// TLSClientKey allows Web eID authentication to add client certificates val to ctx.\nfunc TLSClientKey(ctx context.Context, val any) context.Context {\n\treturn context.WithValue(ctx, tlsClientKey, val)\n}\n\n// AuthenticatedClient returns the name of the authenticated client or nil if\n// no authentication was done in this context.\nfunc AuthenticatedClient(ctx context.Context) *pkix.Name {\n\tif val := ctx.Value(authClientKey); val != nil {\n\t\treturn val.(*pkix.Name)\n\t}\n\treturn nil\n}\n\nfunc WithAuthMethod(ctx context.Context, auth string) context.Context {\n\treturn context.WithValue(ctx, authMethod, auth)\n}\n\nfunc AuthMethod(ctx context.Context) (string, error) {\n\tauth, ok := ctx.Value(authMethod).(string)\n\tif !ok {\n\t\treturn \"\", UnableToGetAuthMethodFromCtxError{Description: _SERVER_AUTHMETHOD}\n\t}\n\treturn auth, nil\n}\n\n// VoteIdentifier returns the vote identifier specified by the authentication\n// token or nil if no authentication was done in this context or the used\n// authentication method does not specify a vote identifier.\nfunc VoteIdentifier(ctx context.Context) []byte {\n\tif val := ctx.Value(voteIDKey); val != nil {\n\t\treturn val.([]byte)\n\t}\n\treturn nil\n}\n\n// VoterIdentity returns the unique identifier of the authenticated client or\n// empty string if no authentication was done in this context.\nfunc VoterIdentity(ctx context.Context) string {\n\tif val := ctx.Value(voterIDKey); val != nil {\n\t\treturn val.(string)\n\t}\n\treturn \"\"\n}\n\n// VoterNumber returns the unique number of the authenticated client or\n// empty string if no authentication was done in this context.\nfunc VoterNumber(ctx context.Context) string {\n\tif val := ctx.Value(voterIDNumber); val != nil {\n\t\treturn val.(string)\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "common/collector/server/controller.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// Controller is a controller which controls the operation of an external service.\ntype Controller struct {\n\tstatus  *status\n\tstartfn func(context.Context) error\n\tcheckfn func(context.Context) error\n\tstopfn  func(context.Context) error\n\tfailed  int\n}\n\n// NewController creates a new external service controller with the provided\n// control functions. startfn and stopfn are used to start and stop the\n// controlled service and checkfn is called regularly in-between to check if\n// the service is still up, calling startfn again if not.\n//\n// The configuration version is necessary for reporting status.\nfunc NewController(v *version.V, startfn, checkfn, stopfn func(context.Context) error) (\n\t*Controller, error) {\n\n\tc := &Controller{\n\t\tstartfn: startfn,\n\t\tcheckfn: checkfn,\n\t\tstopfn:  stopfn,\n\t}\n\n\t// Create a new status reporter for this controller.\n\tvar err error\n\tif c.status, err = newStatus(v); err != nil {\n\t\treturn nil, NewControllerStatusError{Err: err,\n\t\t\tDescription: _SERVER_STAT}\n\t}\n\treturn c, nil\n}\n\n// Control calls startfn and calls stopfn when ctx is cancelled.\nfunc (c *Controller) Control(ctx context.Context) error {\n\t// Start the service.\n\tlog.Log(ctx, StartingControlledService{\n\t\tDescription: _SERVER_SYSD_START,\n\t})\n\tif err := c.startfn(ctx); err != nil {\n\t\treturn ControllerStartError{Err: err,\n\t\t\tDescription: _SERVER_SYSD_START_FAIL}\n\t}\n\tlog.Log(ctx, ControlledServiceStarted{Description: _SERVER_SYSD_STARTED})\n\n\tif err := c.status.serving(); err != nil {\n\t\treturn ControlStatusServingError{Err: err,\n\t\t\tDescription: _SERVER_SYSD_SERVE}\n\t}\n\n\t// Check the service every second until ctx is cancelled. If the poll\n\t// fails, then recurse and attempt to start again.\n\tconst d = time.Second\n\tsleep := time.NewTimer(d)\npoll:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tbreak poll\n\t\tcase <-sleep.C:\n\t\t\tif err := c.checkfn(ctx); err != nil {\n\t\t\t\tlog.Error(ctx, ControllerCheckError{Err: err,\n\t\t\t\t\tDescription: _SERVER_SYSD_CHECK})\n\t\t\t\tc.failed++\n\n\t\t\t\t// If we have restarted three times without a\n\t\t\t\t// check succeeding, then stop trying.\n\t\t\t\tif c.failed >= 3 {\n\t\t\t\t\treturn ControllerAbortError{Description: _SERVER_SYSD_ATTEMPT}\n\t\t\t\t}\n\n\t\t\t\treturn c.Control(ctx)\n\t\t\t}\n\t\t\tc.failed = 0\n\t\t\tsleep.Reset(d)\n\t\t}\n\t}\n\n\t// If updating the status returns an error, then only log it: do not\n\t// skip the stop function.\n\tif err := c.status.stopping(); err != nil {\n\t\tlog.Error(ctx, ControlStatusStoppingError{Err: err,\n\t\t\tDescription: \"Initiate systemd service stopping failed\"})\n\t}\n\n\t// Stop the service.\n\tlog.Log(ctx, StoppingControlledService{Description: _SERVER_SYSD_STOP})\n\tif err := c.stopfn(ctx); err != nil {\n\t\treturn ControllerStopError{Err: err, Description: _SERVER_SYSD_STOP_FAIL}\n\t}\n\tlog.Log(ctx, ControlledServiceStopped{Description: _SERVER_SYSD_STOPPED})\n\treturn nil\n}\n\n// ControlAt waits until start and then calls Control.\nfunc (c *Controller) ControlAt(ctx context.Context, start time.Time) error {\n\tif err := c.status.waiting(); err != nil {\n\t\treturn ControlAtStatusWaitingError{Err: err, Description: _SERVER_SYSD_WAIT}\n\t}\n\treturn waitStart(ctx, start, c.Control)\n}\n"
  },
  {
    "path": "common/collector/server/errors.go",
    "content": "package server\n\nimport \"errors\"\n\n// Server protocol errors. These are the possible errors that can be returned\n// from a JSON-RPC call.\nvar (\n\t// General errors.\n\tErrBadRequest      = errors.New(\"BAD_REQUEST\")\n\tErrCertificate     = errors.New(\"BAD_CERTIFICATE\")\n\tErrIneligible      = errors.New(\"INELIGIBLE_VOTER\")\n\tErrInternal        = errors.New(\"INTERNAL_SERVER_ERROR\")\n\tErrTooYoung        = errors.New(\"VOTER_TOO_YOUNG\")\n\tErrUnauthenticated = errors.New(\"UNAUTHENTICATED\")\n\tErrVotingEnd       = errors.New(\"VOTING_END\")\n\n\t// Mobile ID errors.\n\tErrMIDAbsent      = errors.New(\"MID_ABSENT\")\n\tErrMIDCanceled    = errors.New(\"MID_CANCELED\")\n\tErrMIDCertificate = errors.New(\"MID_BAD_CERTIFICATE\")\n\tErrMIDExpired     = errors.New(\"MID_EXPIRED\")\n\tErrMIDGeneral     = errors.New(\"MID_GENERAL\")\n\tErrMIDNotUser     = errors.New(\"MID_NOT_USER\")\n\tErrMIDOperator    = errors.New(\"MID_OPERATOR\")\n\n\t// Smart ID errors.\n\tErrSmartIDCanceled     = errors.New(\"SMARTID_CANCELED\")\n\tErrSmartIDCertificate  = errors.New(\"SMARTID_BAD_CERTIFICATE\")\n\tErrSmartIDExpired      = errors.New(\"SMARTID_EXPIRED\")\n\tErrSmartIDGeneral      = errors.New(\"SMARTID_GENERAL\")\n\tErrSmartIDVerification = errors.New(\"SMARTID_VERIFICATION\")\n\tErrSmartIDAccount      = errors.New(\"SMARTID_ACCOUNT\")\n\n\t// Vote errors.\n\tErrIdentityMismatch = errors.New(\"IDENTITY_MISMATCH\")\n\tErrOutdatedChoices  = errors.New(\"OUTDATED_CHOICES\")\n\tErrVotingRateLimit  = errors.New(\"RATE_LIMIT_EXCEEDED\")\n)\n"
  },
  {
    "path": "common/collector/server/filter.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"net\"\n\t\"net/rpc\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/age\"\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// connFilter is a filter which works on the network connection level.\ntype connFilter interface {\n\t// filter wraps c to filter data read and written, and calls the next\n\t// filter in the chain. It returns a possibly modified context.\n\t//\n\t// In case of errors, filter logs them, notifies the client via c if\n\t// applicable, closes c, and returns without calling next.\n\tfilter(ctx context.Context, c net.Conn, chain connFilters) context.Context\n}\n\n// connFilters is a chain of connFilters.\ntype connFilters []connFilter\n\n// next calls the next filter in the chain, passing the rest of the chain as\n// the continuation. next panics if we reached the end of the chain: the last\n// filter should always block the chain and not call next.\nfunc (cfs connFilters) next(ctx context.Context, c net.Conn) context.Context {\n\tif len(cfs) == 0 {\n\t\tpanic(\"end of chain\")\n\t}\n\treturn cfs[0].filter(ctx, c, cfs[1:])\n}\n\n// connFilterFunc is a helper type which creates connFilters out of functions.\ntype connFilterFunc func(ctx context.Context, c net.Conn, chain connFilters) context.Context\n\nfunc (f connFilterFunc) filter(ctx context.Context, c net.Conn, chain connFilters) context.Context {\n\treturn f(ctx, c, chain)\n}\n\n// headerFilter is a filter which works on the server header of messages.\ntype headerFilter interface {\n\t// filter filters data in the header and calls the next filter in the\n\t// chain. If filter returns a non-nil error, then the request is not\n\t// handled. The non-nil error is sent to the client, so the filter must\n\t// be as generic as possible and log any relevant error information\n\t// itself.\n\t//\n\t// header.Ctx is used as the context instead of passing it as the first\n\t// argument: see server.Header for the reasoning behind this.\n\tfilter(header *Header, chain headerFilters) error\n}\n\n// headerFilters is a chain of headerFilters.\ntype headerFilters []headerFilter\n\n// next calls the next filter in the chain, passing the rest of the chain as\n// the continuation. next does nothing if the chain is empty.\nfunc (hfs headerFilters) next(header *Header) error {\n\tif len(hfs) == 0 {\n\t\treturn nil\n\t}\n\treturn hfs[0].filter(header, hfs[1:])\n}\n\n// headerFilterFunc is a helper type which creates headerFilters out of functions.\ntype headerFilterFunc func(header *Header, chain headerFilters) error\n\nfunc (f headerFilterFunc) filter(header *Header, chain headerFilters) error {\n\treturn f(header, chain)\n}\n\n// FilterConf is the configuration for filters used by servers.\ntype FilterConf struct {\n\tTLS   TLSConf\n\tCodec CodecConf\n}\n\n// newFilters returns a new chain of mandatory filters.\nfunc newFilters(conf *FilterConf, r *rpc.Server, cert tls.Certificate, end time.Time, certPool *x509.CertPool) (\n\tconnFilters, error) {\n\n\ttlsFilter, err := newTLSFilter(&conf.TLS, cert)\n\tif err != nil {\n\t\treturn nil, TLSConfError{Err: err, Description: _SERVER_FILTER_TLS}\n\t}\n\tif certPool != nil {\n\t\ttlsFilter.tlsConf.ClientCAs = certPool\n\t\ttlsFilter.tlsConf.ClientAuth = tls.RequireAndVerifyClientCert\n\t}\n\treturn connFilters{\n\t\tconnFilterFunc(logFilter),\n\t\tconnFilterFunc(connIDFilter),\n\t\tconnFilterFunc(proxyFilter),\n\t\ttlsFilter,\n\t\t&codecFilter{&conf.Codec, r, headerFilters{\n\t\t\tendFilter(end),\n\t\t\theaderFilterFunc(sessIDFilter),\n\t\t\theaderFilterFunc(addrFilter),\n\t\t\theaderFilterFunc(infoFilter),\n\t\t}},\n\t}, nil\n}\n\n// optional adds optional filters to the chain.\nfunc (cfs connFilters) optional(auth auth.Auther, id identity.Identifier, age *age.Checker) {\n\t// All three need to be added to the codec filter. Feel free to panic\n\t// if f is empty or the last filter is not codecFilter, because that\n\t// means there is a programmer error.\n\tcf := cfs[len(cfs)-1].(*codecFilter)\n\n\t// The auth filter is required for the identity filter and the identity\n\t// filter is required for the age filter, so they need to be added in\n\t// that order and only if the preceding one is added.\n\tif len(auth) > 0 {\n\t\tcf.filters = append(cf.filters, authFilter(auth))\n\n\t\tif id != nil {\n\t\t\tcf.filters = append(cf.filters, identityFilter(id))\n\n\t\t\tif age != nil {\n\t\t\t\tcf.filters = append(cf.filters, (*ageFilter)(age))\n\t\t\t}\n\t\t}\n\t}\n}\n\n// close logs entry if not nil, closes c, and logs any closing errors.\nfunc close(ctx context.Context, c io.Closer, err log.ErrorEntry) { //nolint: revive\n\tif err != nil {\n\t\t// Error to be logged while closing the connection\n\t\tlog.Error(ctx, err)\n\t}\n\tif err := c.Close(); err != nil {\n\t\t// Error while closing connection\n\t\tlog.Error(ctx, CloseError{Err: err, Description: _SERVER_CLOSE})\n\t}\n}\n\n// logFilter logs when a new connection is accepted and closed.\nfunc logFilter(ctx context.Context, c net.Conn, chain connFilters) context.Context {\n\tlog.Log(ctx, AcceptedConnection{Remote: c.RemoteAddr(),\n\t\tDescription: _SERVER_FILTER_LOG})\n\tctx = chain.next(ctx, c)\n\tlog.Log(ctx, ClosedConnection{Remote: c.RemoteAddr(),\n\t\tDescription: _SERVER_FILTER_LOG_CLOSE})\n\treturn ctx\n}\n\n// connIDFilter assigns an identifier to the connection and stores it as a\n// context value.\nfunc connIDFilter(ctx context.Context, c net.Conn, chain connFilters) context.Context {\n\tcid := make([]byte, 16)\n\tif _, err := rand.Read(cid); err != nil {\n\t\t// Error generating a random connection identifier\n\t\tclose(ctx, c, GenerateConnectionIDError{Err: log.Alert(err),\n\t\t\tDescription: _SERVER_FILTER_CONNID})\n\t\treturn ctx\n\t}\n\tctx = log.WithConnectionID(ctx, hex.EncodeToString(cid))\n\t// Log the identifier that we have assigned to the connection\n\tlog.Log(ctx, AssignedConnectionID{Remote: c.RemoteAddr(),\n\t\tDescription: _SERVER_FILTER_CONNID_OK})\n\treturn chain.next(ctx, c)\n}\n\n// proxyFilter checks if the connection is prefixed with the PROXY protocol\n// version 2 (http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If\n// so, it logs the proxied address before passing the rest through unchanged.\n//\n// Actually the PROXY protocol mandates that you either use PROXY or not and\n// should not accept both cases. However, we made it optional to simplify\n// development environments where HAProxy is not used. In a production\n// environment, we always use PROXY and all connections to services come\n// through HAProxy, so there is no danger of spoofed addresses.\nfunc proxyFilter(ctx context.Context, c net.Conn, chain connFilters) context.Context {\n\tc, addr, health, err := readPROXY(c)\n\tif err != nil {\n\t\tclose(ctx, c, PROXYProtocolError{Err: err, Description: _SERVER_FILTER_PROXY_HEADER})\n\t\treturn ctx\n\t}\n\tif health {\n\t\t// Log health check\n\t\tlog.Log(ctx, HealthCheck{Description: _SERVER_FILTER_PROXY_HC})\n\t\tclose(ctx, c, nil)\n\t\treturn ctx\n\t}\n\tif addr != nil {\n\t\t// Log actual remote received via proxy\n\t\tlog.Log(ctx, PROXYProtocol{Address: addr, Description: _SERVER_FILTER_PROXY_CLIENT_ADDR})\n\t}\n\n\t// Put remote address into context for addrFilter. Remove once\n\t// addrFilter is no longer necessary.\n\tctx = context.WithValue(ctx, addrKey, c.RemoteAddr())\n\tif addr != nil {\n\t\tctx = context.WithValue(ctx, addrKey, addr)\n\t}\n\n\treturn chain.next(ctx, c)\n}\n\n// TLSConf is the TLS filter configuration.\ntype TLSConf struct {\n\tHandshakeTimeout int64    // TLS handshake timeout in seconds.\n\tCipherSuites     []string // Supported cipher suites, TLS 1.2 only.\n}\n\n// tlsFilter creates a new TLS connection that uses c as the underlying\n// transport. It performs the handshake and puts any provided client\n// certificates into the context.\ntype tlsFilter struct {\n\tserverConf *TLSConf\n\ttlsConf    *tls.Config\n}\n\n//go:generate ./tlsciphersuites tlsCipherSuites\nfunc newTLSFilter(conf *TLSConf, cert tls.Certificate) (*tlsFilter, error) {\n\tf := &tlsFilter{\n\t\tserverConf: conf,\n\t\ttlsConf: &tls.Config{\n\t\t\tCertificates:           []tls.Certificate{cert},\n\t\t\tClientAuth:             tls.RequestClientCert,\n\t\t\tSessionTicketsDisabled: true,\n\t\t\tMinVersion:             tls.VersionTLS12,\n\t\t},\n\t}\n\tfor _, name := range conf.CipherSuites {\n\t\tvalue, ok := tlsCipherSuites[name] // Generated above.\n\t\tif !ok {\n\t\t\t// Unsupported ciphersuite detected\n\t\t\treturn nil, UnsupportedTLSCipherSuiteError{CipherSuite: name,\n\t\t\t\tDescription: _SERVER_FILTER_TLS_CIPHERS}\n\t\t}\n\t\tf.tlsConf.CipherSuites = append(f.tlsConf.CipherSuites, value)\n\t}\n\treturn f, nil\n}\n\nfunc (f *tlsFilter) filter(ctx context.Context, c net.Conn, chain connFilters) context.Context {\n\t// Create the TLS connection.\n\ttlsc := tls.Server(c, f.tlsConf)\n\n\t// Before interacting with the client, check if we are done\n\t// If it indeed is the case - close with an error\n\tselect {\n\tcase <-ctx.Done():\n\t\tclose(ctx, tlsc, CancelingBeforeHandshake{Err: ctx.Err(),\n\t\t\tDescription: _SERVER_FILTER_TLS_HSHAKE})\n\t\treturn ctx\n\tdefault:\n\t}\n\n\t// Explicitly perform the handshake to catch any errors early.\n\tdeadline := time.Now().Add(time.Duration(f.serverConf.HandshakeTimeout) * time.Second)\n\tif err := tlsc.SetDeadline(deadline); err != nil {\n\t\t// Error in setting TLS handshake timeout\n\t\tclose(ctx, tlsc, SetHandshakeTimeoutError{Err: err,\n\t\t\tDescription: _SERVER_FILTER_TLS_HSHAKE_TIME})\n\t\treturn ctx\n\t}\n\tif err := tlsc.Handshake(); err != nil {\n\t\t// Error in the TLS handshake\n\t\tclose(ctx, tlsc, HandshakeError{Err: err, Description: _SERVER_FILTER_TLS_HSHAKE_ERR})\n\t\treturn ctx\n\t}\n\n\t// Log the TLS connection details for successful connection\n\tstate := tlsc.ConnectionState()\n\tlog.Log(ctx, HandshakeComplete{\n\t\tVersion:            state.Version,\n\t\tCipherSuite:        state.CipherSuite,\n\t\tServerName:         state.ServerName,\n\t\tClientCertificates: state.PeerCertificates,\n\t\tDescription:        _SERVER_FILTER_TLS_HSHAKE_OK,\n\t})\n\n\t// Add PeerCertificates to the context and pass tlsc to next.\n\treturn chain.next(context.WithValue(ctx, tlsClientKey, state.PeerCertificates), tlsc)\n}\n\n// CodecConf if the codec filter configuration.\ntype CodecConf struct {\n\tRWTimeout   int64 // Timeout for reading reading requests and writing responses in seconds.\n\tRequestSize int64 // Maximum accepted request size in bytes. 0 disables size limiting.\n\tLogRequests bool  // Should requests be logged?\n}\n\n// codecFilter terminates the connfilter chain: it passes c to the RPC server\n// codec and closes it when the RPC call has finished. It does not call the\n// next filter in the chain.\ntype codecFilter struct {\n\tconf    *CodecConf\n\tserver  *rpc.Server\n\tfilters headerFilters\n}\n\nfunc (f *codecFilter) filter(ctx context.Context, c net.Conn, _ connFilters) context.Context {\n\tcodec := newCodec(ctx, f.conf, c, f.filters)\n\n\t// ServeRequest can return three types of errors:\n\t//\n\t// 1. An error reading the header of the RPC request: in this case,\n\t//    nothing is sent to the client (because it is assumed that they\n\t//    are not speaking the correct protocol) and the connection is\n\t//    simply closed. The error returned from the codec is converted to\n\t//    a string using its Error method, a prefix is added, and the\n\t//    string is converted to an error using errors.New. This destroys\n\t//    any hierarchical information about the errors and removes any\n\t//    extra implemented interfaces.\n\t//\n\t// 2. The header was read successfully, but the requested method does\n\t//    not exist: in this case, the rpc package generates an error\n\t//    itself, sends it to the client, and returns it here.\n\t//\n\t// 3. An error reading the body of the request or in one of the header\n\t//    filters (because the filters are executed as part of reading the\n\t//    body in serverCodec): in this case the error returned by the\n\t//    codec is sent to the client(!) and returned here (unmodified,\n\t//    i.e., without a prefix).\n\t//\n\t// Note that if the called method or writing the response(!) returns an\n\t// error, it is not returned here.\n\t//\n\t// This behavior means that this filter cannot simply rely on logging\n\t// the error returned by ServeRequest, because the errors will be\n\t// malformed (the first case) or not returned at all (if writing the\n\t// response failed). To solve this, ignore any errors returned here and\n\t// make the codec and filters responsible for logging them. This means\n\t// that the codec must also log errors generated by the rpc package\n\t// itself (the second case): it does this by intercepting any rpc\n\t// package errors sent to the client and logging them.\n\t//\n\t// Additionally, the rpc package sends too much internal error\n\t// information to the client (the third case): the codec and filters\n\t// must be sure to make any returned errors generic enough, that they\n\t// can be sent to the client.\n\tf.server.ServeRequest(codec) //nolint:errcheck // Read above.\n\tclose(ctx, codec, nil)\n\treturn ctx\n}\n\n// endFilter returns ErrVotingEnd to all requests starting from end time.\ntype endFilter time.Time\n\nfunc (e endFilter) filter(header *Header, chain headerFilters) error {\n\tif !time.Now().Before(time.Time(e)) { // not before == equal or after\n\t\t// Time that is set in election.yml for period:servicestop is over\n\t\tlog.Log(header.Ctx, VotingEnded{Description: _SERVER_FILTER_END})\n\t\treturn ErrVotingEnd\n\t}\n\treturn chain.next(header)\n}\n\n// sessIDFilter checks if a session ID is provided by the client or generates a\n// new one if not.\nfunc sessIDFilter(header *Header, chain headerFilters) error {\n\tvar entry log.Entry\n\tif len(header.SessionID) == 0 {\n\t\tsid := make([]byte, 16)\n\t\tif _, err := rand.Read(sid); err != nil {\n\t\t\t// Error in generating new random session identifier\n\t\t\tlog.Error(header.Ctx, GenerateSessionIDError{Err: log.Alert(err),\n\t\t\t\tDescription: _SERVER_FILTER_SESSID})\n\t\t\treturn ErrInternal\n\t\t}\n\t\theader.SessionID = hex.EncodeToString(sid)\n\t\tentry = AssignedSessionID{Description: _SERVER_FILTER_SESSID_OK}\n\t} else {\n\t\tentry = ReadSessionID{Description: _SERVER_FILTER_SESSID_REUSE}\n\t}\n\n\t// Validate that the header.SessionID is valid HEX\n\tinvalidSessionID, _ := regexp.MatchString(\"[^0-9A-Fa-f]\", header.SessionID)\n\tif invalidSessionID {\n\t\tlog.Error(header.Ctx, InvalidSessionID{Value: header.SessionID,\n\t\t\tDescription: _SERVER_FILTER_SESSID_REGEX})\n\t\treturn ErrBadRequest\n\t}\n\n\t// Set session ID in logging context and log.\n\theader.Ctx = log.WithSessionID(header.Ctx, header.SessionID)\n\tlog.Log(header.Ctx, entry)\n\treturn chain.next(header)\n}\n\n// addrFilter re-logs the remote address of the connection after we have a\n// SessionID.\n//\n// This is a temporary filter until the log monitor is capable of\n// extracting the address based on ConnectionID.\nfunc addrFilter(header *Header, chain headerFilters) error {\n\tlog.Log(header.Ctx, RemoteAddress{Address: header.Ctx.Value(addrKey),\n\t\tDescription: _SERVER_FILTER_ADDR})\n\treturn chain.next(header)\n}\n\n// infoFilter logs any client-provided information about the platform used to\n// perform the request, e.g., the operating system, and clears the fields from\n// the header so that they are not included in the response.\nfunc infoFilter(header *Header, chain headerFilters) error {\n\tlog.Log(header.Ctx, OperatingSystem{OS: header.OS,\n\t\tDescription: _SERVER_FILTER_OS})\n\theader.OS = \"\"\n\treturn chain.next(header)\n}\n\n// authFilter verifies the authentication token in the request, clears the\n// fields from the header so that they are not included in the response, and\n// stores the authenticated client's name in the context as a value.\ntype authFilter auth.Auther\n\nfunc (a authFilter) filter(header *Header, chain headerFilters) error {\n\tif len(header.AuthMethod) > 0 {\n\t\t// Initiate authentication with specific method. Token is sensitive.\n\t\tlog.Log(header.Ctx, Authenticating{\n\t\t\tMethod:      header.AuthMethod,\n\t\t\tToken:       log.Sensitive(header.AuthToken),\n\t\t\tDescription: _SERVER_FILTER_AUTH,\n\t\t})\n\t\tname, voteid, err := auth.Auther(a).Verify(\n\t\t\theader.Ctx, auth.Type(header.AuthMethod), header.AuthToken)\n\n\t\t// Record authentication method for SessionID tamper check\n\t\theader.Ctx = WithAuthMethod(header.Ctx, header.AuthMethod)\n\n\t\tif err != nil {\n\t\t\t// Based on client RPC \"Header.AuthMethod\" type, backend has chosen an\n\t\t\t// authentication service and the result of that authentication has failed\n\t\t\tlog.Error(header.Ctx, AuthenticationError{Err: err, Description: _SERVER_FILTER_AUTH_FAIL})\n\t\t\tswitch {\n\t\t\tcase errors.CausedBy(err, new(auth.UnconfiguredTypeError)) != nil:\n\t\t\t\tfallthrough\n\t\t\tcase errors.CausedBy(err, new(auth.MalformedTokenError)) != nil:\n\t\t\t\treturn ErrBadRequest\n\t\t\tcase errors.CausedBy(err, new(auth.CertificateError)) != nil:\n\t\t\t\treturn ErrCertificate\n\t\t\tcase errors.CausedBy(err, new(auth.UnauthorizedError)) != nil:\n\t\t\t\treturn ErrIneligible\n\t\t\t}\n\t\t\treturn ErrInternal\n\t\t}\n\t\t// Log successful authentication\n\t\theader.Ctx = context.WithValue(header.Ctx, authClientKey, name)\n\t\tlog.Log(header.Ctx, Authenticated{ClientName: name, Description: _SERVER_FILTER_AUTH_OK})\n\n\t\tif len(voteid) > 0 {\n\t\t\t// Log VoteID provided with the authentication\n\t\t\theader.Ctx = context.WithValue(header.Ctx, voterIDKey, voteid)\n\t\t\tlog.Log(header.Ctx, AuthenticationVoteID{VoteID: voteid,\n\t\t\t\tDescription: _SERVER_FILTER_AUTH_VID})\n\t\t}\n\t\tif header.DataToken != nil {\n\t\t\t// In case of DataToken, log the value, treating it as sensitive\n\t\t\tlog.Log(header.Ctx, AuthData{\n\t\t\t\tToken:       log.Sensitive(header.DataToken),\n\t\t\t\tDescription: _SERVER_FILTER_AUTH_DTOKEN,\n\t\t\t})\n\t\t\tdata, err := auth.Auther(a).Data(auth.Type(header.AuthMethod), header.DataToken)\n\t\t\tif err != nil {\n\t\t\t\t// If client has passed RPC \"Header.DataToken\" (Smart-ID/mobile-ID) and that\n\t\t\t\t// data token verification has failed\n\t\t\t\tlog.Error(header.Ctx, AuthenticationDataError{Err: err,\n\t\t\t\t\tDescription: _SERVER_FILTER_AUTH_DTOKEN_FAIL})\n\t\t\t\treturn ErrInternal\n\t\t\t}\n\t\t\t// Log data that was encoded in the DataToken\n\t\t\theader.Ctx = context.WithValue(header.Ctx, voterIDNumber, string(data))\n\t\t\tlog.Log(header.Ctx, AuthenticationData{Number: string(data),\n\t\t\t\tDescription: _SERVER_FILTER_AUTH_DTOKEN_OK})\n\t\t}\n\n\t}\n\n\theader.AuthMethod = \"\"\n\theader.AuthToken = nil\n\theader.DataToken = nil\n\treturn chain.next(header)\n}\n\n// identityFilter extracts a unique identifier from an authenticated client's\n// name and stores it as a context value.\ntype identityFilter identity.Identifier\n\nfunc (i identityFilter) filter(header *Header, chain headerFilters) error {\n\tif name := AuthenticatedClient(header.Ctx); name != nil {\n\t\tid, err := identity.Identifier(i)(name)\n\t\tif err != nil {\n\t\t\t// Voter's personal code was not extracted correctly from authenticated data\n\t\t\tlog.Error(header.Ctx, IdentityError{Err: err,\n\t\t\t\tDescription: _SERVER_FILTER_IDENTITY})\n\t\t\treturn ErrIneligible\n\t\t}\n\t\t// Log detected personal code\n\t\theader.Ctx = context.WithValue(header.Ctx, voterIDKey, id)\n\t\tlog.Log(header.Ctx, Identity{Identity: id, Description: _SERVER_FILTER_IDENTITY_OK})\n\t}\n\treturn chain.next(header)\n}\n\n// ageFilter determines the voter's age and checks if they are over a voting\n// age limit.\ntype ageFilter age.Checker\n\nfunc (a *ageFilter) filter(header *Header, chain headerFilters) error {\n\tif id := VoterIdentity(header.Ctx); len(id) > 0 {\n\t\tif err := (*age.Checker)(a).Check(id); err != nil {\n\t\t\t// Voter does not pass the age verification, most likely too young (nested error)\n\t\t\tlog.Error(header.Ctx, AgeError{Err: err, Description: _SERVER_FILTER_AGE})\n\t\t\tif errors.CausedBy(err, new(age.TooYoungError)) != nil {\n\t\t\t\treturn ErrTooYoung\n\t\t\t}\n\t\t\treturn ErrIneligible\n\t\t}\n\t}\n\treturn chain.next(header)\n}\n"
  },
  {
    "path": "common/collector/server/log_desc.go",
    "content": "package server\n\nconst (\n\t_SERVER_LOGGER                   = \"Cannot create JSON logger from a client RPC connection\"\n\t_SERVER_SET_TIMEOUT              = \"Failed to set timeout for client RPC connection\"\n\t_SERVER_HEADER                   = \"Failed to read RPC header\"\n\t_SERVER_BODY                     = \"Failed to read RPC body\"\n\t_SERVER_REQ_SIZE                 = \"RPC request size is too large\"\n\t_SERVER_REQ_FIELD_STRUCT_SIZE    = \"RPC request field size of a type struct is too large\"\n\t_SERVER_REQ_FIELD_SIZE           = \"RPC request field size is too large\"\n\t_SERVER_RESP_ERR                 = \"RPC server registered an error\"\n\t_SERVER_SET_RESP_TIMEOUT         = \"Failed to set timeout for client RPC response transmission\"\n\t_SERVER_RESP                     = \"Failed to write RPC response\"\n\t_SERVER_AUTHMETHOD               = \"Unable to get AuthMethod from context error\"\n\t_SERVER_STAT                     = \"Cannot create JSON status to be reported to systemd when service is ready to serve\"\n\t_SERVER_SYSD_START               = \"Starting service under systemd\"\n\t_SERVER_SYSD_START_FAIL          = \"Cannot start service under systemd\"\n\t_SERVER_SYSD_STARTED             = \"Successfully started service under systemd\"\n\t_SERVER_SYSD_SERVE               = \"Failed to serve a systemd service\"\n\t_SERVER_SYSD_CHECK               = \"Cannot check systemd service status\"\n\t_SERVER_SYSD_ATTEMPT             = \"systemd service has been attempted to restart more than 3 times\"\n\t_SERVER_SYSD_STOP                = \"Stopping systemd service\"\n\t_SERVER_SYSD_STOP_FAIL           = \"Failed to stop systemd service\"\n\t_SERVER_SYSD_STOPPED             = \"systemd service stopped\"\n\t_SERVER_SYSD_WAIT                = \"Failed to wait for systemd service to bring up\"\n\t_SERVER_FILTER_TLS               = \"Failed to configure TLS connection filter\"\n\t_SERVER_CLOSE                    = \"Cannot close client connection gracefully\"\n\t_SERVER_FILTER_LOG               = \"Client TCP connection accessed log filter\"\n\t_SERVER_FILTER_LOG_CLOSE         = \"Server has closed log filter\"\n\t_SERVER_FILTER_CONNID            = \"Cannot create random value for ConnectionID in a TCP connectionID filter\"\n\t_SERVER_FILTER_CONNID_OK         = \"ConnectionID filter assigned ConnectionID to client TCP connection\"\n\t_SERVER_FILTER_PROXY_HEADER      = \"TCP proxy filter detected that client connection doesn't have a 'PROXY' header\"\n\t_SERVER_FILTER_PROXY_HC          = \"Health check by machine proxy\"\n\t_SERVER_FILTER_PROXY_CLIENT_ADDR = \"Reporting remote client address behind machine proxy\"\n\t_SERVER_FILTER_TLS_CIPHERS       = \"Technical configuration contains unsupported TLS ciphers\"\n\t_SERVER_FILTER_TLS_HSHAKE        = \"TLS connection has been closed before TLS handshake\"\n\t_SERVER_FILTER_TLS_HSHAKE_TIME   = \"Cannot set TLS handshake timeout\"\n\t_SERVER_FILTER_TLS_HSHAKE_ERR    = \"TLS handshake error\"\n\t_SERVER_FILTER_TLS_HSHAKE_OK     = \"Handshake is successfully done in a TLS filter\"\n\t_SERVER_FILTER_END               = \"Client RPC connection is after 'electionstoptime' that is set in election configuration\"\n\t_SERVER_FILTER_SESSID            = \"Cannot create random value for SessionID in a RPC sessID filter\"\n\t_SERVER_FILTER_SESSID_OK         = \"Successfully generated new SessionID for client RPC connection\"\n\t_SERVER_FILTER_SESSID_REUSE      = \"Reusing existing SessionID for a client RPC connection\"\n\t_SERVER_FILTER_SESSID_REGEX      = \"Invalid SessionID regex\"\n\t_SERVER_FILTER_ADDR              = \"Address filter reporting RPC client connection IP address\"\n\t_SERVER_FILTER_OS                = \"Info filter reporting RPC client connection OS\"\n\t_SERVER_FILTER_AUTH              = \"Starting RPC client authentication\"\n\t_SERVER_FILTER_AUTH_FAIL         = \"RPC client authentication failed\"\n\t_SERVER_FILTER_AUTH_OK           = \"Client RPC connection has been successfully authenticated\"\n\t_SERVER_FILTER_AUTH_VID          = \"Fetching voter ID from a client RPC connection header\"\n\t_SERVER_FILTER_AUTH_DTOKEN       = \"DataToken present in a client RPC connection header\"\n\t_SERVER_FILTER_AUTH_DTOKEN_FAIL  = \"DataToken authentication failed\"\n\t_SERVER_FILTER_AUTH_DTOKEN_OK    = \"DataToken authentication succeeded\"\n\t_SERVER_FILTER_IDENTITY          = \"Cannot fetch and identify voter ID from RPC client connection header\"\n\t_SERVER_FILTER_IDENTITY_OK       = \"Voter ID has been identified for a client RPC connection\"\n\t_SERVER_FILTER_AGE               = \"Failed to check voter age fetched from RPC client connection header\"\n\t_SERVER_PROXY                    = \"Reading PROXY header from TCP connection failed\"\n\t_SERVER_PROXY_CONN               = \"Invalid PROXY connection version\"\n\t_SERVER_PROXY_IP4                = \"Failed to read TCP IP4 address from PROXY connection\"\n\t_SERVER_PROXY_IP6                = \"Failed to read TCP IP6 address from PROXY connection\"\n\t_SERVER_PROXY_TRS                = \"Invalid PROXY connection OSI level 4 transport protocol\"\n\t_SERVER_PROXY_READ_EX            = \"Failed to read extra bytes from a PROXY connection\"\n\t_SERVER_PROXY_READ               = \"Failed to read PROXY data from TCP connection\"\n\t_SERVER_PROXY_TIME               = \"Failed to set PROXY connection timeout\"\n\t_SERVER_CERT_K                   = \"Failed to load server certificate and key from a file\"\n\t_SERVER_CA                       = \"Failed to load client connections CA certificate\"\n\t_SERVER_FILTERS                  = \"Failed to configure client connection filters\"\n\t_SERVER_ADDR                     = \"Failed to resolve TCP server address\"\n\t_SERVER_STATUS                   = \"Failed to create server status JSON, that will be reported to systemd\"\n\t_SERVER_AUTH                     = \"Failed to configure authenticator for client connections\"\n\t_SERVER_ID_W_N_AUTH              = \"Identity checker cannot be set without authenticator\"\n\t_SERVER_IDENTITY                 = \"Failed to configure identity checker for client connections\"\n\t_SERVER_AGE_W_N_IDENTITY         = \"Age checker cannot be set without identity checker\"\n\t_SERVER_AGE                      = \"Failed to configure age checker for client connection\"\n\t_SERVER_LISTEN_TCP               = \"Cannot listen to TCP socket\"\n\t_SERVER_CLOSED                   = \"Server connections closed unexpectedly\"\n\t_SERVER_CLOSE_ALL                = \"All TCP connections are closed successfully\"\n\t_SERVER_ACC                      = \"Accepting TCP client connections\"\n\t_SERVER_ACC_FAIL                 = \"Failed to accept TCP connections\"\n\t_SERVER_CONN_PANIC               = \"Panic in client connection\"\n\t_SERVER_CONN_FAIL                = \"TCP connection with client failed\"\n\t_SERVER_CONN_STOP                = \"Failed to stop systemd service (this server)\"\n\t_SERVER_CLOSED_GRACE             = \"Failed to close client connections gracefully\"\n\t_SERVER_SYSD_CLOSED              = \"systemd socket closed\"\n\t_SERVER_SYSD_VER                 = \"Failed to create JSON version that will be reported to systemd\"\n\t_SERVER_SYSD_NOTIFY              = \"Failed to create notify JSON message for systemd\"\n\t_SERVER_UNIXGRAM                 = \"Cannot setup unixgram connection for systemd socket\"\n\t_SERVER_UNIXGRAM_CLOSE           = \"Failed to close unixgram connection\"\n\t_SERVER_UNIXGRAM_TIME            = \"Failed to set deadline for unixgram connection\"\n\t_SERVER_SYSD_NOTIFY_FAIL         = \"Failed to notify systemd\"\n\t_SERVER_WAIT                     = \"Waiting for service start\"\n\t_SERVER_WAIT_TIME                = \"Deadline exceeded for waiting a service to bring up\"\n)\n"
  },
  {
    "path": "common/collector/server/proxy.go",
    "content": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"syscall\"\n\t\"time\"\n)\n\nvar proxySig = []byte{0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A}\n\n// readPROXY attempts to read a PROXY protocol prefix and get the client's\n// address from it. It returns a possibly wrapped connection to use for further\n// operations, the read address, and if this was a health check.\nfunc readPROXY(c net.Conn) (wc net.Conn, addr net.Addr, health bool, err error) {\n\t// We are willing to read less, so accept an unexpected EOF.\n\theader := make([]byte, 16)\n\tif err = readTimeout(c, header); err != nil && err != io.ErrUnexpectedEOF {\n\t\treturn c, nil, false, ReadPROXYHeaderError{Err: err,\n\t\t\tDescription: _SERVER_PROXY}\n\t}\n\n\tif !bytes.Equal(header[:12], proxySig) {\n\t\t// The connection is not prefixed with the PROXY protocol:\n\t\t// create a wrapper connection which puts back the read bytes.\n\t\treturn &prefixConn{Conn: c, prefix: header}, nil, false, nil\n\t}\n\n\t// Determine if this is a local or proxied connection.\n\tvar local bool\n\tswitch vercmd := header[12]; vercmd {\n\tcase 0x20:\n\t\tlocal = true\n\tcase 0x21:\n\tdefault:\n\t\treturn c, nil, false, InvalidPROXYVerCmdError{VerCmd: vercmd,\n\t\t\tDescription: _SERVER_PROXY_CONN}\n\t}\n\n\textralen := int(binary.BigEndian.Uint16(header[14:16]))\n\tif !local {\n\t\t// For proxied connections, determine the transport\n\t\t// protocol and source address and port.\n\t\tswitch transport := header[13]; transport {\n\t\tcase 0x11: // TCP over IPv4.\n\t\t\tif addr, err = readTCPAddr(c, 4); err != nil {\n\t\t\t\treturn c, nil, false, ReadPROXYTCPIP4AddressError{\n\t\t\t\t\tErr: err, Description: _SERVER_PROXY_IP4}\n\t\t\t}\n\t\t\textralen -= 12\n\t\tcase 0x21: // TCP over IPv6.\n\t\t\tif addr, err = readTCPAddr(c, 16); err != nil {\n\t\t\t\treturn c, nil, false, ReadPROXYTCPIP6AddressError{Err: err,\n\t\t\t\t\tDescription: _SERVER_PROXY_IP6}\n\t\t\t}\n\t\t\textralen -= 36\n\t\tdefault:\n\t\t\treturn c, nil, false, InvalidPROXYTransportError{\n\t\t\t\tTransport: transport, Description: _SERVER_PROXY_TRS}\n\t\t}\n\t}\n\n\tif extralen > 0 {\n\t\t// Discard any extra bytes.\n\t\tnull := make([]byte, extralen)\n\t\tif _, err = io.ReadFull(c, null); err != nil {\n\t\t\treturn c, nil, false, DiscardPROXYExtraError{Err: err,\n\t\t\t\tDescription: _SERVER_PROXY_READ_EX}\n\t\t}\n\t}\n\n\t// Although the PROXY protocol says that the local command should be\n\t// used for health checks, HAProxy uses proxy commands. In order to\n\t// distinguish health checks from actual connections we try to read\n\t// from the connection: in case of a health check the connection will\n\t// be reset by HAProxy. For actual connections we should be able to\n\t// read at minimum the TLS ClientHello that HAProxy used to dispatch\n\t// the connection.\n\tone := []byte{0}\n\tif err = readTimeout(c, one); err != nil {\n\t\tif ne, ok := err.(*net.OpError); ok {\n\t\t\tif se, ok := ne.Err.(*os.SyscallError); ok {\n\t\t\t\tif se.Err == syscall.ECONNRESET {\n\t\t\t\t\treturn c, addr, true, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn c, nil, false, ReadPostPROXYError{Err: err,\n\t\t\tDescription: _SERVER_PROXY_READ}\n\t}\n\treturn &prefixConn{Conn: c, prefix: one}, addr, false, nil\n}\n\nfunc readTCPAddr(c net.Conn, size int) (net.Addr, error) {\n\taddrblock := make([]byte, 2*size+4) // 2 size addr, 2 uint16 port\n\tif err := readTimeout(c, addrblock); err != nil {\n\t\treturn nil, err\n\t}\n\ttcp := new(net.TCPAddr)\n\ttcp.IP = addrblock[:size]\n\ttcp.Port = int(binary.BigEndian.Uint16(addrblock[2*size : 2*size+2]))\n\treturn tcp, nil\n}\n\nfunc readTimeout(c net.Conn, buf []byte) error {\n\t// This timeout does not need to be configurable, because the messages\n\t// are small and coming from the proxy server.\n\tif err := c.SetDeadline(time.Now().Add(time.Second)); err != nil {\n\t\treturn SetPROXYTimeoutError{Err: err, Description: _SERVER_PROXY_TIME}\n\t}\n\t_, err := io.ReadFull(c, buf)\n\treturn err\n}\n\n// prefixConn is a net.Conn wrapper which reads the prefix before it continues\n// reading the connection.\ntype prefixConn struct {\n\tnet.Conn\n\tprefix []byte\n}\n\nfunc (c *prefixConn) Read(b []byte) (n int, err error) {\n\tif len(c.prefix) > 0 {\n\t\tn = copy(b, c.prefix)\n\t\tc.prefix = c.prefix[n:]\n\t\treturn n, nil\n\t}\n\treturn c.Conn.Read(b)\n}\n"
  },
  {
    "path": "common/collector/server/server.go",
    "content": "/*\nPackage server provides common code for setting up collector services.\n*/\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/rpc\"\n\t\"runtime/debug\"\n\t\"sync\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/age\"\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/conf/version\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// S is a server which listens for incoming connections, filters them, and\n// passes them to the configured handler.\ntype S struct {\n\tend     time.Time\n\tfilters connFilters\n\taddr    *net.TCPAddr\n\tstatus  *status\n}\n\n// Conf is the configuration for a server instance.\ntype Conf struct {\n\t// CertPath and KeyPath are paths to the TLS-certificate and private\n\t// key to use for serving connections.\n\tCertPath string\n\tKeyPath  string\n\n\t// Address is the tcp host:port to listen on for requests.\n\tAddress string\n\n\t// End is the time at which this server will start returning\n\t// ErrVotingEnd to all connections.\n\tEnd time.Time\n\n\tFilter  *FilterConf\n\tVersion *version.V // Necessary for reporting server status.\n\n\tClientCA string\n}\n\n// New creates a new server with the provided configuration and handler.\nfunc New(c *Conf, handler interface{}) (*S, error) {\n\ts := &S{end: c.End}\n\n\t// Setup the RPC server with handler.\n\tr := rpc.NewServer()\n\tif err := r.Register(handler); err != nil {\n\t\t// err can only be non-nil if the handler is not a suitable\n\t\t// type, so this is a programmer error: panic.\n\t\tpanic(err)\n\t}\n\n\t// Parse the TLS certificate-key pair.\n\ttlsCert, err := tls.LoadX509KeyPair(c.CertPath, c.KeyPath)\n\tif err != nil {\n\t\treturn nil, TLSKeyPairError{Err: err, Description: _SERVER_CERT_K}\n\t}\n\tvar certPool *x509.CertPool\n\tif c.ClientCA != \"\" {\n\t\tcertPool, err = cryptoutil.PEMCertificatePool(c.ClientCA)\n\t\tif err != nil {\n\t\t\treturn nil, ClientCAParsingError{Err: err, Description: _SERVER_CA}\n\t\t}\n\t}\n\t// Setup the chain of filters that serves a connection using r.\n\tif s.filters, err = newFilters(c.Filter, r, tlsCert, c.End, certPool); err != nil {\n\t\treturn nil, FilterConfError{Err: err, Description: _SERVER_FILTERS}\n\t}\n\n\t// Resolve the requested listen address.\n\tif s.addr, err = net.ResolveTCPAddr(\"tcp\", c.Address); err != nil {\n\t\treturn nil, ResolveAddressError{Address: c.Address, Err: err,\n\t\t\tDescription: _SERVER_ADDR}\n\t}\n\n\t// Create a new status reporter for this server.\n\tif s.status, err = newStatus(c.Version); err != nil {\n\t\treturn nil, NewStatusError{Err: err, Description: _SERVER_STATUS}\n\t}\n\treturn s, nil\n}\n\n// AuthConf is the configuration for S.WithAuth.\ntype AuthConf struct {\n\tAuth     auth.Auther\n\tIdentity identity.Identifier\n\tAge      *age.Checker\n}\n\n// NewAuthConf initializes an AuthConf with the given configurations.\nfunc NewAuthConf(a auth.Conf, i identity.Type, g *age.Conf) (AuthConf, error) {\n\tvar conf AuthConf\n\tvar err error\n\tif len(a) > 0 {\n\t\tif conf.Auth, err = auth.Configure(a); err != nil {\n\t\t\treturn conf, AuthConfError{Err: err, Description: _SERVER_AUTH}\n\t\t}\n\t}\n\n\tif len(i) > 0 {\n\t\t// Determining the voter's unique identifier requires for them\n\t\t// to be authenticated.\n\t\tif conf.Auth == nil {\n\t\t\treturn conf, IdentityWithoutAuthError{\n\t\t\t\tDescription: _SERVER_ID_W_N_AUTH,\n\t\t\t}\n\t\t}\n\t\tif conf.Identity, err = identity.Get(i); err != nil {\n\t\t\treturn conf, IdentityTypeError{Err: err, Description: _SERVER_IDENTITY}\n\t\t}\n\t}\n\n\tif g != nil && g.Limit > 0 {\n\t\t// Checking the voter's age requires a unique identifier.\n\t\tif conf.Identity == nil {\n\t\t\treturn conf, AgeWithoutIdentityError{\n\t\t\t\tDescription: _SERVER_AGE_W_N_IDENTITY,\n\t\t\t}\n\t\t}\n\t\tif conf.Age, err = age.New(g); err != nil {\n\t\t\treturn conf, AgeConfError{Err: err,\n\t\t\t\tDescription: _SERVER_AGE}\n\t\t}\n\t}\n\treturn conf, nil\n}\n\n// WithAuth configures the server to authenticate clients and enables any\n// features that depend on it, i.e., voter identifiers and age checking.\nfunc (s *S) WithAuth(conf AuthConf) *S {\n\ts.filters.optional(conf.Auth, conf.Identity, conf.Age)\n\treturn s\n}\n\n// Serve starts listening for incoming connections and serves them with the\n// server handler on new goroutines. It blocks until ctx is cancelled or a\n// non-temporary error occurs, after which it waits until all open connections\n// are served.\nfunc (s *S) Serve(ctx context.Context) error {\n\tl, err := net.ListenTCP(\"tcp\", s.addr)\n\tif err != nil {\n\t\treturn ServeListenError{Address: s.addr, Err: err,\n\t\t\tDescription: _SERVER_LISTEN_TCP}\n\t}\n\n\t// Set the server state to serving and set to ended at s.end.\n\tif err := s.status.serving(); err != nil {\n\t\treturn ServeStatusServingError{Err: err, Description: _SERVER_SYSD_SERVE}\n\t}\n\n\t//nolint:errcheck // Only returns nil or context canceled errors.\n\tgo wait(ctx, s.end, func(ctx context.Context) error { // Required by wait.\n\t\tif err := s.status.ended(); err != nil {\n\t\t\tlog.Error(ctx, ServeStatusEndedError{Err: err,\n\t\t\t\tDescription: _SERVER_CLOSED})\n\t\t}\n\t\treturn nil\n\t})\n\n\tvar wg sync.WaitGroup\n\tdefer func() {\n\t\t// Wait until all connections are handled.\n\t\twg.Wait()\n\t\tlog.Log(ctx, AllConnectionsClosed{Description: _SERVER_CLOSE_ALL})\n\t}()\n\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tlog.Log(ctx, AcceptingConnections{Address: l.Addr(),\n\t\t\tDescription: _SERVER_ACC})\n\t\tfor {\n\t\t\tconn, err := l.Accept()\n\t\t\tif err != nil {\n\n\t\t\t\t// Send a non-temporary error on the channel\n\t\t\t\t// and stop accepting.\n\t\t\t\t//\n\t\t\t\t// Note that this path will also be taken in\n\t\t\t\t// the non-error case where the listener was\n\t\t\t\t// closed. This is okay, since then Serve is no\n\t\t\t\t// longer receiving on errc and the error in\n\t\t\t\t// the buffered channel will just get garbage\n\t\t\t\t// collected and never logged.\n\t\t\t\terrc <- log.Alert(AcceptError{Err: err,\n\t\t\t\t\tDescription: _SERVER_ACC_FAIL})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Handle the accepted connection.\n\t\t\twg.Add(1)\n\t\t\tgo func(c net.Conn) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Error(ctx, ConnectionPanicError{\n\t\t\t\t\t\t\tErr:         log.Alert(fmt.Errorf(\"%v\", r)),\n\t\t\t\t\t\t\tStack:       string(debug.Stack()),\n\t\t\t\t\t\t\tDescription: _SERVER_CONN_PANIC,\n\t\t\t\t\t\t})\n\t\t\t\t\t\t// Ensure c is closed. This is\n\t\t\t\t\t\t// a last resort close without\n\t\t\t\t\t\t// any TLS cleanup.\n\t\t\t\t\t\tc.Close()\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\ts.filters.next(ctx, c)\n\t\t\t}(conn)\n\t\t}\n\t}()\n\n\t// Wait until context is cancelled or an error occurs.\n\tselect {\n\tcase <-ctx.Done():\n\tcase err := <-errc:\n\t\tlog.Error(ctx, AcceptingConnectionFailed{Err: err, Description: _SERVER_CONN_FAIL})\n\t}\n\n\t// If updating the status returns an error, then only log it: do not\n\t// skip closing the listener.\n\tif err := s.status.stopping(); err != nil {\n\t\tlog.Error(ctx, ServeStatusStoppingError{Err: err,\n\t\t\tDescription: _SERVER_CONN_STOP})\n\t}\n\n\tif err := l.Close(); err != nil {\n\t\treturn CloseListenerError{Err: err, Description: _SERVER_CLOSED_GRACE}\n\t}\n\tlog.Log(ctx, ListeningSocketClosed{Description: _SERVER_SYSD_CLOSED})\n\treturn nil\n}\n\n// ServeAt waits until start and then calls Serve.\nfunc (s *S) ServeAt(ctx context.Context, start time.Time) error {\n\t// Ensure that the server can bind to s.addr. Preferrably we would\n\t// create and bind a socket here and only start listening on it in\n\t// Serve. However the the net package does not support this very well\n\t// unless we create a socket ourselves using the syscall package.\n\t//\n\t// Another option would be to create the listener here and not accept\n\t// any connections until Serve is called, but that would cause the\n\t// kernel to start establishing connections prematurely. Take the\n\t// naive approach of attempting to listen on the socket here and\n\t// immediately closing it. This does not guarantee that Serve will be\n\t// able to listen on the address, but at least performs some elementary\n\t// checks.\n\tl, err := net.ListenTCP(\"tcp\", s.addr)\n\tif err != nil {\n\t\treturn ServeAtListenError{Address: s.addr, Err: err,\n\t\t\tDescription: _SERVER_LISTEN_TCP}\n\t}\n\tif err := l.Close(); err != nil {\n\t\treturn ServeAtCloseListenerError{Err: err, Description: _SERVER_CLOSED_GRACE}\n\t}\n\n\tif err := s.status.waiting(); err != nil {\n\t\treturn ServeAtStatusWaitingError{Err: err,\n\t\t\tDescription: _SERVER_SYSD_WAIT}\n\t}\n\treturn waitStart(ctx, start, s.Serve)\n}\n"
  },
  {
    "path": "common/collector/server/status.go",
    "content": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/conf/version\"\n)\n\ntype message struct {\n\tStatus  string\n\tVersion *json.RawMessage\n}\n\n// status is used for notifying systemd about the server's status.\ntype status struct {\n\tsocket string\n\tmsg    message\n\tlock   sync.Mutex\n}\n\n// newStatus creates a new status reporter which reports the version v\n// alongside the server status.\nfunc newStatus(v *version.V) (*status, error) {\n\tsocket := os.Getenv(\"NOTIFY_SOCKET\")\n\tif len(socket) == 0 {\n\t\t// If the notify socket is unset or empty, then return a nil\n\t\t// status where state changes do nothing.\n\t\treturn nil, nil\n\t}\n\n\t// Precompute the version JSON.\n\tjsonver, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, PrecomputeVersionJSONError{Err: err,\n\t\t\tDescription: _SERVER_SYSD_VER}\n\t}\n\n\treturn &status{\n\t\tsocket: socket,\n\t\tmsg:    message{Version: (*json.RawMessage)(&jsonver)},\n\t}, nil\n}\n\n// waiting sets the systemd status string to \"waiting\" and notifies systemd\n// that the server is ready.\nfunc (s *status) waiting() error { return s.set(\"waiting\", \"READY=1\") }\n\n// serving sets the systemd status string to \"serving\" and notifies systemd\n// that the server is ready. It is okay to notify systemd multiple times.\nfunc (s *status) serving() error {\n\treturn s.set(\"serving\", \"READY=1\")\n}\n\n// ended sets the systemd status string to \"ended\".\nfunc (s *status) ended() error { return s.set(\"ended\") }\n\n// stopping sets the systemd status string to \"stopping\" and notifies systemd\n// that the server is stopping.\nfunc (s *status) stopping() error { return s.set(\"stopping\", \"STOPPING=1\") }\n\n// set updates the status and sends it and any extra values to systemd over the\n// notification socket.\nfunc (s *status) set(status string, extra ...string) (err error) {\n\tif s == nil {\n\t\treturn nil\n\t}\n\ts.lock.Lock()\n\tdefer s.lock.Unlock()\n\n\tmessage := bytes.NewBufferString(\"STATUS=\")\n\ts.msg.Status = status\n\tif err = json.NewEncoder(message).Encode(s.msg); err != nil {\n\t\treturn JSONEncodeStatusError{Err: err, Description: _SERVER_SYSD_NOTIFY}\n\t}\n\tfor _, line := range extra {\n\t\tmessage.WriteByte('\\n')\n\t\tmessage.WriteString(line)\n\t}\n\n\tconn, err := net.DialTimeout(\"unixgram\", s.socket, time.Second)\n\tif err != nil {\n\t\treturn DialNotifySocketError{Socket: s.socket, Err: err, Description: _SERVER_UNIXGRAM}\n\t}\n\tdefer func() {\n\t\tif cerr := conn.Close(); cerr != nil && err == nil {\n\t\t\terr = CloseNotifyConnectionError{Err: err, Description: _SERVER_UNIXGRAM_CLOSE}\n\t\t}\n\t}()\n\n\tif err = conn.SetDeadline(time.Now().Add(time.Second)); err != nil {\n\t\treturn SetDeadlineNotifyConnectionError{Err: err, Description: _SERVER_UNIXGRAM_TIME}\n\t}\n\tif _, err = message.WriteTo(conn); err != nil {\n\t\treturn WriteNotifyConnectionError{Err: err, Description: _SERVER_SYSD_NOTIFY_FAIL}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/server/tlsciphersuites",
    "content": "#!/bin/sh -eu\n\n# tlsciphersuites: generates map from TLS cipher suite name to integer value.\n# Creating the map by hand is too error-prone, so auto-generate it instead.\n\n# Supported TLS cipher suites. Must be a subset of the constants defined in the\n# crypto/tls package. When changing remember to update the documentation and\n# administration service configuration validator.\n#\n# Currently we are only excluding cipher suites that use RC4 or 3DES.\nciphersuites=\"\nTLS_RSA_WITH_AES_128_CBC_SHA\nTLS_RSA_WITH_AES_256_CBC_SHA\nTLS_RSA_WITH_AES_128_GCM_SHA256\nTLS_RSA_WITH_AES_256_GCM_SHA384\nTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\nTLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\nTLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\nTLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\nTLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\nTLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\nTLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\nTLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\n\"\n\ncat > \"tlsciphersuites.go\" <<HERE\n// Generated by ivxv.ee/common/collector/server/tlsciphersuites. DO NOT EDIT!\n\npackage server\n\nimport \"crypto/tls\"\n\nvar $1 = map[string]uint16 {\n$(for ciphersuite in $ciphersuites; do echo \"\\\"$ciphersuite\\\": tls.$ciphersuite,\"; done)\n}\nHERE\n$GO fmt tlsciphersuites.go > /dev/null\n"
  },
  {
    "path": "common/collector/server/util.go",
    "content": "package server\n\nimport (\n\t\"net/url\"\n)\n\n// VerifyHTTPSOrigin verifies origin to be compliant with https://<URL>:443 schema,\n// e.g. valid URL is https://mydomain.ee:443\nfunc VerifyHTTPSOrigin(origin string) bool {\n\tr, err := url.ParseRequestURI(origin)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// `?` not allowed, e.g. https://mydomain.ee:443?\n\tif r.ForceQuery {\n\t\treturn false\n\t}\n\n\t// Hostname must present\n\tif r.Hostname() == \"\" {\n\t\treturn false\n\t}\n\n\t// Scheme is only `https` lowercase\n\tif r.Scheme != \"https\" {\n\t\treturn false\n\t}\n\n\t// Port only 443\n\tif r.Port() != \"443\" {\n\t\treturn false\n\t}\n\n\t// No query paths allowed, e.g. https://mydomain.ee:443/api/v2,\n\t// even https://mydomain.ee:443/\n\tif r.Path != \"\" {\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "common/collector/server/util_test.go",
    "content": "package server\n\nimport (\n\t\"testing\"\n)\n\nvar validOrigins = []string{\n\t\"https://ehs-logimonitor-01.prod.riaint.ee:443\",\n\t\"https://ehs-ivxv-01.demo.riaint.ee:443\",\n\t\"https://ivxv1.kov.ivxv.ee:443\",\n\t\"https://ivxv1.ep.ivxv.ee:443\",\n\t\"https://irfg3r3rg5g3ree:443\",\n\t\"https://.ep.ivxv.ee:443\",\n\t\"https://.ep.ivxv.ee:443\",\n\t\"https://.e:443\",\n\t\"https://.:443\",\n}\n\nvar invalidOrigins = []string{\n\t\"https://234ehs-rp-01demo.riaint.ee:443/api/v2\",\n\t\"https://ehs-logimonitor-01.prod.riaint.ee:443/\",\n\t\"https://ehs-logimonitor-01.prod.riaint.ee:443#\",\n\t\"ws://ehs-logimonitor--01--.prod.riaint.ee:443/\",\n\t\"https://234ehs-rp-01demo.riaint.ee:443?\",\n\t\"https://.ep.ivxv.ee:443/do?v=IWuSwMC-GjE\",\n\t\"https://ehs-rp-01-demo.riaint.ee:8080\",\n\t\"https://.ep.ivxv.ee:44#1234E\",\n\t\"https://.ep.ivxv.ee:4a43\",\n\t\"https://.ep.ivxv.ee:44?\",\n\t\"https://:443\",\n\t\"http://:443\",\n\t\"htt://:443\",\n\t\"htt:/:443a\",\n\t\"htt::443a\",\n\t\"ivxv1\",\n\t\":443\",\n\t\"443\",\n\t\":\",\n\t\"\",\n}\n\nfunc TestVerifyOriginValid(t *testing.T) {\n\tmsg := \"Expected origin %v to be valid\"\n\tfor _, origin := range validOrigins {\n\t\tif !VerifyHTTPSOrigin(origin) {\n\t\t\tt.Errorf(msg, origin)\n\t\t}\n\t}\n}\n\nfunc TestVerifyOriginInValid(t *testing.T) {\n\tmsg := \"Expected origin %v to be invalid\"\n\tfor _, origin := range invalidOrigins {\n\t\tif VerifyHTTPSOrigin(origin) {\n\t\t\tt.Errorf(msg, origin)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/server/wait.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// wait waits until t, then calls f. wait does not simply start a new timer,\n// but periodically checks the time to see if t is close enough and then starts\n// a timer. This approach makes it obey wall clock changes made after wait has\n// been called, but still precise when close to t.\nfunc wait(ctx context.Context, t time.Time, f func(context.Context) error) error {\n\t// Check wall clock every minute until we are at most one minute away\n\t// from t.\n\tticker := time.NewTicker(time.Minute)\n\tfor time.Until(t) > time.Minute {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tticker.Stop()\n\t\t\treturn ctx.Err()\n\t\tcase <-ticker.C: // Check time again.\n\t\t}\n\t}\n\tticker.Stop()\n\n\t// Do not follow wall clock changes anymore and just wait until t.\n\ttimer := time.NewTimer(time.Until(t))\n\tselect {\n\tcase <-ctx.Done():\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn f(ctx)\n\t}\n}\n\n// waitStart is like wait, but performs additional logging and converts\n// cancellation errors to nil. Meant for delayed start functions.\nfunc waitStart(ctx context.Context, start time.Time, f func(context.Context) error) error {\n\tlog.Log(ctx, WaitingForStart{Start: start, Description: _SERVER_WAIT})\n\tswitch err := wait(ctx, start, f); err {\n\tcase context.Canceled, context.DeadlineExceeded:\n\t\t// We know that waiting was canceled because we only get plain\n\t\t// context errors from wait: all context cancellation errors\n\t\t// from f are expected to be wrapped.\n\t\tlog.Log(ctx, WaitingCanceled{Description: _SERVER_WAIT_TIME})\n\t\treturn nil\n\tdefault:\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "common/collector/smartid/authenticate.go",
    "content": "package smartid\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n)\n\nvar (\n\t// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12, SK specific OID for\n\t// Smart-ID authentication\n\tidKpClientAuthSK = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 62306, 5, 7, 0}\n)\n\n// Challenge generates a Smart-ID authentication session's challenge. Challenge returns a challenge and a\n// verification code.\nfunc (c *Client) Challenge() ([]byte, []byte, error) {\n\tchallenge := make([]byte, c.authHashFunction.Size())\n\tif _, err := rand.Read(challenge); err != nil {\n\t\treturn nil, nil, ChallengeError{Err: err, Description: _SID_RAND}\n\t}\n\n\td := c.authHashFunction.New()\n\td.Write(challenge)\n\n\treturn challenge, d.Sum(nil), nil\n}\n\n// Authenticate starts a Smart-ID authentication session.\nfunc (c *Client) Authenticate(ctx context.Context, identifer string, challengeRnd []byte) (\n\tsesscode string, err error) {\n\n\td := c.authHashFunction.New()\n\td.Write(challengeRnd)\n\tchallenge := d.Sum(nil)\n\n\thashType := hashFunctionNames[c.authHashFunction]\n\n\tsesscode, err = c.startSession(ctx, sessAuth, convertToETSI(identifer), challenge, hashType)\n\tif err != nil {\n\t\terr = AuthenticateError{Err: err, Description: _SID_SESS}\n\t\treturn\n\t}\n\n\treturn\n}\n\n// GetAuthenticateStatus queries the status of a Smart-ID authentication\n// session. If err is nil and signature is empty, then the transaction is still\n// outstanding. If err is nil and signature is non-nil, then the user is\n// authenticated, although callers should use VerifyAuthenticationSignature to\n// double-check.\nfunc (c *Client) GetAuthenticateStatus(ctx context.Context, sesscode string) (\n\tdocumentno string, cert *x509.Certificate, algorithm string, signature []byte, err error) {\n\n\tvar certDER []byte\n\tdocumentno, algorithm, signature, certDER, err = c.getSessionStatus(ctx, sesscode)\n\tif err != nil {\n\t\terr = GetAuthenticateStatusError{Err: err, Description: _SID_SESS_STAT}\n\t\treturn\n\t}\n\n\tif certDER != nil {\n\t\tcert, err = c.parseAndVerify(ctx, certDER)\n\t}\n\n\treturn\n}\n\n// parseAndVerify is a helper function to parse and verify the authentication\n// certificate.\nfunc (c *Client) parseAndVerify(ctx context.Context, certDER []byte) (\n\tcert *x509.Certificate, err error) {\n\t// Parse the authentication certificate.\n\tif cert, err = x509.ParseCertificate(certDER); err != nil {\n\t\terr = ParseAuthenticationCertificateError{\n\t\t\tCertificate: certDER,\n\t\t\tErr:         err,\n\t\t\tDescription: _SID_CERT,\n\t\t}\n\t\treturn\n\t}\n\n\tif cert.UnknownExtKeyUsage != nil {\n\t\t// We only expect one extended key usage, which is either standard id-kp-clientAuth\n\t\t// or id-kp-clientAuthSK, the latter must be marked as unknown to the RFC5280\n\t\tif cert.ExtKeyUsage != nil {\n\t\t\terr = UnexpectedExtKeyUsageError{\n\t\t\t\tCertificate: cert,\n\t\t\t\tExtKeyUsage: cert.ExtKeyUsage[0],\n\t\t\t\tDescription: _SID_EXTKEY_SK,\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tcert.ExtKeyUsage = make([]x509.ExtKeyUsage, len(cert.UnknownExtKeyUsage))\n\t}\n\n\tfor i, ext := range cert.UnknownExtKeyUsage {\n\t\tswitch ext.String() {\n\t\tcase idKpClientAuthSK.String():\n\t\t\tcert.UnknownExtKeyUsage[i] = nil\n\t\t\tcert.ExtKeyUsage[i] = x509.ExtKeyUsageClientAuth\n\t\tdefault:\n\t\t\terr = UnknownExtKeyUsageError{\n\t\t\t\tCertificate: cert,\n\t\t\t\tExtKeyUsage: ext.String(),\n\t\t\t\tDescription: _SID_EXTKEY,\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Verify the authentication certificate and get the issuer.\n\topts := x509.VerifyOptions{\n\t\tRoots:         c.rpool,\n\t\tIntermediates: c.ipool,\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t}\n\tchains, err := cert.Verify(opts)\n\tif err != nil {\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = AuthenticationCertificateVerificationError{\n\t\t\tCertificate: cert,\n\t\t\tErr:         err,\n\t\t\tDescription: _SID_CERT_VERIFY,\n\t\t}\n\t\terr = certerr\n\t\treturn\n\t}\n\tissuer := cert\n\tif len(chains[0]) > 1 { // At least one chain is guaranteed.\n\t\tissuer = chains[0][1]\n\t}\n\n\t// Check OCSP status.\n\tstatus, err := c.ocsp.Check(ctx, cert, issuer, nil)\n\tif err != nil {\n\t\terr = CheckAuthenticationCertOCSPResponsError{\n\t\t\tResponse:    status,\n\t\t\tErr:         err,\n\t\t\tDescription: _SID_OCSP,\n\t\t}\n\t\treturn\n\t}\n\tif !status.Good {\n\t\tvar certerr CertificateError\n\t\tcerterr.Err = AuthenticationCertificateRevokedError{\n\t\t\tReason:      status.RevocationReason,\n\t\t\tDescription: _SID_OCSP_N_GOOD,\n\t\t}\n\t\terr = certerr\n\t\treturn\n\t}\n\n\treturn\n}\n\n// VerifyAuthenticationSignature verifies the certificate signature on the\n// authentication challenge.\nfunc VerifyAuthenticationSignature(cert *x509.Certificate, algorithm string,\n\tsigned, signature []byte) (err error) {\n\n\tsigalg, ok := signatureAlgs[algorithm]\n\tif !ok {\n\t\treturn SigAlgorithmNotSupportedError{\n\t\t\tAlgorithm:   algorithm,\n\t\t\tDescription: _SID_ALG,\n\t\t}\n\t}\n\n\tif err = cert.CheckSignature(sigalg, signed, signature); err != nil {\n\t\treturn VerifyAuthenticationSignatureError{Err: err,\n\t\t\tDescription: _SID_SIG}\n\t}\n\treturn nil\n}\n\n// convertToETSI is a helper function to make identifier to ETSI identifier.\nfunc convertToETSI(identifier string) string {\n\treturn \"PNOEE-\" + identifier\n}\n"
  },
  {
    "path": "common/collector/smartid/certificate.go",
    "content": "package smartid\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n)\n\n// https://github.com/SK-EID/smart-id-documentation#2384-request-parameters\ntype getCertificate struct {\n\tRelyingPartyUUID string   `json:\"relyingPartyUUID\"`\n\tRelyingPartyName string   `json:\"relyingPartyName\"`\n\tCertificateLevel string   `json:\"certificateLevel,omitempty\"`\n\tNonce            string   `json:\"nonce,omitempty\"`\n\tCapabilities     []string `json:\"capabilities,omitempty\"`\n}\n\n// GetCertificateChoice  starts a Smart-ID certificate choice session.\nfunc (c *Client) GetCertificateChoice(ctx context.Context, documentNo string) (sess string, err error) {\n\n\t// We cannot use a struct literal, because gen would report it\n\t// as a duplicate error type.\n\tvar input InputError\n\n\tif len(documentNo) == 0 {\n\t\tinput.Err = GetCertificateNoIDCodeError{Description: _SID_N_ID}\n\t\terr = input\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar resp startSessionResponse\n\tif err = httpPost(ctx, c.url+\"certificatechoice/document/\"+documentNo, getCertificate{\n\t\tRelyingPartyUUID: c.conf.RelyingPartyUUID,\n\t\tRelyingPartyName: c.conf.RelyingPartyName,\n\t\tCertificateLevel: c.conf.CertificateLevel,\n\t}, &resp); err != nil {\n\t\treturn \"\", GetMobileCertificateError{Err: err, Description: _SID_HTTP_CERT}\n\t}\n\n\treturn resp.SessionID, nil\n}\n\n// GetCertificateChoiceStatus queries for a Smart-ID certificate choice session.\nfunc (c *Client) GetCertificateChoiceStatus(ctx context.Context, sesscode string) (\n\tdocumentno string, cert *x509.Certificate, err error) {\n\n\tvar certDER []byte\n\tdocumentno, _, _, certDER, err = c.getSessionStatus(ctx, sesscode)\n\tif err != nil {\n\t\terr = GetMobileCertificateStatusError{Err: err,\n\t\t\tDescription: _SID_SESS_STAT}\n\t\treturn\n\t}\n\n\tif certDER != nil {\n\t\tcert, err = c.parseAndVerify(ctx, certDER)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "common/collector/smartid/http.go",
    "content": "package smartid\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\nconst maxResponseSize = 10240 // 10 KiB.\n\n// https://github.com/SK-EID/smart-id-documentation#2383-error-conditions\ntype errorResponse struct {\n\tSessionID string `json:\"sessionID\"`\n}\n\nfunc httpGet(ctx context.Context, url string, resp interface{}) error {\n\thttpReq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn CreateHTTPGetRequestError{URL: url, Err: err,\n\t\t\tDescription: _SID_URI}\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\treturn httpDo(ctx, \"\", httpReq, resp)\n}\n\nfunc httpPost(ctx context.Context, url string, req interface{}, resp interface{}) error {\n\tjsonReq, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn MarshalJSONRequestError{Err: err,\n\t\t\tDescription: _SID_JSON}\n\t}\n\n\thttpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonReq))\n\tif err != nil {\n\t\treturn CreateHTTPPostRequestError{URL: url, Err: err,\n\t\t\tDescription: _SID_HTTP}\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\treturn httpDo(ctx, fmt.Sprintf(\"%T\", req), httpReq, resp)\n}\n\nfunc httpDo(ctx context.Context, tag string, httpReq *http.Request, resp interface{}) error {\n\treqDump, err := httputil.DumpRequestOut(httpReq, true)\n\tif err != nil {\n\t\treturn DumpHTTPRequestError{Err: err,\n\t\t\tDescription: _SID_HTTP}\n\t}\n\tlog.Debug(ctx, HTTPRequest{Request: string(reqDump),\n\t\tDescription: _SID_HTTP_READY})\n\n\tlog.Log(ctx, SendingRequest{URL: httpReq.URL, Method: httpReq.Method,\n\t\tBodyType: tag, Description: _SID_HTTP_SEND})\n\thttpResp, err := http.DefaultClient.Do(httpReq)\n\tif err != nil {\n\t\treturn log.Alert(SendRequestError{Err: err, Description: _SID_RESP_ERR})\n\t}\n\tdefer func() {\n\t\tif cerr := httpResp.Body.Close(); cerr != nil && err == nil {\n\t\t\terr = ResponseBodyCloseError{Err: cerr, Description: _SID_CLOSE}\n\t\t}\n\t}()\n\tlog.Log(ctx, ReceivedResponse{Description: _SID_RESP})\n\n\trespDump, err := httputil.DumpResponse(httpResp, false)\n\tif err != nil {\n\t\treturn DumpHTTPResponseError{Err: err, Description: _SID_RESP_PARSE}\n\t}\n\tlog.Debug(ctx, HTTPResponse{Response: string(respDump),\n\t\tDescription: _SID_RESP_PARSE_OK})\n\n\t// Does encoding/json.Unmarshal retain any references to the\n\t// original byte slice in the unmarshaled structure? If not, then\n\t// instead of allocating a new byte slice here we could reuse pooled\n\t// buffers for temporarily storing the JSON between reading and\n\t// decoding.\n\tbody, err := io.ReadAll(safereader.New(httpResp.Body, maxResponseSize))\n\tif err != nil {\n\t\treturn ReadHTTPResponseBodyError{Err: err, Description: _SID_RESP_READ}\n\t}\n\tlog.Debug(ctx, HTTPResponseBody{Body: string(body), Description: _SID_RESP_READ_OK})\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\tif len(body) > 0 {\n\t\t\tvar jsonErr errorResponse\n\t\t\tif err = json.Unmarshal(body, &jsonErr); err != nil {\n\t\t\t\treturn UnmarshalJSONErrorResponseError{Err: err,\n\t\t\t\t\tDescription: _SID_RESP_JSONCODE}\n\t\t\t}\n\n\t\t\terr = ErrorResponseError{\n\t\t\t\tHTTPStatus:  httpResp.Status,\n\t\t\t\tSessionID:   jsonErr.SessionID,\n\t\t\t\tDescription: _SID_RESP_ERR,\n\t\t\t}\n\t\t\treturn err\n\n\t\t}\n\t\treturn HTTPStatusError{Status: httpResp.Status, Description: _SID_ERRCODE}\n\t}\n\n\tif err = json.Unmarshal(body, &resp); err != nil {\n\t\treturn UnmarshalJSONResponseError{Err: err,\n\t\t\tDescription: _SID_RESP_JSON}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/smartid/log_desc.go",
    "content": "package smartid\n\nconst (\n\t_SID_RAND          = \"Generate random value for Smart-ID challenge\"\n\t_SID_SESS          = \"Cannot create Smart-ID session via Smart-ID provider\"\n\t_SID_SESS_STAT     = \"HTTP get session status from Smart-ID provider failed\"\n\t_SID_CERT          = \"Parsing client Smart-ID certificate failed\"\n\t_SID_EXTKEY_SK     = \"Not an SK x509 extended key usage\"\n\t_SID_EXTKEY        = \"Unknown x509 extended key usage\"\n\t_SID_CERT_VERIFY   = \"Failed to verify client Smart-ID certificate\"\n\t_SID_OCSP          = \"Failed to check client Smart-ID certificate against OCSP provider\"\n\t_SID_OCSP_N_GOOD   = \"OCSP status of client Smart-ID certificate is not good\"\n\t_SID_ALG           = \"Unsupported Smart-ID signature algorithm\"\n\t_SID_SIG           = \"Failed to verify signature of a Smart-ID client\"\n\t_SID_N_ID          = \"No voter ID\"\n\t_SID_HTTP_CERT     = \"Failed to get Smart-ID certificate from Smart-ID provider\"\n\t_SID_URI           = \"Failed to prepare request URI for Smart-ID provider\"\n\t_SID_JSON          = \"JSON marshalling request for Smart-ID provider failed\"\n\t_SID_HTTP          = \"Failed to prepare HTTP request URI for Smart-ID provider\"\n\t_SID_HTTP_READY    = \"HTTP request for Smart-ID provider has been prepared successfully\"\n\t_SID_HTTP_SEND     = \"Sending HTTP request to Smart-ID provider\"\n\t_SID_RESP_ERR      = \"Smart-ID provider responded with error\"\n\t_SID_CLOSE         = \"Cannot gracefully close HTTP connection with Smart-ID provider\"\n\t_SID_RESP          = \"Successfully received Smart-ID provider response\"\n\t_SID_RESP_PARSE    = \"Cannot parse HTTP Smart-ID provider response\"\n\t_SID_RESP_PARSE_OK = \"Successfully parsed HTTP Smart-ID provider response\"\n\t_SID_RESP_READ     = \"Failed to read Smart-ID response\"\n\t_SID_RESP_READ_OK  = \"Successfully read Smart-ID response\"\n\t_SID_RESP_JSONCODE = \"JSON unmarshalling error code from Smart-ID provider failed\"\n\t_SID_ERRCODE       = \"Error status code from Smart-ID provider\"\n\t_SID_RESP_JSON     = \"JSON unmarshalling response for Smart-ID provider failed\"\n\t_SID_N_HASH        = \"No hashing algorithm provided\"\n\t_SID_N_HASH_T      = \"No hashing type provided\"\n\t_SID_SESS_STATE    = \"Unknown Smart-ID session state\"\n\t_SID_SESS_RES      = \"Unknown Smart-ID session result\"\n\t_SID_IN            = \"Wraps errors which are caused by bad input to smartid functions\"\n\t_SID_VERIF_CODE    = \"Error returned if the voter 3 different code was displayed in app and selected wrong code\"\n\t_SID_ACC           = \"Error returned that are caused by user account configuration\"\n\t_SID_CANCEL        = \"Error returned if the voter canceled the operation\"\n\t_SID_EXP           = \"Error returned if the session expired before the voter did any action\"\n\t_SID_CERT_ERR      = \"Wraps errors which are caused by errors with the voter's certificate (revoked, suspended, not activated, etc)\"\n\t_SID_STAT_ERR      = \"Wraps errors which are caused by an unexpected session status: this is a catch-all for other types of Smart-ID problems\"\n\t_SID_CA            = \"No CA certificates provided to verify Smart-ID provider certificate\"\n\t_SID_CA_PARSE      = \"Failed to parse Smart-ID CA certificate\"\n\t_SID_ICA_PARSE     = \"Failed to parse Smart-ID intermediate CA certificate\"\n\t_SID_OCSP_CFG      = \"Failed to configure Smart-ID OCSP client\"\n\t_SID_HASH          = \"Unsupported hash algorithm\"\n)\n"
  },
  {
    "path": "common/collector/smartid/session.go",
    "content": "package smartid\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"fmt\"\n)\n\ntype sessType string\n\nconst (\n\tsessAuth  sessType = \"authentication/etsi\"\n\tsessSign  sessType = \"signature/document\"\n\tQSCD      string   = \"QSCD\"\n\tQUALIFIED string   = \"QUALIFIED\"\n)\n\nvar (\n\t// hashFunctionNames is map from hash algorithm to it's name for Smart-ID\n\t// REST API.\n\thashFunctionNames = map[crypto.Hash]string{\n\t\tcrypto.SHA256: \"SHA256\",\n\t\tcrypto.SHA384: \"SHA384\",\n\t\tcrypto.SHA512: \"SHA512\",\n\t}\n\n\t// signatureAlgs is map from signature algorithm name in Smart-ID REST API\n\t// to algorithm type.\n\tsignatureAlgs = map[string]x509.SignatureAlgorithm{\n\t\t\"sha256WithRSAEncryption\": x509.SHA256WithRSA,\n\t\t\"sha384WithRSAEncryption\": x509.SHA384WithRSA,\n\t\t\"sha512WithRSAEncryption\": x509.SHA512WithRSA,\n\t}\n)\n\n// https://github.com/SK-EID/smart-id-documentation#2394-request-parameters\ntype startSessionRequest struct {\n\tRelyingPartyUUID         string                     `json:\"relyingPartyUUID\"`\n\tRelyingPartyName         string                     `json:\"relyingPartyName\"`\n\tCertificateLevel         string                     `json:\"certificateLevel,omitempty\"`\n\tHash                     []byte                     `json:\"hash\"`\n\tHashType                 string                     `json:\"hashType\"`\n\tAllowedInteractionsOrder []allowedInteractionsOrder `json:\"allowedInteractionsOrder\"`\n\tNonce                    string                     `json:\"nonce,omitempty\"`\n\tRequestProperties        []byte                     `json:\"requestProperties,omitempty\"`\n\tCapabilities             []string                   `json:\"capabilities,omitempty\"`\n}\n\ntype allowedInteractionsOrder struct {\n\tType           string `json:\"type\"`\n\tDisplayText60  string `json:\"displayText60,omitempty\"`\n\tDisplayText200 string `json:\"displayText200,omitempty\"`\n}\n\n// https://github.com/SK-EID/smart-id-documentation#2395-example-response\ntype startSessionResponse struct {\n\tSessionID string `json:\"sessionID\"`\n}\n\n// startSession is helper function to start either authentication or signing\n// dialog with the user.\nfunc (c *Client) startSession(ctx context.Context, t sessType, identifier string,\n\thash []byte, hashType string) (sesscode string, err error) {\n\n\t// We cannot use a struct literal, because gen would report it\n\t// as a duplicate error type.\n\tvar input InputError\n\tswitch {\n\n\tcase len(hash) == 0:\n\t\tinput.Err = StartSessionNoHashError{Description: _SID_N_HASH}\n\t\terr = input\n\tcase len(hashType) == 0:\n\t\tinput.Err = StartSessionNoHashTypeError{Description: _SID_N_HASH_T}\n\t\terr = input\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\tinteractionsOrder := c.conf.SignInteractionsOrder\n\tcertLevel := c.conf.CertificateLevel\n\tif t == sessAuth {\n\t\tinteractionsOrder = c.conf.AuthInteractionsOrder\n\t\tif certLevel == QSCD {\n\t\t\tcertLevel = QUALIFIED\n\t\t}\n\t}\n\n\tvar resp startSessionResponse\n\tif err = httpPost(ctx, c.url+string(t)+\"/\"+identifier, startSessionRequest{\n\t\tRelyingPartyUUID:         c.conf.RelyingPartyUUID,\n\t\tRelyingPartyName:         c.conf.RelyingPartyName,\n\t\tHash:                     hash,\n\t\tHashType:                 hashType,\n\t\tCertificateLevel:         certLevel,\n\t\tAllowedInteractionsOrder: interactionsOrder,\n\t}, &resp); err != nil {\n\t\terr = StartSessionError{Err: err, Description: _SID_RESP_ERR}\n\t\treturn\n\t}\n\tsesscode = resp.SessionID\n\n\treturn\n}\n\n// https://github.com/SK-EID/smart-id-documentation#23114-response-structure\ntype sessionStatusResponse struct {\n\tState               string            `json:\"state\"`\n\tResult              resultResponse    `json:\"result\"`\n\tSignature           signatureResponse `json:\"signature\"`\n\tCert                certResponse      `json:\"cert\"`\n\tIgnoredProperties   []string          `json:\"ignoredProperties\"`\n\tInteractionFlowUsed string            `json:\"interactionFlowUsed\"`\n\tDeviceIPAddress     string            `json:\"deviceIpAddress\"`\n}\n\ntype resultResponse struct {\n\tEndResult      string `json:\"endResult\"`\n\tDocumentNumber string `json:\"documentNumber\"`\n}\n\ntype certResponse struct {\n\tValue            []byte `json:\"value\"`\n\tCertificateLevel string `json:\"certificateLevel\"`\n}\n\ntype signatureResponse struct {\n\tValue     []byte `json:\"value\"`\n\tAlgorithm string `json:\"algorithm\"`\n}\n\n// getSessionStatus is helper function to get the status of either\n// authentication or signing dialog with the user.\nfunc (c *Client) getSessionStatus(ctx context.Context, sesscode string) (\n\tdocumentNo string, algorithm string, signature []byte, certDER []byte, err error) {\n\n\tvar resp sessionStatusResponse\n\turl := fmt.Sprintf(\"%ssession/%s?timeoutMs=%d\", c.url, sesscode, c.conf.StatusTimeoutMS)\n\tif err = httpGet(ctx, url, &resp); err != nil {\n\t\treturn \"\", \"\", nil, nil, GetSessionStatusError{Err: err,\n\t\t\tDescription: _SID_SESS_STAT}\n\t}\n\n\tswitch resp.State {\n\tcase \"RUNNING\":\n\t\treturn\n\tcase \"COMPLETE\":\n\tdefault:\n\t\tvar status StatusError\n\t\tstatus.Err = UnexpectedSessionStateError{State: resp.State, Description: _SID_SESS_STATE}\n\t\treturn \"\", \"\", nil, nil, status\n\t}\n\tswitch resp.Result.EndResult {\n\tcase \"OK\":\n\t\treturn resp.Result.DocumentNumber, resp.Signature.Algorithm, resp.Signature.Value, resp.Cert.Value, nil\n\tcase \"TIMEOUT\":\n\t\tvar expired ExpiredError\n\t\treturn \"\", \"\", nil, nil, expired\n\tcase \"DOCUMENT_UNUSABLE\":\n\t\tvar account AccountError\n\t\treturn \"\", \"\", nil, nil, account\n\tcase \"REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP\":\n\t\tvar account AccountError\n\t\treturn \"\", \"\", nil, nil, account\n\tcase \"WRONG_VC\":\n\t\tvar verification VerificationError\n\t\treturn \"\", \"\", nil, nil, verification\n\tcase \"USER_REFUSED\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tcase \"USER_REFUSED_CERT_CHOICE\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tcase \"USER_REFUSED_DISPLAYTEXTANDPIN\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tcase \"USER_REFUSED_VC_CHOICE\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tcase \"USER_REFUSED_CONFIRMATIONMESSAGE\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tcase \"USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE\":\n\t\tvar canceled CanceledError\n\t\treturn \"\", \"\", nil, nil, canceled\n\tdefault:\n\t\tvar status StatusError\n\t\tstatus.Err = UnexpectedSessionResultError{Result: resp.Result, Description: _SID_SESS_RES}\n\t\treturn \"\", \"\", nil, nil, status\n\t}\n}\n"
  },
  {
    "path": "common/collector/smartid/sign.go",
    "content": "package smartid\n\nimport (\n\t\"context\"\n)\n\n// SignHash starts a Smart-ID signing session to sigh hash.\nfunc (c *Client) SignHash(ctx context.Context, documentno string, hash []byte,\n\thashType string) (sesscode string, err error) {\n\n\tsesscode, err = c.startSession(ctx, sessSign, documentno, hash, hashType)\n\tif err != nil {\n\t\terr = SignHashError{Err: err, Description: _SID_RESP_ERR}\n\t\treturn\n\t}\n\n\treturn\n}\n\n// GetSignHashStatus queries the status of a Smart-ID signing session.\n// If err is nil and signature is empty, then the transaction is still\n// outstanding.\nfunc (c *Client) GetSignHashStatus(ctx context.Context, sesscode string) (\n\talgorithm string, signature []byte, err error) {\n\n\t_, algorithm, signature, _, err = c.getSessionStatus(ctx, sesscode)\n\tif err != nil {\n\t\terr = GetSignHashStatusError{Err: err, Description: _SID_SESS_STAT}\n\t\treturn\n\t}\n\treturn\n}\n"
  },
  {
    "path": "common/collector/smartid/smartid.go",
    "content": "/*\nPackage smartid provides client for the Smart-ID REST service.\n\nhttps://github.com/SK-EID/smart-id-documentation\n*/\npackage smartid\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"strings\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/ocsp\"\n)\n\nvar (\n\t// InputError wraps errors which are caused by bad input to smartid functions.\n\t_ = InputError{Err: nil, Description: _SID_IN}\n\n\t// VerificationError is the error returned if the voter 3 different code\n\t// was displayed in app and selected wrong code.\n\t_ = VerificationError{Description: _SID_VERIF_CODE}\n\n\t// AccountError is the error returned that are caused by user account configuration.\n\t_ = AccountError{Description: _SID_ACC}\n\n\t// CanceledError is the error returned if the voter canceled the\n\t// operation.\n\t_ = CanceledError{Description: _SID_CANCEL}\n\n\t// ExpiredError is the error returned if the session expired\n\t// before the voter did any action.\n\t_ = ExpiredError{Description: _SID_EXP}\n\n\t// CertificateError wraps errors which are caused by errors with the\n\t// voter's certificate (revoked, suspended, not activated, etc).\n\t_ = CertificateError{Err: nil, Description: _SID_CERT_ERR}\n\n\t// StatusError wraps errors which are caused by an unexpected session\n\t// status: this is a catch-all for other types of Smart-ID problems.\n\t_ = StatusError{Err: nil, Description: _SID_STAT_ERR}\n\n\t// allowedAuthHashFunctions is list of hash functions allowed for\n\t// authentication. Since the hash function is determined by it's hash\n\t// size (Conf.AuthChallengeSize), the functions should have hash sizes\n\t// unique in the list. If Conf.AuthChallengeSize is zero, then the\n\t// first one from this list is used.\n\tallowedAuthHashFunctions = []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512}\n)\n\n// Conf contains the configurable options for the Smart-ID REST API client. It\n// only contains serialized values such that it can easily be unmarshaled from\n// a file.\ntype Conf struct {\n\tURL              string // URL of Smart-ID REST API.\n\tRelyingPartyUUID string // The UUID of the relying party, i.e service consumer.\n\tRelyingPartyName string // The name of the relying party, i.e service consumer.\n\n\tCertificateLevel      string                     // Certificate level for requests\n\tAuthInteractionsOrder []allowedInteractionsOrder // Interactions to display during authentication.\n\tSignInteractionsOrder []allowedInteractionsOrder // Interactions to display during signing.\n\n\tAuthChallengeSize int64 // The authentication challenge size.\n\tStatusTimeoutMS   int64 // The long-polling timeout for authentication/signing status request.\n\n\tRoots         []string  // PEM-encoded authentication certificate verification roots.\n\tIntermediates []string  // PEM-encoded authentication certificate verification intermediates.\n\tOCSP          ocsp.Conf // OCSP configuration for checking authentication certificate revocation.\n}\n\n// Client implements Smart-ID REST API authentication and signing.\ntype Client struct {\n\tconf  Conf\n\trpool *x509.CertPool\n\tipool *x509.CertPool\n\tocsp  *ocsp.Client\n\t// authHashFunction is the hash function to use for authentication.\n\t// Determined by Conf.AuthChallengeSize.\n\tauthHashFunction crypto.Hash\n\t// url is the same as 'conf.URL', but guaranteed to end with slash.\n\turl string\n}\n\n// New returns a new Smart-ID REST API client with the provided configuration.\nfunc New(conf *Conf) (c *Client, err error) {\n\tif len(conf.Roots) == 0 {\n\t\treturn nil, UnconfiguredRootsError{Description: _SID_CA}\n\t}\n\n\tc = &Client{conf: *conf} // Save a copy of conf so it cannot be changed.\n\tif c.rpool, err = cryptoutil.PEMCertificatePool(c.conf.Roots...); err != nil {\n\t\treturn nil, RootsParsingError{Err: err, Description: _SID_CA_PARSE}\n\t}\n\tif c.ipool, err = cryptoutil.PEMCertificatePool(c.conf.Intermediates...); err != nil {\n\t\treturn nil, IntermediatesParsingError{Err: err, Description: _SID_ICA_PARSE}\n\t}\n\tif c.ocsp, err = ocsp.New(&c.conf.OCSP); err != nil {\n\t\treturn nil, OCSPClientError{Err: err, Description: _SID_OCSP_CFG}\n\t}\n\tif c.authHashFunction, err = findAuthHashFunction(c.conf.AuthChallengeSize); err != nil {\n\t\treturn nil, err\n\t}\n\tc.url = conf.URL\n\tif !strings.HasSuffix(c.url, \"/\") {\n\t\tc.url += \"/\"\n\t}\n\treturn\n}\n\n// findAuthHashFunction is helper function to find allowed authentication hash\n// algorithm by it's hash size. If the size is not configured, the first value\n// in the allowedAuthHashFunctions is used.\nfunc findAuthHashFunction(size int64) (crypto.Hash, error) {\n\tif size == 0 {\n\t\treturn allowedAuthHashFunctions[0], nil\n\t}\n\tvar sizes []int\n\tfor _, hf := range allowedAuthHashFunctions {\n\t\tif hf.Size() == int(size) {\n\t\t\treturn hf, nil\n\t\t}\n\t\tsizes = append(sizes, hf.Size())\n\t}\n\treturn 0, AuthChallengeSizeError{Size: size, AllowedSizes: sizes, Description: _SID_HASH}\n}\n"
  },
  {
    "path": "common/collector/smartid/smartid_test.go",
    "content": "package smartid\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/ocsp\"\n)\n\nconst (\n\ttestIdentifier = \"30303039914\"\n\t// G101: Potential hardcoded credentials\n\ttestDocumentNo = \"PNOEE-30303039914-MOCK-Q\"\n)\n\nvar (\n\thashFun    = crypto.SHA384\n\ttestClient *Client\n)\n\nfunc TestMain(m *testing.M) {\n\tread := func(path string) string {\n\t\tb, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"failed to read file:\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\treturn string(b)\n\t}\n\n\tvar err error\n\tif testClient, err = New(&Conf{\n\t\tURL:              \"https://sid.demo.sk.ee/smart-id-rp/v2/\",\n\t\tRelyingPartyUUID: \"00000000-0000-0000-0000-000000000000\",\n\t\tRelyingPartyName: \"DEMO\",\n\t\tCertificateLevel: \"QUALIFIED\",\n\t\tAuthInteractionsOrder: []allowedInteractionsOrder{{\n\t\t\tType:          \"displayTextAndPIN\",\n\t\t\tDisplayText60: \"authenticating\",\n\t\t}},\n\t\tSignInteractionsOrder: []allowedInteractionsOrder{{\n\t\t\tType:          \"displayTextAndPIN\",\n\t\t\tDisplayText60: \"signing\",\n\t\t}},\n\t\tAuthChallengeSize: 64,\n\t\tRoots:             []string{read(\"testdata/TEST_of_EE_Certification_Centre_Root_CA.pem\")},\n\t\tIntermediates:     []string{read(\"testdata/TEST_of_EID-SK_2016.pem\")},\n\t\tOCSP: ocsp.Conf{\n\t\t\tURL:        \"http://demo.sk.ee/ocsp\",\n\t\t\tResponders: []string{read(\"testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem\")},\n\t\t\tMaxSkew:    300,\n\t\t\tMaxAge:     1,\n\t\t},\n\t}); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"failed to create test client:\", err)\n\t\tos.Exit(1)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc authenticate(t *testing.T) (string, error) {\n\tchallengeRnd, _, err := testClient.Challenge()\n\tif err != nil {\n\t\tt.Fatal(\"failed to obtain challenge:\", err)\n\t}\n\n\tctx := log.TestContext(context.Background())\n\tcode, err := testClient.Authenticate(ctx, testIdentifier, challengeRnd)\n\tif err != nil {\n\t\tt.Fatal(\"failed to start authentication session:\", err)\n\t}\n\n\tvar cert *x509.Certificate\n\tvar algo string\n\tvar signature []byte\n\tvar documentNo string\n\tfor len(signature) == 0 {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling authentication\") // Use fmt instead of t.Log to get running output.\n\t\tif documentNo, cert, algo, signature, err = testClient.GetAuthenticateStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check authentication status:\", err)\n\t\t}\n\t}\n\tif !strings.Contains(cert.Subject.SerialNumber, testIdentifier) {\n\t\tt.Error(\"unexpected subject serial number:\", cert.Subject.SerialNumber)\n\t}\n\n\tif err = VerifyAuthenticationSignature(cert, algo, challengeRnd, signature); err != nil {\n\t\tt.Error(\"failed to verify authentication challenge signature:\", err)\n\t}\n\n\treturn documentNo, nil\n}\n\nfunc TestAuthentication(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-SmartID-REST service\")\n\t}\n\tt.Parallel()\n\n\tif _, err := authenticate(t); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestCertificate(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-SmartID-REST service\")\n\t}\n\tt.Parallel()\n\n\tdocumentNo, err := authenticate(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx := log.TestContext(context.Background())\n\tcode, err := testClient.GetCertificateChoice(ctx, documentNo)\n\tif err != nil {\n\t\tt.Fatal(\"failed to get certificate choice:\", err)\n\t}\n\n\tvar cert *x509.Certificate\n\tvar documentno string\n\tfor cert == nil {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling certificate choice\") // Use fmt instead of t.Log to get running output.\n\t\tif documentno, cert, err = testClient.GetCertificateChoiceStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check certificate choice status:\", err)\n\t\t}\n\t}\n\tif documentno != testDocumentNo {\n\t\tt.Fatal(\"different document numbers:\", documentno, testDocumentNo)\n\t}\n}\n\nfunc TestSigning(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping test against test-MID-REST service\")\n\t}\n\tt.Parallel()\n\n\tdocumentNo, err := authenticate(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx := log.TestContext(context.Background())\n\tcode, err := testClient.GetCertificateChoice(ctx, documentNo)\n\tif err != nil {\n\t\tt.Fatal(\"failed to get certificate choice:\", err)\n\t}\n\n\tvar cert *x509.Certificate\n\tvar documentno string\n\tfor cert == nil {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling certificate choice\") // Use fmt instead of t.Log to get running output.\n\t\tif documentno, cert, err = testClient.GetCertificateChoiceStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check certificate choice status:\", err)\n\t\t}\n\t}\n\tif documentno != testDocumentNo {\n\t\tt.Fatal(\"different document numbers:\", documentno, testDocumentNo)\n\t}\n\n\trnd := make([]byte, 100)\n\tif _, err = rand.Read(rnd); err != nil {\n\t\tt.Fatal(\"failed to generate data to sign:\", err)\n\t}\n\th := hashFun.New()\n\tif _, err = h.Write(rnd); err != nil {\n\t\tt.Fatal(\"failed to hash data to sign:\", err)\n\t}\n\thash := h.Sum(nil)\n\n\tcode, err = testClient.SignHash(ctx, testDocumentNo, hash, hashFunctionNames[hashFun])\n\tif err != nil {\n\t\tt.Fatal(\"failed to start signing session:\", err)\n\t}\n\n\tvar algo string\n\tvar signature []byte\n\tfor len(signature) == 0 {\n\t\ttime.Sleep(1 * time.Second)\n\t\tfmt.Println(\"polling signing\") // Use fmt instead of t.Log to get running output.\n\t\tif algo, signature, err = testClient.GetSignHashStatus(ctx, code); err != nil {\n\t\t\tt.Fatal(\"failed to check signing status:\", err)\n\t\t}\n\t}\n\n\t// Although VerifyAuthenticationSignature is meant for authentication\n\t// (as the name suggests), we can reuse it in this test case.\n\tif err = VerifyAuthenticationSignature(cert, algo, rnd, signature); err != nil {\n\t\tt.Error(\"failed to verify authentication challenge signature:\", err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/smartid/testdata/TEST_of_EE_Certification_Centre_Root_CA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEEzCCAvugAwIBAgIQc/jtqiMEFERMtVvsSsH7sjANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIhgPMjAxMDEwMDcxMjM0NTZa\nGA8yMDMwMTIxNzIzNTk1OVowfTELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNl\ncnRpZml0c2VlcmltaXNrZXNrdXMxMDAuBgNVBAMMJ1RFU1Qgb2YgRUUgQ2VydGlm\naWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVl\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gGpqCtDmNNEHUjC8LXq\nxRdC1kpjDgkzOTxQynzDxw/xCjy5hhyG3xX4RPrW9Z6k5ZNTNS+xzrZgQ9m5U6uM\nywYpx3F3DVgbdQLd8DsLmuVOz02k/TwoRt1uP6xtV9qG0HsGvN81q3HvPR/zKtA7\nMmNZuwuDFQwsguKgDR2Jfk44eKmLfyzvh+Xe6Cr5+zRnsVYwMA9bgBaOZMv1TwTT\nVNi9H1ltK32Z+IhUX8W5f2qVP33R1wWCKapK1qTX/baXFsBJj++F8I8R6+gSyC3D\nkV5N/pOlWPzZYx+kHRkRe/oddURA9InJwojbnsH+zJOa2VrNKakNv2HnuYCIonzu\npwIDAQABo4GKMIGHMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G\nA1UdDgQWBBS1NAqdpS8QxechDr7EsWVHGwN2/jBFBgNVHSUEPjA8BggrBgEFBQcD\nAgYIKwYBBQUHAwEGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgGCCsGAQUF\nBwMJMA0GCSqGSIb3DQEBBQUAA4IBAQAj72VtxIw6p5lqeNmWoQ48j8HnUBM+6mI0\nI+VkQr0EfQhfmQ5KFaZwnIqxWrEPaxRjYwV0xKa1AixVpFOb1j+XuVmgf7khxXTy\nBmd8JRLwl7teCkD1SDnU/yHmwY7MV9FbFBd+5XK4teHVvEVRsJ1oFwgcxVhyoviR\nSnbIPaOvk+0nxKClrlS6NW5TWZ+yG55z8OCESHaL6JcimkLFjRjSsQDWIEtDvP4S\ntH3vIMUPPiKdiNkGjVLSdChwkW3z+m0EvAjyD9rnGCmjeEm5diLFu7VMNVqupsbZ\nSfDzzBLc5+6TqgQTOG7GaZk2diMkn03iLdHGFrh8ML+mXG9SjEPI\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/smartid/testdata/TEST_of_EID-SK_2016.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIG+DCCBeCgAwIBAgIQUkCP5k8r59RXxWzfbx+GsjANBgkqhkiG9w0BAQwFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwIBcNMTYwODMwMTEyNDE1WhgP\nMjAzMDEyMTcyMzU5NTlaMGgxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0\naWZpdHNlZXJpbWlza2Vza3VzMRcwFQYDVQRhDA5OVFJFRS0xMDc0NzAxMzEcMBoG\nA1UEAwwTVEVTVCBvZiBFSUQtU0sgMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP\nADCCAgoCggIBAOrKOByrJqS1QsKD4tXhqkZafPMd5sfxem6iVbMAAHKpvOs4Ia2o\nXdSvJ2FjrMl5szeT4lpHyzfECzO3nx7pvRLKHufi6lMwMGjtSI6DK8BiH9z7Lm+k\nNLunNFdIir0hPijjbIkjg9iwfaeST9Fi5502LsK7duhKuCnH7O0uMrS/MynJ4StA\nNGY13X2FvPW4qkrtbwsmhdN0Btro72O6/3O+0vbnq/yCWtcQrBGv3+8XEBdCqH5S\n/Rt0EugKX4UlVy5l0QUc8IrjGtdMsr9KDtvmVwlefXYKoLqkC7guMGOUNf6Y4AYG\nsPqfY4dG3N5YNp5FHDL7IO93h7TpRV3gyR38LiJsPHk5nES5mdPkNuEkCyg0zEKI\n7uJ4LUuBbjzZPp2gP7PN8Iqi9GP7V2NCz8vUVN3WpHvctsf0DMvZdV5pxqLY5ojy\nfhMsU4aMcGSQA9EK8ES3O1zBK1DW+btjbQjUFW1SIwCkB2yofFxge+vvzZGbvt2U\nGOE8oAL8/JzNxi9FbjTAbycrGWgEMQ0sM1fKc+OsvoaSy9m3ZQGph0+dbsouQpl3\nkpJvjDMzxxkrMqxdhlVMreLKGCMMxJMAGQEwVS5P93Nnmz8UbkmeomUJr3NrBo4+\nV9L5S4Kx1vTvD0p72xRYFyfifLOjs8qs7lR3yhkcBPQI78ERqxv31FWDAgMBAAGj\nggKFMIICgTAfBgNVHSMEGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jAdBgNVHQ4E\nFgQUrrDq4Tb4JqulzAtmVf46HQK/ErQwDgYDVR0PAQH/BAQDAgEGMIHEBgNVHSAE\ngbwwgbkwPAYHBACL7EABAjAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5l\nZS9yZXBvc2l0b29yaXVtL0NQUzA8BgcEAIvsQAEAMDEwLwYIKwYBBQUHAgEWI2h0\ndHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRvb3JpdW0vQ1BTMDsGBgQAj3oBAjAxMC8G\nCCsGAQUFBwIBFiNodHRwczovL3d3dy5zay5lZS9yZXBvc2l0b29yaXVtL0NQUzAS\nBgNVHRMBAf8ECDAGAQH/AgEAMCcGA1UdJQQgMB4GCCsGAQUFBwMJBggrBgEFBQcD\nAgYIKwYBBQUHAwQwfAYIKwYBBQUHAQEEcDBuMCAGCCsGAQUFBzABhhRodHRwOi8v\nb2NzcC5zay5lZS9DQTBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5zay5lZS9jZXJ0\ncy9FRV9DZXJ0aWZpY2F0aW9uX0NlbnRyZV9Sb290X0NBLmRlci5jcnQwQQYDVR0e\nBDowOKE2MASCAiIiMAqHCAAAAAAAAAAAMCKHIAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvs\nSQEBMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHBzOi8vd3d3LnNrLmVlL3JlcG9zaXRv\ncnkvY3Jscy90ZXN0X2VlY2NyY2EuY3JsMA0GCSqGSIb3DQEBDAUAA4IBAQAiw1VN\nxp1Ho7FwcPlFqlLl6zb225IvpNelFX2QMbq1SPe41LuBW7WRZIV4b6bRQug55k8l\nAm8eX3zEXL9I+4Bzai/IBlMSTYNpqAQGNVImQVwMa64uN8DWo8LNWSYNYYxQzO7s\nTnqsqxLPWeKZRMkREI0RaVNoIPsciJvid9iBKTcGnMVkbrgyLzlXblLMU4I0pL2R\nWlfs2tr+XtCtWAvJPFskM2QZ2NnLjW8WroZr8TooocRA1vl/ruIAPC3FxW7zebKc\nA2B66j4tW7uyF2kPx4WWA3xgR5QZnn4ePEAYjJdu1eWd9KbeAbxPCfFOST43t0fm\n20HfV2Wp2PMEq4b2\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/smartid/testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\nMjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\nb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\nLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\ngOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\nr1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\nz00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\nOM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\nwB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\nRrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\nFIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\nAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\naQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\nJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\nGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\nczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\nBgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\naC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\nMHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\nnH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\nmawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\ndh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/status/client/client.go",
    "content": "// Package client defines an API for any implementation services that\n// wish to communicate with a status reporting service (server).\npackage client\n\n// All possible user authentication methods against IVXV backend.\nconst (\n\tIDcardAuth   = \"id\"\n\tMobileIDAuth = \"mid\"\n\tSmartIDAuth  = \"sid\"\n\tWebeIDAuth   = \"wid\"\n\tNoAuth       = \"\"\n)\n\n// Verifier is the interface that each implementation should use in order\n// to embed business logic for status service response validation.\ntype Verifier interface {\n\t// Verify returns ok as true if request req is successfully verified.\n\tVerify(req interface{}) (ok bool, err error)\n}\n\n// TLSDialer interface governs the rules of interaction with a\n// status service. Data, that is passed in req/resp as an\n// interface{} (DTO), is used by implementations to build\n// up and then perform requests against a status service.\n//\n// This means, that implementations should provide DTOs for any\n// req/resp.\ntype TLSDialer interface {\n\t// TLSDial sends a request req over the TLS connection to a\n\t// status server, which responds with a resp and an error err.\n\tTLSDial(req interface{}) (resp interface{}, err error)\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/builder.go",
    "content": "package rpc\n\ntype StatusReqBuilder struct {\n\tserviceMethod string\n\trequest       any\n}\n\n// NewStatusReqBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusReq.\nfunc NewStatusReqBuilder() *StatusReqBuilder {\n\treturn new(StatusReqBuilder)\n}\n\nfunc (srb *StatusReqBuilder) WithServiceMethod(s string) *StatusReqBuilder {\n\tsrb.serviceMethod = s\n\treturn srb\n}\n\nfunc (srb *StatusReqBuilder) WithRequest(r any) *StatusReqBuilder {\n\tsrb.request = r\n\treturn srb\n}\n\n// Build returns a StatusReq.\nfunc (srb *StatusReqBuilder) Build() StatusReq {\n\treturn StatusReq{\n\t\tServiceMethod: srb.serviceMethod,\n\t\tRequest:       srb.request,\n\t}\n}\n\ntype StatusRespBuilder struct {\n\tresponse map[string]any\n}\n\n// NewStatusRespBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusResp.\nfunc NewStatusRespBuilder() *StatusRespBuilder {\n\treturn new(StatusRespBuilder)\n}\n\nfunc (srb *StatusRespBuilder) WithResponse(r any) *StatusRespBuilder {\n\tresp, ok := r.(*StatusResp)\n\tif ok {\n\t\t// If r is a *StatusResp, then add a response, otherwise keep it nil\n\t\tsrb.response = resp.Response\n\t}\n\treturn srb\n}\n\n// Build returns a StatusResp.\nfunc (srb *StatusRespBuilder) Build() StatusResp {\n\treturn StatusResp{\n\t\tResponse: srb.response,\n\t}\n}\n\ntype VerifyReqBuilder struct {\n\tserviceMethod string\n\trequest       any\n}\n\n// NewVerifyReqBuilder is a Builder-pattern constructor, which is used\n// to prepare a VerifyReq.\nfunc NewVerifyReqBuilder() *VerifyReqBuilder {\n\treturn new(VerifyReqBuilder)\n}\n\nfunc (vrb *VerifyReqBuilder) WithServiceMethod(s string) *VerifyReqBuilder {\n\tvrb.serviceMethod = s\n\treturn vrb\n}\n\nfunc (vrb *VerifyReqBuilder) WithRequest(r any) *VerifyReqBuilder {\n\tvrb.request = r\n\treturn vrb\n}\n\n// Build returns a VerifyReq.\nfunc (vrb *VerifyReqBuilder) Build() VerifyReq {\n\treturn VerifyReq{\n\t\tServiceMethod: vrb.serviceMethod,\n\t\tRequest:       vrb.request,\n\t}\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/builder_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tstatus \"ivxv.ee/common/collector/status/client\"\n)\n\nconst (\n\tmsgTwoStructsNotEqual   = \"Two structs are not equal. Expected %v, got %v\\n\"\n\tsomeServiceMethod       = \"RPC.SomeServiceMethod\"\n\tsomeCallerServiceMethod = \"RPC.SomeCallerServiceMethod\"\n)\n\nvar header = server.Header{\n\tCtx:        context.Background(),\n\tSessionID:  \"0101e9342abab1577b8b2844d6a1d317\",\n\tOS:         \"Ubuntu Jammy 22.04 LTS\",\n\tAuthMethod: \"\",\n\tAuthToken:  nil,\n\tDataToken:  nil,\n}\n\nvar statusReqs = []map[string]any{\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\": struct {\n\t\t\tHeader server.Header\n\t\t}{\n\t\t\tHeader: header,\n\t\t},\n\t},\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\": struct {\n\t\t\tHeader server.Header\n\t\t\tCaller string\n\t\t\tAuth   string\n\t\t\tLease  string\n\t\t\tTTL    string\n\t\t}{\n\t\t\tHeader: header,\n\t\t\tCaller: someCallerServiceMethod,\n\t\t\tAuth:   status.NoAuth,\n\t\t\t// int64 = 0\n\t\t\tLease: \"0\",\n\t\t\tTTL:   \"0\",\n\t\t},\n\t},\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\": struct {\n\t\t\tHeader server.Header\n\t\t\tCaller string\n\t\t\tAuth   string\n\t\t\tLease  string\n\t\t\tTTL    string\n\t\t}{\n\t\t\tHeader: header,\n\t\t\tCaller: someCallerServiceMethod,\n\t\t\tAuth:   status.SmartIDAuth,\n\t\t\t// int64 = 768699984047453265\n\t\t\tLease: \"aaaf8900fff3451\",\n\t\t\tTTL:   \"200\",\n\t\t},\n\t},\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\":       nil,\n\t},\n}\n\nvar statusResps = []*StatusResp{\n\t{\n\t\tResponse: map[string]any{\n\t\t\t\"Header\": header,\n\t\t\t\"Caller\": someCallerServiceMethod,\n\t\t\t\"Auth\":   status.WebeIDAuth,\n\t\t\t// int64 = 0\n\t\t\t\"Lease\": \"\",\n\t\t\t\"TTL\":   \"\",\n\t\t},\n\t},\n\t{\n\t\tResponse: map[string]any{\n\t\t\t\"Header\": header,\n\t\t\t\"Caller\": someCallerServiceMethod,\n\t\t\t\"Auth\":   status.WebeIDAuth,\n\t\t\t// int64 = 768699984047453265\n\t\t\t\"Lease\": \"aaaf8900fff3451\",\n\t\t\t\"TTL\":   \"120\",\n\t\t},\n\t},\n\t{\n\t\tResponse: map[string]any{\n\t\t\t\"Header\": header,\n\t\t\t\"Ok\":     true,\n\t\t},\n\t},\n\t{\n\t\tResponse: map[string]any{\n\t\t\t\"Header\": header,\n\t\t\t\"Ok\":     false,\n\t\t},\n\t},\n}\n\nvar verifyReqs = []map[string]any{\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\": struct {\n\t\t\tHeader server.Header\n\t\t}{\n\t\t\tHeader: header,\n\t\t},\n\t},\n\t{\n\t\t\"ServiceMethod\": someServiceMethod,\n\t\t\"Request\":       nil,\n\t},\n}\n\nfunc TestStatusReqBuilder(t *testing.T) {\n\tfor _, statusReq := range statusReqs {\n\t\texpected := StatusReq{\n\t\t\tServiceMethod: statusReq[\"ServiceMethod\"].(string),\n\t\t\tRequest:       statusReq[\"Request\"],\n\t\t}\n\n\t\tgot := NewStatusReqBuilder().\n\t\t\tWithServiceMethod(statusReq[\"ServiceMethod\"].(string)).\n\t\t\tWithRequest(statusReq[\"Request\"]).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(msgTwoStructsNotEqual, expected, got)\n\t\t}\n\t}\n}\n\nfunc TestStatusRespBuilder(t *testing.T) {\n\tfor _, statusResp := range statusResps {\n\t\texpected := StatusResp{\n\t\t\tResponse: statusResp.Response,\n\t\t}\n\n\t\tgot := NewStatusRespBuilder().\n\t\t\tWithResponse(statusResp).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(msgTwoStructsNotEqual, expected, got)\n\t\t}\n\t}\n}\n\nfunc TestVerifyReqBuilder(t *testing.T) {\n\tfor _, verifyReq := range verifyReqs {\n\t\texpected := VerifyReq{\n\t\t\tServiceMethod: verifyReq[\"ServiceMethod\"].(string),\n\t\t\tRequest:       verifyReq[\"Request\"],\n\t\t}\n\n\t\tgot := NewVerifyReqBuilder().\n\t\t\tWithServiceMethod(verifyReq[\"ServiceMethod\"].(string)).\n\t\t\tWithRequest(verifyReq[\"Request\"]).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(msgTwoStructsNotEqual, expected, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"crypto/tls\"\n\t\"net/rpc/jsonrpc\"\n\n\tstatus \"ivxv.ee/common/collector/status/client\"\n)\n\nconst tcp = \"tcp\"\n\n// tlsClient uses TLS configuration tls to establish TLS connection\n// to a status server on a given addr.\ntype tlsClient struct {\n\taddr string\n\ttls  *tls.Config\n}\n\n// NewTLSClient returns a new RPC TLS Client to the caller.\n//\n// TLS connection should be authenticated on both ends,\n// which means that caller should include `RootCAs:` and\n// its own `Certificates:` into TLS configuration conf.\n//\n// Also, if addr (without port and https:// prefix, e.g.\n// session.status.inttest.ivxv.ee) doesn't match server certificate\n// `X509v3 Subject Alternative Name:DNS:`, then client should\n// also provide a ServerName to a TLS configuration conf.\nfunc NewTLSClient(addr string, conf *tls.Config) status.TLSDialer {\n\treturn &tlsClient{\n\t\taddr: addr,\n\t\ttls:  conf,\n\t}\n}\n\nfunc (r *tlsClient) TLSDial(req interface{}) (interface{}, error) {\n\t// Establish TLS connection, server should respond\n\ttlsConn, err := tls.Dial(tcp, r.addr, r.tls)\n\tif err != nil {\n\t\treturn nil, TLSDialError{Err: err, Addr: r.addr, Description: _RPC_TLS}\n\t}\n\n\t// Any data that is passed to TLSDial should be of a\n\t// *StatusReq type, otherwise error\n\tstatusReq, err := castAnyToStatusReq(req)\n\tif err != nil {\n\t\treturn nil, CastAnyToStatusReqError{Err: err, Description: _RPC_ANY_TO_STATUSREQ}\n\t}\n\n\t// Pass TLS connection to RPC connection and establish it lazily, i.e.\n\t// establish RPC connection only if rpcConn.Call() is invoked\n\trpcConn := jsonrpc.NewClient(tlsConn)\n\n\t// rpcConn.Call(..., ..., reply), where reply is a map[string]any\n\tresp := new(StatusResp)\n\n\t// RPC call to status server, resp.Response is a map[string]any\n\terr = rpcConn.Call(statusReq.ServiceMethod, statusReq.Request, &resp.Response)\n\tif err != nil {\n\t\treturn nil, RPCCallError{\n\t\t\tServiceMethod: statusReq.ServiceMethod,\n\t\t\tErr:           err,\n\t\t\tDescription:   _RPC_CALL,\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/dto.go",
    "content": "package rpc\n\n// VerifyReq is a DTO to pass as a req to a Verify() interface method.\ntype VerifyReq struct {\n\t// ServiceMethod is an RPC endpoint of a status server, e.g. RPC.SessionStatusRead.\n\tServiceMethod string\n\n\t// Request is a data to pass to the RPC endpoint.\n\tRequest any\n}\n\n// StatusReq is a DTO to pass as a req to a TLSDial() interface method.\ntype StatusReq struct {\n\t// ServiceMethod is an RPC endpoint of a status server, e.g. RPC.SessionStatusRead.\n\tServiceMethod string\n\n\t// Request is a data to pass to the RPC endpoint.\n\tRequest any\n}\n\n// StatusResp is a DTO that TLSDial() interface method returns as a resp.\ntype StatusResp struct {\n\t// Response is a data that RPC endpoint returns to a caller.\n\tResponse map[string]any\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_RPC_TLS              = \"Failed to TLS deal to RPC server\"\n\t_RPC_ANY_TO_STATUSREQ = \"Failed to cast any to StatusReq\"\n\t_RPC_CALL             = \"RPC server has responded with error\"\n\t_RPC_ANY_TO_VERIFYREQ = \"Failed to cast any to VerifyReq\"\n)\n"
  },
  {
    "path": "common/collector/status/client/rpc/util.go",
    "content": "package rpc\n\nimport \"reflect\"\n\nconst (\n\texpectedCastForStatusReq = \"*rpc.StatusReq\"\n\texpectedCastForVerifyReq = \"*rpc.VerifyReq\"\n)\n\n// castAnyToStatusReq tries to cast request req to *StatusReq.\nfunc castAnyToStatusReq(req interface{}) (*StatusReq, error) {\n\t// Cast req to *StatusReq\n\tstatusReq, ok := req.(*StatusReq)\n\tif !ok {\n\t\treturn nil, CastToStatusReqError{\n\t\t\tExpected:    expectedCastForStatusReq,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _RPC_ANY_TO_STATUSREQ,\n\t\t}\n\t}\n\n\treturn statusReq, nil\n}\n\n// CastAnyToVerifyReq tries to cast request req to *VerifyReq.\nfunc CastAnyToVerifyReq(req interface{}) (*VerifyReq, error) {\n\t// Cast req to *VerifyReq\n\tverifyReq, ok := req.(*VerifyReq)\n\tif !ok {\n\t\treturn nil, CastToVerifyReqError{\n\t\t\tExpected:    expectedCastForVerifyReq,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _RPC_ANY_TO_VERIFYREQ,\n\t\t}\n\t}\n\n\treturn verifyReq, nil\n}\n"
  },
  {
    "path": "common/collector/status/client/rpc/util_test.go",
    "content": "package rpc\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tstatus \"ivxv.ee/common/collector/status/client\"\n)\n\nvar goodRPCStatusReqs = []*StatusReq{\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: struct{ Header server.Header }{\n\t\t\tHeader: header,\n\t\t},\n\t},\n\t// Example of StatusReq with embedded StatusUpdateReq\n\t// of Smart-ID RPC.GetCertificate endpoint\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: struct {\n\t\t\tHeader server.Header\n\t\t\tCaller string\n\t\t\tAuth   string\n\t\t}{\n\t\t\tHeader: header,\n\t\t\tCaller: someCallerServiceMethod,\n\t\t\tAuth:   status.SmartIDAuth,\n\t\t},\n\t},\n}\n\nvar badRPCStatusReqs = []any{\n\t// not a pointer!\n\tStatusReq{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: struct{ Header server.Header }{\n\t\t\tHeader: header,\n\t\t},\n\t},\n\t// not a StatusReq\n\tStatusResp{\n\t\tResponse: nil,\n\t},\n\t// anonymous struct\n\tstruct{ Header server.Header }{\n\t\tHeader: header,\n\t},\n\t// emptiness\n\tnil,\n}\n\nfunc TestCastAnyToRPCStatusReq(t *testing.T) {\n\tfor _, goodRPCStatusReq := range goodRPCStatusReqs {\n\t\treq, err := castAnyToStatusReq(goodRPCStatusReq)\n\t\tif req == nil || err != nil {\n\t\t\tmsg := \"Expected value %v and no error, got value: %v, error: %v\\n\"\n\t\t\tt.Errorf(msg, goodRPCStatusReq, req, err)\n\t\t}\n\t}\n\n\tfor _, badRPCStatusReq := range badRPCStatusReqs {\n\t\treq, err := castAnyToStatusReq(badRPCStatusReq)\n\t\tif req != nil || err == nil {\n\t\t\tmsg := \"Expected value nil and error, got value: %v, error: %v\\n\"\n\t\t\tt.Errorf(msg, req, err)\n\t\t}\n\n\t\t// Check that error is exactly the same as expected\n\t\texpected := new(CastToStatusReqError)\n\t\texpected.Expected = expectedCastForStatusReq\n\t\texpected.Got = reflect.TypeOf(badRPCStatusReq)\n\t\texpected.Description = _RPC_ANY_TO_STATUSREQ\n\n\t\tif !reflect.DeepEqual(err, *expected) {\n\t\t\tmsg := \"Expected %v, got: %v¸\\n\"\n\t\t\tt.Errorf(msg, expected, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/status/conf.go",
    "content": "package status\n\n// Conf is a configuration for each Observable.\ntype Conf struct {\n\t// Session is an Observable for a SessionID status.\n\tSession *Observable\n}\n\n// Observable represents a Status service configuration.\ntype Observable struct {\n\t// ServerName is used to resolve server SNI during a TLS handshake.\n\tServerName string\n\n\t// AuthTTL is a time in seconds for user to authenticate.\n\tAuthTTL int64\n\n\t// ChoiceTTL is a time in seconds for user to make a choice.\n\tChoiceTTL int64\n\n\t// VoteTTL is a time in seconds for user to confirm a choice.\n\tVoteTTL int64\n\n\t// VerifyTTL is a time in seconds for user to verify a choice.\n\tVerifyTTL int64\n}\n"
  },
  {
    "path": "common/collector/status/status.go",
    "content": "// Package status defines an API for any implementation services that\n// wish to act as a status reporting service (server).\n//\n// The implementation can report literally anything to the caller.\npackage status\n\nimport \"context\"\n\n// Status interface governs the rules of interaction with an\n// underlying status server repository. Data, that is passed in\n// req/resp as an interface{} (DTO), is used by implementations\n// to build up and then perform queries against the repository.\n//\n// This means, that implementations should provide DTOs for any\n// req/resp.\ntype Status interface {\n\t// Read returns a status server response resp based on a request req.\n\t//\n\t// Request req can be anything from an SQL query to a NoSQL key.\n\tRead(ctx context.Context, req interface{}) (resp interface{}, err error)\n\n\t// Update returns an error err if request req wasn't updated in a repository.\n\t//\n\t// Request req can be anything from an SQL query to a NoSQL key.\n\tUpdate(ctx context.Context, req interface{}) (err error)\n\n\t// Delete returns an error err if request req wasn't deleted from a repository.\n\t//\n\t// Request req can be anything from an SQL query to a NoSQL key.\n\tDelete(ctx context.Context, req interface{}) (err error)\n}\n"
  },
  {
    "path": "common/collector/storage/batch.go",
    "content": "package storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// getAll calls GetAll in batches of BatchSize if the storage protocol\n// implements Batcher, otherwise fallbacks to just getting the keys one-by-one.\n//\n// getAll currently does not utilize worker pools, since we do not yet use\n// it for any large gets (we have GetWithPrefix for that). If this changes,\n// then we can start using them here as well.\nfunc (c *Client) getAll(ctx context.Context, keys ...string) (values map[string][]byte, err error) {\n\tvalues = make(map[string][]byte)\n\tif batcher, ok := c.prot.(Batcher); ok {\n\t\tsize := batcher.BatchSize()\n\t\tvar index int\n\t\tfor index < len(keys) {\n\t\t\tend := index + size\n\t\t\tif end > len(keys) {\n\t\t\t\tend = len(keys)\n\t\t\t}\n\n\t\t\tbval, err := batcher.GetAll(ctx, keys[index:end]...)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, GetAllError{Err: err, Description: _STORAGE_BATCH_GET_ALL}\n\t\t\t}\n\t\t\tfor key, value := range bval {\n\t\t\t\tvalues[key] = value\n\t\t\t}\n\n\t\t\tindex = end\n\t\t}\n\t\treturn\n\t}\n\n\tfor _, key := range keys {\n\t\tvalue, err := c.prot.Get(ctx, key)\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\tvalues[key] = value\n\t\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\t\t// Ignore missing keys to match the real GetAll.\n\t\tdefault:\n\t\t\treturn nil, GetAllSingleError{Key: key, Err: err, Description: _STORAGE_GET}\n\t\t}\n\t}\n\treturn\n}\n\n// getAllStrict calls getAll and returns an error if any key is missing.\nfunc (c *Client) getAllStrict(ctx context.Context, keys ...string) (\n\tvalues map[string][]byte, err error) {\n\n\tif values, err = c.getAll(ctx, keys...); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, key := range keys {\n\t\tif _, ok := values[key]; !ok {\n\t\t\tvar ne NotExistError\n\t\t\tne.Key = key\n\t\t\tne.Err = GetAllStrictMissingError{Description: _STORAGE_BATCH_GET_ALL}\n\t\t\treturn nil, ne\n\t\t}\n\t}\n\treturn values, nil\n}\n\n// putAll attempts to optimize putting many keys at once. It starts a worker\n// pool which calls PutAll in batches of BatchSize if the storage protocol\n// implements Batcher, otherwise fallbacks to just putting the keys one-by-one.\n//\n// Additionally, putAll prepends prefix to all keys: this allows to do this\n// common operation here, where we need to iterate over the maps anyway.\n//\n// If ensure is true, then existing keys are allowed, as long as they have the\n// value as the one being put.\n//\n// Progress of the operation is reported to progress as well as logged\n// periodically.\nfunc (c *Client) putAll(ctx context.Context, prefix string,\n\tvalues map[string][]byte, ensure bool, progress status.Add) error {\n\n\t// Child context to stop goroutines early on errors.\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\t// Define a common job type.\n\ttype job struct {\n\t\t// Used by Batchers.\n\t\tbatch []PutAllRequest\n\t\tkeys  []string\n\n\t\t// Used by non-Batchers.\n\t\tkey   string\n\t\tvalue []byte\n\t}\n\n\t// Start feeding jobs into a channel.\n\tjobc := make(chan job)\n\n\tbatcher, isBatcher := c.prot.(Batcher)\n\tjobs := len(values)\n\tif isBatcher {\n\t\tsize := batcher.BatchSize()\n\t\tjobs = (jobs + size - 1) / size\n\t}\n\n\tworkers := runtime.NumCPU()\n\terrc := make(chan error, workers+1) // All goroutines can error without blocking.\n\tif isBatcher {\n\t\tgo func() {\n\t\t\tdefer close(jobc)\n\t\t\tbatch := make([]PutAllRequest, 0, batcher.BatchSize())\n\t\t\tkeys := make([]string, 0, cap(batch))\n\n\t\t\tfor key, value := range values {\n\t\t\t\tbatch = append(batch, PutAllRequest{prefix + key, value})\n\t\t\t\tkeys = append(keys, prefix+key)\n\t\t\t\tif len(batch) < cap(batch) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase jobc <- job{batch: batch, keys: keys}:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Create new slices instead of modifying the\n\t\t\t\t// ones sent to workers.\n\t\t\t\tbatch = make([]PutAllRequest, 0, cap(batch))\n\t\t\t\tkeys = make([]string, 0, cap(batch))\n\t\t\t}\n\t\t\tif len(batch) > 0 {\n\t\t\t\tselect {\n\t\t\t\tcase jobc <- job{batch: batch, keys: keys}:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t} else {\n\t\tgo func() {\n\t\t\tdefer close(jobc)\n\t\t\tfor key, value := range values {\n\t\t\t\tselect {\n\t\t\t\tcase jobc <- job{key: prefix + key, value: value}:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Determine the actual function to call per job.\n\tvar work func(job) (int, error)\n\tswitch {\n\tcase isBatcher && ensure:\n\t\twork = func(j job) (int, error) { return len(j.batch), c.ensureBatch(ctx, j.batch, j.keys) }\n\tcase isBatcher && !ensure:\n\t\twork = func(j job) (int, error) { return len(j.batch), batcher.PutAll(ctx, j.batch...) }\n\tcase !isBatcher && ensure:\n\t\twork = func(j job) (int, error) { return 1, c.ensure(ctx, j.key, j.value) }\n\tcase !isBatcher && !ensure:\n\t\twork = func(j job) (int, error) { return 1, c.prot.Put(ctx, j.key, j.value) }\n\t}\n\n\t// Start an ad-hoc worker pool to perform jobs from the channel. Use\n\t// min(jobs, threads) workers. No need to reduce errc buffer.\n\tif jobs < workers {\n\t\tworkers = jobs\n\t}\n\tvar wg sync.WaitGroup\n\twg.Add(workers)\n\n\tconst logstep = 10000 // Log progress after each logstep.\n\tlogat := uint64(logstep)\n\tfor i := 0; i < workers; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tcountlog := PutAllProgress{Current: 0, Description: _STORAGE_PUT_PROGRESS}\n\t\t\tfor job := range jobc {\n\t\t\t\tn, err := work(job)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrc <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ttotal := progress(uint64(n)) //nolint:gosec\n\t\t\t\tif old := atomic.LoadUint64(&logat); total >= old &&\n\t\t\t\t\tatomic.CompareAndSwapUint64(&logat, old, old+logstep) {\n\n\t\t\t\t\tcountlog.Current = total\n\t\t\t\t\tlog.Log(ctx, countlog)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(errc)\n\t}()\n\n\t// Either we got an error or wg.Wait closed errc returning nil.\n\treturn <-errc\n}\n\n// ensureBatch is used by putAll to perform a single Batcher.PutAll with ensure\n// logic, i.e., allowing keys to exist if their values match the ones being\n// put.\nfunc (c *Client) ensureBatch(ctx context.Context, batch []PutAllRequest, keys []string) error {\n\tbatcher := c.prot.(Batcher)\n\n\t// First try to blindly put everything, hoping that this will succeed\n\t// in the majority of cases.\n\tswitch err := batcher.PutAll(ctx, batch...); {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.CausedBy(err, new(ExistError)) == nil:\n\t\treturn PutAllError{Err: err, Description: _STORAGE_PUT}\n\t}\n\n\t// Some keys in the batch already exist. Get their values and ensure\n\t// that they match the ones being put.\n\texisting, err := batcher.GetAll(ctx, keys...)\n\tif err != nil {\n\t\treturn EnsureBatchGetExistingError{Err: err, Description: _STORAGE_BATCH_GET_ALL}\n\t}\n\n\t// Batch and keys for values that are missing.\n\tvar mbatch []PutAllRequest\n\tvar mkeys []string\n\n\tfor _, kv := range batch {\n\t\tval, ok := existing[kv.Key]\n\t\tif !ok {\n\t\t\tmbatch = append(mbatch, kv)\n\t\t\tmkeys = append(mkeys, kv.Key)\n\t\t\tcontinue\n\t\t}\n\n\t\tif !bytes.Equal(val, kv.Value) {\n\t\t\treturn EnsureBatchExistingMismatchError{\n\t\t\t\tKey:         kv.Key,\n\t\t\t\tExisting:    val,\n\t\t\t\tNew:         kv.Value,\n\t\t\t\tDescription: _STORAGE_BATCH_RACE,\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(mbatch) > 0 {\n\t\t// Recurse to try to put the keys that did not already exist.\n\t\treturn c.ensureBatch(ctx, mbatch, mkeys)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/storage/etcd/etcd.go",
    "content": "/*\nPackage etcd implements a storage protocol which reads and writes data from an\netcd cluster.\n*/\npackage etcd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"go.etcd.io/etcd/api/v3/v3rpc/rpctypes\"\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\t\"go.etcd.io/etcd/client/v3/clientv3util\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/grpclog\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tstorage.Register(storage.Etcd, func(n yaml.Node, services *storage.Services) (\n\t\ts storage.PutGetter, err error) {\n\n\t\tc := new(client)\n\n\t\tif len(services.Servers) == 0 {\n\t\t\treturn nil, NoStorageServersError{Description: _ETCD_URI}\n\t\t}\n\t\tc.endpoints = services.Servers\n\n\t\tvar cfg Conf\n\t\tif err = yaml.Apply(n, &cfg); err != nil {\n\t\t\treturn nil, ConfigurationError{Err: err, Description: _ETCD_CFG}\n\t\t}\n\t\tca, err := cryptoutil.PEMCertificatePool(cfg.CA)\n\t\tif err != nil {\n\t\t\treturn nil, ConfigurationCAError{Err: err, Description: _ETCD_CA}\n\t\t}\n\n\t\tcert, err := tls.LoadX509KeyPair(conf.TLS(services.Sensitive))\n\t\tif err != nil {\n\t\t\treturn nil, TLSKeyPairError{Err: err, Description: _ETCD_KEY}\n\t\t}\n\n\t\t// Parse the leaf certificate and verify that it is issued by\n\t\t// CA and can be used for client authentication.\n\t\tif cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {\n\t\t\treturn nil, ParseTLSCertificateError{Err: err, Description: _ETCD_CERT}\n\t\t}\n\t\tif _, err = cert.Leaf.Verify(x509.VerifyOptions{\n\t\t\tRoots:     ca,\n\t\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t\t}); err != nil {\n\t\t\treturn nil, VerifyTLSCertificateError{Err: err, Description: _ETCD_CERT_VERIFY}\n\t\t}\n\n\t\tc.tls = &tls.Config{\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\tRootCAs:      ca,\n\t\t\tCertificates: []tls.Certificate{cert},\n\t\t}\n\n\t\tc.conntime = time.Duration(cfg.ConnTimeout) * time.Second\n\t\tc.optime = time.Duration(cfg.OpTimeout) * time.Second\n\t\treturn c, nil\n\t})\n}\n\n// Conf is the etcd storage protocol configuration.\ntype Conf struct {\n\t// CA is the PEM-encoding of the CA certificate which issued etcd TLS\n\t// certificates.\n\tCA string\n\n\t// ConnTimeout is the timeout for connecting to etcd endpoints in seconds.\n\tConnTimeout int64\n\n\t// OpTimeout is the timeout for performing a single operation in seconds.\n\tOpTimeout int64\n\n\t// Bootstrap lists the IDs of etcd servers that are used to bootstrap\n\t// the cluster. Necessary so a server knows if they are part of the\n\t// bootstrap or joining an existing cluster.\n\tBootstrap []string\n\n\t// Size is the size of each etcd node.\n\t// If 0, then etcd defaults to 2147483648 bytes, i.e 2 GB.\n\tSize uint64\n\n\t// SnapshotCount is the amount of revisions to be made before making a\n\t// full cluster copy. That copy can be used to completely restore a\n\t// state of a cluster before copy was made.\n\t// If 0, then etcd defaults to 100 000 revisions.\n\tSnapshotCount uint64\n\n\t// HeartbeatTimeout is the time interval which each etcd node uses to\n\t// ping a cluster master node.\n\t// If 0, then etcd defaults to 100 ms.\n\tHeartbeatTimeout uint64\n\n\t// ElectionTimeout is the time interval which each etcd node can tolerate\n\t// without receiving a heartbeat response, however, once that interval\n\t// has expired and a node didn't receive a heartbeat response from a master,\n\t// that node will propose a master node election.\n\t// If 0, then etcd defaults to 1000 ms.\n\tElectionTimeout uint64\n}\n\ntype client struct {\n\tendpoints []string\n\ttls       *tls.Config\n\tconntime  time.Duration\n\toptime    time.Duration\n\n\t// cli is the etcd client and clierr is the (potential) error that we\n\t// got when creating the client. clionce ensures we only initialize the\n\t// client once.\n\tcli     *clientv3.Client\n\tclierr  error\n\tclionce sync.Once\n}\n\nfunc (c *client) BatchSize() int {\n\treturn 128 // Not configurable in etcd v3.2.\n}\n\n// kv returns the KV interface of the client, initializing it if not already\n// done. If initialization failed, then all calls to kv will return that error.\nfunc (c *client) kv(ctx context.Context) (kv clientv3.KV, err error) {\n\tc.clionce.Do(func() {\n\t\t// Strip connection and session ID of the first connection to\n\t\t// call kv from the context, because the logger will be reused\n\t\t// for all future connections.\n\t\tctx = log.WithConnectionID(ctx, \"\")\n\t\tctx = log.WithSessionID(ctx, \"\")\n\t\tgrpclog.SetLoggerV2(newLogger(ctx))\n\n\t\tc.cli, c.clierr = clientv3.New(clientv3.Config{\n\t\t\tEndpoints:   c.endpoints,\n\t\t\tDialTimeout: c.conntime,\n\t\t\tTLS:         c.tls,\n\t\t})\n\t\tif c.clierr == nil {\n\t\t\tlog.Log(ctx, Connected{Endpoints: c.endpoints, Description: _ETCD_CLIENT_CONN})\n\t\t}\n\t})\n\tif err = c.clierr; err == nil {\n\t\tkv = c.cli.KV\n\t}\n\treturn\n}\n\nfunc (c *client) Put(ctx context.Context, key string, value []byte) (err error) {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(PutKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, PutRequest{Key: key, Value: value, Description: _ETCD_PUT})\n\tresp, err := c.put(ctx, kv, storage.PutAllRequest{Key: key, Value: value})\n\tif err != nil {\n\t\treturn log.Alert(PutError{Key: key, Err: err, Description: _ETCD_PUT_DB_FAIL})\n\t}\n\tlog.Debug(ctx, PutResponse{Response: resp, Description: _ETCD_PUT_REQ_OK})\n\n\tif !resp.Succeeded {\n\t\treturn storage.ExistError{Key: key, Err: PutExistingKeyError{Description: _ETCD_PUT_EXISTING}}\n\t}\n\treturn\n}\n\nfunc (c *client) PutAll(ctx context.Context, reqs ...storage.PutAllRequest) (err error) {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(PutAllKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, PutAllRequest{Count: len(reqs), Description: _ETCD_PUT_ALL})\n\tresp, err := c.put(ctx, kv, reqs...)\n\tif err != nil {\n\t\treturn log.Alert(PutAllError{Err: err, Description: _ETCD_PUT_ALL_DB_FAIL})\n\t}\n\tlog.Debug(ctx, PutAllResponse{Response: resp, Description: _ETCD_PUT_ALL_REQ_OK})\n\n\tif !resp.Succeeded {\n\t\tvar key string\n\t\tfor _, rop := range resp.Responses { // Find the first existing key.\n\t\t\tif kvs := rop.GetResponseRange().GetKvs(); len(kvs) > 0 {\n\t\t\t\tkey = string(kvs[0].Key)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn storage.ExistError{Key: key, Err: PutAllExistingKeyError{Description: _ETCD_PUT_ALL_EXISTING}}\n\t}\n\treturn\n}\n\nfunc (c *client) put(ctx context.Context, kv clientv3.KV, reqs ...storage.PutAllRequest) (\n\tresp *clientv3.TxnResponse, err error) {\n\n\tvar cmps []clientv3.Cmp\n\tvar ops []clientv3.Op\n\tvar fail []clientv3.Op\n\tfor _, req := range reqs {\n\t\tcmps = append(cmps, clientv3util.KeyMissing(req.Key))\n\t\tops = append(ops, clientv3.OpPut(req.Key, string(req.Value)))\n\t\tfail = append(fail, clientv3.OpGet(req.Key, clientv3.WithKeysOnly()))\n\t}\n\n\treturn c.doRetry(ctx, func(ctx context.Context) (*clientv3.TxnResponse, error) {\n\t\treturn kv.Txn(ctx).If(cmps...).Then(ops...).Else(fail...).Commit()\n\t})\n}\n\nfunc (c *client) Get(ctx context.Context, key string) (value []byte, err error) {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn nil, log.Alert(GetKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, GetRequest{Key: key, Description: _ETCD_GET})\n\tctx, cancel := context.WithTimeout(ctx, c.optime)\n\tdefer cancel()\n\tresp, err := kv.Get(ctx, key)\n\tif err != nil {\n\t\treturn nil, log.Alert(GetError{Key: key, Err: err, Description: _ETCD_GET_DB_FAIL})\n\t}\n\tlog.Debug(ctx, GetResponse{Response: resp, Description: _ETCD_GET_REQ_OK})\n\n\tif len(resp.Kvs) == 0 {\n\t\treturn nil, storage.NotExistError{Key: key, Err: GetMissingKeyError{\n\t\t\tDescription: _ETCD_GET_NONE,\n\t\t}}\n\t}\n\tvalue = resp.Kvs[0].Value\n\treturn\n}\n\nfunc (c *client) GetAll(ctx context.Context, keys ...string) (values map[string][]byte, err error) {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn nil, log.Alert(GetAllKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\t// Use a transaction to batch all the gets together.\n\tvar ops []clientv3.Op\n\tfor _, key := range keys {\n\t\tops = append(ops, clientv3.OpGet(key))\n\t}\n\n\tlog.Debug(ctx, GetAllRequest{Count: len(keys), Description: _ETCD_GET_ALL})\n\tctx, cancel := context.WithTimeout(ctx, c.optime)\n\tdefer cancel()\n\tresp, err := kv.Txn(ctx).Then(ops...).Commit()\n\tif err != nil {\n\t\treturn nil, log.Alert(GetAllError{Err: err, Description: _ETCD_GET_ALL_DB_FAIL})\n\t}\n\tlog.Debug(ctx, GetAllResponse{Response: resp, Description: _ETCD_GET_ALL_REQ_OK})\n\n\tvalues = make(map[string][]byte)\n\tfor _, rop := range resp.Responses {\n\t\tif kvs := rop.GetResponseRange().GetKvs(); len(kvs) > 0 {\n\t\t\tvalues[string(kvs[0].Key)] = kvs[0].Value\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *client) GetWithPrefix(ctx context.Context, prefix string) (\n\t<-chan storage.GetWithPrefixResult, <-chan error) {\n\n\tch := make(chan storage.GetWithPrefixResult)\n\n\t// Since there are likely too many keys to get in a single request, we\n\t// need to perform multiple queries. In order to speed this up, perform\n\t// them in parallel using workers up to the number of threads.\n\tworkers := runtime.NumCPU()\n\terrc := make(chan error, workers+1) // All goroutines can send without blocking.\n\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\tclose(ch)\n\t\terrc <- log.Alert(GetWithPrefixKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t\treturn ch, errc\n\t}\n\n\t// First get the number of keys with the prefix to know how many jobs\n\t// are needed to get them all.\n\tlog.Debug(ctx, GetWithPrefixCountRequest{Prefix: prefix, Description: _ETCD_GET_PREFIX})\n\tmctx, cancel := context.WithTimeout(ctx, c.optime)\n\tdefer cancel()\n\tresp, err := kv.Get(mctx, prefix, clientv3.WithPrefix(), clientv3.WithCountOnly())\n\tif err != nil {\n\t\tclose(ch)\n\t\terrc <- log.Alert(GetWithPrefixCountError{Err: err, Description: _ETCD_GET_PREFIX_DB_FAIL})\n\t\treturn ch, errc\n\t}\n\tlog.Debug(ctx, GetWithPrefixCountResponse{Response: resp, Description: _ETCD_GET_PREFIX_REQ_OK})\n\n\tif resp.Count == 0 { // No such keys, stop now.\n\t\tclose(ch)\n\t\tclose(errc)\n\t\treturn ch, errc\n\t}\n\n\t// Determine the number of requests that must be performed given the\n\t// count of keys and block size. If less than the number of workers,\n\t// reduce workers. No need to reduce errc buffer.\n\tif jobs := (resp.Count + blockSize - 1) / blockSize; jobs < int64(workers) {\n\t\tworkers = int(jobs)\n\t}\n\n\t// Divide the keyspace into *roughly* equal segments between all\n\t// workers. Simplify this into dividing the first byte into segments:\n\t// assumes that the keys are equally distributed, segments will be\n\t// unequal otherwise.\n\tstep := byte(256 / workers) // Overflows if workers == 1, but will not be used then.\n\tvar current byte\n\tsegments := make([]string, workers+1)\n\tsegments[0] = prefix\n\tfor i := 1; i < workers; i++ {\n\t\tcurrent += step\n\t\tsegments[i] = prefix + string(current)\n\t}\n\tsegments[len(segments)-1] = clientv3.GetPrefixRangeEnd(prefix)\n\n\t// Start worker pool where each goroutine sends the key-values from\n\t// their segment on blockc.\n\tblockc := make(chan *clientv3.GetResponse)\n\twctx, cancel := context.WithCancel(ctx) // Canceled on worker error.\n\n\tvar wg sync.WaitGroup\n\twg.Add(workers)\n\tfor i := 0; i < len(segments)-1; i++ {\n\t\tgo func(from, to string) {\n\t\t\tdefer wg.Done()\n\t\t\tif err := c.getRange(wctx, kv, blockc, from, to); err != nil {\n\t\t\t\terrc <- err\n\t\t\t\tcancel() // Notify other workers of failure.\n\t\t\t}\n\t\t}(segments[i], segments[i+1])\n\t}\n\tgo func() {\n\t\tdefer cancel() // Free worker context resources on success.\n\t\twg.Wait()\n\t\tclose(blockc) // All workers are done.\n\t}()\n\n\t// Stream values from blocks to the caller.\n\tgo func() {\n\t\tdefer close(ch)\n\t\tfor block := range blockc {\n\t\t\tfor _, kv := range block.Kvs {\n\t\t\t\tselect {\n\t\t\t\tcase ch <- storage.GetWithPrefixResult{\n\t\t\t\t\tKey:   string(kv.Key),\n\t\t\t\t\tValue: kv.Value,\n\t\t\t\t}:\n\t\t\t\t// Check ctx instead of wctx so that we still\n\t\t\t\t// send all values that we got before a worker\n\t\t\t\t// error occured.\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Only close on success and after all workers are done, i.e.,\n\t\t// blockc is closed.\n\t\tclose(errc)\n\t}()\n\n\treturn ch, errc\n}\n\nfunc (c *client) GetWithSerial(ctx context.Context, key string) (\n\tvalue []byte, serial int64, err error) {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn nil, 0, log.Alert(GetWithSerialKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, GetWithSerialRequest{Key: key, Description: _ETCD_GET_W_SERIAL})\n\tctx, cancel := context.WithTimeout(ctx, c.optime)\n\tdefer cancel()\n\tresp, err := kv.Get(ctx, key)\n\tif err != nil {\n\t\treturn nil, 0, log.Alert(GetWithSerialError{Key: key, Err: err,\n\t\t\tDescription: _ETCD_GET_W_SERIAL_DB_FAIL})\n\t}\n\tlog.Debug(ctx, GetWithSerialResponse{Response: resp, Description: _ETCD_GET_W_SERIAL_REQ_OK})\n\n\tif len(resp.Kvs) == 0 {\n\t\treturn nil, 0, storage.NotExistError{Key: key,\n\t\t\tErr: GetWithSerialMissingKeyError{Description: _ETCD_GET_NONE}}\n\t}\n\tvalue = resp.Kvs[0].Value\n\tserial = resp.Kvs[0].Version\n\treturn\n}\n\n// etcd does not have an iterating API so getRange queries data in blocks using\n// WithRange and WithLimit. Through testing on a real-life setup we determined\n// 6144 to be a good balance between speed and query time. Note that this\n// number was reached when using an OpTimeout of 10 seconds. If a smaller\n// timeout is used, then this probably needs to be reduced as well.\nconst blockSize = 6144\n\n// getRange sends the keys in range [from, to) that were created in or before\n// revision rev on ch in blocks of blockSize. It sends the results in blocks so\n// that it does not have to wait sending individual keys and can fetch the next\n// block while the previous one is being processed.\nfunc (c *client) getRange(ctx context.Context, kv clientv3.KV,\n\tch chan<- *clientv3.GetResponse, from, to string) error {\n\n\t// Although etcd provides a WithMaxCreateRev option so that we would\n\t// get only keys that existed at a given time, it slows down requests\n\t// drastically. Rather, accept that the results can contain some keys\n\t// that did not exist when getRange was called.\n\tops := []clientv3.OpOption{\n\t\tclientv3.WithRange(to),\n\t\tclientv3.WithLimit(blockSize),\n\t}\n\tvar n int // Keep count of keys retrieved by this thread for debugging.\n\tkey := from\n\tfor {\n\t\t// Get the next block of key-values.\n\t\tlog.Debug(ctx, GetRangeRequest{Key: key, Range: to, Description: _ETCD_GET_RANGE})\n\t\topctx, cancel := context.WithTimeout(ctx, c.optime)\n\t\tresp, err := kv.Get(opctx, key, ops...)\n\t\tcancel() // We do not want to defer in a loop.\n\t\tif err != nil {\n\t\t\treturn log.Alert(GetRangeError{Key: key, Range: to, Err: err,\n\t\t\t\tDescription: _ETCD_GET_RANGE_DB_FAIL})\n\t\t}\n\t\t// Only log the three first and three last values as to not\n\t\t// explode the logs.\n\t\tvar first, last string\n\t\tif len(resp.Kvs) > 0 {\n\t\t\tfirst = string(resp.Kvs[0].Key)\n\t\t\tlast = string(resp.Kvs[len(resp.Kvs)-1].Key)\n\t\t}\n\t\tlog.Debug(ctx, GetRangeResponse{\n\t\t\tHeader:      resp.Header,\n\t\t\tCount:       len(resp.Kvs), // resp.Count was not asked for.\n\t\t\tFirst:       first,\n\t\t\tLast:        last,\n\t\t\tMore:        resp.More,\n\t\t\tDescription: _ETCD_GET_RANGE_REQ_OK,\n\t\t})\n\t\tn += len(resp.Kvs)\n\n\t\tif len(resp.Kvs) == 0 {\n\t\t\tlog.Debug(ctx, GetRangeCount{From: from, To: to, Count: n,\n\t\t\t\tDescription: _ETCD_GET_NONE})\n\t\t\treturn nil // No more keys.\n\t\t}\n\n\t\tselect {\n\t\tcase ch <- resp:\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\t}\n\n\t\t// The next block will be queried from the key immediately\n\t\t// succeeding the last key from this block.\n\t\tkey = nextKey(resp.Kvs[len(resp.Kvs)-1].Key)\n\t}\n}\n\n// nextKey returns the key that lexicographically immediately follows key.\nfunc nextKey(key []byte) string {\n\treturn string(append(key, 0))\n}\n\nfunc (c *client) CAS(ctx context.Context, cas string, old, new []byte) (err error) { //nolint:revive\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(CASKVError{Err: err, Description: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, CASRequest{CAS: cas, Old: old, New: new,\n\t\tDescription: _ETCD_CAS})\n\tresp, err := c.doRetry(ctx, func(ctx context.Context) (*clientv3.TxnResponse, error) {\n\t\treturn kv.Txn(ctx).\n\t\t\tIf(clientv3.Compare(clientv3.Value(cas), \"=\", string(old))).\n\t\t\tThen(clientv3.OpPut(cas, string(new))).\n\t\t\tElse(clientv3.OpGet(cas)).\n\t\t\tCommit()\n\t})\n\tif err != nil {\n\t\treturn log.Alert(CASError{CAS: cas, Err: err,\n\t\t\tDescription: _ETCD_CAS_DB_FAIL})\n\t}\n\tlog.Debug(ctx, CASResponse{Response: resp,\n\t\tDescription: _ETCD_CAS_REQ_OK})\n\n\tif !resp.Succeeded {\n\t\t// Assume resp has the correct amount of Reponses.\n\t\tif kvs := resp.Responses[0].GetResponseRange().GetKvs(); len(kvs) > 0 {\n\t\t\treturn storage.UnexpectedValueError{\n\t\t\t\tKey: cas,\n\t\t\t\tErr: CASValueMismatchError{\n\t\t\t\t\tHave:        string(kvs[0].Value),\n\t\t\t\t\tWant:        string(old),\n\t\t\t\t\tDescription: _ETCD_CAS_CMP,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\treturn storage.NotExistError{\n\t\t\tKey: cas,\n\t\t\tErr: CASMissingCASKeyError{Description: _ETCD_GET_NONE},\n\t\t}\n\t}\n\treturn\n}\n\n// doRetry performs a transaction, automatically retrying up to three times in\n// total if there are transient errors.\n//\n// doRetry takes a function which constructs and commits the transaction. The\n// transaction needs to be reconstructed each time, so that it can use a new\n// context per retry, refreshing the operation timeout each time.\n//\n// Usually clientv3.Txn.Commit retries any operations itself until the context\n// expires or the operation succeeds, but not in case of write requests.\n// However, since all our transactions are reentrant we still wish to retry if\n// the error was caused by the service being currently unavailable: this works\n// around any write errors caused by network issues, leader changes, etc.\nfunc (c *client) doRetry(ctx context.Context, f func(context.Context) (*clientv3.TxnResponse, error)) (\n\tresp *clientv3.TxnResponse, err error) {\n\n\tfor attempt := 1; ; attempt++ {\n\t\trctx, cancel := context.WithTimeout(ctx, c.optime)\n\t\tdefer cancel()\n\t\tif resp, err = f(rctx); err != nil {\n\t\t\tvar code codes.Code\n\t\t\tif rerr, ok := err.(rpctypes.EtcdError); ok {\n\t\t\t\tcode = rerr.Code() // etcd-level error.\n\t\t\t} else if serr, ok := status.FromError(err); ok {\n\t\t\t\tcode = serr.Code() // grpc-level error.\n\t\t\t}\n\t\t\tif attempt < 3 && code == codes.Unavailable {\n\t\t\t\tlog.Log(ctx, RetryingTxn{Attempt: attempt, Err: err,\n\t\t\t\t\tDescription: _ETCD_CONN_RETRY})\n\n\t\t\t\t// Very naive backoff, allowing time to recover.\n\t\t\t\ttime.Sleep(time.Duration(attempt) * 500 * time.Millisecond)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tbreak\n\t}\n\treturn resp, err // Return resp and err from last attempt.\n}\n"
  },
  {
    "path": "common/collector/storage/etcd/log_desc.go",
    "content": "package etcd\n\nconst (\n\t_ETCD_URI                   = \"No etcd nodes URIs provided\"\n\t_ETCD_CFG                   = \"Invalid 'etcd:' section in a YAML configuration file\"\n\t_ETCD_CA                    = \"Failed to parse TLS CA certificate to verify client and peer connections\"\n\t_ETCD_KEY                   = \"Failed to parse etcd node TLS key\"\n\t_ETCD_CERT                  = \"Failed to parse etcd node TLS certificate\"\n\t_ETCD_CERT_VERIFY           = \"Failed to verify etcd node certificate against CA\"\n\t_ETCD_CLIENT_CONN           = \"Client is successfully connected to etcd cluster\"\n\t_ETCD_CLIENT_N_CONN         = \"Client is not connected to etcd cluster\"\n\t_ETCD_PUT                   = \"etcd put request\"\n\t_ETCD_PUT_DB_FAIL           = \"etcd responded with error on put request\"\n\t_ETCD_PUT_REQ_OK            = \"etcd processed put request successfully\"\n\t_ETCD_PUT_EXISTING          = \"Trying to put value into an existing key\"\n\t_ETCD_PUT_ALL               = \"etcd put many values request\"\n\t_ETCD_PUT_ALL_DB_FAIL       = \"etcd responded with error on put many values request\"\n\t_ETCD_PUT_ALL_REQ_OK        = \"etcd processed put many values request successfully\"\n\t_ETCD_PUT_ALL_EXISTING      = \"Trying to put many values into corresponding keys, where at least one of keys already has a value\"\n\t_ETCD_GET                   = \"etcd get request\"\n\t_ETCD_GET_DB_FAIL           = \"etcd responded with error on get request\"\n\t_ETCD_GET_REQ_OK            = \"etcd processed get request successfully\"\n\t_ETCD_GET_NONE              = \"No such a key in etcd\"\n\t_ETCD_GET_ALL               = \"etcd get many values request\"\n\t_ETCD_GET_ALL_DB_FAIL       = \"etcd responded with error on get many values request\"\n\t_ETCD_GET_ALL_REQ_OK        = \"etcd processed get many values request successfully\"\n\t_ETCD_GET_PREFIX            = \"etcd get with prefix ('etcdctl --prefix') request\"\n\t_ETCD_GET_PREFIX_DB_FAIL    = \"etcd responded with error on get with prefix ('etcdctl --prefix') request\"\n\t_ETCD_GET_PREFIX_REQ_OK     = \"etcd processed get with prefix ('etcdctl --prefix') request successfully\"\n\t_ETCD_GET_W_SERIAL          = \"etcd get with serial (serial value of a key is also returned) request\"\n\t_ETCD_GET_W_SERIAL_DB_FAIL  = \"etcd responded with error on get with serial (serial value of a key is also returned) request\"\n\t_ETCD_GET_W_SERIAL_REQ_OK   = \"etcd processed get with serial (serial value of a key is also returned) request successfully\"\n\t_ETCD_GET_RANGE             = \"etcd get range ([from, to)) request\"\n\t_ETCD_GET_RANGE_DB_FAIL     = \"etcd responded with error on get range ([from, to)) request\"\n\t_ETCD_GET_RANGE_REQ_OK      = \"etcd processed get range ([from, to)) request successfully\"\n\t_ETCD_CAS                   = \"etcd compare-and-swap request\"\n\t_ETCD_CAS_DB_FAIL           = \"etcd responded with error on compare-and-swap request\"\n\t_ETCD_CAS_REQ_OK            = \"etcd processed compare-and-swap request successfully\"\n\t_ETCD_CAS_CMP               = \"There are more than one key matched during compare operation\"\n\t_ETCD_CONN_RETRY            = \"etcd is currently unavailable, trying to reconnect\"\n\t_ETCD_LOG_INFO              = \"etcd INFO log\"\n\t_ETCD_LOG_WARN              = \"etcd WARN log\"\n\t_ETCD_LOG_ERROR             = \"etcd ERROR log\"\n\t_ETCD_LOG_FATAL             = \"etcd FATAL log\"\n\t_ETCD_GET_W_LEASE           = \"etcd get with lease ('etcdctl --lease') request\"\n\t_ETCD_GET_W_LEASE_DB_FAIL   = \"etcd responded with error on get with lease ('etcdctl --lease') request\"\n\t_ETCD_GET_W_LEASE_COMMITTED = \"etcd committed get with lease ('etcdctl --lease') request successfully\"\n\t_ETCD_PUT_OPTS              = \"Invalid put request options\"\n\t_ETCD_PUT_LEASE             = \"No --lease provided\"\n\t_ETCD_PUT_LEASE_INT         = \"--lease is invalid base-10 integer\"\n\t_ETCD_PUT_LEASE_TTL         = \"No TTL (sec) provided for lease, i.e. 'etcdctl lease grant [TTL]\"\n\t_ETCD_PUT_LEASE_TTL_INT     = \"TTL is invalid base-10 integer\"\n\t_ETCD_PUT_REQ_COMMITTED     = \"etcd committed put request successfully\"\n\t_ETCD_LEASE_GRANT           = \"'etcdctl lease grant [sec]' failed\"\n\t_ETCD_DELETE                = \"etcd delete request\"\n\t_ETCD_DELETE_DB_FAIL        = \"etcd responded with error on delete request\"\n\t_ETCD_DELETE_REQ_COMMITTED  = \"etcd committed delete request successfully\"\n\t_ETCD_TXN_BEGIN             = \"'etcdctl txn -i' request\"\n\t_ETCD_TXN_FAIL              = \"etcd transaction failed\"\n\t_ETCD_TXN_STOP              = \"etcd transaction either committed or rolled back, that depends on error returned\"\n\t_ETCD_CTX_DONE              = \"etcd has cancelled context\"\n\t_ETCD_AUTOCOMMIT_UNSET      = \"Unset AUTOCOMMIT mode for etcd client connection\"\n\t_ETCD_CAST_REQ              = \"Cannot cast etcd txn request to expected format\"\n\t_ETCD_TXN_START             = \"etcd transaction commit process starts now\"\n\t_ETCD_ROLLBACK              = \"etcd transaction is rolled back\"\n\t_ETCD_COMMIT                = \"etcd transaction is processed by etcd successfully\"\n\t_ETCD_CMP                   = \"etcd transaction is not committed\"\n)\n"
  },
  {
    "path": "common/collector/storage/etcd/logger.go",
    "content": "package etcd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"ivxv.ee/common/collector/log\"\n)\n\n// logger implements the clientv3.Logger interface.\ntype logger struct {\n\tctx context.Context\n}\n\nfunc newLogger(ctx context.Context) *logger {\n\treturn &logger{ctx}\n}\n\nfunc (l *logger) info(msg string) {\n\tlog.Log(l.ctx, ClientInfo{Message: msg, Description: _ETCD_LOG_INFO})\n}\n\nfunc (l *logger) warning(msg string) {\n\tlog.Log(l.ctx, ClientWarning{Message: msg, Description: _ETCD_LOG_WARN})\n}\n\nfunc (l *logger) error(msg string) {\n\tlog.Error(l.ctx, ClientError{Err: errors.New(msg), Description: _ETCD_LOG_ERROR})\n}\n\nfunc (l *logger) fatal(msg string) {\n\tlog.Error(l.ctx, ClientFatal{Err: log.Alert(errors.New(msg)), Description: _ETCD_LOG_FATAL})\n}\n\nfunc (l *logger) Info(a ...interface{})                    { l.info(fmt.Sprint(a...)) }\nfunc (l *logger) Infoln(a ...interface{})                  { l.info(fmt.Sprintln(a...)) }\nfunc (l *logger) Infof(format string, a ...interface{})    { l.info(fmt.Sprintf(format, a...)) }\nfunc (l *logger) Warning(a ...interface{})                 { l.warning(fmt.Sprint(a...)) }\nfunc (l *logger) Warningln(a ...interface{})               { l.warning(fmt.Sprintln(a...)) }\nfunc (l *logger) Warningf(format string, a ...interface{}) { l.warning(fmt.Sprintf(format, a...)) }\nfunc (l *logger) Error(a ...interface{})                   { l.error(fmt.Sprint(a...)) }\nfunc (l *logger) Errorln(a ...interface{})                 { l.error(fmt.Sprintln(a...)) }\nfunc (l *logger) Errorf(format string, a ...interface{})   { l.error(fmt.Sprintf(format, a...)) }\nfunc (l *logger) Fatal(a ...interface{})                   { l.fatal(fmt.Sprint(a...)) }\nfunc (l *logger) Fatalf(format string, a ...interface{})   { l.fatal(fmt.Sprintf(format, a...)) }\nfunc (l *logger) Fatalln(a ...interface{})                 { l.fatal(fmt.Sprintln(a...)) }\nfunc (l *logger) V(_ int) bool                             { return true }\n"
  },
  {
    "path": "common/collector/storage/etcd/status.go",
    "content": "package etcd\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n)\n\nconst (\n\temptyLeaseID = \"\"\n\tbase10       = 10\n)\n\nfunc (c *client) GetWithLease(ctx context.Context, key string) ([]byte, string, error) {\n\t// Get etcd key-value store. It is a same as an etcd client,\n\t// just with restricted operations, mostly CRUD-only\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn nil, emptyLeaseID, log.Alert(GetWithLeaseKVError{Err: err,\n\t\t\tDescription: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, GetWithLeaseRequest{Key: key, Description: _ETCD_GET_W_LEASE})\n\n\t// If etcd doesn't respond, then don't hung longer than c.optime\n\tctx, cancel := context.WithTimeout(ctx, c.optime)\n\tdefer cancel()\n\n\t// Send GET query\n\tresp, err := kv.Get(ctx, key)\n\tif err != nil {\n\t\treturn nil, emptyLeaseID, log.Alert(GetWithLeaseError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _ETCD_GET_W_LEASE_DB_FAIL,\n\t\t})\n\t}\n\n\t// There is no such a key in etcd\n\tif resp.Kvs == nil {\n\t\tlog.Debug(ctx, GetWithLeaseEmptyResponse{Key: key,\n\t\t\tDescription: _ETCD_GET_NONE})\n\t\treturn nil, emptyLeaseID, nil\n\t}\n\n\t// Convert from int64 to UTF-8 string.\n\t// This helps to prevent any numerical casting issues,\n\t// it is probably safer to hold numerical values in a UTF-8 string\n\tleaseID := strconv.FormatInt(resp.Kvs[0].Lease, base10)\n\n\tlog.Debug(ctx, GetWithLeaseResponse{\n\t\tResponse:    resp,\n\t\tKey:         key,\n\t\tValue:       resp.Kvs[0].Value,\n\t\tLeaseID:     leaseID,\n\t\tDescription: _ETCD_GET_W_LEASE_COMMITTED,\n\t})\n\n\treturn resp.Kvs[0].Value, leaseID, err\n}\n\nfunc (c *client) PutForceWithOpts(ctx context.Context, key string, value []byte,\n\topts interface{}) (err error) {\n\t// Currently only TTL option is expected as opts,\n\t// however nothing stops you from switch opts.(type) {...}\n\tputOptsWithTTL, ok := opts.(*storage.PutOpOptionWithTTL)\n\tif !ok {\n\t\treturn log.Alert(PutForceWithOptsCastToPutOpOptionsWithTTLError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _ETCD_PUT_OPTS,\n\t\t})\n\t}\n\n\t// Get etcd client key-value store\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(PutForceWithOptsKVError{Err: err,\n\t\t\tDescription: _ETCD_GET_NONE})\n\t}\n\n\t// Ensure that LeaseID is not \"\", otherwise Go will panic at strconv.ParseInt\n\tif putOptsWithTTL.LeaseID == \"\" {\n\t\tlog.Debug(ctx, PutForceWithOptsEmptyLeaseID{\n\t\t\tKey:         key,\n\t\t\tValue:       value,\n\t\t\tLeaseID:     putOptsWithTTL.LeaseID,\n\t\t\tTTL:         putOptsWithTTL.TTL,\n\t\t\tDescription: _ETCD_PUT_LEASE,\n\t\t})\n\n\t\tputOptsWithTTL.LeaseID = \"0\"\n\t}\n\n\t// LeaseID is at least \"0\"\n\tleaseID, err := strconv.ParseInt(putOptsWithTTL.LeaseID, 10, 64)\n\tif err != nil {\n\t\treturn log.Alert(PutForceWithOptsConvertLeaseIDToInt64Error{\n\t\t\tLeaseID:     putOptsWithTTL.LeaseID,\n\t\t\tDescription: _ETCD_PUT_LEASE_INT,\n\t\t})\n\t}\n\n\t// Ensure that TTL is not \"\", otherwise Go will panic at strconv.ParseInt\n\tif putOptsWithTTL.TTL == \"\" {\n\t\tlog.Debug(ctx, PutForceWithOptsEmptyTTL{\n\t\t\tKey:         key,\n\t\t\tValue:       value,\n\t\t\tLeaseID:     putOptsWithTTL.LeaseID,\n\t\t\tTTL:         putOptsWithTTL.TTL,\n\t\t\tDescription: _ETCD_PUT_LEASE_TTL,\n\t\t})\n\n\t\tputOptsWithTTL.TTL = \"0\"\n\t}\n\n\t// TTL is \"0\" if not set, which means that value will disappear immediately\n\tttl, err := strconv.ParseInt(putOptsWithTTL.TTL, 10, 64)\n\tif err != nil {\n\t\treturn log.Alert(PutForceWithOptsConvertTTLToInt64Error{\n\t\t\tLeaseID:     putOptsWithTTL.LeaseID,\n\t\t\tDescription: _ETCD_PUT_LEASE_TTL_INT,\n\t\t})\n\t}\n\n\tlog.Debug(ctx, PutForceWithOptsRequest{\n\t\tKey:         key,\n\t\tValue:       value,\n\t\tLeaseID:     putOptsWithTTL.LeaseID,\n\t\tTTL:         ttl,\n\t\tDescription: _ETCD_PUT,\n\t})\n\n\tresp, err := c.putForceWithOpts(ctx, kv, storage.PutAllRequestWithTTL{\n\t\tKey:     key,\n\t\tValue:   value,\n\t\tTTL:     ttl,\n\t\tLeaseID: leaseID,\n\t})\n\tif err != nil || !resp.Succeeded {\n\t\treturn log.Alert(PutForceWithOptsError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _ETCD_PUT_DB_FAIL,\n\t\t})\n\t}\n\n\tlog.Debug(ctx, PutForceWithOptsResponse{\n\t\tResponse:    resp,\n\t\tSuccess:     resp.Succeeded,\n\t\tDescription: _ETCD_PUT_REQ_COMMITTED,\n\t})\n\treturn\n}\n\nfunc (c *client) putForceWithOpts(ctx context.Context, kv clientv3.KV,\n\treq storage.PutAllRequestWithTTL) (\n\tresp *clientv3.TxnResponse, err error) {\n\tvar ops []clientv3.Op\n\n\t// Translate int64 to clientv3.LeaseID\n\tvar leaseID clientv3.LeaseID\n\tleaseID = clientv3.LeaseID(req.LeaseID)\n\n\t// If LeaseID doesn't exist for a key\n\tif leaseID == 0 {\n\t\t// If etcd doesn't respond, then don't hung longer than c.optime\n\t\tctx1, cancel1 := context.WithTimeout(ctx, c.optime)\n\n\t\t// Create new LeaseID for a key with an expiration time of req.TTL seconds\n\t\tlease, err := c.cli.Grant(ctx1, req.TTL)\n\t\tcancel1()\n\t\tif err != nil {\n\t\t\treturn nil, GrantNewLeaseIDError{\n\t\t\t\tErr:         err,\n\t\t\t\tKey:         req.Key,\n\t\t\t\tDescription: _ETCD_LEASE_GRANT,\n\t\t\t}\n\t\t}\n\t\tleaseID = lease.ID\n\t}\n\n\t// Create Lease OpOption for a transaction\n\tleaseOp := clientv3.WithLease(leaseID)\n\n\t// Transaction will include only PUT with TTL query\n\tops = append(ops, clientv3.OpPut(req.Key, string(req.Value), leaseOp))\n\n\t// doRetry will wrap ctx to context.WithTimeout(ctx)\n\treturn c.doRetry(ctx, func(ctx context.Context) (*clientv3.TxnResponse, error) {\n\t\treturn kv.Txn(ctx).Then(ops...).Commit()\n\t})\n}\n\nfunc (c *client) Delete(ctx context.Context, key string) error {\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(DeleteKVError{Err: err,\n\t\t\tDescription: _ETCD_CLIENT_N_CONN})\n\t}\n\n\tlog.Debug(ctx, DeleteRequest{Key: key, Description: _ETCD_DELETE})\n\n\t// Delete a key\n\tresp, err := c.delete(ctx, kv, key)\n\tif err != nil || !resp.Succeeded {\n\t\treturn log.Alert(DeleteError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _ETCD_DELETE_DB_FAIL,\n\t\t})\n\t}\n\n\tlog.Debug(ctx, DeleteResponse{\n\t\tResponse:    resp,\n\t\tSuccess:     resp.Succeeded,\n\t\tDescription: _ETCD_DELETE_REQ_COMMITTED,\n\t})\n\treturn nil\n}\n\nfunc (c *client) delete(ctx context.Context, kv clientv3.KV, key string) (\n\tresp *clientv3.TxnResponse, err error) {\n\tvar ops []clientv3.Op\n\n\t// Transaction will include only DELETE query\n\tops = append(ops, clientv3.OpDelete(key))\n\n\t// doRetry will wrap ctx to context.WithTimeout(ctx)\n\treturn c.doRetry(ctx, func(ctx context.Context) (*clientv3.TxnResponse, error) {\n\t\treturn kv.Txn(ctx).Then(ops...).Commit()\n\t})\n}\n"
  },
  {
    "path": "common/collector/storage/etcd/txn.go",
    "content": "package etcd\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\t\"go.etcd.io/etcd/client/v3/clientv3util\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n)\n\nconst expectedCastForTxnOp = \"*txnOp\"\n\n// txnOp is an implementation for a transaction.\ntype txnOp struct {\n\tcmps   []clientv3.Cmp\n\tthens  []clientv3.Op\n\telses  []clientv3.Op\n\treadyc chan bool\n\terrorc chan error\n}\n\n// Begin a transaction with lazy initialization.\nfunc (c *client) Begin(ctx context.Context) (storage.TxnOp, error) {\n\tlog.Debug(ctx, BeginTxn{Description: _ETCD_TXN_BEGIN})\n\n\treturn &txnOp{\n\t\treadyc: make(chan bool, 1),\n\t\terrorc: make(chan error, 1),\n\t}, nil\n}\n\n// Ready sends bool over the ready chan and waits for an error\n// on the error chan.\nfunc (c *txnOp) Ready(ctx context.Context) error {\n\t// Send true to readyc\n\tc.readyc <- true\n\n\t// Once true is sent to readyc, start listening on errorc and ctx.Done.\n\t// It is also possible that by this time there is already an answer on\n\t// errorc. This is possible since errorc is a buffered channel.\n\tselect {\n\tcase err := <-c.errorc:\n\t\tlog.Debug(ctx, ReadyReceivedOnErrorChannel{Err: err, Description: _ETCD_TXN_FAIL})\n\t\treturn err\n\tcase <-ctx.Done():\n\t\treturn log.Alert(ReadyContextCancelled{Description: _ETCD_TXN_STOP})\n\t}\n}\n\n// AutoCommit waits for a bool on the ready chan and once received,\n// attempts to Commit and send an error over the error chan.\nfunc (c *client) AutoCommit(ctx context.Context, op storage.TxnOp) {\n\t// Don't check casting success here, instead work as a mediator and\n\t// just send op as it is to the Commit method\n\tunit := op.(*txnOp)\n\n\t// Move this function to the background (daemon).\n\t// Once select {...} block is passed - finish the daemon\n\tgo func() {\n\t\tdefer close(unit.readyc)\n\t\tdefer close(unit.errorc)\n\n\t\t// Listen on readyc. It is possible that by this time there is already\n\t\t// an answer present. It is possible, since readyc is buffered channel\n\t\tselect {\n\t\tcase <-unit.readyc:\n\t\t\tunit.errorc <- c.Commit(ctx, op)\n\t\t\tlog.Debug(ctx, AutoCommitSentOnErrorc{Description: _ETCD_TXN_STOP})\n\t\tcase <-ctx.Done():\n\t\t\t// AutoCommit doesn't act on context deadline exceeded error,\n\t\t\t// instead it just exits the function\n\t\t\tlog.Debug(ctx, AutoCommitContextCancelled{Description: _ETCD_CTX_DONE})\n\t\t}\n\n\t\tlog.Debug(ctx, AutoCommitClose{Description: _ETCD_AUTOCOMMIT_UNSET})\n\t\t// Close readyc and errorc channels\n\t}()\n}\n\n// Commit a transaction.\nfunc (c *client) Commit(ctx context.Context, op storage.TxnOp) error {\n\t// Get etcd key-value store. It is a same as an etcd client,\n\t// just with restricted operations, mostly CRUD-only\n\tkv, err := c.kv(ctx)\n\tif err != nil {\n\t\treturn log.Alert(BeginKVError{Err: err,\n\t\t\tDescription: _ETCD_CLIENT_N_CONN})\n\t}\n\n\t// Must cast to *txnOp\n\tunit, ok := op.(*txnOp)\n\tif !ok {\n\t\treturn log.Alert(CommitCastToTxnOpError{\n\t\t\tExpected:    expectedCastForTxnOp,\n\t\t\tGot:         reflect.TypeOf(op),\n\t\t\tDescription: _ETCD_CAST_REQ,\n\t\t})\n\t}\n\n\tlog.Debug(ctx, CommitRequest{Description: _ETCD_TXN_START})\n\n\t// Include all If, Then, Else OpOptions to the Transaction COMMIT operation.\n\t// \"All or nothing!\" means that etcd either applies all changes or none of them\n\tresp, err := c.doRetry(ctx, func(ctx context.Context) (*clientv3.TxnResponse, error) {\n\t\treturn kv.Txn(ctx).\n\t\t\tIf(unit.cmps...).\n\t\t\tThen(unit.thens...).\n\t\t\tElse(unit.elses...).\n\t\t\tCommit()\n\t})\n\tif err != nil {\n\t\treturn log.Alert(CommitError{Err: err,\n\t\t\tDescription: _ETCD_ROLLBACK})\n\t}\n\n\tlog.Debug(ctx, CommitResponse{\n\t\tResponse:    resp,\n\t\tSuccess:     resp.Succeeded,\n\t\tDescription: _ETCD_COMMIT,\n\t})\n\n\tif !resp.Succeeded {\n\t\treturn storage.UnexpectedValueError{\n\t\t\tErr: CommitResponseError{Response: resp, Success: resp.Succeeded,\n\t\t\t\tDescription: _ETCD_CMP}}\n\t}\n\treturn nil\n}\n\n// If adds cmp into a list of predicates. List of predicates is said to be\n// true if and only if all predicates are true, otherwise false.\nfunc (c *txnOp) If(cmp clientv3.Cmp) {\n\tc.cmps = append(c.cmps, cmp)\n}\n\n// Then adds then into a list of operations, that are executed if list of\n// predicates is true.\nfunc (c *txnOp) Then(then clientv3.Op) {\n\tc.thens = append(c.thens, then)\n}\n\n// Else adds elze into a list of operations, that are executed if list of\n// predicates is false.\nfunc (c *txnOp) Else(elze clientv3.Op) {\n\tc.elses = append(c.elses, elze)\n}\n\n// Put adds key-value into a buffer to be further committed.\n// Note, that only if each key is unique, value is added,\n// due to KeyMissing policy.\nfunc (c *txnOp) Put(key string, value []byte) {\n\tc.If(clientv3util.KeyMissing(key))\n\tc.Then(clientv3.OpPut(key, string(value)))\n\tc.Else(clientv3.OpGet(key))\n}\n\n// PutAll calls Put on each req in reqs.\nfunc (c *txnOp) PutAll(reqs ...*storage.PutAllRequest) {\n\tfor _, req := range reqs {\n\t\tc.Put(req.Key, req.Value)\n\t}\n}\n\n// PutForce adds key-value into a buffer without utilizing If and Else.\nfunc (c *txnOp) PutForce(key string, value []byte) {\n\tc.Then(clientv3.OpPut(key, string(value)))\n}\n\n// CAS adds key-value into a buffer to be further committed.\n// Note, that only if key's current value equals to old,\n// then new value is added to key.\nfunc (c *txnOp) CAS(key string, old, new []byte) { //nolint:revive\n\tc.If(clientv3.Compare(clientv3.Value(key), \"=\", string(old)))\n\tc.Then(clientv3.OpPut(key, string(new)))\n\tc.Else(clientv3.OpGet(key))\n}\n"
  },
  {
    "path": "common/collector/storage/file/file.go",
    "content": "//go:development\n/*\nPackage file implements a storage protocol which read and writes data from the\nlocal filesystem.\n\nThe keys are Base64-encoded and stored as files in the working directory.\n\nNote! This protocol does not provide any synchronization between concurrent\nusers and is not suitable for production. It should only be used for testing.\n*/\npackage file\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tstorage.Register(storage.File, func(n yaml.Node, _ *storage.Services) (\n\t\ts storage.PutGetter, err error) {\n\n\t\tvar f F\n\t\tif err = yaml.Apply(n, &f); err != nil {\n\t\t\treturn nil, ConfigurationError{Err: err}\n\t\t}\n\t\tif err = os.MkdirAll(f.WD, 0770); err != nil {\n\t\t\treturn nil, WorkingDirectoryError{Path: f.WD, Err: err}\n\t\t}\n\t\treturn f, nil\n\t})\n}\n\n// F implements the file storage protocol.\ntype F struct {\n\tWD string // All keys are stored in this working directory.\n}\n\n// Put implements the storage.PutGetter interface.\nfunc (f F) Put(_ context.Context, key string, value []byte) (err error) {\n\tfp, err := os.OpenFile(filepath.Join(f.WD, encode(key)),\n\t\tos.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)\n\tif err != nil {\n\t\tif os.IsExist(err) {\n\t\t\treturn storage.ExistError{Key: key, Err: PutExistingKeyError{Err: err}}\n\t\t}\n\t\treturn log.Alert(PutKeyError{Key: key, Err: err})\n\t}\n\tdefer func() {\n\t\tif cerr := fp.Close(); cerr != nil && err == nil {\n\t\t\terr = log.Alert(PutCloseError{Key: key, Err: cerr})\n\t\t}\n\t}()\n\n\tif _, err = fp.Write(value); err != nil {\n\t\treturn log.Alert(PutValueError{Key: key, Err: err})\n\t}\n\treturn\n}\n\n// Get implements the storage.PutGetter interface.\nfunc (f F) Get(_ context.Context, key string) (value []byte, err error) {\n\tif value, err = os.ReadFile(filepath.Join(f.WD, encode(key))); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, storage.NotExistError{Key: key, Err: GetMissingKeyError{Err: err}}\n\t\t}\n\t\treturn nil, log.Alert(GetError{Key: key, Err: err})\n\t}\n\treturn\n}\n\n// GetWithPrefix implements the storage.PutGetter interface.\nfunc (f F) GetWithPrefix(ctx context.Context, prefix string) (\n\t<-chan storage.GetWithPrefixResult, <-chan error) {\n\n\tc := make(chan storage.GetWithPrefixResult)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(errc)\n\t\tdefer close(c)\n\n\t\td, err := os.Open(f.WD)\n\t\tif err != nil {\n\t\t\terrc <- log.Alert(GetWithPrefixOpenWDError{Err: err})\n\t\t\treturn\n\t\t}\n\t\tdefer d.Close()\n\n\t\tfor {\n\t\t\t// Read next set of keys. Do not read all at once in\n\t\t\t// case we have a lot of keys.\n\t\t\tkeys, err := d.Readdirnames(128) // n chosen arbitrarily.\n\t\t\tswitch err {\n\t\t\tcase nil:\n\t\t\tcase io.EOF:\n\t\t\t\treturn // No more keys.\n\t\t\tdefault:\n\t\t\t\terrc <- log.Alert(GetWithPrefixReadDirError{Err: err})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Get the values for matching keys and stream them to\n\t\t\t// the channel.\n\t\t\tfor _, key := range keys {\n\t\t\t\tvar r storage.GetWithPrefixResult\n\t\t\t\tif r.Key, err = decode(key); err != nil {\n\t\t\t\t\terrc <- log.Alert(GetWithPrefixDecodeError{\n\t\t\t\t\t\tKey: key,\n\t\t\t\t\t\tErr: err,\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif !strings.HasPrefix(r.Key, prefix) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif r.Value, err = f.Get(ctx, r.Key); err != nil {\n\t\t\t\t\terrc <- log.Alert(GetWithPrefixGetError{\n\t\t\t\t\t\tKey: key,\n\t\t\t\t\t\tErr: err,\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase c <- r:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn c, errc\n}\n\n// GetWithSerial implements the storage.PutGetter interface, however serial is always 1.\nfunc (f F) GetWithSerial(_ context.Context, key string) (value []byte, serial int64, err error) {\n\tif value, err = os.ReadFile(filepath.Join(f.WD, encode(key))); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, 0, storage.NotExistError{Key: key, Err: GetWithSerialMissingKeyError{Err: err}}\n\t\t}\n\t\treturn nil, 0, log.Alert(GetWithSerialError{Key: key, Err: err})\n\t}\n\tserial = 1\n\treturn\n}\n\n// CAS implements the storage.PutGetter interface.\n//\n// Note! This method is not actually synchronized.\nfunc (f F) CAS(_ context.Context, cas string, old, new []byte) (err error) { //nolint:revive\n\n\t// Read the old value and compare it to the expected one.\n\tfp, err := os.OpenFile(filepath.Join(f.WD, encode(cas)), os.O_RDWR, 0)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn storage.NotExistError{\n\t\t\t\tKey: cas,\n\t\t\t\tErr: CASMissingCASKeyError{Err: err},\n\t\t\t}\n\t\t}\n\t\treturn log.Alert(CASKeyError{Key: cas, Err: err})\n\t}\n\tdefer func() {\n\t\tif cerr := fp.Close(); cerr != nil && err == nil {\n\t\t\terr = log.Alert(CASCloseError{Key: cas, Err: err})\n\t\t}\n\t}()\n\n\tvalue, err := io.ReadAll(fp)\n\tif err != nil {\n\t\treturn log.Alert(CASReadValueError{Key: cas, Err: err})\n\t}\n\tif !bytes.Equal(value, old) {\n\t\treturn storage.UnexpectedValueError{\n\t\t\tKey: cas,\n\t\t\tErr: CASValueMismatchError{\n\t\t\t\tHave:     value,\n\t\t\t\tExpected: old,\n\t\t\t}}\n\t}\n\n\t// Set the new value.\n\tif _, err = fp.WriteAt(new, 0); err != nil {\n\t\treturn log.Alert(CASWriteValueError{Err: err})\n\t}\n\tif err = fp.Truncate(int64(len(new))); err != nil {\n\t\treturn log.Alert(CASTruncateValueError{Err: err})\n\t}\n\treturn\n}\n\nfunc encode(key string) string {\n\treturn base64.URLEncoding.EncodeToString([]byte(key))\n}\n\nfunc decode(key string) (string, error) {\n\tb, err := base64.URLEncoding.DecodeString(key)\n\treturn string(b), err\n}\n"
  },
  {
    "path": "common/collector/storage/log_desc.go",
    "content": "package storage\n\nconst (\n\t_STORAGE_BATCH_GET_ALL    = \"Failed to get all keys by batches from a database\"\n\t_STORAGE_PUT_DIST_EXIST   = \"Failed to put existing districts into a database\"\n\t_STORAGE_GET              = \"Failed to get a key from a database\"\n\t_STORAGE_PUT_PROGRESS     = \"Progress of putting a value into a database in a batch\"\n\t_STORAGE_PUT              = \"Failed to put a key into a database\"\n\t_STORAGE_PUT_EXISTS       = \"Putting key into a database failed, because key already exists\"\n\t_STORAGE_BATCH_RACE       = \"Race condition check, ensure that value that we got in a batch, is still in a database\"\n\t_STORAGE_WRAP_EXISTS      = \"Wraps errors which are caused by a 'key already existing error'\"\n\t_STORAGE_WRAP_N_EXISTS    = \"Wraps errors which are caused by a 'key not existing error'\"\n\t_STORAGE_WRAP_CAS         = \"Wraps errors which are caused by compare-and-swap encountering an unexpected old value\"\n\t_STORAGE_PROT             = \"Unknown storage protocol\"\n\t_STORAGE_PROT_CFG         = \"Failed to configure storage service\"\n\t_STORAGE_PUT_GET_FAIL     = \"Key was already in a database mismatches the key we just attempted to put into a database\"\n\t_STORAGE_UPDATE           = \"Failed to update (replace) a value in a database for a given key\"\n\t_STORAGE_CAS_UNEXPECTED   = \"There are more than one key matched during compare operation\"\n\t_STORAGE_COUNTY           = \"Districts list contains 'counties' ID for district, however district should only have numerical ID\"\n\t_STORAGE_PUT_DIST         = \"Failed to put districts into a database\"\n\t_STORAGE_VER              = \"Districts list contains 'version' ID for district, however district should only have numerical ID\"\n\t_STORAGE_PUT_ENSURE       = \"Failed to ensure that value exists in a database\"\n\t_STORAGE_DECODE_PAIR      = \"Pair expects only two values\"\n\t_STORAGE_GET_OK           = \"Successfully got value from a database\"\n\t_STORAGE_CHOICES_VER      = \"Choices list contains 'version' ID for choice, however choice should only have numerical ID\"\n\t_STORAGE_EXPECT_INITIAL   = \"Voters list version should not be uploaded before IVXV initialization\"\n\t_STORAGE_INITIAL_VER      = \"When uploading non-initial voters list into database, the old one (at least initial) should already exist there\"\n\t_STORAGE_VER_MISMATCH     = \"Unexpected voters list version in a database\"\n\t_STORAGE_BDOC_VER         = \"Failed to fetch voters list .bdoc container version from a database\"\n\t_STORAGE_VOTERS_VER       = \"No such 'version' voter expected in a voters list\"\n\t_STORAGE_VOTERS_PREV      = \"No such 'previous' voter expected in a voters list\"\n\t_STORAGE_CAS              = \"Failed to perform compare-and-swap with a given value against a database\"\n\t_STORAGE_DELETED_VOTER    = \"Voter doesn't exist in a voters list\"\n\t_STORAGE_GET_N_EXISTS     = \"Key doesn't exist in a database\"\n\t_STORAGE_TS_PARSE         = \"Failed to parse a timestamp into RFC3339 (nano precision)\"\n\t_STORAGE_DET_STATS_IDX    = \"Detail statistics should always have more unique votes recorded than 'votesorder' service SeqNo, since detail statistics are stored first\"\n\t_STORAGE_TXN              = \"Database transaction error\"\n\t_STORAGE_VOTESORDER       = \"Successfully update votesorder table in a database\"\n\t_STORAGE_RATE_BYTES       = \"Rate value (how many times voter has voted) should be exactly 8 bytes\"\n\t_STORAGE_INCOMPLETE_VOTE  = \"Incomplete vote\"\n\t_STORAGE_VOTEID_FROM_KEY  = \"Failed to parse vote ID from an etcd key\"\n\t_STORAGE_UNKNOWN_ETCD_KEY = \"Unknown etcd key\"\n\t_STORAGE_RATE_VERIF_BYTES = \"Rate value of verifications (how many times voter has verified a vote) should be exactly 8 bytes\"\n\t_STORAGE_KEY_PREX         = \"Database key has no '/vote/' prefix\"\n\t_STORAGE_KEY_PREX_SLASH   = \"Database key has no '/' between '/vote' and 'key'\"\n\t_STORAGE_KEY_PREX_KEY     = \"Database key has no key ('/vote/[key]')\"\n\t_STORAGE_TXN_PUT          = \"Successfully added value into a database transaction\"\n\t_STORAGE_TXN_FINISHING    = \"Finishing database transaction\"\n\t_STORAGE_TXN_READY        = \"Database transaction is failed to commit\"\n\t_STORAGE_TXN_COMMITTED    = \"Database transaction is successfully committed\"\n\t_STORAGE_TEST_VOTE        = \"Test vote\"\n)\n"
  },
  {
    "path": "common/collector/storage/memory/memory.go",
    "content": "//go:development\n/*\nPackage memory implements a storage protocol which stores everything in memory,\nused for testing.\n\nThe contents of the memory storage protocol can either be initialized via the\nNew method or through the configuration when using the storage protocol\nregistry interface.\n\nThe configuration is a simple YAML mapping with string values which will be\nused as the initial contents. As an exception, when a string value is wrapped\nwith \"base64(\" and \")\", then the contents will be Base64 decoded during\ninitialization. This way we can use raw literals in most cases, which is useful\nfor testing, but still have a way for specifying raw byte values. For example:\n\n\t/some/string/value: foobar\n\t/some/byte/value:   base64(Zm9vYmFy)\n*/\npackage memory\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\nfunc init() {\n\tstorage.Register(storage.Memory, func(n yaml.Node, _ *storage.Services) (\n\t\ts storage.PutGetter, err error) {\n\n\t\tm := New(nil)\n\n\t\t// Only apply if n is non-nil, otherwise the map we just made\n\t\t// in New will be replaced by nil.\n\t\tif n != nil {\n\t\t\tif err = yaml.Apply(n, &m.db); err != nil {\n\t\t\t\treturn nil, ConfigurationApplicationError{Err: err}\n\t\t\t}\n\t\t}\n\t\tfor k, v := range m.db {\n\t\t\tif m.db[k], err = decode(v); err != nil {\n\t\t\t\treturn nil, DecodeValueError{Key: k, Err: err}\n\t\t\t}\n\t\t}\n\t\treturn m, nil\n\t})\n}\n\n// decode checks if s has a \"base64(\" prefix and a \")\" suffix and Base64\n// decodes the inner content. If the prefix or suffix is not found, then s is\n// returned unmodified.\nfunc decode(s string) (string, error) {\n\tconst prefix, suffix = \"base64(\", \")\"\n\tt := strings.TrimSpace(s)\n\tif strings.HasPrefix(t, prefix) && strings.HasSuffix(t, suffix) {\n\t\tb, err := base64.StdEncoding.DecodeString(t[len(prefix) : len(t)-len(suffix)])\n\t\treturn string(b), err\n\t}\n\treturn s, nil\n}\n\n// M maps keys to values in the memory storage protocol.\ntype M struct {\n\tdb   map[string]string\n\tlock sync.Mutex\n}\n\n// New creates a new M with the provided initial contents.\nfunc New(initial map[string]string) *M {\n\tm := &M{db: make(map[string]string)}\n\tfor k, v := range initial {\n\t\tm.db[k] = v\n\t}\n\treturn m\n}\n\n// Put implements the storage.PutGetter interface.\nfunc (m *M) Put(_ context.Context, key string, value []byte) error {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\tif _, ok := m.db[key]; ok {\n\t\treturn storage.ExistError{Key: key, Err: PutExistingKeyError{}}\n\t}\n\tm.db[key] = string(value)\n\treturn nil\n}\n\n// Get implements the storage.PutGetter interface.\nfunc (m *M) Get(_ context.Context, key string) ([]byte, error) {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\tvalue, ok := m.db[key]\n\tif !ok {\n\t\treturn nil, storage.NotExistError{Key: key, Err: GetMissingKeyError{}}\n\t}\n\treturn []byte(value), nil\n}\n\n// GetWithPrefix implements the storage.PutGetter interface. GetWithPrefix\n// blocks all other operations on M until the last result is read.\nfunc (m *M) GetWithPrefix(ctx context.Context, prefix string) (\n\t<-chan storage.GetWithPrefixResult, <-chan error) {\n\n\tm.lock.Lock()\n\tc := make(chan storage.GetWithPrefixResult)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer m.lock.Unlock()\n\t\tdefer close(errc)\n\t\tdefer close(c)\n\t\tfor k, v := range m.db {\n\t\t\tif strings.HasPrefix(k, prefix) {\n\t\t\t\tselect {\n\t\t\t\tcase c <- storage.GetWithPrefixResult{Key: k, Value: []byte(v)}:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn c, errc\n}\n\n// GetWithSerial implements the storage.PutGetter interface, howeever serial is always 1.\nfunc (m *M) GetWithSerial(_ context.Context, key string) ([]byte, int64, error) {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\tvalue, ok := m.db[key]\n\tif !ok {\n\t\treturn nil, 0, storage.NotExistError{Key: key, Err: GetWithSerialMissingKeyError{}}\n\t}\n\treturn []byte(value), 1, nil\n}\n\n// CAS implements the storage.PutGetter interface.\nfunc (m *M) CAS(_ context.Context, cas string, old, new []byte) error { //nolint:revive\n\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tvalue, ok := m.db[cas]\n\tif !ok {\n\t\treturn storage.NotExistError{Key: cas, Err: CASMissingCASKeyError{}}\n\t}\n\tif value != string(old) {\n\t\treturn storage.UnexpectedValueError{\n\t\t\tKey: cas,\n\t\t\tErr: CASValueMismatchError{\n\t\t\t\tHave:     value,\n\t\t\t\tExpected: string(old),\n\t\t\t}}\n\t}\n\tm.db[cas] = string(new)\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/storage/registry.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// Protocol identifies a storage protocol. The actual protocol client\n// implementations are in other packages.\ntype Protocol string\n\n// Enumeration of storage protocols.\nconst (\n\tMemory Protocol = \"memory\"\n\tFile   Protocol = \"file\"\n\tEtcd   Protocol = \"etcd\"\n)\n\n// Here we \"declare\" error types, but instead of defining them ourselves, we\n// want them to be generated so that they implement all the extra interfaces of\n// generated errors.\n//\n// Although these are errors in themselves, they still all nest an error to\n// uniquely specify where the nesting error came from. So you would use these\n// like\n//\n//\treturn storage.ExistError{Key: key, Err: ExistingKeyError{}}\n//\n// where ExistingKeyError will specify the package that returned the error.\nvar (\n\t// ExistError wraps errors which are caused by a key already existing.\n\t_ = ExistError{Key: \"\", Err: nil, Description: _STORAGE_WRAP_EXISTS}\n\n\t// NotExistError wraps errors which are caused by a key not existing.\n\t_ = NotExistError{Key: \"\", Err: nil, Description: _STORAGE_WRAP_N_EXISTS}\n\n\t// UnexpectedValueError wraps errors which are caused by\n\t// compare-and-swap encountering an unexpected old value.\n\t_ = UnexpectedValueError{Key: \"\", Err: nil, Description: _STORAGE_WRAP_CAS}\n)\n\n// GetWithPrefixResult is a single result from PutGetter.GetWithPrefix.\ntype GetWithPrefixResult struct {\n\tKey   string\n\tValue []byte\n}\n\n// PutGetter is the interface that must be implemented by storage protocol\n// clients and is used by Client as the underlying protocol. By design, it does\n// not have methods for deleting or overwriting values (except for CASAndGet),\n// because we want to keep everything we store.\ntype PutGetter interface {\n\t// Put puts value into the storage service with the given key. It is an\n\t// error for the key to already exist. Implementations must obey\n\t// cancellation signals from ctx.Done().\n\tPut(ctx context.Context, key string, value []byte) error\n\n\t// Get returns the value stored with the given key. It is an error for\n\t// the key to not exist. Implementations must obey cancellation signals\n\t// from ctx.Done().\n\tGet(ctx context.Context, key string) ([]byte, error)\n\n\t// GetWithPrefix returns the stored keys and values where the key has\n\t// the provided prefix.\n\t//\n\t// The result channel should be read until closed after which the error\n\t// channel should be read to determine if the first one was closed due\n\t// to completion or an error. If reading from the result channel is\n\t// stopped early, then the context should be cancelled to stop\n\t// GetWithPrefix from sending more results.\n\t//\n\t// Implementations must ensure that at least one (possibly nil) error\n\t// can be read from the error channel after the result channel is\n\t// closed. This can be done by simply closing the channel.\n\t//\n\t// Implementations must obey cancellation signals from ctx.Done().\n\tGetWithPrefix(ctx context.Context, prefix string) (<-chan GetWithPrefixResult, <-chan error)\n\n\t// CAS compare-and-swaps the value stored with the key cas. This is the\n\t// only method which can overwrite values and is only meant for read\n\t// counters.\n\t//\n\t// Implementations must obey cancellation signals from ctx.Done().\n\tCAS(ctx context.Context, cas string, old, new []byte) error //nolint:revive\n\n\t// GetWithSerial returns a value and a serial number for the given key.\n\t// Serial number for the given key follows the same idea as SQL SERIAL.\n\t//\n\t// If error occurs then serial == 0, otherwise serial >= 1.\n\tGetWithSerial(ctx context.Context, key string) ([]byte, int64, error)\n}\n\n// PutAllRequest is a single key-value pair to put in a Batcher.PutAll request.\ntype PutAllRequest struct {\n\tKey   string\n\tValue []byte\n}\n\n// Batcher is an optional interface that PutGetters can implement for batch\n// processing. Storage operations will prefer methods in this interface when\n// working on multiple keys at once.\ntype Batcher interface {\n\tPutGetter\n\n\t// BatchSize returns the maximum number of operations that can be\n\t// batched by this protocol. Other Batcher methods return errors if\n\t// more than BatchSize operations are given.\n\tBatchSize() int\n\n\t// GetAll returns all the values stored with the given keys. If a key\n\t// is missing, then it will not be included in the returned map: it is\n\t// the caller's responsibility to ensure that all required keys are\n\t// returned.\n\t//\n\t// Implementations must obey cancellation signals from ctx.Done().\n\tGetAll(ctx context.Context, keys ...string) (map[string][]byte, error)\n\n\t// PutAll puts all the key-value pairs into the storage service. It is\n\t// an error for ANY key to exist. On error, no values will be stored.\n\t// Implementations must obey cancellation signals from ctx.Done().\n\tPutAll(ctx context.Context, reqs ...PutAllRequest) error\n}\n\n// NewFunc is the type of functions that create a storage protocol client with\n// a provided configuration and service instance information.\ntype NewFunc func(yaml.Node, *Services) (PutGetter, error)\n\nvar (\n\treglock  sync.RWMutex\n\tregistry = make(map[Protocol]NewFunc)\n)\n\n// Register registers a storage protocol client implementation. It is intended\n// to be called from init functions of packages that implement storage protocol\n// clients.\n//\n// n is a constructor function used to create a new storage protocol client\n// with provided configuration.\nfunc Register(p Protocol, n NewFunc) {\n\treglock.Lock()\n\tdefer reglock.Unlock()\n\tregistry[p] = n\n}\n"
  },
  {
    "path": "common/collector/storage/status.go",
    "content": "package storage\n\nimport (\n\t\"context\"\n)\n\n// PutAllRequestWithTTL is a data structure used to pass a data to a storage\n// implementation (DAO).\n// Storage implementation should parse this struct to extract necessary fields.\ntype PutAllRequestWithTTL struct {\n\tKey     string\n\tValue   []byte\n\tTTL     int64\n\tLeaseID int64\n}\n\n// PutOpOptionWithTTL is an opts used in PutGetterWithOpts interface's\n// PutForceWithOpts method.\ntype PutOpOptionWithTTL struct {\n\t// LeaseID is not a TTL, instead it is an ID of a database key lease.\n\t// Database key lease is kind of a ticket, that is valid for TTL period,\n\t// once TTL period is over, ticket is not valid.\n\tLeaseID string\n\n\t// TTL is an additional parameter that could be used to set brand-new\n\t// database key lease with an expiration time of TTL.\n\t// TTL unit is a second.\n\tTTL string\n}\n\n// PutGetterWithOpts is an extension of the fundamental PutGetter\n// interface. Implementing these methods is completely optional,\n// because features like Lease (value with TTL) may be enabled in one\n// storage implementation, but be absent in another.\ntype PutGetterWithOpts interface {\n\t// GetWithLease should get a key from a storage along with a\n\t// TTL value assigned to that key (0 if not assigned)\n\tGetWithLease(ctx context.Context, key string) ([]byte, string, error)\n\n\t// PutForceWithOpts not only puts value into key without any\n\t// conditional checks, e.g. \"insert only if absent, etc.),\n\t// but also allows to pass custom options opts to the underlying\n\t// implementation, which may parse it according to its own rules.\n\t//\n\t// As an example, you can pass TTL to the opts.\n\tPutForceWithOpts(ctx context.Context, key string, value []byte, opts interface{}) error\n\n\t// Delete a key from a storage permanently.\n\tDelete(ctx context.Context, key string) error\n}\n\n// SessionStatusRepository is an interface that all Status services should\n// implement!\n//\n// To recall, Status service is a service that just reports\n// something. That something depends on the Status server implementation,\n// e.g. some Status server can report SessionID lifetime, others can\n// report OS used in a system, etc.\ntype SessionStatusRepository interface {\n\tPutGetterWithOpts\n}\n\n// SessionStatusRepository is a factory method to return a\n// session status interface for the repository interaction.\nfunc (c *Client) SessionStatusRepository() SessionStatusRepository {\n\treturn c.prot.(SessionStatusRepository)\n}\n"
  },
  {
    "path": "common/collector/storage/storage.go",
    "content": "/*\nPackage storage provides common code for communicating with storage services.\n*/\npackage storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\n// timefmt is the time format used to store timestamps in storage.\nconst timefmt = time.RFC3339Nano\n\n// Conf is the storage service client protocol configuration.\ntype Conf struct {\n\tProtocol     Protocol  // The protocol which the client must implement.\n\tConf         yaml.Node // Protocol-specific configuration.\n\tOrderTimeout int64\n}\n\n// Services contains necessary information about the storage client and server\n// service instances not part of the client protocol configuration.\ntype Services struct {\n\t// Sensitive is the path to the client service directory which can\n\t// contain sensitive storage client information that can not be passed\n\t// through the configuration, e.g., authentication credentials.\n\tSensitive string\n\n\t// Servers are the addresses that the storage client protocol will\n\t// connect to if using a networked storage service.\n\t//\n\t// This is not part of the client protocol configuration to avoid\n\t// having to duplicate these addresses from the network configuration.\n\t// Instead it is the callers responsibility to retrieve these addresses\n\t// from there.\n\tServers []string\n}\n\n// Client is used to access the storage service.\ntype Client struct {\n\tprot         PutGetter // The underlying protocol.\n\torderTimeout int64\n}\n\n// New initializes a new storage service client with the provided configuration\n// and service instance information.\nfunc New(c *Conf, services *Services) (client *Client, err error) {\n\treglock.RLock()\n\tdefer reglock.RUnlock()\n\tn, ok := registry[c.Protocol]\n\tif !ok {\n\t\treturn nil, UnlinkedProtocolError{Protocol: c.Protocol,\n\t\t\tDescription: _STORAGE_PROT}\n\t}\n\tclient = new(Client)\n\tif client.prot, err = n(c.Conf, services); err != nil {\n\t\treturn nil, ConfigureProtocolError{Protocol: c.Protocol, Err: err,\n\t\t\tDescription: _STORAGE_PROT_CFG}\n\t}\n\tif c.OrderTimeout == 0 {\n\t\tclient.orderTimeout = 5\n\t} else {\n\t\tclient.orderTimeout = c.OrderTimeout\n\t}\n\n\treturn\n}\n\n// NewWithProtocol initializes a new storage service client with the provided\n// underlying protocol implementation.\n//\n// Using New with a proper configuration is preferred to NewWithProtocol, but\n// the latter can be useful for testing.\nfunc NewWithProtocol(protocol PutGetter) *Client {\n\treturn &Client{prot: protocol}\n}\n\n// ensure attempts to put value into the storage service with the given key,\n// but if the key already exists, then checks if it already has the specified\n// value.\n//\n// Note that ensure does this in the exact order documented: attempts to put\n// first and then checks. This is because it expects the put to generally\n// succeed and avoids unnecessary gets.\nfunc (c *Client) ensure(ctx context.Context, key string, value []byte) error {\n\tswitch err := c.prot.Put(ctx, key, value); {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.CausedBy(err, new(ExistError)) == nil:\n\t\treturn EnsurePutError{Err: err, Description: _STORAGE_PUT}\n\t}\n\n\texisting, err := c.prot.Get(ctx, key)\n\tif err != nil {\n\t\treturn EnsureGetExistingError{Err: err, Description: _STORAGE_GET}\n\t}\n\n\tif !bytes.Equal(existing, value) {\n\t\treturn EnsureExistingMismatchError{\n\t\t\tExisting:    existing,\n\t\t\tNew:         value,\n\t\t\tDescription: _STORAGE_PUT_GET_FAIL,\n\t\t}\n\t}\n\treturn nil\n}\n\n// update updates the value of key. It first attempts to put the value, but if\n// the key already exists, then compare-and-swaps in a loop until it succeeds.\n//\n// value is a function which must return the updated value to use. It is\n// first called with a nil argument for the put request, and then called with\n// existing values of key if compare-and-swaps are required instead. If value\n// returns nil, then update cancels and keeps the existing value.\n//\n// Warning: Updating values is not something that the PutGetter interface and\n// storage package in general was designed for. Use this function sparingly,\n// being very mindful of the values being overwritten.\nfunc (c *Client) update(ctx context.Context,\n\tkey string, value func(existing []byte) ([]byte, error)) error {\n\n\tv, err := value(nil)\n\tif err != nil {\n\t\treturn UpdateInitialValueError{Err: err, Description: _STORAGE_UPDATE}\n\t}\n\n\tswitch err := c.prot.Put(ctx, key, v); {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.CausedBy(err, new(ExistError)) == nil:\n\t\treturn UpdatePutError{Err: err, Description: _STORAGE_PUT}\n\t}\n\n\tfor {\n\t\texisting, err := c.prot.Get(ctx, key)\n\t\tif err != nil {\n\t\t\treturn UpdateGetExistingError{Err: err, Description: _STORAGE_GET}\n\t\t}\n\n\t\tif v, err = value(existing); err != nil {\n\t\t\treturn UpdateNewValueError{Err: err, Description: _STORAGE_UPDATE}\n\t\t}\n\t\tif v == nil {\n\t\t\treturn nil // Keep the existing value as-is.\n\t\t}\n\n\t\tswitch err := c.prot.CAS(ctx, key, existing, v); {\n\t\tcase err == nil:\n\t\t\treturn nil\n\t\tcase errors.CausedBy(err, new(UnexpectedValueError)) == nil:\n\t\t\treturn UpdateCASError{Err: err, Description: _STORAGE_CAS_UNEXPECTED}\n\t\t}\n\t}\n}\n\nconst (\n\tdistrictsPrefix = \"/districts/\"\n\tcountiesKey     = \"counties\"\n\tversionKey      = \"version\"\n)\n\n// PutDistricts stores the district list, i.e., map from administrative unit\n// codes and district numbers to district identifiers, with the given version\n// string. The administrative unit codes and district numbers must be encoded\n// using EncodeAdminDistrict for use as map keys. The district identifiers act\n// as choices list identifiers.\n//\n// PutDistricts also stores the counties list, i.e., how the administrative\n// units from the district list are organized into counties. Since this list is\n// only used by other applications as a whole and not needed by the storage\n// package internally, its format is not specified and the list is simply\n// stored as a serialized byte array.\n//\n// Progress of the operation is reported to progress as well as logged\n// periodically.\nfunc (c *Client) PutDistricts(ctx context.Context, version string,\n\tdistricts map[string][]byte, counties []byte, progress status.Add) (err error) {\n\n\t// Check if districts are already stored.\n\toldver, err := c.GetDistrictsVersion(ctx)\n\tswitch {\n\tcase err == nil:\n\t\treturn PutDistrictsExistsError{Version: oldver,\n\t\t\tDescription: _STORAGE_PUT_DIST_EXIST}\n\tcase errors.CausedBy(err, new(NotExistError)) == nil:\n\t\treturn PutDistrictsCheckExistingError{Err: err, Description: _STORAGE_GET}\n\t}\n\n\t// Ensure that no IDs clash with our counties or version key.\n\tif _, ok := districts[countiesKey]; ok {\n\t\treturn PutDistrictsCountiesIDNotAllowedError{Description: _STORAGE_COUNTY}\n\t}\n\tif _, ok := districts[versionKey]; ok {\n\t\treturn PutDistrictsVersionIDNotAllowedError{Description: _STORAGE_VER}\n\t}\n\n\tif err = c.putAll(ctx, districtsPrefix, districts, true, progress); err != nil {\n\t\treturn PutDistrictsError{Err: err, Description: _STORAGE_PUT_DIST}\n\t}\n\tif err = c.ensure(ctx, districtsPrefix+countiesKey, counties); err != nil {\n\t\treturn PutDistrictsCountiesError{Err: err, Description: _STORAGE_PUT_ENSURE}\n\t}\n\n\t// Store the district list version.\n\tif err = c.prot.Put(ctx, districtsPrefix+versionKey, []byte(version)); err != nil {\n\t\treturn PutDistrictsVersionError{Err: err, Description: _STORAGE_PUT}\n\t}\n\treturn\n}\n\n// GetCounties retrieves the serialized counties list.\nfunc (c *Client) GetCounties(ctx context.Context) (counties []byte, err error) {\n\tif counties, err = c.prot.Get(ctx, districtsPrefix+countiesKey); err != nil {\n\t\terr = GetCountiesError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn\n}\n\n// GetDistrictsVersion retrieves the district list version string.\nfunc (c *Client) GetDistrictsVersion(ctx context.Context) (version string, err error) {\n\tvb, err := c.prot.Get(ctx, districtsPrefix+versionKey)\n\tversion = string(vb)\n\tif err != nil {\n\t\terr = GetDistrictsVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn\n}\n\n// EncodeAdminDistrict encodes an administrative unit code and district number\n// to the form used by the storage package. Although an internal detail of the\n// storage package, this function is exported for use with Client methods which\n// optimize batch operations by having callers pre-encode values.\nfunc EncodeAdminDistrict(adminCode, district string) []byte {\n\treturn encodePair(adminCode, district)\n}\n\nfunc encodePair(first, second string) []byte {\n\tn := len(first)\n\tif n > 255 {\n\t\tpanic(fmt.Sprintf(\"data too large: %d > 255\", n))\n\t}\n\tencoded := make([]byte, 1, 1+n+len(second))\n\tencoded[0] = byte(n)\n\tencoded = append(encoded, first...)\n\tencoded = append(encoded, second...)\n\treturn encoded\n}\n\nfunc decodePair(encoded []byte) (first, second string, err error) {\n\tif len(encoded) == 0 || len(encoded) < 1+int(encoded[0]) {\n\t\treturn \"\", \"\", EncodedPairLengthError{Len: len(encoded),\n\t\t\tDescription: _STORAGE_DECODE_PAIR}\n\t}\n\tn := encoded[0]\n\treturn string(encoded[1 : 1+n]), string(encoded[1+n:]), nil\n}\n\nconst (\n\tchoicesPrefix = \"/choices/\"\n\t// versionKey is already defined.\n)\n\n// PutChoices stores the map of choices lists with the given version string.\n// Progress of the operation is reported to progress as well as logged\n// periodically.\nfunc (c *Client) PutChoices(ctx context.Context, version string,\n\tchoices map[string][]byte, progress status.Add) (err error) {\n\n\t// Check if a choices list is already stored.\n\toldver, err := c.GetChoicesVersion(ctx)\n\tswitch {\n\tcase err == nil:\n\t\treturn PutChoicesExistsError{Version: oldver, Description: _STORAGE_GET_OK}\n\tcase errors.CausedBy(err, new(NotExistError)) == nil:\n\t\treturn PutChoicesCheckExistingError{Err: err, Description: _STORAGE_GET}\n\t}\n\n\t// Ensure that no IDs clash with our version key.\n\tif _, ok := choices[versionKey]; ok {\n\t\treturn PutChoicesVersionIDNotAllowedError{Description: _STORAGE_CHOICES_VER}\n\t}\n\n\tif err = c.putAll(ctx, choicesPrefix, choices, true, progress); err != nil {\n\t\treturn PutChoicesError{Err: err, Description: _STORAGE_PUT}\n\t}\n\n\t// Set the marker that choices lists were successfully stored.\n\tif err = c.prot.Put(ctx, choicesPrefix+versionKey, []byte(version)); err != nil {\n\t\treturn PutChoicesSetDoneError{Err: err, Description: _STORAGE_PUT}\n\t}\n\treturn\n}\n\n// GetChoices retrieves the choices list with the given identifier.\nfunc (c *Client) GetChoices(ctx context.Context, choices string) (list []byte, err error) {\n\tif list, err = c.prot.Get(ctx, choicesPrefix+choices); err != nil {\n\t\terr = GetChoicesError{Choices: choices, Err: err, Description: _STORAGE_GET}\n\t}\n\treturn\n}\n\n// GetChoicesVersion retrieves the choices list version string.\nfunc (c *Client) GetChoicesVersion(ctx context.Context) (version string, err error) {\n\tvb, err := c.prot.Get(ctx, choicesPrefix+versionKey)\n\tversion = string(vb)\n\tif err != nil {\n\t\terr = GetChoicesVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn\n}\n\nconst (\n\tvotersPrefix = \"/voters/\"\n\tpreviousKey  = \"previous\"\n)\n\n// PutVoters stores a new version of the voters list, i.e., map from voter\n// identifiers to administrative unit codes and district numbers where the\n// voters are assigned. The administrative unit codes and district numbers must\n// be encoded using EncodeAdminDistrict.\n//\n// A non-initial voter list acts as an overlay to the previous version of the\n// list: if a voter is not specified in the version, then their entry from the\n// previous version will persist, recursively. A voter with an empty value is\n// considered a deletion of that voter from the list and will be reported as\n// not existing.\n//\n// Voters lists have two different version strings attached to them: the\n// container versions and the list version. The former is a concatenation of\n// the versions of signed containers used to import voters list changes. The\n// latter is a version number identifying the actual contents of the voters\n// list. Therefore if the same changes are imported but in different signed\n// containers, then the container versions will differ but the list version\n// will not.\n//\n// The container versions are used to provide feedback about which signed\n// containers were used to build the voters list. The list version is used to\n// compare actual contents of two different lists.\n//\n// PutVoters concatenates cversion to the current container version and\n// compare-and-swaps the list version from oldver to newver, unless oldver is\n// empty, in which case it creates a new version file.\n//\n// Progress of the operation is reported to progress as well as logged\n// periodically.\nfunc (c *Client) PutVoters(ctx context.Context, cversion string, voters map[string][]byte,\n\toldver, newver string, progress status.Add) (err error) {\n\n\tconst vkey = votersPrefix + versionKey\n\tprefix := versionPrefix(newver)\n\n\t// Do an early check of the old version.\n\tinitial := len(oldver) == 0\n\tversion, err := c.GetVotersListVersion(ctx)\n\tnotexist := errors.CausedBy(err, new(NotExistError)) != nil\n\n\t// During an initial load, we expect the version to not exist.\n\tif initial && !notexist {\n\t\tif err != nil {\n\t\t\t// We encountered some other error.\n\t\t\treturn PutVotersCheckVersionExistsError{Err: err, Description: _STORAGE_GET}\n\t\t}\n\t\treturn PutVotersVersionExistsError{Version: version, Description: _STORAGE_EXPECT_INITIAL}\n\t}\n\n\tvar oldcver string\n\tif !initial {\n\t\t// When putting a new list version, the old one must exist and\n\t\t// match expected.\n\t\tif err != nil {\n\t\t\treturn PutVotersGetVersionError{Err: err, Description: _STORAGE_INITIAL_VER}\n\t\t}\n\t\tif version != oldver {\n\t\t\t// We cannot use a struct literal, because gen would\n\t\t\t// report it as a duplicate error type.\n\t\t\tvar err UnexpectedValueError\n\t\t\terr.Key = vkey\n\t\t\terr.Err = PutVotersVersionMismatchError{\n\t\t\t\tVersion:     version,\n\t\t\t\tExpected:    oldver,\n\t\t\t\tDescription: _STORAGE_VER_MISMATCH,\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\t// Store a reference to the previous list version.\n\t\tif err = c.ensure(ctx, prefix+previousKey, []byte(oldver)); err != nil {\n\t\t\treturn PutVotersPreviousVersionError{Version: newver, Err: err,\n\t\t\t\tDescription: _STORAGE_PUT_ENSURE}\n\t\t}\n\n\t\t// Get the current container versions to concatenate to.\n\t\toldcver, err = c.getCVersion(ctx, version)\n\t\tif err != nil {\n\t\t\treturn PutVotersGetContainerVersionsError{\n\t\t\t\tListVersion: version,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _STORAGE_BDOC_VER,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Ensure that no voters clash with our version or previous key.\n\tif _, ok := voters[versionKey]; ok {\n\t\treturn PutVotersVersionKeyNotAllowedError{Description: _STORAGE_VOTERS_VER}\n\t}\n\tif _, ok := voters[previousKey]; ok {\n\t\treturn PutVotersPreviousKeyNotAllowedError{Description: _STORAGE_VOTERS_PREV}\n\t}\n\n\t// Create a new voters list version.\n\tif err = c.putAll(ctx, prefix, voters, true, progress); err != nil {\n\t\treturn PutVotersError{Version: newver, Err: err, Description: _STORAGE_PUT}\n\t}\n\tif err = c.ensure(ctx, prefix+versionKey, []byte(oldcver+cversion+\"\\n\")); err != nil {\n\t\treturn PutVotersContainerVersionsError{Version: newver, Err: err, Description: _STORAGE_PUT_ENSURE}\n\t}\n\n\t// Either create or CAS the new version number.\n\tif initial {\n\t\tif err = c.prot.Put(ctx, vkey, []byte(newver)); err != nil {\n\t\t\terr = PutVotersNewVersionError{Version: newver, Err: err, Description: _STORAGE_PUT}\n\t\t}\n\t\treturn\n\t}\n\n\tif err = c.prot.CAS(ctx, vkey, []byte(oldver), []byte(newver)); err != nil {\n\t\terr = PutVotersCASVersionError{Old: oldver, New: newver, Err: err, Description: _STORAGE_CAS}\n\t}\n\treturn\n}\n\n// GetVoter returns the administrative unit code and district number of the\n// requested voter for the specified voter list version.\nfunc (c *Client) GetVoter(ctx context.Context, version, voter string) (\n\tadminCode, district string, err error) {\n\n\tencoded, err := c.getVoter(ctx, version, voter)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tif adminCode, district, err = decodePair(encoded); err != nil {\n\t\terr = log.Alert(GetVoterParseEntryError{\n\t\t\tVoter:       voter,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_DECODE_PAIR,\n\t\t})\n\t}\n\treturn adminCode, district, err\n}\n\n// getVoter performs the internal operation of GetVoter without decoding the\n// voter entry. Avoids decode-encode round-trip if used within this package.\nfunc (c *Client) getVoter(ctx context.Context, version, voter string) (encoded []byte, err error) {\n\t// Recursively walk back through version until we find the voter or hit\n\t// the beginning.\n\tcurrentver := version\n\tfor {\n\t\tprefix := versionPrefix(currentver)\n\t\tencoded, err = c.prot.Get(ctx, prefix+voter)\n\t\tswitch {\n\t\tcase err == nil: // We found an entry for the voter.\n\t\t\tif len(encoded) == 0 { // The voter has been deleted.\n\t\t\t\tvar ne NotExistError\n\t\t\t\tne.Key = prefix + voter\n\t\t\t\tne.Err = GetVoterDeletedError{\n\t\t\t\t\tVersion:     version,\n\t\t\t\t\tVoter:       voter,\n\t\t\t\t\tDeletedAt:   currentver,\n\t\t\t\t\tDescription: _STORAGE_DELETED_VOTER,\n\t\t\t\t}\n\t\t\t\treturn nil, ne\n\t\t\t}\n\t\t\treturn encoded, err // Existing voter.\n\n\t\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\t\tprevb, verr := c.prot.Get(ctx, prefix+previousKey)\n\t\t\tswitch {\n\t\t\tcase verr == nil: // Try the previous version.\n\t\t\t\tcurrentver = string(prevb)\n\t\t\t\tcontinue\n\t\t\tcase errors.CausedBy(verr, new(NotExistError)) != nil:\n\t\t\t\t// We are at the beginning: the voter was not found.\n\t\t\t\tvar ne NotExistError\n\t\t\t\tne.Key = versionPrefix(version) + voter\n\t\t\t\tne.Err = GetVoterNotFoundError{\n\t\t\t\t\tVersion:     version,\n\t\t\t\t\tVoter:       voter,\n\t\t\t\t\tDescription: _STORAGE_GET_N_EXISTS,\n\t\t\t\t}\n\t\t\t\treturn nil, ne\n\t\t\t}\n\t\t\terr = verr\n\t\t}\n\t\treturn nil, GetVoterError{\n\t\t\tVersion:     version,\n\t\t\tVoter:       voter,\n\t\t\tStep:        currentver,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_GET,\n\t\t}\n\t}\n}\n\n// VoterChoices returns the the current voter list version and the identifier\n// of voter's choices list, i.e., the district identifier.\n//\n// If the voter belongs to a foreign administrative unit, then foreignAdminCode\n// is used as the administrative unit code to look up their voting district.\nfunc (c *Client) VoterChoices(ctx context.Context, voter, foreignAdminCode string) (\n\tversion, choices string, err error) {\n\n\tif version, err = c.GetVotersListVersion(ctx); err != nil {\n\t\treturn \"\", \"\", VoterChoicesVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\tadminDistrict, err := c.getVoter(ctx, version, voter)\n\tif err != nil {\n\t\treturn \"\", \"\", VoterChoicesError{Err: err, Description: _STORAGE_GET}\n\t}\n\n\t// It is kind of ugly to handle foreign voter encoding in the storage\n\t// package, but this is the best way if we want to support the foreign\n\t// admin code changing after lists are imported.\n\tconst foreignPrefix = \"\\x07FOREIGN\"\n\tif bytes.HasPrefix(adminDistrict, []byte(foreignPrefix)) {\n\t\tdistrict := string(adminDistrict[len(foreignPrefix):])\n\t\tadminDistrict = EncodeAdminDistrict(foreignAdminCode, district)\n\t}\n\n\tchoicesb, err := c.prot.Get(ctx, districtsPrefix+string(adminDistrict))\n\tif err != nil {\n\t\treturn \"\", \"\", VoterChoicesDistrictError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn version, string(choicesb), nil\n}\n\n// VoterChoicesByVersion is similar to VoterChoices with the only difference,\n// it uses a version to return an identifier of voter's choices list\nfunc (c *Client) VoterChoicesByVersion(ctx context.Context, version, voter, foreignAdminCode string) (\n\tstring, error) {\n\n\tadminDistrict, err := c.getVoter(ctx, version, voter)\n\tif err != nil {\n\t\treturn \"\", VoterChoicesByVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\n\t// It is kind of ugly to handle foreign voter encoding in the storage\n\t// package, but this is the best way if we want to support the foreign\n\t// admin code changing after lists are imported.\n\tconst foreignPrefix = \"\\x07FOREIGN\"\n\tif bytes.HasPrefix(adminDistrict, []byte(foreignPrefix)) {\n\t\tdistrict := string(adminDistrict[len(foreignPrefix):])\n\t\tadminDistrict = EncodeAdminDistrict(foreignAdminCode, district)\n\t}\n\n\tdistrictID, err := c.prot.Get(ctx, districtsPrefix+string(adminDistrict))\n\tif err != nil {\n\t\treturn \"\", VoterChoicesByVersionDistrictError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn string(districtID), nil\n}\n\n// GetVotersContainerVersions returns the container versions of the current\n// voters list.\nfunc (c *Client) GetVotersContainerVersions(ctx context.Context) (cversion string, err error) {\n\tversion, err := c.GetVotersListVersion(ctx)\n\tif err != nil {\n\t\treturn \"\", GetVotersContainerVersionsVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\tcversion, err = c.getCVersion(ctx, version)\n\tif err != nil {\n\t\terr = GetVotersContainerVersionsError{\n\t\t\tListVersion: version,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_BDOC_VER,\n\t\t}\n\t}\n\treturn\n}\n\n// GetVotersListVersion returns the list version of the current voters list.\nfunc (c *Client) GetVotersListVersion(ctx context.Context) (version string, err error) {\n\tversionb, err := c.prot.Get(ctx, votersPrefix+versionKey)\n\tif err != nil {\n\t\terr = GetVotersListVersionError{Err: err, Description: _STORAGE_GET}\n\t}\n\treturn string(versionb), err\n}\n\n// getCVersion is a helper method used to get the voters list container\n// versions of a list version.\nfunc (c *Client) getCVersion(ctx context.Context, version string) (cversion string, err error) {\n\tcvb, err := c.prot.Get(ctx, versionPrefix(version)+versionKey)\n\treturn string(cvb), err\n}\n\nfunc versionPrefix(version string) string {\n\treturn votersPrefix + version + \"/\"\n}\n\nconst (\n\tvotedStatsPrefix         = \"/voted/stats/\"\n\tvotedLatestPrefix        = \"/voted/latest/\"\n\tvotesStatsPrefix         = \"/votes/stats\"\n\tvotesStatsPerVoterPrefix = \"/votes/voter/stats/\"\n\tvotesPrefix              = \"/votes/order/\"\n\tvoterIDKey               = \"voterid\"\n\tvoterNameKey             = \"votername\"\n\tadmincodeKey             = \"admincode\"\n\tdistrictKey              = \"district\"\n)\n\n// SetVoted notifies storage that voteID was successful, meaning all configured\n// qualifying properties for it are received and stored. ctime is the canonical\n// time of the vote, which is usually the time of a qualifying property, e.g.,\n// vote registration timestamp.\n//\n// Deprecated: use TxnSetVoted instead.\nfunc (c *Client) SetVoted(ctx context.Context, voteID []byte, voterName string, ctime time.Time, testVote bool) error {\n\t// voteID was successful: refresh indexes related to the voter. Keep in\n\t// mind that voteID is not guaranteed to be the latest vote from the\n\t// voter, so be careful when updating the information.\n\n\t// Get vote metadata in a single batch call.\n\tidVoterKey := voteIDPrefix(voteID) + voterKey     // Voter assigned to voteID.\n\tidVersionKey := voteIDPrefix(voteID) + versionKey // Effective voter list version.\n\tdata, err := c.getAllStrict(ctx, idVoterKey, idVersionKey)\n\tif err != nil {\n\t\treturn SetVotedGetAllError{VoteID: voteID, Err: err, Description: _STORAGE_GET}\n\t}\n\n\t// Get the administrative unit code of voter in voter list version.\n\tidVoter := string(data[idVoterKey])\n\tidAdminCode, district, err := c.GetVoter(ctx, string(data[idVersionKey]), idVoter)\n\tswitch {\n\tcase err == nil:\n\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\t// The voter has voted, yet is not in the voter list. Assume\n\t\t// that the voting service is ignoring voter lists (see\n\t\t// ivxv.ee/common/collector/conf.Election.IgnoreVoterList). Use an empty\n\t\t// administrative unit code.\n\t\tidAdminCode = \"\"\n\t\tdistrict = \"\"\n\tdefault:\n\t\treturn SetVotedGetVoterError{Voter: idVoter, Err: err, Description: _STORAGE_GET}\n\t}\n\n\tctimeStr := ctime.Format(timefmt)\n\tif !testVote { // Only count non-test votes in statistics.\n\t\t// Update the voted stats index: if the administrative unit codes\n\t\t// match, then use the older timestamp; if they differ, then use the\n\t\t// newer timestamp. See GetVotedStats for the reasoning behind this.\n\t\tvotedStatsValue := encodePair(idAdminCode, ctimeStr)\n\t\tif err := c.update(ctx, votedStatsPrefix+idVoter, func(existing []byte) ([]byte, error) {\n\t\t\tif existing == nil {\n\t\t\t\treturn votedStatsValue, nil // Initial value.\n\t\t\t}\n\t\t\tif bytes.Equal(votedStatsValue, existing) {\n\t\t\t\treturn nil, nil // Fast path if already same value.\n\t\t\t}\n\n\t\t\toldAdminCode, oldTimeStr, err := decodePair(existing)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, log.Alert(SetVotedStatsDecodeError{\n\t\t\t\t\tVoter:       idVoter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_DECODE_PAIR,\n\t\t\t\t})\n\t\t\t}\n\t\t\toldTime, err := time.Parse(timefmt, oldTimeStr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, log.Alert(SetVotedStatsParseTimeError{\n\t\t\t\t\tVoter:       idVoter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_TS_PARSE,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase idAdminCode == oldAdminCode && ctime.After(oldTime):\n\t\t\t\treturn nil, nil // Keep older vote in same admin.\n\t\t\tcase idAdminCode != oldAdminCode && ctime.Before(oldTime):\n\t\t\t\treturn nil, nil // Keep newer vote in different admin.\n\t\t\tdefault:\n\t\t\t\treturn votedStatsValue, nil // Update the index.\n\t\t\t}\n\t\t}); err != nil {\n\t\t\treturn SetVotedUpdateStatsError{Err: err, Description: _STORAGE_UPDATE}\n\t\t}\n\t\t// voterName is empty when rebuilding voted stats\n\t\tif voterName != \"\" {\n\t\t\terr = c.AddVoteOrder(ctx, voterName, idVoter, district, idAdminCode)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Update the voted latest index: store the vote identifier with the\n\t// newer timestamp, unless they match in which case corrupt the\n\t// identifier, since we have no way of ordering the two votes.\n\tvotedLatestValue := encodePair(ctimeStr, string(voteID))\n\tif err := c.update(ctx, votedLatestPrefix+idVoter, func(existing []byte) ([]byte, error) {\n\t\tif existing == nil {\n\t\t\treturn votedLatestValue, nil // Initial value.\n\t\t}\n\t\tif bytes.Equal(votedLatestValue, existing) {\n\t\t\treturn nil, nil // Fast path if already same value.\n\t\t}\n\n\t\toldTimeStr, oldVoteID, err := decodePair(existing)\n\t\tif err != nil {\n\t\t\treturn nil, log.Alert(SetVotedLatestDecodeError{Voter: idVoter, Err: err,\n\t\t\t\tDescription: _STORAGE_DECODE_PAIR})\n\t\t}\n\t\toldTime, err := time.Parse(timefmt, oldTimeStr)\n\t\tif err != nil {\n\t\t\treturn nil, log.Alert(SetVotedLatestParseOldTimeError{Voter: idVoter, Err: err,\n\t\t\t\tDescription: _STORAGE_TS_PARSE})\n\t\t}\n\n\t\tswitch {\n\t\tcase oldTime.After(ctime):\n\t\t\treturn nil, nil // Keep newer vote.\n\t\tcase oldTime.Equal(ctime):\n\t\t\t// Corrupt the index until a newer vote comes in. Keep\n\t\t\t// both vote identifiers for manual debugging.\n\t\t\tvoteID = encodePair(string(voteID), oldVoteID)\n\t\t\treturn encodePair(ctimeStr, string(voteID)), nil\n\t\tdefault:\n\t\t\treturn votedLatestValue, nil // Update the index.\n\t\t}\n\t}); err != nil {\n\t\treturn SetVotedUpdateLatestError{Err: err, Description: _STORAGE_UPDATE}\n\t}\n\n\treturn nil\n}\n\n// CheckVoted checks if the voter has already voted.\nfunc (c *Client) CheckVoted(ctx context.Context, voter string) (voted bool, err error) {\n\tswitch _, err = c.prot.Get(ctx, votedStatsPrefix+voter); {\n\tcase err == nil:\n\t\treturn true, nil\n\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, CheckVotedError{Voter: voter, Err: err, Description: _STORAGE_GET}\n\t}\n}\n\n// GetVotesCount returns votes count.\nfunc (c *Client) GetVotesCount(ctx context.Context) (count uint64, err error) {\n\tswitch countb, err := c.prot.Get(ctx, votesStatsPrefix); {\n\tcase err == nil:\n\t\treturn binary.BigEndian.Uint64(countb), nil\n\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\treturn 0, nil\n\tdefault:\n\t\treturn 0, GetVotesCountError{Err: err, Description: _STORAGE_GET}\n\t}\n}\n\ntype VoteOrder struct {\n\tSeqNo      string\n\tIDCode     string\n\tVoterName  string\n\tKovCode    string\n\tDistrictNo string\n}\n\n// AddVoteOrder tries to add vote order record\nfunc (c *Client) AddVoteOrder(ctx context.Context, voterName string, idVoter string,\n\tdistrict string, idAdminCode string) error {\n\tvar err error\n\n\t// Get amount of successful votes per voter using detail statistics\n\t_, statsSerial, statsErr := c.prot.GetWithSerial(ctx, votedStatsPrefix+idVoter)\n\tif statsErr != nil {\n\t\t// If statsErr was caused by NotExistError, then that means that\n\t\t// there is no detail statistics for the given voter, which in\n\t\t// turn means that we don't have to add SeqNo for that voter\n\t\tif errors.CausedBy(statsErr, new(NotExistError)) != nil {\n\t\t\treturn AddVoteOrderErrorGetDetailStatsForNonVotedVoter{\n\t\t\t\tErr: statsErr, Description: _STORAGE_GET_N_EXISTS,\n\t\t\t}\n\t\t}\n\t\t// Other errors could possibly be caused by database connectivity\n\t\treturn AddVoteOrderGetDetailStatsForVoterError{\n\t\t\tVoterName:   voterName,\n\t\t\tVoterID:     idVoter,\n\t\t\tAdminCode:   idAdminCode,\n\t\t\tDistrict:    district,\n\t\t\tErr:         statsErr,\n\t\t\tDescription: _STORAGE_GET,\n\t\t}\n\t}\n\n\t// Get amount of successful votes per voter using SeqNo+Batch records\n\t_, seqNoSerial, seqNoErr := c.prot.GetWithSerial(ctx, votesStatsPerVoterPrefix+idVoter)\n\tif seqNoErr != nil {\n\t\t// If seqNoErr was not caused by NotExistError, then that means that\n\t\t// there is a seqNoSerial value for the given voter in a database,\n\t\t// however database is currently not available\n\t\tif errors.CausedBy(seqNoErr, new(NotExistError)) == nil {\n\t\t\treturn AddVoteOrderGetSeqNoStatsForVoterError{\n\t\t\t\tVoterName:   voterName,\n\t\t\t\tVoterID:     idVoter,\n\t\t\t\tAdminCode:   idAdminCode,\n\t\t\t\tDistrict:    district,\n\t\t\t\tErr:         seqNoErr,\n\t\t\t\tDescription: _STORAGE_GET}\n\t\t}\n\t}\n\n\t// Detail statistics index should always be greater than SeqNo index\n\t// for a given voter\n\tif statsSerial <= seqNoSerial {\n\t\tlog.Log(ctx, AddVoteOrderSeqNoForVoterAlreadyLatest{\n\t\t\tVoter:       idVoter,\n\t\t\tLatestSeqNo: seqNoSerial,\n\t\t\tDescription: _STORAGE_DET_STATS_IDX,\n\t\t})\n\t\treturn nil\n\t}\n\n\tcount, countErr := c.GetVotesCount(ctx)\n\tif countErr != nil {\n\t\treturn AddVoteOrderGetVotesCountError{\n\t\t\tVoterName:   voterName,\n\t\t\tVoterID:     idVoter,\n\t\t\tAdminCode:   idAdminCode,\n\t\t\tDistrict:    district,\n\t\t\tErr:         countErr,\n\t\t\tDescription: _STORAGE_GET,\n\t\t}\n\t}\n\tvar newCount uint64\n\treqAll := make([]*PutAllRequest, 4)\n\n\t// Change regular storage client to a transactional storage client\n\ttransaction := c.Txn()\n\n\t// switch-case is used to escape from conditional statements using \"break\"\n\tswitch count {\n\tcase 0:\n\t\tnewCount = count + 1\n\t\tnewValue := make([]byte, 8)\n\t\tbinary.BigEndian.PutUint64(newValue, newCount)\n\n\t\ttxnOp, txnOpErr := transaction.Begin(ctx)\n\t\tif txnOpErr != nil {\n\t\t\terr = txnOpErr\n\t\t\tbreak\n\t\t}\n\n\t\treqAll[0] = &PutAllRequest{\n\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + voterIDKey,\n\t\t\tValue: []byte(idVoter),\n\t\t}\n\t\treqAll[1] = &PutAllRequest{\n\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + admincodeKey,\n\t\t\tValue: []byte(idAdminCode),\n\t\t}\n\t\treqAll[2] = &PutAllRequest{\n\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + districtKey,\n\t\t\tValue: []byte(district),\n\t\t}\n\t\treqAll[3] = &PutAllRequest{\n\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + voterNameKey,\n\t\t\tValue: []byte(voterName),\n\t\t}\n\n\t\ttxnOp.PutAll(reqAll...)\n\n\t\ttxnOp.Put(votesStatsPrefix, newValue)\n\n\t\t// Just add any value to the key, since we are only interested\n\t\t// in SERIAL of that key, not a value\n\t\ttxnOp.PutForce(votesStatsPerVoterPrefix+idVoter, []byte{})\n\n\t\terr = transaction.Commit(ctx, txnOp)\n\tdefault:\n\t\ttimeout := time.Duration(c.orderTimeout) * time.Second\n\n\t\t// \"break\" will only exit from a for loop, however since\n\t\t// switch-default has only for loop inside, \"break\" will\n\t\t// force to exit from switch as well\n\t\tfor start := time.Now(); time.Since(start) < timeout; {\n\t\t\t// Since we skip err here from a transaction.Commit, which\n\t\t\t// also reports about ctx being closed, we should explicitly\n\t\t\t// check here that ctx is not closed yet, otherwise there is\n\t\t\t// no point to continue looping, since nobody wants that response\n\t\t\t// from above\n\t\t\tif ctx.Err() != nil {\n\t\t\t\terr = ctx.Err()\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\toldValue := make([]byte, 8)\n\t\t\tnewCount = count + 1\n\t\t\tnewValue := make([]byte, 8)\n\t\t\tbinary.BigEndian.PutUint64(oldValue, count)\n\t\t\tbinary.BigEndian.PutUint64(newValue, newCount)\n\n\t\t\ttxnOp, txnOpErr := transaction.Begin(ctx)\n\t\t\tif txnOpErr != nil {\n\t\t\t\terr = txnOpErr\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treqAll[0] = &PutAllRequest{\n\t\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + voterIDKey,\n\t\t\t\tValue: []byte(idVoter),\n\t\t\t}\n\t\t\treqAll[1] = &PutAllRequest{\n\t\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + admincodeKey,\n\t\t\t\tValue: []byte(idAdminCode),\n\t\t\t}\n\t\t\treqAll[2] = &PutAllRequest{\n\t\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + districtKey,\n\t\t\t\tValue: []byte(district),\n\t\t\t}\n\t\t\treqAll[3] = &PutAllRequest{\n\t\t\t\tKey:   votesPrefix + strconv.FormatUint(newCount, 10) + \"/\" + voterNameKey,\n\t\t\t\tValue: []byte(voterName),\n\t\t\t}\n\n\t\t\ttxnOp.PutAll(reqAll...)\n\n\t\t\ttxnOp.CAS(votesStatsPrefix, oldValue, newValue)\n\n\t\t\t// Just add any value to the key, since we are only interested\n\t\t\t// in SERIAL of that key, not a value\n\t\t\ttxnOp.PutForce(votesStatsPerVoterPrefix+idVoter, []byte{})\n\n\t\t\tif err = transaction.Commit(ctx, txnOp); err != nil {\n\t\t\t\tcount++\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Catch all errors here\n\tif err != nil {\n\t\tlog.Log(ctx, AddVoteOrderError{Err: err,\n\t\t\tDescription: _STORAGE_TXN})\n\t\treturn AddVoteOrderStoreVotesStatsError{\n\t\t\tVoterName:   voterName,\n\t\t\tVoterID:     idVoter,\n\t\t\tAdminCode:   idAdminCode,\n\t\t\tDistrict:    district,\n\t\t\tDescription: _STORAGE_TXN,\n\t\t}\n\t}\n\n\t// Informational purpose, to compare SeqNo with this log records count (should match)\n\tlog.Debug(ctx, AddVoteOrderSuccess{VoterID: idVoter, Description: _STORAGE_VOTESORDER})\n\n\treturn nil\n}\n\n// GetVotesOrder returns for each successful vote record with order number and voter info.\nfunc (c *Client) GetVotesOrder(ctx context.Context, countFrom int, batchSize int) ([]VoteOrder, error) {\n\tvar keys []string\n\tfor i := countFrom; i < countFrom+batchSize; i++ {\n\t\tkeys = append(keys, votesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+admincodeKey,\n\t\t\tvotesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+voterIDKey,\n\t\t\tvotesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+voterNameKey,\n\t\t\tvotesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+districtKey)\n\t}\n\tvalues, err := c.getAll(ctx, keys...)\n\tif err != nil {\n\t\treturn []VoteOrder{}, GetAllVotesOrderError{Err: err, Description: _STORAGE_GET}\n\t}\n\tvar votesOrder []VoteOrder\n\tfor i := countFrom; i < countFrom+batchSize; i++ {\n\t\tvar voterID []byte\n\t\tvar ok bool\n\t\t// do not add empty values to the list\n\t\tif voterID, ok = values[votesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+voterIDKey]; !ok {\n\t\t\tbreak\n\t\t}\n\t\tvotesOrder = append(votesOrder, VoteOrder{strconv.FormatInt(int64(i), 10),\n\t\t\tstring(voterID),\n\t\t\tstring(values[votesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+voterNameKey]),\n\t\t\tstring(values[votesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+admincodeKey]),\n\t\t\tstring(values[votesPrefix+strconv.FormatInt(int64(i), 10)+\"/\"+districtKey])})\n\t}\n\treturn votesOrder, nil\n}\n\n// VotedStats is a single entry from GetVotedStats.\ntype VotedStats struct {\n\t// Voter is the identity of a successful voter.\n\tVoter string\n\n\t// AdminCode is the code of the administrative unit Voter voted in\n\t// last. If there is no administrative unit data for Voter, e.g., if no\n\t// voter lists are being used, then AdminCode is empty.\n\tAdminCode string\n\n\t// Time is the submission time of the first vote that Voter cast in the\n\t// administrative unit with code AdminCode.\n\tTime time.Time\n}\n\n// GetVotedStats returns for each successful voter the administrative unit that\n// they voted in last and their earliest vote submission time in that unit.\n// This data can be used for reporting voter participation statistics while\n// accounting for revotes and voters changing administrative units.\n//\n// Entries must be read from result channel until it is closed and then a\n// single error must be read from the error channel.\nfunc (c *Client) GetVotedStats(ctx context.Context) (<-chan VotedStats, <-chan error) {\n\tvotedc := make(chan VotedStats)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(errc)\n\t\tdefer close(votedc)\n\n\t\tpctx, cancel := context.WithCancel(ctx)\n\t\tdefer cancel()\n\t\tprotc, proterrc := c.prot.GetWithPrefix(pctx, votedStatsPrefix)\n\t\tfor voted := range protc {\n\t\t\tvoter := voted.Key[len(votedStatsPrefix):]\n\n\t\t\t// The value is an encoded pair of admin unit code and\n\t\t\t// timestamp.\n\t\t\tadminCode, timestr, err := decodePair(voted.Value)\n\t\t\tif err != nil {\n\t\t\t\terrc <- log.Alert(GetVotedStatsDecodeError{\n\t\t\t\t\tVoter:       voter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_DECODE_PAIR,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstime, err := time.Parse(timefmt, timestr)\n\t\t\tif err != nil {\n\t\t\t\terrc <- log.Alert(GetVotedStatsParseTimeError{\n\t\t\t\t\tVoter:       voter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_TS_PARSE,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvotedc <- VotedStats{\n\t\t\t\tVoter:     voter,\n\t\t\t\tAdminCode: adminCode,\n\t\t\t\tTime:      stime,\n\t\t\t}\n\t\t}\n\n\t\tvar err error\n\t\tif err = <-proterrc; err != nil {\n\t\t\terr = log.Alert(GetWithVotedPrefixError{Err: err, Description: _STORAGE_GET})\n\t\t}\n\t\terrc <- err\n\t}()\n\treturn votedc, errc\n}\n\nconst ratePrefix = \"/rate/\"\n\n// GetVoterRateStats returns per-voter vote submission rate statistics: the\n// number of times the voter has submitted a vote and the timestamp of the last\n// submission.\nfunc (c *Client) GetVoterRateStats(ctx context.Context, voter string) (\n\tsubmissions uint64, last time.Time, err error) {\n\n\tstats, err := c.prot.Get(ctx, ratePrefix+voter)\n\tswitch {\n\tcase err == nil:\n\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\terr = nil // No attempts yet.\n\t\treturn\n\tdefault:\n\t\terr = GetVoterRateStatsError{Voter: voter, Err: err, Description: _STORAGE_GET}\n\t\treturn\n\t}\n\n\t// The value is 8 bytes for submissions followed by timestamp.\n\tif len(stats) < 8 {\n\t\terr = log.Alert(InvalidVoterRateStatsLengthError{\n\t\t\tVoter:       voter,\n\t\t\tLength:      len(stats),\n\t\t\tDescription: _STORAGE_RATE_BYTES,\n\t\t})\n\t\treturn\n\t}\n\tsubmissions = binary.BigEndian.Uint64(stats[:8])\n\n\tif last, err = time.Parse(timefmt, string(stats[8:])); err != nil {\n\t\terr = log.Alert(ParseVoterRateStatsTimeError{Voter: voter, Err: err,\n\t\t\tDescription: _STORAGE_TS_PARSE})\n\t}\n\treturn\n}\n\n// SetVoterRateStats updates per-voter vote submission rate statistics based on\n// last known information from GetVoterRateStats. It increases the submission\n// counter and updates the last attempt timestamp, given that the old\n// information is unchanged.\nfunc (c *Client) SetVoterRateStats(ctx context.Context, voter string,\n\tsubmissions uint64, last, now time.Time) (\n\terr error) {\n\n\t// Serialize the CAS values: 8 bytes for submissions followed by\n\t// timestamp.\n\told := make([]byte, 8, 8+len(timefmt))\n\tbinary.BigEndian.PutUint64(old, submissions)\n\told = last.AppendFormat(old, timefmt)\n\n\tnewv := make([]byte, 8, 8+len(timefmt))\n\tbinary.BigEndian.PutUint64(newv, submissions+1)\n\tnewv = now.AppendFormat(newv, timefmt)\n\n\tkey := ratePrefix + voter\n\n\t// If either value is non-zero, then the key should already exist and\n\t// we attempt to perform the usual compare-and-swap.\n\tif submissions > 0 || !last.IsZero() {\n\t\tif err = c.prot.CAS(ctx, key, old, newv); err != nil {\n\t\t\terr = SetVoterRateStatsError{Voter: voter, Err: err, Description: _STORAGE_CAS}\n\t\t}\n\t\treturn\n\t}\n\n\t// Otherwise this must be the first time this voter has submitted a\n\t// vote. Use Put to create the key instead.\n\tswitch err = c.prot.Put(ctx, key, newv); {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.CausedBy(err, new(ExistError)) != nil:\n\t\t// Somebody has created the key before us. Wrap err in\n\t\t// UnexpectedValueError to mask the fact that we did a\n\t\t// Put instead of CAS and the caller can still only\n\t\t// check for that error type to determine if they need\n\t\t// to try again with fresh values.\n\t\t//\n\t\t// We cannot use a struct literal, because gen would\n\t\t// report it as a duplicate error type.\n\t\tvar unexpected UnexpectedValueError\n\t\tunexpected.Err = SetVoterRateStatsPutExistsError{\n\t\t\tVoter:       voter,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_PUT_EXISTS,\n\t\t}\n\t\treturn unexpected\n\tdefault:\n\t\treturn SetVoterRateStatsPutError{Voter: voter, Err: err, Description: _STORAGE_PUT}\n\t}\n}\n\n// StoredVote contains a vote and accompanying data. The type is used for\n// storing and exporting votes.\ntype StoredVote struct {\n\tVoteID        []byte          // Unique identifier of this vote.\n\tTime          time.Time       // Time the vote was received.\n\tVoteType      container.Type  // Type of the signed container.\n\tVote          []byte          // Signed container that is the actual vote.\n\tVoter         string          // Identity of the voter.\n\tVersion       string          // Active voter list version when the vote was stored.\n\tQualification q11n.Properties // Qualifying properties for the signed container.\n}\n\n// missing returns the names of fields of s which are empty. qps lists the\n// qualifying properties which must be present.\nfunc (s StoredVote) missing(qps ...q11n.Protocol) (m []string) {\n\tif len(s.VoteID) == 0 {\n\t\tm = append(m, \"VoteID\")\n\t}\n\tif s.Time.IsZero() {\n\t\tm = append(m, \"Time\")\n\t}\n\tif len(s.VoteType) == 0 {\n\t\tm = append(m, \"VoteType\")\n\t}\n\tif len(s.Vote) == 0 {\n\t\tm = append(m, \"Vote\")\n\t}\n\tif len(s.Voter) == 0 {\n\t\tm = append(m, \"Voter\")\n\t}\n\tif len(s.Version) == 0 {\n\t\tm = append(m, \"Version\")\n\t}\n\tfor _, qp := range qps {\n\t\tif prop, ok := s.Qualification[qp]; !ok || len(prop) == 0 {\n\t\t\tm = append(m, string(qp))\n\t\t}\n\t}\n\treturn\n}\n\nconst (\n\tvotePrefix = \"/vote/\"\n\n\ttimeKey  = \"time\"\n\ttypeKey  = \"type\"\n\tvoteKey  = \"vote\"\n\tvoterKey = \"voter\"\n\t// versionKey is already defined.\n\tcountKey = \"count\" // The number of times a vote has been verified.\n)\n\n// StoreVote stores the provided vote and accompanying data in the storage\n// service.\nfunc (c *Client) StoreVote(ctx context.Context, vote StoredVote) error {\n\tif m := vote.missing(); len(m) > 0 {\n\t\treturn StoreIncompleteVoteError{VoteID: vote.VoteID, Missing: m,\n\t\t\tDescription: _STORAGE_INCOMPLETE_VOTE}\n\t}\n\n\tif err := c.putAll(ctx, voteIDPrefix(vote.VoteID), map[string][]byte{\n\t\ttimeKey:    []byte(vote.Time.Format(timefmt)),\n\t\ttypeKey:    []byte(vote.VoteType),\n\t\tvoteKey:    vote.Vote,\n\t\tvoterKey:   []byte(vote.Voter),\n\t\tversionKey: []byte(vote.Version),\n\t\tcountKey:   make([]byte, 8),\n\t}, false, noopAdd); err != nil {\n\t\treturn log.Alert(StoreVoteError{VoteID: vote.VoteID, Err: err, Description: _STORAGE_PUT})\n\t}\n\treturn nil\n}\n\nfunc noopAdd(uint64) uint64 { return 0 }\n\n// StoreQualifyingProperty stores a qualifying property for a vote in the\n// storage service.\nfunc (c *Client) StoreQualifyingProperty(ctx context.Context, voteID []byte,\n\tprotocol q11n.Protocol, property []byte) error {\n\n\tprefix := voteIDPrefix(voteID)\n\tif err := c.prot.Put(ctx, prefix+string(protocol), property); err != nil {\n\t\treturn log.Alert(StoreQualifyingPropertyError{\n\t\t\tVoteID:      voteID,\n\t\t\tProtocol:    protocol,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_PUT,\n\t\t})\n\t}\n\treturn nil\n}\n\n// GetVotes retrieves the list of currently stored votes. qps lists the\n// qualifying properties which are expected for each vote and optional lists\n// the fields of votes that may be missing for the vote to still get exported.\n//\n// The channels returned by GetVotes behave a little bit different from usual\n// pipelines. Instead of reading from the result channel until closed and then\n// reading a single error from errc, results and errors should be read in\n// parallel. GetVotes will not stop sending results if a non-fatal error is\n// encountered: it will just send the error on the error channel and skip the\n// errenous entry, continuing with following votes. This way bad entries will\n// not cause the entire operation to fail.\n//\n// Sending on both channels happens in the same goroutine, so callers must read\n// from both in parallel for GetVotes not to get blocked on a send. Sending\n// results will only stop once all results are sent, the context is cancelled,\n// or a fatal error occurs. Sending errors will continue until all errors have\n// been read. This means that if the caller cancels the context, they must\n// still drain the error channel to unblock GetVotes! Only after this will the\n// result and error channels be closed.\n//\n// Fatal errors will be wrapped in GetVotesFatalError and will never be\n// followed by more errors.\nfunc (c *Client) GetVotes(ctx context.Context, qps []q11n.Protocol, optional []string) (\n\t<-chan *StoredVote, <-chan error) {\n\n\t// It might make more sense to iterate over keys only to get vote\n\t// identifiers and then use getAll to get entire votes. This would\n\t// reduce our running memory usage and probably be a lot easier to\n\t// read, however increase the number of storage queries, resulting in a\n\t// performance hit. This change would need to be benchmarked to see if\n\t// even viable.\n\n\tch := make(chan *StoredVote)\n\terrc := make(chan error) // Not buffered!\n\tgo func() {\n\t\tdefer close(errc)\n\t\tdefer close(ch)\n\n\t\t// partial contains StoredVotes which are still missing fields.\n\t\tpartial := make(map[string]*StoredVote)\n\t\tprotc, proterrc := c.prot.GetWithPrefix(ctx, votePrefix)\n\t\tfor r := range protc {\n\t\t\t// Split the storage key into vote ID and value key.\n\t\t\tvoteid, key, err := splitVoteKey(r.Key)\n\t\t\tif err != nil {\n\t\t\t\terrc <- log.Alert(GetVotesSplitKeyError{Key: r.Key, Err: err,\n\t\t\t\t\tDescription: _STORAGE_VOTEID_FROM_KEY})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif key == countKey {\n\t\t\t\tcontinue // Verification counts are not exported.\n\t\t\t}\n\n\t\t\t// Get or create the vote ID entry.\n\t\t\tvote, ok := partial[string(voteid)]\n\t\t\tif !ok {\n\t\t\t\tvote = &StoredVote{\n\t\t\t\t\tVoteID:        voteid,\n\t\t\t\t\tQualification: make(q11n.Properties),\n\t\t\t\t}\n\t\t\t\tpartial[string(voteid)] = vote\n\t\t\t}\n\n\t\t\t// Set the field according to the key.\n\t\tkeySwitch:\n\t\t\tswitch key {\n\t\t\tcase timeKey:\n\t\t\t\tif vote.Time, err = time.Parse(timefmt, string(r.Value)); err != nil {\n\t\t\t\t\terrc <- log.Alert(GetVotesParseTimeError{\n\t\t\t\t\t\tVoteID:      voteid,\n\t\t\t\t\t\tErr:         err,\n\t\t\t\t\t\tDescription: _STORAGE_TS_PARSE,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase typeKey:\n\t\t\t\tvote.VoteType = container.Type(r.Value)\n\t\t\tcase voteKey:\n\t\t\t\tvote.Vote = r.Value\n\t\t\tcase voterKey:\n\t\t\t\tvote.Voter = string(r.Value)\n\t\t\tcase versionKey:\n\t\t\t\tvote.Version = string(r.Value)\n\t\t\tdefault:\n\t\t\t\t// Check for qualifying properties.\n\t\t\t\tfor _, qp := range qps {\n\t\t\t\t\tif key == string(qp) {\n\t\t\t\t\t\tvote.Qualification[qp] = r.Value\n\t\t\t\t\t\tbreak keySwitch\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\terrc <- log.Alert(GetVotesInvalidKeyError{\n\t\t\t\t\tVoteID:      voteid,\n\t\t\t\t\tKey:         key,\n\t\t\t\t\tDescription: _STORAGE_UNKNOWN_ETCD_KEY,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// If vote is now complete, then send it.\n\t\t\tif len(vote.missing(qps...)) == 0 {\n\t\t\t\tdelete(partial, string(voteid))\n\t\t\t\tselect {\n\t\t\t\tcase ch <- vote:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := <-proterrc; err != nil {\n\t\t\terrc <- log.Alert(GetVotesFatalError{Err: err, Description: _STORAGE_GET})\n\t\t\treturn\n\t\t}\n\n\t\t// Process any remaining partial votes.\n\t\tfor _, vote := range partial {\n\t\t\t// Send votes that are missing only optional fields.\n\t\t\tif allIn(vote.missing(qps...), optional) {\n\t\t\t\tselect {\n\t\t\t\tcase ch <- vote:\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\terrc <- ctx.Err()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Send an error about any incomplete entries, even if\n\t\t\t// we sent the partial vote.\n\t\t\terrc <- GetVotesIncompleteVoteError{\n\t\t\t\tVoteID:      vote.VoteID,\n\t\t\t\tMissing:     vote.missing(qps...),\n\t\t\t\tDescription: _STORAGE_INCOMPLETE_VOTE,\n\t\t\t}\n\t\t}\n\t}()\n\treturn ch, errc\n}\n\nfunc allIn(needles, haystack []string) bool {\nnext:\n\tfor _, n := range needles {\n\t\tfor _, p := range haystack {\n\t\t\tif n == p {\n\t\t\t\tcontinue next\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\n// GetVerificationStats returns the statistics about a vote required for\n// allowing access to verification: the number of times the vote has been\n// verified, when it was cast, and if it is the latest vote by a voter.\nfunc (c *Client) GetVerificationStats(ctx context.Context, voteid []byte, foreignAdminCode string,\n\tpredefinedDistrictID string) (count uint64, at time.Time, latest bool, choicesList []byte, err error) {\n\n\tprefix := voteIDPrefix(voteid)\n\tm, err := c.getAllStrict(ctx, prefix+timeKey, prefix+countKey, prefix+voterKey, prefix+versionKey)\n\tif err != nil {\n\t\terr = GetVerificationStatsError{VoteID: voteid, Err: err, Description: _STORAGE_GET}\n\t\treturn\n\t}\n\n\tif at, err = time.Parse(timefmt, string(m[prefix+timeKey])); err != nil {\n\t\terr = log.Alert(ParseVerificationTimeError{VoteID: voteid, Err: err, Description: _STORAGE_TS_PARSE})\n\t\treturn\n\t}\n\n\tcountb := m[prefix+countKey]\n\tif len(countb) != 8 {\n\t\terr = log.Alert(InvalidVerificationCountLengthError{\n\t\t\tVoteID:      voteid,\n\t\t\tLength:      len(countb),\n\t\t\tDescription: _STORAGE_RATE_VERIF_BYTES,\n\t\t})\n\t\treturn\n\t}\n\tcount = binary.BigEndian.Uint64(countb)\n\n\tvoter := string(m[prefix+voterKey])\n\n\t// Only retrieve a version that was used during voting.\n\t//\n\t// This prevents a situation when between voting and verification processes\n\t// district of a voter changes. This will lead to inconsistency, and to prevent this\n\t// we only interested in a district's version that was used during voting.\n\tversionAsBytes, ok := m[prefix+versionKey]\n\tif !ok {\n\t\terr = log.Alert(VersionDoesntExistError{VoteID: voteid, Description: _STORAGE_VER_MISMATCH})\n\t\treturn\n\t}\n\n\tversion := string(versionAsBytes)\n\tdistrictID := predefinedDistrictID\n\n\t// predefinedDistrictID is an empty string\n\tif len(predefinedDistrictID) == 0 {\n\t\t// Get district ID for a particular voter by voterlist version of a stored vote ID\n\t\tdistrictID, err = c.VoterChoicesByVersion(ctx, version, voter, foreignAdminCode)\n\t\tif err != nil {\n\t\t\terr = log.Alert(GetVerificationDistrictIDError{Voter: voter, Err: err,\n\t\t\t\tDescription: _STORAGE_GET})\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Get all choices for particular district ID\n\tif choicesList, err = c.GetChoices(ctx, districtID); err != nil {\n\t\terr = log.Alert(GetVerificationChoicesListError{Voter: voter, Err: err,\n\t\t\tDescription: _STORAGE_GET})\n\t\treturn\n\t}\n\n\tvotedLatest, err := c.prot.Get(ctx, votedLatestPrefix+voter)\n\tif err != nil {\n\t\terr = log.Alert(GetVerificationLatestError{Voter: voter, Err: err,\n\t\t\tDescription: _STORAGE_GET})\n\t\treturn\n\t}\n\t_, latestVoteID, err := decodePair(votedLatest)\n\tif err != nil {\n\t\terr = log.Alert(GetVerificationDecodeLatestError{Voter: voter, Err: err,\n\t\t\tDescription: _STORAGE_DECODE_PAIR})\n\t\treturn\n\t}\n\tlatest = bytes.Equal(voteid, []byte(latestVoteID))\n\n\treturn\n}\n\n// GetVerification returns the subset of a stored vote and accompanying data\n// necessary for verification - the vote type, the vote itself and the\n// qualifying properties listed in qps - and increases the verification count.\n// It does this only if the old verification count is unchanged.\nfunc (c *Client) GetVerification(ctx context.Context,\n\tvoteid []byte, count uint64, qps ...q11n.Protocol) (\n\tvote StoredVote, err error) {\n\n\tprefix := voteIDPrefix(voteid)\n\n\t// First ensure that we can read the the verification data.\n\tkeys := make([]string, 2, len(qps)+2)\n\tkeys[0] = prefix + typeKey\n\tkeys[1] = prefix + voteKey\n\tfor _, qp := range qps {\n\t\tkeys = append(keys, prefix+string(qp))\n\t}\n\tm, err := c.getAllStrict(ctx, keys...)\n\tif err != nil {\n\t\terr = GetVerificationError{VoteID: voteid, Err: err, Description: _STORAGE_GET}\n\t\treturn\n\t}\n\n\t// Map verification data into the vote.\n\tvote.VoteType = container.Type(m[prefix+typeKey])\n\tvote.Vote = m[prefix+voteKey]\n\tvote.Qualification = make(q11n.Properties)\n\tfor _, qp := range qps {\n\t\tvote.Qualification[qp] = m[prefix+string(qp)]\n\t}\n\n\t// Serialize the CAS values.\n\told := make([]byte, 8)\n\tnew1 := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(old, count)\n\tbinary.BigEndian.PutUint64(new1, count+1)\n\n\t// Perform the CAS.\n\tif err = c.prot.CAS(ctx, prefix+countKey, old, new1); err != nil {\n\t\terr = GetVerificationCASError{VoteID: voteid, Err: err, Description: _STORAGE_CAS}\n\t\treturn\n\t}\n\treturn\n}\n\nfunc voteIDPrefix(voteid []byte) string {\n\t// Do not encode voteid: it is up to the storage protocol to encode it\n\t// in any way if necessary. Without encoding we get a nice distribution\n\t// of vote keys (given that vote IDs are uniformly distributed) which\n\t// makes it easier to retrieve them in some cases (e.g., etcd).\n\t//\n\t// Note that this means that the vote ID can also contain slashes. Be\n\t// careful when handling these cases!\n\t//\n\t// Because of this we can also not use path.Join, since that would call\n\t// path.Clean on the result, possibly corrupting it.\n\treturn votePrefix + string(voteid) + \"/\"\n}\n\nfunc splitVoteKey(fullkey string) (voteid []byte, key string, err error) {\n\t// Note that the vote ID can contain slashes so be careful!\n\tif !strings.HasPrefix(fullkey, votePrefix) {\n\t\treturn nil, \"\", SplitVoteKeyNoPrefixError{FullKey: fullkey,\n\t\t\tDescription: _STORAGE_KEY_PREX}\n\t}\n\tstripped := fullkey[len(votePrefix):]\n\tslash := strings.LastIndex(stripped, \"/\")\n\tswitch slash {\n\tcase -1:\n\t\treturn nil, \"\", SplitVoteKeyNoSlashError{FullKey: fullkey,\n\t\t\tDescription: _STORAGE_KEY_PREX_SLASH}\n\tcase len(stripped) - 1:\n\t\treturn nil, \"\", SplitVoteKeyNoKeyError{FullKey: fullkey,\n\t\t\tDescription: _STORAGE_KEY_PREX_KEY}\n\t}\n\treturn []byte(stripped[:slash]), stripped[slash+1:], nil\n}\n"
  },
  {
    "path": "common/collector/storage/storage_dev.go",
    "content": "//ivxv:development\n\npackage storage\n\nimport (\n\t\"context\"\n\n\t\"ivxv.ee/common/collector/command/status\"\n)\n\n// This file contains storage.Client methods which are only available in\n// development mode. These provide direct access to the storage service and\n// must be disabled in production code.\n\n// PutAll exports the internal c.putAll method used for optimally storing many\n// keys at once.\nfunc (c *Client) PutAll(ctx context.Context, prefix string,\n\tvalues map[string][]byte, ensure bool, progress status.Add) error {\n\n\treturn c.putAll(ctx, prefix, values, ensure, progress)\n}\n"
  },
  {
    "path": "common/collector/storage/txn.go",
    "content": "package storage\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n)\n\n// Transaction makes it possible to call \"Txn\" prefixed storage services\n// within a single transaction. This is the new approach, since traditionally\n// IVXV supported only 1 operation per a single transaction.\ntype Transaction interface {\n\t// Begin a transaction, all subsequent calls to the \"Txn\"\n\t// prefixed storage services, will be added to a single transaction.\n\tBegin(context.Context) (TxnOp, error)\n\n\t// Commit guarantees that transaction is committed, and therefore all\n\t// operations that have been added to the transaction in a \"Txn\" prefixed\n\t// storage services will be applied with \"All or nothing\" principle.\n\tCommit(context.Context, TxnOp) error\n\n\t// AutoCommit allows a caller to assume that once transaction has begun,\n\t// it will be auto committed, without manual Commit operation.\n\t//\n\t// Please note, that AutoCommit doesn't guarantee that transaction is\n\t// committed as well as context is deadlined. This guarantee completely\n\t// relies on the \"Txn\" prefixed storage methods.\n\t//\n\t// Roughly speaking, the main difference between Commit and AutoCommit\n\t// is that former allows a caller to commit a transaction and the\n\t// latter delegates committing of a transaction to the \"Txn\" prefixed\n\t// storage services.\n\tAutoCommit(context.Context, TxnOp)\n}\n\n// TxnOp represents an operation that is done in a single Transaction.\ntype TxnOp interface {\n\t// PutAll calls Put for each req in reqs.\n\tPutAll(reqs ...*PutAllRequest)\n\n\t// Put creates a single INSERT request a for key-value pair and\n\t// adds it to the Transaction.\n\tPut(key string, value []byte)\n\n\t// PutForce is almost same as Put, the only difference between\n\t// them is that former should not do any conditional checks against\n\t// the underlying storage before making an INSERT request.\n\tPutForce(key string, value []byte)\n\n\t// CAS performs \"compare-and-swap\", replacing old with new\n\t// for a given key.\n\tCAS(key string, old, new []byte) //nolint:revive\n\n\t// Ready reports that TxnOp is ready to be committed.\n\tReady(ctx context.Context) error\n}\n\n// Txn returns a Transaction to the caller.\nfunc (c *Client) Txn() Transaction {\n\treturn c.prot.(Transaction)\n}\n\n// TxnStoreQualifyingProperty stores qualifying properties (ocsp/tspreg)\n// for a vote in the underlying storage.\nfunc (c *Client) TxnStoreQualifyingProperty(ctx context.Context,\n\tvoteID []byte, protocol q11n.Protocol, property []byte, op TxnOp) {\n\tprefix := voteIDPrefix(voteID)\n\top.Put(prefix+string(protocol), property)\n\n\tlog.Debug(ctx, TxnStoreQualifyingPropertyAddPropertyToTxn{Property: protocol,\n\t\tDescription: _STORAGE_TXN_PUT})\n}\n\n// TxnSetVoted notifies storage that voteID was successful, meaning all configured\n// qualifying properties for it are received and stored. ctime is the canonical\n// time of the vote, which is usually the time of a qualifying property, e.g.,\n// vote registration timestamp.\nfunc (c *Client) TxnSetVoted(ctx context.Context,\n\ttxnOp TxnOp, voteID []byte, voterName string, ctime time.Time,\n\ttestVote bool) error {\n\t// voteID was successful: refresh indexes related to the voter. Keep in\n\t// mind that voteID is not guaranteed to be the latest vote from the\n\t// voter, so be careful when updating the information.\n\n\t// Critical error, if occurs then vote cannot be stored\n\tvar unexpected UnexpectedValueError\n\n\t// Get vote metadata in a single batch call.\n\tidVoterKey := voteIDPrefix(voteID) + voterKey     // Voter assigned to voteID.\n\tidVersionKey := voteIDPrefix(voteID) + versionKey // Effective voter list version.\n\tdata, err := c.getAllStrict(ctx, idVoterKey, idVersionKey)\n\tif err != nil {\n\t\tunexpected.Err = TxnSetVotedGetAllStrictError{\n\t\t\tVoteID:      voteID,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_GET,\n\t\t}\n\t\treturn unexpected\n\t}\n\n\t// Get the administrative unit code of voter in voter list version.\n\tidVoter := string(data[idVoterKey])\n\tidAdminCode, district, err := c.GetVoter(ctx, string(data[idVersionKey]), idVoter)\n\tswitch {\n\tcase err == nil:\n\tcase errors.CausedBy(err, new(NotExistError)) != nil:\n\t\t// The voter has voted, yet is not in the voter list. Assume\n\t\t// that the voting service is ignoring voter lists (see\n\t\t// ivxv.ee/common/collector/conf.Election.IgnoreVoterList). Use an empty\n\t\t// administrative unit code.\n\t\tidAdminCode = \"\"\n\t\tdistrict = \"\"\n\tdefault:\n\t\tunexpected.Err = TxnSetVotedGetVoterError{\n\t\t\tVoter:       idVoter,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_GET,\n\t\t}\n\t\treturn unexpected\n\t}\n\n\tctimeStr := ctime.Format(timefmt)\n\tif !testVote { // Only count non-test votes in statistics.\n\t\t// Update the voted stats index: if the administrative unit codes\n\t\t// match, then use the older timestamp; if they differ, then use the\n\t\t// newer timestamp. See GetVotedStats for the reasoning behind this.\n\t\tvotedStatsValue := encodePair(idAdminCode, ctimeStr)\n\t\tkey := votedStatsPrefix + idVoter\n\t\texisting, err := c.prot.Get(ctx, key)\n\n\t\t// err == NotExistError, i.e key doesn't exist in db\n\t\tif errors.CausedBy(err, new(NotExistError)) != nil {\n\t\t\t// Add votedStatsValue to transaction\n\t\t\ttxnOp.Put(key, votedStatsValue)\n\n\t\t\tlog.Debug(ctx, TxnSetVotedAddedNewVotedStatsToTxn{\n\t\t\t\tVoterID:     idVoter,\n\t\t\t\tAdminCode:   idAdminCode,\n\t\t\t\tVotedAt:     ctimeStr,\n\t\t\t\tDescription: _STORAGE_TXN_PUT,\n\t\t\t})\n\t\t} else {\n\t\t\toldAdminCode, oldTimeStr, err := decodePair(existing)\n\t\t\tif err != nil {\n\t\t\t\tunexpected.Err = TxnSetVotedDecodePairError{\n\t\t\t\t\tKey:         key,\n\t\t\t\t\tValue:       string(existing),\n\t\t\t\t\tVoter:       idVoter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_DECODE_PAIR,\n\t\t\t\t}\n\t\t\t\treturn unexpected\n\t\t\t}\n\n\t\t\toldTime, err := time.Parse(timefmt, oldTimeStr)\n\t\t\tif err != nil {\n\t\t\t\tunexpected.Err = TxnSetVotedParseTimeError{\n\t\t\t\t\tVoter:       idVoter,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _STORAGE_TS_PARSE,\n\t\t\t\t}\n\t\t\t\treturn unexpected\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase bytes.Equal(existing, votedStatsValue):\n\t\t\t\ttxnOp.PutForce(key, existing) // Keep already existing vote.\n\t\t\tcase idAdminCode == oldAdminCode && ctime.After(oldTime):\n\t\t\t\ttxnOp.PutForce(key, existing) // Keep older vote in same admin.\n\t\t\tcase idAdminCode != oldAdminCode && ctime.Before(oldTime):\n\t\t\t\ttxnOp.PutForce(key, existing) // Keep newer vote in different admin.\n\t\t\tdefault:\n\t\t\t\ttxnOp.PutForce(key, votedStatsValue) // Update the index.\n\n\t\t\t\tlog.Debug(ctx, TxnSetVotedOverrideOldVotedStatsAndAddToTxn{\n\t\t\t\t\tVoterID:     idVoter,\n\t\t\t\t\tAdminCode:   idAdminCode,\n\t\t\t\t\tVotedAt:     ctimeStr,\n\t\t\t\t\tDescription: _STORAGE_TXN_PUT,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tlog.Debug(ctx, TxnSetVotedStartTxnCommit{Description: _STORAGE_TXN_FINISHING})\n\n\t\t// Send ready signal to AutoCommit and wait until committed\n\t\terr = txnOp.Ready(ctx)\n\t\tif err != nil {\n\t\t\t// Transaction had been committed but other errors has arisen.\n\t\t\t// Additional info in error is needed for restoring AddVoteOrder data.\n\t\t\tif errors.CausedBy(err, new(UnexpectedValueError)) != nil {\n\t\t\t\treturn TxnSetVotedAutoCommitUnsuccessfullTxnError{\n\t\t\t\t\tVoterName:   voterName,\n\t\t\t\t\tVoterID:     idVoter,\n\t\t\t\t\tAdminCode:   idAdminCode,\n\t\t\t\t\tDistrict:    district,\n\t\t\t\t\tKey:         key,\n\t\t\t\t\tErr:         err, // UnexpectedValueError is nested here\n\t\t\t\t\tDescription: _STORAGE_TXN_READY,\n\t\t\t\t}\n\t\t\t}\n\t\t\tunexpected.Err = TxnSetVotedAutoCommitError{\n\t\t\t\tKey:         key,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _STORAGE_TXN_READY,\n\t\t\t}\n\t\t\treturn unexpected\n\t\t}\n\n\t\tlog.Debug(ctx, TxnSetVotedTxnSuccessfullyCommited{Description: _STORAGE_TXN_COMMITTED})\n\n\t\t// voterName is empty when rebuilding voted stats\n\t\tif voterName != \"\" {\n\t\t\terr = c.AddVoteOrder(ctx, voterName, idVoter, district, idAdminCode)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlog.Debug(ctx, TxnSetVotedStartTxnCommitTestVote{Description: _STORAGE_TEST_VOTE})\n\n\t\t// Send ready signal to AutoCommit and wait until committed\n\t\terr = txnOp.Ready(ctx)\n\t\tif err != nil {\n\t\t\tunexpected.Err = TxnSetVotedTestVoteAutoCommitError{Err: err, Description: _STORAGE_TXN_READY}\n\t\t\treturn unexpected\n\t\t}\n\n\t\tlog.Debug(ctx, TxnSetVotedTxnSuccessfullyCommitedTestVote{Description: _STORAGE_TXN_COMMITTED})\n\t}\n\t// Update the voted latest index: store the vote identifier with the\n\t// newer timestamp, unless they match in which case corrupt the\n\t// identifier, since we have no way of ordering the two votes.\n\tvotedLatestValue := encodePair(ctimeStr, string(voteID))\n\tif err := c.update(ctx, votedLatestPrefix+idVoter, func(existing []byte) ([]byte, error) {\n\t\tif existing == nil {\n\t\t\treturn votedLatestValue, nil // Initial value.\n\t\t}\n\t\tif bytes.Equal(votedLatestValue, existing) {\n\t\t\treturn nil, nil // Fast path if already same value.\n\t\t}\n\n\t\toldTimeStr, oldVoteID, err := decodePair(existing)\n\t\tif err != nil {\n\t\t\treturn nil, log.Alert(TxnSetVotedVotedLatestDecodePairError{\n\t\t\t\tVoter:       idVoter,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _STORAGE_DECODE_PAIR,\n\t\t\t})\n\t\t}\n\t\toldTime, err := time.Parse(timefmt, oldTimeStr)\n\t\tif err != nil {\n\t\t\treturn nil, log.Alert(TxnSetVotedVotedLatestParseTimeError{\n\t\t\t\tVoter:       idVoter,\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _STORAGE_TS_PARSE,\n\t\t\t})\n\t\t}\n\n\t\tswitch {\n\t\tcase oldTime.After(ctime):\n\t\t\treturn nil, nil // Keep newer vote.\n\t\tcase oldTime.Equal(ctime):\n\t\t\t// Corrupt the index until a newer vote comes in. Keep\n\t\t\t// both vote identifiers for manual debugging.\n\t\t\tvoteID = encodePair(string(voteID), oldVoteID)\n\t\t\treturn encodePair(ctimeStr, string(voteID)), nil\n\t\tdefault:\n\t\t\treturn votedLatestValue, nil // Update the index.\n\t\t}\n\t}); err != nil {\n\t\t// Only log error because \"err\" may have nested UnexpectedValueError\n\t\t// which may result in undesired error message to the client\n\t\tlog.Log(ctx, TxnSetVotedUpdateVotedLatestPrefixError{Err: err, Description: _STORAGE_UPDATE})\n\t\treturn TxnSetVotedUpdateVotedLatestError{Description: _STORAGE_UPDATE}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/token/custom/bearer_test.go",
    "content": "package custom\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cookie\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n)\n\nvar sharedSecret *cookie.C\n\nvar msg = \"Expected no errors, got %v\\n\"\nvar msg2 = \"Expected RawBearerRegexError, got %v\\n\"\n\nfunc init() {\n\tvar err error\n\tsharedSecret, err = cookie.New(make([]byte, 16))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TestCorrectBearer(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tbearerToken := NewFromEmptyBuilder().\n\t\tWithNonce(nonce).\n\t\tWithTimeStamp(time.Now()).\n\t\tBuild()\n\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig := sharedSecret.Create([]byte(payload))\n\n\trawComplete, err := NewFromExistingBuilder().\n\t\tWithPayload(payload).\n\t\tWithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\ttBearer, err := NewFromRawBuilder().\n\t\tWithBearer(rawComplete).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig2, err := tBearer.Signature()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tdata, err := sharedSecret.Open(sig2)\n\tif err != nil || data == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tp, err := tBearer.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\terr = tBearer.Verify()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tif nonce != p {\n\t\t//nolint:lll\n\t\tmsg = \"Expected generatedNonce == parsedFromBearerNone, got generatedNonce: %v, parsedFromBearerNone: %v\\n\"\n\t\tt.Errorf(msg, nonce, p)\n\t}\n}\n\nfunc TestWithoutTimeStampBearer(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tbearerToken := NewFromEmptyBuilder().\n\t\tWithNonce(nonce).\n\t\t// WithTimeStamp(time.Now()).\n\t\tBuild()\n\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig := sharedSecret.Create([]byte(payload))\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\trawComplete, err := NewFromExistingBuilder().\n\t\tWithPayload(payload).\n\t\tWithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\ttBearer, err := NewFromRawBuilder().\n\t\tWithBearer(rawComplete).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig2, err := tBearer.Signature()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tdata, err := sharedSecret.Open(sig2)\n\tif err != nil || data == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\terr = tBearer.Verify()\n\tif err == nil {\n\t\tmsg := \"Expected error, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n\n\tif _, ok := err.(ExpiredBearerTokenError); !ok {\n\t\tmsg := \"Expected ExpiredBearerTokenError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestWithoutNonceBearer(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tbearerToken := NewFromEmptyBuilder().\n\t\t// WithNonce(nonce).\n\t\tWithTimeStamp(time.Now()).\n\t\tBuild()\n\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig := sharedSecret.Create([]byte(payload))\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\trawComplete, err := NewFromExistingBuilder().\n\t\tWithPayload(payload).\n\t\tWithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\ttBearer, err := NewFromRawBuilder().\n\t\tWithBearer(rawComplete).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig2, err := tBearer.Signature()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tdata, err := sharedSecret.Open(sig2)\n\tif err != nil || data == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\terr = tBearer.Verify()\n\tif err != nil {\n\t\tmsg := \"Expected error, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n\n\tp, err := tBearer.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tif nonce == p {\n\t\t//nolint:lll\n\t\tmsg = \"Expected generatedNonce != parsedFromBearerNone, got generatedNonce: %v, parsedFromBearerNone: %v\\n\"\n\t\tt.Errorf(msg, nonce, p)\n\t}\n\n\tif p != \"\" {\n\t\tmsg = `Expected parsedFromBearerNone == \"\", got parsedFromBearerNone: %v\\n`\n\t\tt.Errorf(msg, p)\n\t}\n}\n\nfunc TestWithoutSignatureBearer(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tbearerToken := NewFromEmptyBuilder().\n\t\tWithNonce(nonce).\n\t\tWithTimeStamp(time.Now()).\n\t\tBuild()\n\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\trawComplete, err := NewFromExistingBuilder().\n\t\tWithPayload(payload).\n\t\t// WithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\t_, err = NewFromRawBuilder().\n\t\tWithBearer(rawComplete).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(RawBearerRegexError); !ok {\n\t\tt.Errorf(msg2, err)\n\t}\n}\n\nfunc TestWithoutPayloadBearer(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tbearerToken := NewFromEmptyBuilder().\n\t\tWithNonce(nonce).\n\t\tWithTimeStamp(time.Now()).\n\t\tBuild()\n\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\tsig := sharedSecret.Create([]byte(payload))\n\n\trawComplete, err := NewFromExistingBuilder().\n\t\t// WithPayload(payload).\n\t\tWithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\t_, err = NewFromRawBuilder().\n\t\tWithBearer(rawComplete).\n\t\tBuild().\n\t\tUnmarshal()\n\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(RawBearerRegexError); !ok {\n\t\tt.Errorf(msg2, err)\n\t}\n}\n\nfunc TestWithoutRawBearerBearer(t *testing.T) {\n\t_, err := NewFromRawBuilder().\n\t\tBuild().\n\t\tUnmarshal()\n\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(RawBearerRegexError); !ok {\n\t\tt.Errorf(msg2, err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/empty_bearer.go",
    "content": "package custom\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n)\n\n// Header is not implemented.\nfunc (t *fromEmpty) Header() (string, error) {\n\treturn \"\", nil\n}\n\n// Payload will serialize t into []byte and base64([]byte).\nfunc (t *fromEmpty) Payload() (string, error) {\n\t// Serialize t\n\tb, err := json.Marshal(t)\n\tif err != nil {\n\t\treturn \"\", JSONMarshalError{Err: err,\n\t\t\tDescription: _BEARER_PAYLOAD}\n\t}\n\n\t// Base64 encode marshalled bearer token.\n\t// Note, that no signature was added to the bearer token\n\tt.Data = base64.StdEncoding.EncodeToString(b)\n\treturn t.Data, nil\n}\n\n// Signature is not implemented.\nfunc (t *fromEmpty) Signature() ([]byte, error) {\n\treturn nil, nil\n}\n\n// Verify is not implemented.\nfunc (t *fromEmpty) Verify() error {\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/token/custom/empty_bearer_test.go",
    "content": "package custom\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestEmptyBearerDoesntImplementHeader(t *testing.T) {\n\temptyBearer := NewFromEmptyBuilder().Build()\n\theader, err := emptyBearer.Header()\n\n\tif header != \"\" && err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestEmptyBearerDoesntImplementSignature(t *testing.T) {\n\temptyBearer := NewFromEmptyBuilder().Build()\n\tsig, err := emptyBearer.Signature()\n\n\tif !bytes.Equal(sig, nil) || err != nil {\n\t\tmsg := \"Expected sig: nil, err: nil, got sig %v, err: %v\\n\"\n\t\tt.Errorf(msg, sig, err)\n\t}\n}\n\nfunc TestEmptyBearerDoesntImplementVerify(t *testing.T) {\n\temptyBearer := NewFromEmptyBuilder().Build()\n\terr := emptyBearer.Verify()\n\n\tif err != nil {\n\t\tmsg := \"Expected err == nil, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/empty_builder.go",
    "content": "package custom\n\nimport (\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/token\"\n)\n\ntype fromEmpty struct {\n\tNonce     string    `json:\"nonce\"`\n\tCreatedAt time.Time `json:\"createdAt\"`\n\tSig       string    `json:\"-\"`\n\tData      string    `json:\"-\"`\n}\n\ntype FromEmptyBuilder struct {\n\tnonce     string\n\ttimestamp time.Time\n}\n\n// NewFromEmptyBuilder is a Builder-pattern constructor, which is used\n// to create brand-new Bearer token, which is in an incomplete form.\n//\n// Incomplete form means that Bearer token doesn't have a signature\n// and a payload yet. This particular Bearer token implementation generates\n// payload on a Payload() call, but doesn't generate signature on a Signature()\n// call, use NewFromExistingBuilder to create a Bearer token with a signature\n// and don't forget to add a payload.\nfunc NewFromEmptyBuilder() *FromEmptyBuilder {\n\treturn new(FromEmptyBuilder)\n}\n\nfunc (feb *FromEmptyBuilder) WithNonce(n string) *FromEmptyBuilder {\n\tfeb.nonce = n\n\treturn feb\n}\n\nfunc (feb *FromEmptyBuilder) WithTimeStamp(t time.Time) *FromEmptyBuilder {\n\tfeb.timestamp = t\n\treturn feb\n}\n\n// Build returns a Bearer token with only Payload() method implemented.\n//\n// Payload() method will serialize nonce and timestamp into []byte.\nfunc (feb *FromEmptyBuilder) Build() token.Token {\n\treturn &fromEmpty{\n\t\tNonce:     feb.nonce,\n\t\tCreatedAt: feb.timestamp,\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/existing_bearer.go",
    "content": "package custom\n\nimport \"encoding/base64\"\n\nfunc (b *fromExisting) Marshal() (string, error) {\n\t// Bearer token == <base64>.<base64>\n\treturn b.Data + \".\" + base64.StdEncoding.EncodeToString([]byte(b.Sig)), nil\n}\n"
  },
  {
    "path": "common/collector/token/custom/existing_builder.go",
    "content": "package custom\n\nimport \"ivxv.ee/common/collector/token\"\n\ntype fromExisting struct {\n\tSig  string `json:\"-\"`\n\tData string `json:\"-\"`\n}\n\ntype FromExistingBuilder struct {\n\tpayload   string\n\tsignature string\n}\n\n// NewFromExistingBuilder is a Builder-pattern constructor, which is used\n// to finalize a Bearer token before Marshalling it to a raw Bearer token.\nfunc NewFromExistingBuilder() *FromExistingBuilder {\n\treturn new(FromExistingBuilder)\n}\n\nfunc (feb *FromExistingBuilder) WithPayload(p string) *FromExistingBuilder {\n\tfeb.payload = p\n\treturn feb\n}\n\nfunc (feb *FromExistingBuilder) WithSignature(s string) *FromExistingBuilder {\n\tfeb.signature = s\n\treturn feb\n}\n\n// Build will prepare a Bearer token to be marshalled into a raw Bearer token string.\nfunc (feb *FromExistingBuilder) Build() token.Marshaller {\n\treturn &fromExisting{\n\t\tSig:  feb.signature,\n\t\tData: feb.payload,\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/log_desc.go",
    "content": "package custom\n\nconst (\n\t_BEARER_PAYLOAD           = \"JSON marshal token payload failed\"\n\t_BEARER_REGEX             = \"Bearer token regex failed\"\n\t_BEARER_FORMAT            = \"Invalid Bearer token format\"\n\t_BEARER_B64_PAYLOAD       = \"base-64 decoding Bearer token payload failed\"\n\t_BEARER_B64_SIG           = \"base-64 decoding Bearer token signature failed\"\n\t_BEARER_PAYLOAD_UNMARSHAL = \"JSON unmarshal token payload failed\"\n\t_BEARER_EXPIRED           = \"Expired Bearer token\"\n)\n"
  },
  {
    "path": "common/collector/token/custom/raw_bearer.go",
    "content": "package custom\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/token\"\n)\n\nfunc (c *fromRaw) Unmarshal() (token.Token, error) {\n\t// Regex over raw bearer token\n\tok := rawBearerRegex(c.Bearer)\n\tif !ok {\n\t\treturn nil, RawBearerRegexError{Bearer: c.Bearer,\n\t\t\tDescription: _BEARER_REGEX}\n\t}\n\n\t// Split raw bearer token <payload>.<signature>\n\tbJSON64, bSig64 := splitRawBearer(c.Bearer)\n\tif bJSON64 == \"\" || bSig64 == \"\" {\n\t\treturn nil, SplitRawBearerTokenError{\n\t\t\tPayload:     bJSON64,\n\t\t\tSignature:   bSig64,\n\t\t\tDescription: _BEARER_FORMAT,\n\t\t}\n\t}\n\n\t// Base64 decode payload\n\tbJSON, err := base64.StdEncoding.DecodeString(bJSON64)\n\tif err != nil {\n\t\treturn nil, Base64DecodePayloadError{Err: err,\n\t\t\tDescription: _BEARER_B64_PAYLOAD}\n\t}\n\n\t// Base64 decode signature\n\tbSig, err := base64.StdEncoding.DecodeString(bSig64)\n\tif err != nil {\n\t\treturn nil, Base64DecodeSignatureError{Err: err,\n\t\t\tDescription: _BEARER_B64_SIG}\n\t}\n\n\t// JSON unmarshal payload\n\terr = json.Unmarshal(bJSON, c)\n\tif err != nil {\n\t\treturn nil, JSONUnmarshalError{Err: err,\n\t\t\tDescription: _BEARER_PAYLOAD_UNMARSHAL}\n\t}\n\n\tc.Sig = bSig\n\treturn c, nil\n}\n\n// Header is not implemented.\nfunc (c *fromRaw) Header() (string, error) {\n\treturn \"\", nil\n}\n\n// Payload returns a nonce.\nfunc (c *fromRaw) Payload() (string, error) {\n\treturn c.Nonce, nil\n}\n\n// Signature returns a signature.\nfunc (c *fromRaw) Signature() ([]byte, error) {\n\treturn c.Sig, nil\n}\n\n// Verify only verifies expiration time of a token.\nfunc (c *fromRaw) Verify() error {\n\tnow := time.Now()\n\texpiresAt := c.CreatedAt.Add(ttl)\n\tif !now.Before(expiresAt) {\n\t\treturn ExpiredBearerTokenError{\n\t\t\tNow:         now.String(),\n\t\t\tExpiredAt:   expiresAt.String(),\n\t\t\tDescription: _BEARER_EXPIRED,\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/token/custom/raw_bearer_test.go",
    "content": "package custom\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRawBearerUnmarshalBadRawBearer(t *testing.T) {\n\tb64Payload := base64.StdEncoding.EncodeToString([]byte(\"Hello\"))\n\tb64Sig := base64.StdEncoding.EncodeToString([]byte(\"World\"))\n\n\trawBearer := NewFromRawBuilder().\n\t\tWithBearer(b64Payload + \".\" + b64Sig).\n\t\tBuild()\n\n\tmsg := \"Expected JSONUnmarshalError, got %v\\n\"\n\t_, err := rawBearer.Unmarshal()\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(JSONUnmarshalError); !ok {\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestRawBearerBadBase64Signature(t *testing.T) {\n\tb64Payload := base64.StdEncoding.EncodeToString([]byte(\"Hello\"))\n\tb64Sig := \"World\"\n\n\trawBearer := NewFromRawBuilder().\n\t\tWithBearer(b64Payload + \".\" + b64Sig).\n\t\tBuild()\n\n\tmsg := \"Expected Base64DecodeSignature, got %v\\n\"\n\t_, err := rawBearer.Unmarshal()\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(Base64DecodeSignatureError); !ok {\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestRawBearerBadBase64Payload(t *testing.T) {\n\tb64Payload := \"Hello\"\n\tb64Sig := base64.StdEncoding.EncodeToString([]byte(\"World\"))\n\n\trawBearer := NewFromRawBuilder().\n\t\tWithBearer(b64Payload + \".\" + b64Sig).\n\t\tBuild()\n\n\tmsg := \"Expected Base64DecodePayload, got %v\\n\"\n\t_, err := rawBearer.Unmarshal()\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(Base64DecodePayloadError); !ok {\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestRawBearerBadSplitRawBearerToken(t *testing.T) {\n\tb64Payload := base64.StdEncoding.EncodeToString([]byte(\"Hello\"))\n\tb64Sig := base64.StdEncoding.EncodeToString([]byte(\"World\"))\n\n\trawBearer := NewFromRawBuilder().\n\t\tWithBearer(b64Payload + \".\" + b64Sig + \".\").\n\t\tBuild()\n\n\tmsg := \"Expected SplitRawBearerTokenError, got %v\\n\"\n\t_, err := rawBearer.Unmarshal()\n\tif err == nil {\n\t\tt.Errorf(msg, err)\n\t}\n\tif _, ok := err.(SplitRawBearerTokenError); !ok {\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestRawBearerDoesntImplementHeader(t *testing.T) {\n\trawBearer := fromRaw{Nonce: \"123\", CreatedAt: time.Now()}\n\n\tmarshalled, err := json.Marshal(rawBearer)\n\tif err != nil {\n\t\tif err == nil {\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\t}\n\n\tb64Payload := base64.StdEncoding.EncodeToString(marshalled)\n\tb64Sig := base64.StdEncoding.EncodeToString([]byte(\"World\"))\n\n\ttoUnmarshalBearer := NewFromRawBuilder().\n\t\tWithBearer(b64Payload + \".\" + b64Sig).\n\t\tBuild()\n\n\tbearer, err := toUnmarshalBearer.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n\n\theader, err := bearer.Header()\n\tif header != \"\" && err != nil {\n\t\tt.Errorf(msg, err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/raw_builder.go",
    "content": "package custom\n\nimport (\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/token\"\n)\n\ntype fromRaw struct {\n\tNonce     string    `json:\"nonce\"`\n\tCreatedAt time.Time `json:\"createdAt\"`\n\tBearer    string    `json:\"-\"`\n\tSig       []byte    `json:\"-\"`\n}\n\ntype FromRawBuilder struct {\n\tbearer string\n}\n\n// NewFromRawBuilder is a Builder-pattern constructor, which is used\n// to prepare a raw Bearer token to be unmarshalled it to a token.Token.\nfunc NewFromRawBuilder() *FromRawBuilder {\n\treturn new(FromRawBuilder)\n}\n\nfunc (frb *FromRawBuilder) WithBearer(b string) *FromRawBuilder {\n\tfrb.bearer = b\n\treturn frb\n}\n\n// Build will prepare a raw Bearer token to be unmarshalled into a Bearer token.\n//\n// Unmarshalled Bearer token implements all methods of a token.Token, except\n// Header().\n//\n// It returns a nonce on a Payload() call.\n//\n// Verify() method will only check expiration time of a Bearer token, which is\n// 2 minutes by default.\n//\n// Signature() will return a signature over a Bearer token, all crypto checks\n// are done outside the implementation, since shared secret is used.\nfunc (frb *FromRawBuilder) Build() token.Unmarshaler {\n\treturn &fromRaw{\n\t\tBearer: frb.bearer,\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/custom/util.go",
    "content": "package custom\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\t// <at least 1 char>.<at least 1 char>\n\tpattern = `(.+)\\.(.+)`\n\tttl     = 2 * time.Minute\n)\n\n// rawBearerRegex checks regex over a raw Bearer token b.\nfunc rawBearerRegex(b string) bool {\n\treturn regexp.MustCompile(pattern).MatchString(b)\n}\n\n// splitRaw splits raw Bearer token b into 2 parts (payload, signature).\nfunc splitRawBearer(b string) (string, string) {\n\trawList := strings.Split(b, \".\")\n\tif len(rawList) != 2 {\n\t\treturn \"\", \"\"\n\t}\n\treturn rawList[0], rawList[1]\n}\n"
  },
  {
    "path": "common/collector/token/custom/util_test.go",
    "content": "package custom\n\nimport \"testing\"\n\nfunc TestRawBearerRegex(t *testing.T) {\n\tgoodRawBearerRegexes := []string{\n\t\t\"Hello.World\",\n\t\t\"World.Hello\",\n\t\t`<script>alert(\"Hello World!\")</script>.1`,\n\t\t\"1.1\",\n\t}\n\n\tfor _, goodRawBearerRegex := range goodRawBearerRegexes {\n\t\tif ok := rawBearerRegex(goodRawBearerRegex); !ok {\n\t\t\tmsg := \"Expected true for %v, got %v\\n\"\n\t\t\tt.Errorf(msg, goodRawBearerRegex, ok)\n\t\t}\n\t}\n\n\tbadRawBearerRegexes := []string{\n\t\t\"HelloWorld\",\n\t\t\".HelloWorld\",\n\t\t\"HelloWorld.\",\n\t\t\"\",\n\t\t\".\",\n\t}\n\n\tfor _, badRawBearerRegex := range badRawBearerRegexes {\n\t\tif ok := rawBearerRegex(badRawBearerRegex); ok {\n\t\t\tmsg := \"Expected false for %v, got %v\\n\"\n\t\t\tt.Errorf(msg, badRawBearerRegex, ok)\n\t\t}\n\t}\n}\n\nfunc TestSplitRawBearer(t *testing.T) {\n\tmsg := \"Expected payload: %v and signature %v, got payload: %v and signature %v\\n\"\n\n\tgoodRawBearers := map[string][]string{\n\t\t\"Hello.World\": {\"Hello\", \"World\"},\n\t\t\"World.Hello\": {\"World\", \"Hello\"},\n\t\t`<script>alert(\"Hello World!\")</script>.1`: {`<script>alert(\"Hello World!\")</script>`, \"1\"},\n\t\t\"1.1\": {\"1\", \"1\"},\n\t}\n\n\tfor k, v := range goodRawBearers {\n\t\tpayload, signature := splitRawBearer(k)\n\t\tif v[0] != payload && v[1] != signature {\n\t\t\tt.Errorf(msg, v[0], v[1], payload, signature)\n\t\t}\n\t}\n\n\tbadRawBearers := map[string][]string{\n\t\t\"HelloWorld\":  {\"\", \"\"},\n\t\t\".HelloWorld\": {\"\", \"\"},\n\t\t\"HelloWorld.\": {\"\", \"\"},\n\t\t\"\":            {\"\", \"\"},\n\t\t\".\":           {\"\", \"\"},\n\t}\n\n\tfor k, v := range badRawBearers {\n\t\tpayload, signature := splitRawBearer(k)\n\t\tif v[0] != payload && v[1] != signature {\n\t\t\tt.Errorf(msg, v[0], v[1], payload, signature)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/token.go",
    "content": "package token\n\nimport (\n\t\"crypto/x509\"\n)\n\n// Certifier is the interface that allows any implementation to define\n// its own way to retrieve a x509 certificate.\ntype Certifier interface {\n\t// Certify may return a x509 certificate to the caller, if any exists.\n\tCertify() *x509.Certificate\n}\n\n// Token is the interface that defines basic operations on a Web token.\ntype Token interface {\n\t// Header may return a header part of a token, if any exists.\n\t// However, if you have some implementation that defines its own\n\t// meaning for a header, then it is also OK and should be documented\n\t// for that use case.\n\tHeader() (string, error)\n\n\t// Payload may return a payload part of a token, if any exists.\n\t// However, if you have some implementation that defines its own\n\t// meaning for a payload, then it is also OK and should be documented\n\t// for that use case.\n\tPayload() (string, error)\n\n\t// Signature may return a signature part of a token, if any exists.\n\tSignature() ([]byte, error)\n\n\t// Verify should verify a token. Note, that here is not specified\n\t// what has to be verified exactly, this is up to the implementation.\n\tVerify() error\n}\n\n// Unmarshaler is the interface used to deserialize raw token into a Token.\ntype Unmarshaler interface {\n\n\t// Unmarshal will deserialize raw token into a Token.\n\tUnmarshal() (Token, error)\n}\n\n// Marshaller is the interface used to serialize Token into a raw token.\ntype Marshaller interface {\n\n\t// Marshal will serialize Token into a raw token.\n\tMarshal() (string, error)\n}\n"
  },
  {
    "path": "common/collector/token/webeid/log_desc.go",
    "content": "package webeid\n\nconst (\n\t_WEBEID_REGEX      = \"Web-eID token regex failed\"\n\t_WEBEID_VER        = \"Web-eID token major release is not supported by IVXV\"\n\t_WEBEID_HASH_ALG   = \"Hashing Web-eID origin with specified in Web-eID token algorithm failed\"\n\t_WEBEID_HASH_NONCE = \"Hashing Web-eID nonce with specified in Web-eID token algorithm failed\"\n\t_WEBEID_SIG        = \"Failed to verify Web-eID token signature\"\n\t_WEBEID_JSON       = \"JSON unmarshal Web-eID token failed\"\n\t_WEBEID_CERT_B64   = \"base-64 decoding Web-eID token certificate failed\"\n\t_WEBEID_SIG_B64    = \"base-64 decoding Web-eID token signature failed\"\n\tWEBEID_ALG         = \"Unknown hashing algorithm\"\n\tWEBEID_ALG_REGEX   = \"Hashing algorithm regex failed\"\n)\n"
  },
  {
    "path": "common/collector/token/webeid/raw_builder.go",
    "content": "package webeid\n\nimport (\n\t\"crypto/x509\"\n\n\t\"ivxv.ee/common/collector/token\"\n)\n\ntype fromRaw struct {\n\tToken     string            `json:\"-\"`\n\tNonce     []byte            `json:\"-\"`\n\tOrigin    []byte            `json:\"-\"`\n\tSig       []byte            `json:\"-\"`\n\tFormat    string            `json:\"-\"`\n\tAlgorithm string            `json:\"-\"`\n\tCert      *x509.Certificate `json:\"-\"`\n}\n\ntype FromRawBuilder struct {\n\ttoken  string\n\tnonce  string\n\torigin []byte\n}\n\n// NewFromRawBuilder is a Builder-pattern constructor, which is used\n// to prepare a raw Web eID token to be unmarshalled it to a token.Token.\nfunc NewFromRawBuilder() *FromRawBuilder {\n\treturn new(FromRawBuilder)\n}\n\nfunc (frb *FromRawBuilder) WithToken(t string) *FromRawBuilder {\n\tfrb.token = t\n\treturn frb\n}\n\nfunc (frb *FromRawBuilder) WithNonce(n string) *FromRawBuilder {\n\tfrb.nonce = n\n\treturn frb\n}\n\nfunc (frb *FromRawBuilder) WithOrigin(o []byte) *FromRawBuilder {\n\tfrb.origin = o\n\treturn frb\n}\n\n// Build will produce a Web eID token Unmarshaler with only Unmarshal() method\n// implemented. Use that method to convert raw Web eID token to a token.Token.\n//\n// Once unmarshalled you get access to following methods:\n//\n// a) Verify() -> verifies Web eID token according to the Web eID token specification\n//\n// b) Signature() -> returns a Signature of a Web eID token\nfunc (frb *FromRawBuilder) Build() token.Unmarshaler {\n\treturn &fromRaw{\n\t\tToken:  frb.token,\n\t\tNonce:  []byte(frb.nonce),\n\t\tOrigin: frb.origin,\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/webeid/raw_webeid.go",
    "content": "package webeid\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/token\"\n)\n\nconst (\n\t// Major release of a Web eID authentication token.\n\t// This number should match the number that is retrieved from a Web eID\n\t// auth token while parsing. If numbers doesn't match, then backend should\n\t// refuse further operations with that particular Web eID auth token, since\n\t// major releases are incompatible.\n\tmajorRelease = \"1\"\n\t// Regex for Format field in Web eID token.\n\tformatRegexp = `^web-eid:([0-9]+).([0-9]+)$`\n\t// Allowed signature algorithms:\n\t//\n\t// ECDSA:\n\t// \"ES256\", \"ES384\", \"ES512\",\n\t//\n\t// RSASSA-PSS:\n\t// \"PS256\", \"PS384\", \"PS512\",\n\t//\n\t// RSASSA-PKCS1-v1_5:\n\t// \"RS256\", \"RS384\", \"RS512\",\n\talgorithm = `^(ES|PS|RS)(256|384|512)$`\n)\n\n// webEidAuthToken is a Web eID authentication token that client\n// sends to the webeid service.\n//\n// Reference:\n// https://github.com/web-eid/web-eid-system-architecture-doc#web-eid-authentication-token-specification\ntype webEidAuthToken struct {\n\t// UnverifiedCertificate is a base64 encoded eID user's certificate\n\tUnverifiedCertificate string `json:\"unverifiedCertificate\"`\n\t// Algorithm that was used to produce a Signature\n\tAlgorithm string `json:\"algorithm\"`\n\t// Signature = Sign(Hash(origin) + Hash(nonce))\n\tSignature string `json:\"signature\"`\n\t// Format specifies current version of Web eID (important to be major compatible)\n\tFormat string `json:\"format\"`\n\t// AppVersion is just an additional info (not important)\n\tAppVersion string `json:\"appVersion\"`\n}\n\n// Certify returns Web eID token UnverifiedCertificate field.\nfunc (w *fromRaw) Certify() *x509.Certificate {\n\treturn w.Cert\n}\n\n// Header is not implemented.\nfunc (w *fromRaw) Header() (string, error) {\n\treturn \"\", nil\n}\n\n// Payload is not implemented.\nfunc (w *fromRaw) Payload() (string, error) {\n\treturn \"\", nil\n}\n\n// Signature returns Web eID token Signature field.\nfunc (w *fromRaw) Signature() ([]byte, error) {\n\treturn w.Sig, nil\n}\n\nfunc (w *fromRaw) Verify() error {\n\t// Regex over Format field\n\trelease, err := formatRegex(w.Format)\n\tif err != nil {\n\t\treturn VerifyFormatError{Err: err,\n\t\t\tDescription: _WEBEID_REGEX}\n\t}\n\n\t// Web eID auth token major release should be supported by backend\n\terr = formatMajorRelease(release)\n\tif err != nil {\n\t\treturn VerifyMajorReleaseError{Err: err,\n\t\t\tDescription: _WEBEID_VER}\n\t}\n\n\t// Hash origin with specified algorithm\n\thashOrigin, err := hashIt(w.Origin, w.Algorithm)\n\tif err != nil {\n\t\treturn OriginHashError{Err: err,\n\t\t\tDescription: _WEBEID_HASH_ALG}\n\t}\n\n\t// Hash nonce with specified algorithm\n\thashNonce, err := hashIt(w.Nonce, w.Algorithm)\n\tif err != nil {\n\t\treturn NonceHashError{Err: err,\n\t\t\tDescription: _WEBEID_HASH_NONCE}\n\t}\n\n\t// [1, 2, 3, 4, 5] = append([1, 2, 3], [4, 5])\n\thashOrigin = append(hashOrigin, hashNonce...)\n\n\tsha384SigAlgo := anySignatureAlgorithmToSHA384(w.Cert.SignatureAlgorithm)\n\n\tsignature := w.Sig\n\n\t// ECDSA signatures can be in 2 forms:\n\t// a) ASN1-encoded\n\t// b) Encoded as 2 params (R and S)\n\t// Check whether w.Sig belongs to a) type signature\n\terr = cryptoutil.IsECDSAASN1EncodedSignature(w.Sig)\n\tif err != nil {\n\t\t// w.Sig doesn't belong to a) type, but maybe it is a) type?\n\t\tecdsaSig, err := cryptoutil.ReEncodeECDSASignature(w.Sig)\n\t\tif err == nil {\n\t\t\t// Yes, it is an a) type\n\t\t\tsignature = ecdsaSig\n\t\t}\n\t\t// No, w.Sig is an RSA-based signature\n\t}\n\n\t// This method does 2 things:\n\t// 1. data = SHA384(hashOrigin)\n\t// 2. VerifySignature(data, signature)\n\terr = w.Cert.CheckSignature(sha384SigAlgo, hashOrigin, signature)\n\tif err != nil {\n\t\treturn CheckSignatureError{Err: err,\n\t\t\tDescription: _WEBEID_SIG}\n\t}\n\n\treturn nil\n}\n\nfunc (w *fromRaw) Unmarshal() (token.Token, error) {\n\t// Try to unmarshal raw Web eID auth token\n\tauthToken := webEidAuthToken{}\n\terr := json.Unmarshal([]byte(w.Token), &authToken)\n\tif err != nil {\n\t\treturn nil, JSONUnmarshalError{Err: err,\n\t\t\tDescription: _WEBEID_JSON}\n\t}\n\n\t// Base64 decode eID user's certificate\n\tcert, err := cryptoutil.Base64Certificate(authToken.UnverifiedCertificate)\n\tif err != nil {\n\t\treturn nil, Base64DecodeUnverifiedCertificateError{Err: err,\n\t\t\tDescription: _WEBEID_CERT_B64}\n\t}\n\n\t// Base64 decode eID user's signature\n\tsignature, err := base64.StdEncoding.DecodeString(authToken.Signature)\n\tif err != nil {\n\t\treturn nil, Base64DecodeSignatureError{Err: err,\n\t\t\tDescription: _WEBEID_SIG_B64}\n\t}\n\n\tw.Sig = signature\n\tw.Cert = cert\n\tw.Format = authToken.Format\n\tw.Algorithm = authToken.Algorithm\n\treturn w, nil\n}\n"
  },
  {
    "path": "common/collector/token/webeid/raw_webeid_test.go",
    "content": "package webeid\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\ttkn \"ivxv.ee/common/collector/token\"\n)\n\nfunc TestUnmarshalWithoutRawToken(t *testing.T) {\n\t// Build Web eID token without raw Web eID token\n\ttoken := NewFromRawBuilder().Build()\n\n\t// Unmarshal literally empty raw Web eID token\n\t_, err := token.Unmarshal()\n\n\tif _, ok := err.(JSONUnmarshalError); !ok {\n\t\tmsg := \"Expected JSONUnmarshalError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\n//nolint:lll\nfunc TestUnmarshalWithBadBase64Signature(t *testing.T) {\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"\",\n\t\tSignature:             \"Hello World\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().WithToken(string(rawToken)).Build()\n\n\t_, err = token.Unmarshal()\n\tif _, ok := err.(Base64DecodeSignatureError); !ok {\n\t\tmsg := \"Expected Base64DecodeSignatureError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\n//nolint:lll\nfunc TestVerifyWithBadHashAlgoForOrigin(t *testing.T) {\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n\t\tFormat:                \"web-eid:1.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().WithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\terr = unmarshalled.Verify()\n\tif _, ok := err.(OriginHashError); !ok {\n\t\tmsg := \"Expected OriginHashError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\n//nolint:lll\nfunc TestVerifyWithBadMajorRelease(t *testing.T) {\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n\t\tFormat:                \"web-eid:2.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().\n\t\tWithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\terr = unmarshalled.Verify()\n\tif _, ok := err.(VerifyMajorReleaseError); !ok {\n\t\tmsg := \"Expected VerifyMajorReleaseError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\n//nolint:lll\nfunc TestRawWebeidTokenDoesntImplementHeaderMethod(t *testing.T) {\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n\t\tFormat:                \"web-eid:1.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().\n\t\tWithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\theader, err := unmarshalled.Header()\n\tif header != \"\" && err != nil {\n\t\tmsg := `Expected header: \"\" and err: nil, got header: %v, err: %v\\n`\n\t\tt.Errorf(msg, header, err)\n\t}\n}\n\n//nolint:lll\nfunc TestRawWebeidTokenDoesntImplementPayload(t *testing.T) {\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n\t\tFormat:                \"web-eid:1.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().\n\t\tWithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\theader, err := unmarshalled.Payload()\n\tif header != \"\" && err != nil {\n\t\tmsg := `Expected payload: \"\" and err: nil, got payload: %v, err: %v\\n`\n\t\tt.Errorf(msg, header, err)\n\t}\n}\n\n//nolint:lll\nfunc TestCertifyReturnsUserCert(t *testing.T) {\n\tcertPEM := \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\"\n\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n\t\tFormat:                \"web-eid:1.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().\n\t\tWithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\tcert := unmarshalled.(tkn.Certifier).Certify()\n\n\tcertFromString, err := cryptoutil.Base64Certificate(certPEM)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\tif !reflect.DeepEqual(cert, certFromString) {\n\t\tmsg := \"Expected two x509 certificates are equal, but they aren't\"\n\t\tt.Errorf(msg)\n\t}\n}\n\n//nolint:lll\nfunc TestRawWebeidTokenImplementsSignature(t *testing.T) {\n\tsig := \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\"\n\n\ttemplate := webEidAuthToken{\n\t\tUnverifiedCertificate: \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n\t\tAlgorithm:             \"NO111\",\n\t\tSignature:             sig,\n\t\tFormat:                \"web-eid:1.0\",\n\t}\n\n\trawToken, err := json.Marshal(&template)\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttoken := NewFromRawBuilder().\n\t\tWithToken(string(rawToken)).Build()\n\n\tunmarshalled, err := token.Unmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\tsignature, err := unmarshalled.Signature()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\tb64Sig := base64.StdEncoding.EncodeToString(signature)\n\tif b64Sig != sig {\n\t\tmsg := \"Expected sig == sig from Web eID, got sig: %v, sig from Web eID: %v\\n\"\n\t\tt.Errorf(msg, sig, b64Sig)\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/webeid/testdata/token.example.json",
    "content": "{\n    \"unverifiedCertificate\": \"MIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIwEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0ZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQyNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpLT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYDVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqGSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vCtEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2COX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mnq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQEAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0EAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDlTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBSEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\",\n    \"algorithm\": \"ES512\",\n    \"signature\": \"MGQCMG4DQut8Zi3EU0eySnxCvHA2pW42gj3OqZrHnIRs3a70DnXq9JrANPtsi13FxhppOgIwfvvkhyiM8p1iQg42rScFxYZ+8hecBl8Sd/4GzuFnyysmi7Rk4g9P6Z5QQBQrTDAm\",\n    \"format\": \"web-eid:1.0\",\n    \"appVersion\": \"https://web-eid.eu/web-eid-app/releases/v2.0.0\"\n}"
  },
  {
    "path": "common/collector/token/webeid/testdata/voter.auth.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDBu/aLBGQF2ZMDzYWn\nsvg1JuWITRWWO8dPj+Hqa7TZt02yjchAT4ksFMgCvE+2jZKhZANiAAQa7H2snByT\n8wMy0/RKG+U1kYnbCMyH/TBo2K4ixT+7wrRLlV2a0VFSZak72eyiCYtp36cWCYDP\n4qJonsFXeTSI55GWimP77wpTTP92t0dgjl9Qv/Qv24n3pNT3NdWW+hQ=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "common/collector/token/webeid/testdata/voter.auth.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICnDCCAf6gAwIBAgIDBwqKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYTAkVFMRIw\nEAYDVQQKDAlTQ0NFSVYgT1kxHzAdBgNVBAsMFklWWFYgVGVzdCBDZXJ0aWZpY2F0\nZXMxHzAdBgNVBAMMFlBlcnNvbiBDQSBJbnRlcm1lZGlhdGUwIBcNMjEwNDI4MTQy\nNzEwWhgPMjEyMTA0MDQxNDI3MTBaMHExCzAJBgNVBAYTAkVFMSMwIQYDVQQDDBpL\nT0JSQVMsRVVST09QQSw0MTYwMjI5MDA3ODEPMA0GA1UEBAwGS09CUkFTMRAwDgYD\nVQQqDAdFVVJPT1BBMRowGAYDVQQFExFQTk9FRS00MTYwMjI5MDA3ODB2MBAGByqG\nSM49AgEGBSuBBAAiA2IABBrsfaycHJPzAzLT9Eob5TWRidsIzIf9MGjYriLFP7vC\ntEuVXZrRUVJlqTvZ7KIJi2nfpxYJgM/iomiewVd5NIjnkZaKY/vvClNM/3a3R2CO\nX1C/9C/bifek1Pc11Zb6FKN0MHIwHQYDVR0OBBYEFK2G9S7BGci2AicpZ281+9mn\nq2rMMB8GA1UdIwQYMBaAFIH/R+CRqJF+ti34SbD0J4rYyaY2MA4GA1UdDwEB/wQE\nAwIDiDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwCgYIKoZIzj0E\nAwIDgYsAMIGHAkF+VHMSTvhUd90uEB2fXMIK2ZHcuBeUy/5qx4HQQMlpGy+tUiDl\nTtpzx3ca7FHlEUy77uNv+nFU/9hu2tI6D5bxgAJCAPoBI1Eilu5BoV3l8RC6ngBS\nEGTuBRWCP+yPqqNb/I+pnOck3TC76W8nUSOs2l8Q9uuFt0IBlJnde04kBntWPaOw\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/token/webeid/testdata.go",
    "content": "package webeid\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"os\"\n)\n\n// getCertAndSig will generate test eID user's certificate and a signature.\nfunc getCertAndSig(keyPath, certPath string, originURL, nonce []byte) ([]byte, []byte) {\n\t// Read authentication private key of eID user\n\tkeyPem, err := os.ReadFile(keyPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Read authentication certificate of eID user\n\tcertPem, err := os.ReadFile(certPath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Private key is in PEM format, so decode it to DER\n\tkeyDer, _ := pem.Decode(keyPem)\n\n\t// Certificate is in PEM format, so decode it to DER\n\tcertDer, _ := pem.Decode(certPem)\n\n\t// Parse private key in DER form to PKCS8\n\tpkcs8Key, err := x509.ParsePKCS8PrivateKey(keyDer.Bytes)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Extract private key as Go struct from PKCS8\n\tecdsaKey, ok := pkcs8Key.(*ecdsa.PrivateKey)\n\tif !ok {\n\t\tpanic(\"Cannot cast PKCS8 private key to ECDSA private key\")\n\t}\n\n\t// SHA256(origin), it is hardcoded here, because certDer has\n\t// SignatureAlgorithm == ECDSA-SHA256\n\th1 := sha256.New()\n\th1.Write(originURL)\n\toriginHash := h1.Sum(nil)\n\n\t// SHA256(nonce)\n\th2 := sha256.New()\n\th2.Write(nonce)\n\tnonceHash := h2.Sum(nil)\n\n\t// concatHash = SHA384(SHA256(origin), SHA256(nonce))\n\toriginHash = append(originHash, nonceHash...)\n\th3 := sha512.New384()\n\th3.Write(originHash)\n\tconcatHash := h3.Sum(nil)\n\n\t// Sign result with a eID user's private key\n\tsig, err := ecdsa.SignASN1(rand.Reader, ecdsaKey, concatHash)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Return eID user's authentication certificate and signature\n\treturn certDer.Bytes, sig\n}\n\n// GenerateTestToken will generate test Web eID authentication token\n// that is used in RPC TokenReq.\nfunc GenerateTestToken(keyPath, certPath string, originURL, nonce []byte) string {\n\t// Generate eID user's auth cert and signature\n\tcert, sig := getCertAndSig(keyPath, certPath, originURL, nonce)\n\n\t// Build Web eID auth token up\n\tt := &webEidAuthToken{\n\t\tUnverifiedCertificate: base64.StdEncoding.EncodeToString(cert),\n\t\tAlgorithm:             \"ES256\", // OID 1.2.840.10045.4.3.2\n\t\tSignature:             base64.StdEncoding.EncodeToString(sig),\n\t\tFormat:                \"web-eid:1.0\",\n\t\tAppVersion:            \"https://web-eid.eu/web-eid-app/releases/v2.0.0\",\n\t}\n\n\t// Convert from Go struct to []byte\n\ttoken, err := json.Marshal(t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Return Web eID auth token []byte as a string\n\treturn string(token)\n}\n\n// GenerateBadUnverifiedCertificateToken will generate Web eID token,\n// where incorrect eID user's auth cert is used.\nfunc GenerateBadUnverifiedCertificateToken(keyPath, certPath string, originURL, nonce []byte) string {\n\t_, sig := getCertAndSig(keyPath, certPath, originURL, nonce)\n\n\tt := &webEidAuthToken{\n\t\t// This is bad certificate\n\t\tUnverifiedCertificate: base64.StdEncoding.EncodeToString([]byte(\"Reji\")),\n\t\tAlgorithm:             \"ES256\",\n\t\tSignature:             base64.StdEncoding.EncodeToString(sig),\n\t\tFormat:                \"web-eid:1.0\",\n\t\tAppVersion:            \"https://web-eid.eu/web-eid-app/releases/v2.0.0\",\n\t}\n\n\ttoken, err := json.Marshal(t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(token)\n}\n\n// GenerateBadAlgorithmToken will generate Web eID auth token which\n// declares one Algorithm field, but uses different one for the actual\n// signing.\nfunc GenerateBadAlgorithmToken(keyPath, certPath string, originURL, nonce []byte) string {\n\tcert, sig := getCertAndSig(keyPath, certPath, originURL, nonce)\n\n\tt := &webEidAuthToken{\n\t\tUnverifiedCertificate: base64.StdEncoding.EncodeToString(cert),\n\t\tAlgorithm:             \"ES512\", // should be ES256\n\t\tSignature:             base64.StdEncoding.EncodeToString(sig),\n\t\tFormat:                \"web-eid:1.0\",\n\t\tAppVersion:            \"https://web-eid.eu/web-eid-app/releases/v2.0.0\",\n\t}\n\n\ttoken, err := json.Marshal(t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(token)\n}\n\n// GenerateBadSignatureToken will generate Web eID with the incorrect Signature.\nfunc GenerateBadSignatureToken(keyPath, certPath string, originURL, nonce []byte) string {\n\tcert, _ := getCertAndSig(keyPath, certPath, originURL, nonce)\n\n\tt := &webEidAuthToken{\n\t\tUnverifiedCertificate: base64.StdEncoding.EncodeToString(cert),\n\t\tAlgorithm:             \"ES256\",\n\t\t// This is incorrect signature\n\t\tSignature:  base64.StdEncoding.EncodeToString([]byte(\"Defi\")),\n\t\tFormat:     \"web-eid:1.0\",\n\t\tAppVersion: \"https://web-eid.eu/web-eid-app/releases/v2.0.0\",\n\t}\n\n\ttoken, err := json.Marshal(t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(token)\n}\n\n// GenerateBadFormatToken will generate Web eID with incorrect Format field.\nfunc GenerateBadFormatToken(keyPath, certPath string, originURL, nonce []byte) string {\n\tcert, sig := getCertAndSig(keyPath, certPath, originURL, nonce)\n\n\tt := &webEidAuthToken{\n\t\tUnverifiedCertificate: base64.StdEncoding.EncodeToString(cert),\n\t\tAlgorithm:             \"ES256\",\n\t\tSignature:             base64.StdEncoding.EncodeToString(sig),\n\t\tFormat:                \"web-eid1.0\", // correct is web-eid:1.0\n\t\tAppVersion:            \"https://web-eid.eu/web-eid-app/releases/v2.0.0\",\n\t}\n\n\ttoken, err := json.Marshal(t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(token)\n}\n"
  },
  {
    "path": "common/collector/token/webeid/util.go",
    "content": "package webeid\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"crypto/x509\"\n\t\"hash\"\n\t\"regexp\"\n)\n\n// formatRegex checks whether Format field of a Web eID auth token is correct,\n// according to the regex rules.\n//\n// If correct, then major release of a Web eID token is returned.\nfunc formatRegex(format string) (string, error) {\n\t// format is expected to be web-eid:1.0\n\tre := regexp.MustCompile(formatRegexp)\n\tif !re.MatchString(format) {\n\t\treturn \"\", FormatRegexError{Format: format,\n\t\t\tDescription: _WEBEID_REGEX}\n\t}\n\n\t// [web-eid:1.0, 1, 0]\n\treturn re.FindStringSubmatch(format)[1], nil\n}\n\n// formatMajorRelease ensures that backend and Web eID support\n// the same major release, otherwise incompatible.\nfunc formatMajorRelease(release string) error {\n\tif release != majorRelease {\n\t\treturn FormatMajorReleaseError{\n\t\t\tExpected:    majorRelease,\n\t\t\tGot:         release,\n\t\t\tDescription: _WEBEID_VER,\n\t\t}\n\t}\n\treturn nil\n}\n\n// hashIt hashes it using hash algorithm algo.\nfunc hashIt(it []byte, algo string) ([]byte, error) {\n\t// Web eID algo is a custom formatted string, so parse it first\n\th, err := hashAlgorithm(algo)\n\n\tif err != nil {\n\t\treturn nil, HashDataError{Err: err,\n\t\t\tDescription: WEBEID_ALG}\n\t}\n\n\t// Hash it with algo hashing algorithm\n\th.Write(it)\n\treturn h.Sum(nil), nil\n}\n\n// algorithmRegex checks regex over Web eID auth token Algorithm field.\nfunc algorithmRegex(algo string) (string, error) {\n\tre := regexp.MustCompile(algorithm)\n\tif !re.MatchString(algo) {\n\t\treturn \"\", AlgorithmRegexError{Algorithm: algo,\n\t\t\tDescription: WEBEID_ALG_REGEX}\n\t}\n\n\t// For example \"ES512\" is [ES512, ES, 512]\n\treturn re.FindStringSubmatch(algo)[2], nil\n}\n\n// hashAlgorithm parses Web eID auth token Algorithm field, extract hash\n// algorithm from it and returns it as a hash.Hash.\nfunc hashAlgorithm(algo string) (hash.Hash, error) {\n\t// Check regex over Algorithm first\n\tsigHash, err := algorithmRegex(algo)\n\tif err != nil {\n\t\treturn nil, MalformedAlgorithmError{Err: err,\n\t\t\tDescription: WEBEID_ALG_REGEX}\n\t}\n\n\tswitch sigHash {\n\tcase \"256\":\n\t\treturn sha256.New(), nil\n\tcase \"384\":\n\t\treturn sha512.New384(), nil\n\tcase \"512\":\n\t\treturn sha512.New(), nil\n\tdefault:\n\t\treturn nil, UnsupportedAlgorithmError{Algorithm: algo,\n\t\t\tDescription: WEBEID_ALG}\n\t}\n}\n\n//nolint:lll\n/*\nNB! IMPORTANT\n\nTHIS CONFRONTS WITH THE OFFICIAL DOCUMENTATION OF WEB EID,\nSINCE CPP LIBRARY LIBELECTRONIC-ID FOR ID CARD SIGNING\nhttps://github.com/web-eid/libelectronic-id/blob/0b5e58cf8141df49a1fd14b5e0c588bc6bae410a/src/electronic-ids/pcsc/EstEIDIDEMIA.cpp#L53\nTRUNCATES A HASH THAT IS LONGER THAN 48 BYTES (SHA384) AND\nAPPENDS NULL BYTES IF HASH IS SHORTER THAN 32 BYTES (SHA256).\n\nTHEREFORE WE HAVE TO OVERCOME THAT BY HASHING RESULTING HASH AGAIN WITH SHA384.\n*/\nfunc anySignatureAlgorithmToSHA384(sigAlgo x509.SignatureAlgorithm) x509.SignatureAlgorithm {\n\tswitch sigAlgo {\n\t// 256 --> 384\n\tcase x509.ECDSAWithSHA256:\n\t\treturn x509.ECDSAWithSHA384\n\tcase x509.SHA256WithRSAPSS:\n\t\treturn x509.SHA384WithRSAPSS\n\tcase x509.SHA256WithRSA:\n\t\treturn x509.SHA384WithRSA\n\t// 384 --> 384\n\tcase x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS, x509.SHA384WithRSA:\n\t\treturn sigAlgo\n\t// 512 --> 384\n\tcase x509.ECDSAWithSHA512:\n\t\treturn x509.ECDSAWithSHA384\n\tcase x509.SHA512WithRSAPSS:\n\t\treturn x509.SHA384WithRSAPSS\n\tcase x509.SHA512WithRSA:\n\t\treturn x509.SHA384WithRSA\n\tdefault:\n\t\treturn x509.ECDSAWithSHA384\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/webeid/util_test.go",
    "content": "package webeid\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"hash\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nvar msgExpectNoErrors = \"Expected no errors, got %v\\n\"\n\nfunc TestFormatRegex(t *testing.T) {\n\tgoodRegexFormats := []string{\n\t\t\"web-eid:2.0\",\n\t\t\"web-eid:1.0\",\n\t\t\"web-eid:1.0\",\n\t\t\"web-eid:1.3\",\n\t\t\"web-eid:1.999999999999\",\n\t\t\"web-eid:0.999999999999\",\n\t}\n\n\tfor _, goodRegexFormat := range goodRegexFormats {\n\t\t_, err := formatRegex(goodRegexFormat)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t}\n\n\tbadRegexFormats := []string{\n\t\t\"Hello World!\",\n\t\t\"web-eid:1.\",\n\t\t\"web-eid:.9\",\n\t\t\"web-eid:.\",\n\t\t\"web-eid:\",\n\t\t\"web-eid\",\n\t\t\"\",\n\t}\n\n\tfor _, badRegexFormat := range badRegexFormats {\n\t\t_, err := formatRegex(badRegexFormat)\n\t\t_, ok := err.(FormatRegexError)\n\t\tif err == nil || !ok {\n\t\t\tmsg := \"Expected FormatRegexError, got no error at %v\\n\"\n\t\t\tt.Errorf(msg, badRegexFormat)\n\t\t}\n\t}\n}\n\nfunc TestFormatMajorRelease(t *testing.T) {\n\tgoodFormatMajorReleases := []string{\n\t\t\"web-eid:1.0\",\n\t\t\"web-eid:1.3\",\n\t}\n\n\tfor _, goodFormatMajorRelease := range goodFormatMajorReleases {\n\t\trelease, err := formatRegex(goodFormatMajorRelease)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\n\t\terr = formatMajorRelease(release)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t}\n\n\tbadFormatMajorReleases := []string{\n\t\t\"web-eid:2.0\",\n\t\t\"web-eid:0.999999999999\",\n\t}\n\n\tfor _, badFormatMajorRelease := range badFormatMajorReleases {\n\t\trelease, err := formatRegex(badFormatMajorRelease)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\n\t\terr = formatMajorRelease(release)\n\t\t_, ok := err.(FormatMajorReleaseError)\n\t\tif err == nil || !ok {\n\t\t\tmsg := \"Expected FormatMajorReleaseError, got no error at %v\\n\"\n\t\t\tt.Errorf(msg, badFormatMajorRelease)\n\t\t}\n\t}\n}\n\nfunc TestAlgorithmRegex(t *testing.T) {\n\tonlySupportedAlgos := map[string]string{\n\t\t\"ES256\": \"256\",\n\t\t\"ES384\": \"384\",\n\t\t\"ES512\": \"512\",\n\t\t\"PS256\": \"256\",\n\t\t\"PS384\": \"384\",\n\t\t\"PS512\": \"512\",\n\t\t\"RS256\": \"256\",\n\t\t\"RS384\": \"384\",\n\t\t\"RS512\": \"512\",\n\t}\n\n\tfor onlySupportedAlgo, algo1 := range onlySupportedAlgos {\n\t\talgo2, err := algorithmRegex(onlySupportedAlgo)\n\t\tif algo1 != algo2 {\n\t\t\tmsg := \"Expected both algorithms equal, got a1: %v, a2: %v\\n\"\n\t\t\tt.Errorf(msg, algo1, algo2)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t}\n\n\tbadAlgos := map[string]string{\n\t\t\"ES257\":        \"256\",\n\t\t\"EZ384\":        \"384\",\n\t\t\"512\":          \"512\",\n\t\t\"PS\":           \"256\",\n\t\t\"\":             \"384\",\n\t\t\"PS512PS512\":   \"512\",\n\t\t\"256RS\":        \"256\",\n\t\t\" \":            \"384\",\n\t\t\"Hello World!\": \"512\",\n\t}\n\n\tfor badAlgo, algo1 := range badAlgos {\n\t\talgo2, err := algorithmRegex(badAlgo)\n\t\tif algo1 == algo2 {\n\t\t\tmsg := \"Expected both algorithms not equal, got a1: %v, a2: %v\\n\"\n\t\t\tt.Errorf(msg, algo1, algo2)\n\t\t}\n\t\t_, ok := err.(AlgorithmRegexError)\n\t\tif err == nil || !ok {\n\t\t\tmsg := \"Expected AlgorithmRegexError, got %v\\n\"\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\t}\n}\n\nfunc TestHashIt(t *testing.T) {\n\tgoodAlgoAndHash := map[string][]byte{\n\t\t\"ES256\": []byte(\"\"),\n\t\t\"ES384\": []byte(\"384\"),\n\t\t\"ES512\": nil,\n\t}\n\n\tfor algo, data := range goodAlgoAndHash {\n\t\t_, err := hashIt(data, algo)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t}\n\n\tbadAlgoAndHash := map[string][]byte{\n\t\t\"ES257\": []byte(\"256\"),\n\t\t\"EZ384\": nil,\n\t\t\"\":      []byte(\"\"),\n\t}\n\n\tfor algo, data := range badAlgoAndHash {\n\t\t_, err := hashIt(data, algo)\n\t\t_, ok := err.(HashDataError)\n\t\tif err == nil || !ok {\n\t\t\tmsg := \"Expected HashDataError, got %v\\n\"\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\t}\n}\n\nfunc TestHashAlgorithm(t *testing.T) {\n\tgoodAlgos := map[string]hash.Hash{\n\t\t\"ES256\": sha256.New(),\n\t\t\"ES384\": sha512.New384(),\n\t\t\"RS512\": sha512.New(),\n\t}\n\n\tfor algo1, algo2 := range goodAlgos {\n\t\thashAlgo, err := hashAlgorithm(algo1)\n\t\tif hashAlgo.Size() != algo2.Size() {\n\t\t\tmsg := \"Expected equal hashes, got h1: %v, h2: %v\\n\"\n\t\t\tt.Errorf(msg, hashAlgo, algo2)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t}\n\n\tbadAlgos := map[string]hash.Hash{\n\t\t\"\":    sha512.New384(),\n\t\t\"512\": sha512.New(),\n\t\t\"RS\":  sha256.New(),\n\t}\n\n\tfor algo1, algo2 := range badAlgos {\n\t\thashAlgo, err := hashAlgorithm(algo1)\n\t\tif reflect.DeepEqual(hashAlgo, algo2) {\n\t\t\tmsg := \"Expected no hashes, got h1: %v, h2: %v\\n\"\n\t\t\tt.Errorf(msg, hashAlgo, algo2)\n\t\t}\n\t\t_, ok := err.(MalformedAlgorithmError)\n\t\tif err == nil || !ok {\n\t\t\tmsg := \"Expected MalformedAlgorithmError, got %v\\n\"\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "common/collector/token/webeid/webeid_test.go",
    "content": "package webeid\n\nimport (\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n)\n\nconst (\n\ttestKeyPath  = \"testdata/voter.auth.key\"\n\ttestCertPath = \"testdata/voter.auth.pem\"\n)\n\n// RPC.TokenReq.Token\nvar tokenReqToken string\n\nvar originURL = []byte(\"https://ivxv1.test.ivxv.ee:443\")\nvar challenge []byte\n\nfunc TestCorrectWebeidToken(t *testing.T) {\n\t// Generate nonce\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\t// Generate test Web eID token just like \"Token\" you send to RPC.TokenReq\n\ttokenReqToken = GenerateTestToken(\n\t\ttestKeyPath, testCertPath, originURL, []byte(nonce))\n\tchallenge = []byte(nonce)\n\n\t// Unmarshal raw Web eID token to token.Token\n\twToken, err := NewFromRawBuilder().\n\t\tWithToken(tokenReqToken).\n\t\tWithNonce(string(challenge)).\n\t\tWithOrigin(originURL).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\t// Verify Web eID token according to Web eID specification\n\terr = wToken.Verify()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n}\n\nfunc TestBadUnverifiedCertificateWebeidToken(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttokenReqToken = GenerateBadUnverifiedCertificateToken(\n\t\ttestKeyPath, testCertPath, originURL, []byte(nonce))\n\tchallenge = []byte(nonce)\n\n\t// Unmarshalling should be unsuccessful\n\t_, err = NewFromRawBuilder().\n\t\tWithToken(tokenReqToken).\n\t\tWithNonce(string(challenge)).\n\t\tWithOrigin(originURL).\n\t\tBuild().\n\t\tUnmarshal()\n\tif _, ok := err.(Base64DecodeUnverifiedCertificateError); !ok {\n\t\tmsg := \"Expected Base64DecodeUnverifiedCertificateError, got %v\\n\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestBadAlgorithmWebeidToken(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttokenReqToken = GenerateBadAlgorithmToken(\n\t\ttestKeyPath, testCertPath, originURL, []byte(nonce))\n\tchallenge = []byte(nonce)\n\n\twToken, err := NewFromRawBuilder().\n\t\tWithToken(tokenReqToken).\n\t\tWithNonce(string(challenge)).\n\t\tWithOrigin(originURL).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\terr = wToken.Verify()\n\tif _, ok := err.(CheckSignatureError); !ok {\n\t\tmsg := \"Expected CheckSignatureError, got %v\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestBadSignatureWebeidToken(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttokenReqToken = GenerateBadSignatureToken(\n\t\ttestKeyPath, testCertPath, originURL, []byte(nonce))\n\tchallenge = []byte(nonce)\n\n\twToken, err := NewFromRawBuilder().\n\t\tWithToken(tokenReqToken).\n\t\tWithNonce(string(challenge)).\n\t\tWithOrigin(originURL).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\terr = wToken.Verify()\n\tif _, ok := err.(CheckSignatureError); !ok {\n\t\tmsg := \"Expected CheckSignatureError, got %v\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n\nfunc TestBadFormatWebeidToken(t *testing.T) {\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\ttokenReqToken = GenerateBadFormatToken(\n\t\ttestKeyPath, testCertPath, originURL, []byte(nonce))\n\tchallenge = []byte(nonce)\n\n\twToken, err := NewFromRawBuilder().\n\t\tWithToken(tokenReqToken).\n\t\tWithNonce(string(challenge)).\n\t\tWithOrigin(originURL).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tt.Errorf(msgExpectNoErrors, err)\n\t}\n\n\terr = wToken.Verify()\n\tif _, ok := err.(VerifyFormatError); !ok {\n\t\tmsg := \"Expected VerifyFormatError, got %v\"\n\t\tt.Errorf(msg, err)\n\t}\n}\n"
  },
  {
    "path": "common/collector/tsp/cms.go",
    "content": "package tsp\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\n\t// Import all hash functions supported by this package.\n\t_ \"crypto/sha256\"\n\t_ \"crypto/sha512\"\n)\n\nconst (\n\t// OIDs of supported digest and signature algorithms and signed\n\t// attributes. Use strings instead of asn1.ObjectIdentifier, since they\n\t// will be used as keys in maps.\n\tidSHA256 = \"2.16.840.1.101.3.4.2.1\"\n\tidSHA384 = \"2.16.840.1.101.3.4.2.2\"\n\tidSHA512 = \"2.16.840.1.101.3.4.2.3\"\n\n\tidSHA256WithRSAEncryption = \"1.2.840.113549.1.1.11\"\n\tidSHA384WithRSAEncryption = \"1.2.840.113549.1.1.12\"\n\tidSHA512WithRSAEncryption = \"1.2.840.113549.1.1.13\"\n\n\tidECDSAWithSHA256 = \"1.2.840.10045.4.3.2\"\n\tidECDSAWithSHA384 = \"1.2.840.10045.4.3.3\"\n\tidECDSAWithSHA512 = \"1.2.840.10045.4.3.4\"\n\n\t// https://tools.ietf.org/html/rfc5652#section-11\n\tidContentType   = \"1.2.840.113549.1.9.3\"\n\tidMessageDigest = \"1.2.840.113549.1.9.4\"\n\tidSigningTime   = \"1.2.840.113549.1.9.5\"\n\n\t// https://tools.ietf.org/html/rfc2634#section-5.4\n\tidSigningCert = \"1.2.840.113549.1.9.16.2.12\"\n\n\t// https://tools.ietf.org/html/rfc5035#section-3\n\tidSigningCertV2 = \"1.2.840.113549.1.9.16.2.47\"\n\n\t// https://tools.ietf.org/html/rfc6211#section-2\n\tidCMSAlgorithmProtection = \"1.2.840.113549.1.9.52\"\n)\n\nvar (\n\tdigestAlgs = map[string]crypto.Hash{\n\t\tidSHA256: crypto.SHA256,\n\t\tidSHA384: crypto.SHA384,\n\t\tidSHA512: crypto.SHA512,\n\t}\n\n\t// TODO: We should have unit tests that cover all of these cases to\n\t//       ensure that we are using correct OIDs.\n\tsignatureAlgs = map[string]x509.SignatureAlgorithm{\n\t\tidSHA256WithRSAEncryption: x509.SHA256WithRSA,\n\t\tidSHA384WithRSAEncryption: x509.SHA384WithRSA,\n\t\tidSHA512WithRSAEncryption: x509.SHA512WithRSA,\n\n\t\tidECDSAWithSHA256: x509.ECDSAWithSHA256,\n\t\tidECDSAWithSHA384: x509.ECDSAWithSHA384,\n\t\tidECDSAWithSHA512: x509.ECDSAWithSHA512,\n\t}\n\n\tsignatureDigestOIDs = map[string]string{\n\t\tidSHA256WithRSAEncryption: idSHA256,\n\t\tidSHA384WithRSAEncryption: idSHA384,\n\t\tidSHA512WithRSAEncryption: idSHA512,\n\n\t\tidECDSAWithSHA256: idSHA256,\n\t\tidECDSAWithSHA384: idSHA384,\n\t\tidECDSAWithSHA512: idSHA512,\n\t}\n\n\t// https://tools.ietf.org/html/rfc5652#section-5.1\n\tidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}\n)\n\nfunc (c *Client) checkSignedData(token timeStampToken, gen time.Time) error {\n\tif !token.ContentType.Equal(idSignedData) {\n\t\treturn UnexpectedTSTContentType{ContentType: token.ContentType,\n\t\t\tDescription: _TSP_CONTENT_TYPE}\n\t}\n\tsData := token.Content\n\n\t// https://tools.ietf.org/html/rfc5652#page-10\n\t// Since eContentType is other than id-data the version is 3.\n\tif sData.Version != 3 {\n\t\treturn UnexpectedSignedDataVersionError{\n\t\t\tVersion: sData.Version, Description: _TSP_TOKEN_CONTENT_VERSION,\n\t\t}\n\t}\n\n\t// Ignore the contents of SignedData.DigestAlgorithms: the point of it\n\t// is to allow us to compute the digests of EncapsulatedContentInfo in\n\t// the same pass as we are ASN.1 decoding SignedData. However, we are\n\t// not performing stream processing, so we have to make multiple passes\n\t// anyway.\n\n\t// We expect only one signature and one signer\n\tif len(sData.SignerInfos) != 1 {\n\t\treturn NotASingleSignerError{Count: len(sData.SignerInfos),\n\t\t\tDescription: _TSP_ONE_SIG}\n\t}\n\tsInfo := sData.SignerInfos[0]\n\n\t// Find the signer's certificate from our trusted pool.\n\tcert, err := findCertificate(sInfo, c.signers)\n\tif err != nil {\n\t\treturn UntrustedSigningCertificateError{Err: err,\n\t\t\tDescription: _TSP_CERT}\n\t}\n\n\t// Require that the certificate be included in the response.\n\tvar included bool\n\tfor _, scert := range sData.Certificates {\n\t\tif bytes.Equal(cert.Raw, scert.RawContent) {\n\t\t\tincluded = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !included {\n\t\treturn MissingSignerCertificateError{Signer: cert.Subject.CommonName,\n\t\t\tDescription: _TSP_CERT_EQUAL}\n\t}\n\n\tif err := c.checkSignedAttributes(sInfo, sData.EncapContentInfo, gen, cert); err != nil {\n\t\treturn SignedAttributeCheckError{Err: err, Description: _TSP_ATTR}\n\t}\n\n\tif err := checkSignature(sInfo, cert); err != nil {\n\t\treturn CheckSignatureError{Err: err, Description: _TSP_SIG}\n\t}\n\n\treturn nil\n}\n\nfunc findCertificate(sInfo signerInfo, certs []*x509.Certificate) (c *x509.Certificate, err error) {\n\t// https://tools.ietf.org/html/rfc5652#page-14\n\t// SignerInfo.Version depends on the choice of SignerIdentifier\n\tswitch sInfo.Version {\n\tcase 1:\n\t\tissuer := sInfo.IssuerAndSerialNumber.Issuer\n\t\tserial := sInfo.IssuerAndSerialNumber.SerialNumber\n\t\tif len(issuer) == 0 {\n\t\t\treturn nil, Version1MissingIASNError{Description: _TSP_CERT_ISSUER}\n\t\t}\n\t\tfor _, c := range certs {\n\t\t\tif hasIssuerSerial(c, issuer, serial) {\n\t\t\t\treturn c, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, IASNCertificateNotFoundError{\n\t\t\tIssuer:      issuer,\n\t\t\tSerial:      serial,\n\t\t\tDescription: _TSP_CERT_SERIAL,\n\t\t}\n\tcase 3:\n\t\tif len(sInfo.SubjectKeyIdentifier) == 0 {\n\t\t\treturn nil, Version3MissingSKIError{Description: _TSP_SKI}\n\t\t}\n\t\tfor _, c := range certs {\n\t\t\tif bytes.Equal(sInfo.SubjectKeyIdentifier, c.SubjectKeyId) {\n\t\t\t\treturn c, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, SKICertificateNotFoundError{SKI: sInfo.SubjectKeyIdentifier,\n\t\t\tDescription: _TSP_CERT_SKI}\n\tdefault:\n\t\treturn nil, SignerInfoVersionError{Version: sInfo.Version,\n\t\t\tDescription: _TSP_CERT_VER}\n\t}\n}\n\nfunc hasIssuerSerial(cert *x509.Certificate, issuer pkix.RDNSequence, serial *big.Int) bool {\n\t// Add all parsed non-standard names to serialized RDN sequence.\n\tcert.Issuer.ExtraNames = cert.Issuer.Names\n\treturn cert.SerialNumber.Cmp(serial) == 0 &&\n\t\tcryptoutil.RDNSequenceEqual(cert.Issuer.ToRDNSequence(), issuer)\n}\n\nfunc (c *Client) checkSignedAttributes(sInfo signerInfo, encap encapsulatedContentInfo,\n\tgen time.Time, signer *x509.Certificate) (err error) {\n\n\tattrMap := make(map[string]bool)\n\tfor _, attr := range sInfo.SignedAttrs {\n\t\t// Check that there are no duplicate signed attributes\n\t\tattrID := attr.AttrType.String()\n\t\tif attrMap[attrID] {\n\t\t\treturn DuplicateSignedAttrError{Attribute: attrID,\n\t\t\t\tDescription: _TSP_ATTR_DUP}\n\t\t}\n\t\tattrMap[attrID] = true\n\n\t\t// All of the attributes that we are checking here require that\n\t\t// the attribute value be a SET with a single entry. Check that\n\t\t// the value is a SET and pass all content bytes as a single\n\t\t// entry.\n\t\t//\n\t\t// Since unmarshaling of a raw value has succeeded, we can\n\t\t// assume that at least the tag and length bytes exist.\n\t\tif tag := attr.AttrValue.FullBytes[0]; tag != 49 {\n\t\t\treturn AttributeValueNotASetError{Tag: tag, Description: _TSP_ATTR_TAG}\n\t\t}\n\t\tvalue := attr.AttrValue.Bytes\n\n\t\tswitch attrID {\n\t\tcase idContentType:\n\t\t\tif err = checkContentType(value, encap.EContentType); err != nil {\n\t\t\t\treturn CheckContentTypeError{Err: err, Description: _TSP_ATTR_VER}\n\t\t\t}\n\t\tcase idMessageDigest:\n\t\t\tif err = checkMessageDigest(value,\n\t\t\t\tsInfo.DigestAlgorithm, encap.EContent); err != nil {\n\n\t\t\t\treturn CheckMsgDigestError{Err: err, Description: _TSP_ATTR_DIG}\n\t\t\t}\n\t\tcase idSigningTime:\n\t\t\tif err = c.checkSigningTime(value, gen); err != nil {\n\t\t\t\treturn CheckSigningTimeError{Err: err, Description: _TSP_ATTR_TIME}\n\t\t\t}\n\t\tcase idSigningCert:\n\t\t\tif err = checkSigningCert(value, signer, false); err != nil {\n\t\t\t\treturn CheckSigningCertError{Err: err, Description: _TSP_ATTR_CERT}\n\t\t\t}\n\t\tcase idSigningCertV2:\n\t\t\tif err = checkSigningCert(value, signer, true); err != nil {\n\t\t\t\treturn CheckSigningCertV2Error{Err: err, Description: _TSP_ATTR_CERT}\n\t\t\t}\n\t\tcase idCMSAlgorithmProtection:\n\t\t\tif err = checkCMSAlgorithmProtection(value, sInfo); err != nil {\n\t\t\t\treturn CheckCMSAlgorithmProtectionError{Err: err, Description: _TSP_ATTR_ALG_PROT}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn UnknownAttributeError{Attr: attrID, Description: _TSP_ATTR_UNKNOWN}\n\t\t}\n\t}\n\tif !attrMap[idContentType] {\n\t\treturn NoSignedContentTypeError{Description: _TSP_ATTR_NO_VER}\n\t}\n\tif !attrMap[idMessageDigest] {\n\t\treturn NoSignedMsgDigestError{Description: _TSP_ATTR_NO_DIG}\n\t}\n\tif !attrMap[idSigningTime] {\n\t\treturn NoSignedGenTimeError{Description: _TSP_ATTR_NO_TIME}\n\t}\n\tif !attrMap[idSigningCert] && !attrMap[idSigningCertV2] {\n\t\treturn NoSigningCertError{Description: _TSP_ATTR_NO_CERT}\n\t}\n\treturn\n}\n\nfunc checkContentType(value []byte, encapType asn1.ObjectIdentifier) (err error) {\n\tvar oid asn1.ObjectIdentifier\n\trest, err := asn1.Unmarshal(value, &oid)\n\tif err != nil {\n\t\treturn ContentTypeUnmarshalError{Err: err, Description: _TSP_JSON_OID}\n\t}\n\tif len(rest) > 0 {\n\t\treturn ContentTypeUnmarshalExcessBytesError{Bytes: rest, Description: _TSP_JSON_OID}\n\t}\n\tif !oid.Equal(encapType) {\n\t\treturn SignedAttrContentTypeMismatchError{\n\t\t\tSignedAttribute: oid,\n\t\t\tEContentType:    encapType,\n\t\t\tDescription:     _TSP_OID_EQ,\n\t\t}\n\t}\n\treturn\n}\n\nfunc checkMessageDigest(value []byte, alg pkix.AlgorithmIdentifier, encap []byte) (err error) {\n\tvar digest []byte\n\trest, err := asn1.Unmarshal(value, &digest)\n\tif err != nil {\n\t\treturn MessageDigestUnmarshalError{Err: err, Description: _TSP_JSON_DIG}\n\t}\n\tif len(rest) > 0 {\n\t\treturn MessageDigestUnmarshalExcessBytesError{Bytes: rest, Description: _TSP_JSON_DIG}\n\t}\n\n\tchash, ok := digestAlgs[alg.Algorithm.String()]\n\tif !ok {\n\t\treturn UnsupportedDigestAlgorithm{Algorithm: alg.Algorithm,\n\t\t\tDescription: _TSP_ALG_SUP}\n\t}\n\thash := chash.New()\n\thash.Write(encap)\n\tcalculated := hash.Sum(nil)\n\n\tif !bytes.Equal(digest, calculated) {\n\t\treturn SignedAttributeMsgDigestMismatchError{\n\t\t\tSignedAttribute: digest,\n\t\t\tCalculatedHash:  calculated,\n\t\t\tDescription:     _TSP_DIG_DIFF,\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Client) checkSigningTime(value []byte, gen time.Time) (err error) {\n\tvar t time.Time\n\trest, err := asn1.Unmarshal(value, &t)\n\tif err != nil {\n\t\treturn SigningTimeUnmarshalError{Err: err, Description: _TSP_JSON_TIME}\n\t}\n\tif len(rest) > 0 {\n\t\treturn SigningTimeUnmarshalExcessBytesError{Bytes: rest, Description: _TSP_JSON_TIME}\n\t}\n\n\tdiff := t.Sub(gen)\n\tif diff < 0 || diff > c.delay {\n\t\treturn SignedAttrSigningTimeMismatch{\n\t\t\tSignedAttribute: t,\n\t\t\tGenTime:         gen,\n\t\t\tDescription:     _TSP_TIME_OFF,\n\t\t}\n\t}\n\treturn\n}\n\nfunc checkSigningCert(value []byte, signer *x509.Certificate, v2 bool) (err error) {\n\t// Since the SigningCertificateV2 structure is backwards-compatible to\n\t// v1 we can use it in both cases.\n\tvar signingCert signingCertificateV2\n\trest, err := asn1.Unmarshal(value, &signingCert)\n\tif err != nil {\n\t\treturn SigningCertUnmarshalError{Err: err, Description: _TSP_JSON_CERT}\n\t}\n\tif len(rest) > 0 {\n\t\treturn SigningCertUnmarshalExcessBytes{Bytes: rest, Description: _TSP_JSON_CERT}\n\t}\n\n\tif len(signingCert.Certs) == 0 {\n\t\treturn SigningCertAttrCertsMissing{Description: _TSP_ATTR_NO_CERT}\n\t}\n\n\t// https://tools.ietf.org/html/rfc5035#section-3\n\t// \"The first certificate identified in the sequence of certificate\n\t// identifiers MUST be the certificate used to verify the signature.\"\n\t// We ignore all other chain certificates, because we receive the\n\t// trusted certificate via the configuration.\n\tessCert := signingCert.Certs[0]\n\n\t// Check the signed attribute hash to the certificate hash\n\thashOID := essCert.HashAlgorithm.Algorithm\n\tvar chash crypto.Hash\n\tif v2 {\n\t\t// For a SigningCertificateV2 attribute, the hash algorithm\n\t\t// defaults to SHA-256, but can be explicitly provided.\n\t\tchash = crypto.SHA256\n\t\tif hashOID != nil {\n\t\t\tvar ok bool\n\t\t\tif chash, ok = digestAlgs[hashOID.String()]; !ok {\n\t\t\t\treturn SigningCertUnsupportedDigestAlgorithm{\n\t\t\t\t\tAlgoritm:    hashOID.String(),\n\t\t\t\t\tDescription: _TSP_ALG_SUP,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// For a SigningCertificateV1 attribute, the hash algorithm is\n\t\t// SHA-1 and no algorithm must be provided.\n\t\tchash = crypto.SHA1\n\t\tif hashOID != nil {\n\t\t\treturn SigningCertV1AttributeAlgorithmError{\n\t\t\t\tAlgorithm:   hashOID.String(),\n\t\t\t\tDescription: _TSP_NO_DIG,\n\t\t\t}\n\t\t}\n\t}\n\thash := chash.New()\n\thash.Write(signer.Raw)\n\tcertHash := hash.Sum(nil)\n\tif !bytes.Equal(certHash, essCert.CertHash) {\n\t\treturn SingingCertAttrHashMismatch{\n\t\t\tSigningAttrHash: essCert.CertHash,\n\t\t\tCertificateHash: certHash,\n\t\t\tDescription:     _TSP_DIG_DIFF,\n\t\t}\n\t}\n\n\t// Check the issuer and serial number, if present.\n\tissuer := essCert.IssuerAndSerialNumber.Issuer.DirectoryName\n\tserial := essCert.IssuerAndSerialNumber.SerialNumber\n\tif len(issuer) > 0 {\n\t\tif !hasIssuerSerial(signer, issuer, serial) {\n\t\t\treturn SigningCertIASNMismatchError{\n\t\t\t\tSignerIssuer: signer.Issuer,\n\t\t\t\tSignerSerial: signer.SerialNumber,\n\t\t\t\tAttrIssuer:   issuer,\n\t\t\t\tAttrSerial:   serial,\n\t\t\t\tDescription:  _TSP_CERT_SERIAL,\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc checkCMSAlgorithmProtection(value []byte, sInfo signerInfo) (err error) {\n\tvar protection cmsAlgorithmProtection\n\trest, err := asn1.Unmarshal(value, &protection)\n\tif err != nil {\n\t\treturn CMSAlgorithmProtectionUnmarshalError{Err: err, Description: _TSP_JSON_ALG_PROT}\n\t}\n\tif len(rest) > 0 {\n\t\treturn CMSAlgorithmProtectionUnmarshalExcessBytesError{Bytes: rest, Description: _TSP_JSON_ALG_PROT}\n\t}\n\n\tif !cryptoutil.AlgorithmIdentifierCmp(\n\t\tsInfo.DigestAlgorithm, protection.DigestAlgorithm) {\n\n\t\treturn SignedAttrCMSAlgorithmProtectionDigestMismatchError{\n\t\t\tSignerInfo:             sInfo.DigestAlgorithm.Algorithm,\n\t\t\tCMSAlgorithmProtection: protection.DigestAlgorithm.Algorithm,\n\t\t\tDescription:            _TSP_DIG_DIFF,\n\t\t}\n\t}\n\n\tif !cryptoutil.AlgorithmIdentifierCmp(\n\t\tsInfo.SignatureAlgorithm, protection.SignatureAlgorithm) {\n\n\t\treturn SignedAttrCMSAlgorithmProtectionSignatureMismatchError{\n\t\t\tSignerInfo:             sInfo.SignatureAlgorithm.Algorithm,\n\t\t\tCMSAlgorithmProtection: protection.SignatureAlgorithm.Algorithm,\n\t\t\tDescription:            _TSP_DIG_DIFF,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkSignature(sInfo signerInfo, cert *x509.Certificate) (err error) {\n\tif len(sInfo.Signature) == 0 {\n\t\treturn NoSignatureError{Description: _TSP_NO_SIG}\n\t}\n\n\tsignatureOID := sInfo.SignatureAlgorithm.Algorithm.String()\n\talgo, ok := signatureAlgs[signatureOID]\n\tif !ok {\n\t\treturn SigAlgorithmNotSupportedError{\n\t\t\tAlgorithm:   sInfo.SignatureAlgorithm.Algorithm,\n\t\t\tDescription: _TSP_SIG_ALG,\n\t\t}\n\t}\n\n\tif sInfo.DigestAlgorithm.Algorithm.String() != signatureDigestOIDs[signatureOID] {\n\t\treturn SigDigestAlgorithmMismatchError{\n\t\t\tSignature:   sInfo.SignatureAlgorithm.Algorithm,\n\t\t\tDigest:      sInfo.DigestAlgorithm.Algorithm,\n\t\t\tDescription: _TSP_SIG_ALG_DIFF,\n\t\t}\n\t}\n\n\tvar content []byte\n\tif content, err = asn1.Marshal(sInfo.SignedAttrs); err != nil {\n\t\treturn SignedAttrsMarshalError{Err: err, Description: _TSP_ATTR_JSON}\n\t}\n\n\t// https://tools.ietf.org/html/rfc5652#section-5.4\n\tcontent[0] = 49 // SET OF\n\n\tif err = cert.CheckSignature(algo, content, sInfo.Signature); err != nil {\n\t\treturn CertificateCheckSignatureError{Err: err, Description: _TSP_SIG_VERIFY}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "common/collector/tsp/log_desc.go",
    "content": "package tsp\n\nconst (\n\t_TSP_CONTENT_TYPE          = \"Unexpected timestamp token content type\"\n\t_TSP_TOKEN_CONTENT_VERSION = \"Unsupported timestamp token content version\"\n\t_TSP_ONE_SIG               = \"Only 1 signature in timestamp token is expected\"\n\t_TSP_CERT                  = \"Failed to find a certificate from IVXV trusted certificates pool based on timestamp token info\"\n\t_TSP_CERT_EQUAL            = \"Certificate from IVXV trusted pool is not the same as provided in timestamp token\"\n\t_TSP_ATTR                  = \"Failed to verify signed attributed of timestamp token certificate\"\n\t_TSP_SIG                   = \"Failed to verify timestamp token signature\"\n\t_TSP_CERT_ISSUER           = \"No 'Issuer' field found in a certificate\"\n\t_TSP_CERT_SERIAL           = \"Issuer certificate doesn't have provided 'Serial' value in a certificate\"\n\t_TSP_SKI                   = \"No 'Subject Key Identifier' field found in a certificate\"\n\t_TSP_CERT_SKI              = \"Issuer certificate doesn't have provided 'Subject Key Identifier' value in a certificate\"\n\t_TSP_CERT_VER              = \"Unknown signer info version\"\n\t_TSP_ATTR_DUP              = \"Duplicate signed attributes found in a certificate\"\n\t_TSP_ATTR_TAG              = \"Unknown signed attribute tag\"\n\t_TSP_ATTR_VER              = \"Invalid format of signed attribute version\"\n\t_TSP_ATTR_DIG              = \"Invalid format of signed attributes digest\"\n\t_TSP_ATTR_TIME             = \"Invalid format of signed attributes signing time\"\n\t_TSP_ATTR_CERT             = \"Invalid format of signed attributes signing certificate\"\n\t_TSP_ATTR_ALG_PROT         = \"Invalid format of signed attributes CMS algorithm protection\"\n\t_TSP_ATTR_UNKNOWN          = \"Unknown signed attribute\"\n\t_TSP_ATTR_NO_VER           = \"No version found in signed attributes\"\n\t_TSP_ATTR_NO_DIG           = \"No digest found in signed attributes\"\n\t_TSP_ATTR_NO_TIME          = \"No signing time found in signed attributes\"\n\t_TSP_ATTR_NO_CERT          = \"No version signing certificate in signed attributes\"\n\t_TSP_JSON_OID              = \"Failed to JSON unmarshal Object Identifier\"\n\t_TSP_OID_EQ                = \"Object Identifiers are different\"\n\t_TSP_JSON_DIG              = \"Failed to JSON unmarshal digest bytes\"\n\t_TSP_ALG_SUP               = \"Unknown digest algorithm\"\n\t_TSP_DIG_DIFF              = \"Digests are different\"\n\t_TSP_JSON_TIME             = \"Failed to JSON unmarshal timestamp\"\n\t_TSP_TIME_OFF              = \"Timestamp is expired\"\n\t_TSP_JSON_CERT             = \"Failed to JSON unmarshal certificate\"\n\t_TSP_NO_DIG                = \"No digest algorithm is expected\"\n\t_TSP_JSON_ALG_PROT         = \"Failed to JSON unmarshal CMS algorithm protection\"\n\t_TSP_NO_SIG                = \"No signature provided\"\n\t_TSP_SIG_ALG               = \"Unknown signature algorithm\"\n\t_TSP_SIG_ALG_DIFF          = \"Signature algorithms are different\"\n\t_TSP_ATTR_JSON             = \"Failed to JSON marshal signed attributes\"\n\t_TSP_SIG_VERIFY            = \"Failed to verify signature\"\n\t_TSP_SIGNERS               = \"No intermediate/CA certificates provided to verify TSA response\"\n\t_TSP_SIGNERS_VERIFY        = \"Failed to verify intermediate/CA certificates from configuration\"\n\t_TSP_NO_URI                = \"No TSA provider URI provided\"\n\t_TSP_NONCE                 = \"Failed to generate nonce (20-byte random value)\"\n\t_TSP_PROV_CONN             = \"Failed to connect to TSA provider, trying again\"\n\t_TSP_PROV_ERR              = \"TSA provider responded with error code\"\n\t_TSP_TST_INFO              = \"Failed to parse timestamp token from a TSA provider response\"\n\t_TSP_TST_TIME              = \"Failed to parse timestamp from timestamp token\"\n\t_TSP_TST_DATA              = \"Failed to parse signed data from timestamp token\"\n\t_TSP_REQ                   = \"Failed to ASN.1 marshal a TSP request for TSA provider\"\n\t_TSP_REQ_FINAL             = \"Failed to prepare HTTP request (inside TSP request) for TSA provider\"\n\t_TSP_REQ_FINAL_OK          = \"Successfully prepared HTTP request (inside TSP request) for TSA provider\"\n\t_TSP_REQ_HTTP              = \"Failed to establish HTTP connection to TSA provider\"\n\t_TSP_RESP_HTTP_OK          = \"Successfully got HTTP response from TSA provider\"\n\t_TSP_RESP_HTTP_PARSE       = \"Failed to parse HTTP response from TSA provider\"\n\t_TSP_RESP_HTTP_PARSE_OK    = \"Successfully parsed HTTP response from TSA provider\"\n\t_TSP_RESP_HTTP_RESP_NOK    = \"HTTP response from TSA provider is not HTTP 200\"\n\t_TSP_RESP_HTTP_HEADER      = \"Failed to parse 'application/timestamp-reply' from HTTP header of TSA provider response\"\n\t_TSP_RESP_READ             = \"Failed to read HTTP response from TSA provider\"\n\t_TSP_RESP_READ_OK          = \"Successfully read HTTP response from TSA provider\"\n\t_TSP_RESP_READ_ASN1        = \"Failed to ASN.1 unmarshal HTTP response from TSA provider\"\n\t_TSP_STATUS                = \"TSA provider response status is not 0\"\n\t_TSP_TST_ASN1              = \"Failed to ASN.1 unmarshal timestamp token\"\n\t_TSP_TST_CONTENT_TYPE      = \"Invalid content type ('Object Identifier') of a timestamp token\"\n\t_TSP_TST_CONTENT_ASN1      = \"Failed to ASN.1 unmarshal timestamp token content\"\n\t_TSP_TOKEN_VERSION         = \"Unsupported timestamp token version\"\n\t_TSP_TST_NONCE             = \"Nonce is missing in timestamp token\"\n\t_TSP_TST_NONCE_NOK         = \"Invalid timestamp token nonce\"\n\t_TSP_TIME_FUTURE           = \"Timestamp is set in future\"\n)\n"
  },
  {
    "path": "common/collector/tsp/testdata/DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\nMjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\nVVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\nA1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\nQOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\nQZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\nEuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\nnAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\nAwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\nAwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\nCp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\nhhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\nLnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\ndHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\nn8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\nfbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\nF5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\ndknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\nAgm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\npIX8UV3N9w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "common/collector/tsp/testdata/demo_tsu_ecc_2023.pem.crt",
    "content": "-----BEGIN CERTIFICATE-----\r\nMIIDEjCCApigAwIBAgIQM7BQCImkdt18qWDYdbfOtjAKBggqhkjOPQQDAjBlMSAw\r\nHgYDVQQDDBdURVNUIG9mIFNLIFRTQSBDQSAyMDIzRTEXMBUGA1UEYQwOTlRSRUUt\r\nMTA3NDcwMTMxGzAZBgNVBAoMElNLIElEIFNvbHV0aW9ucyBBUzELMAkGA1UEBhMC\r\nRUUwHhcNMjMwNjE1MDcxMjA0WhcNMjkwNjE0MDcxMjAzWjByMS0wKwYDVQQDDCRE\r\nRU1PIFNLIFRJTUVTVEFNUElORyBBVVRIT1JJVFkgMjAyM0UxFzAVBgNVBGEMDk5U\r\nUkVFLTEwNzQ3MDEzMRswGQYDVQQKDBJTSyBJRCBTb2x1dGlvbnMgQVMxCzAJBgNV\r\nBAYTAkVFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFlmfS6324KQUsz5xSbkG\r\n0PxwZfi94mYeuZkculhxkgmIAD3/sSOIoNqRTHg9Jl4tR2VNcMocjLRli474M6SK\r\nLqOCARswggEXMB8GA1UdIwQYMBaAFGkForSjh0uOXxhFLdWxlzTPZzu3MG8GCCsG\r\nAQUFBwEBBGMwYTA7BggrBgEFBQcwAoYvaHR0cHM6Ly9jLnNrLmVlL1RFU1Rfb2Zf\r\nU0tfVFNBX0NBXzIwMjNFLmRlci5jcnQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9kZW1v\r\nLnNrLmVlL29jc3AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwPAYDVR0fBDUwMzAx\r\noC+gLYYraHR0cHM6Ly9jLnNrLmVlL1RFU1Rfb2ZfU0tfVFNBX0NBXzIwMjNFLmNy\r\nbDAdBgNVHQ4EFgQUPmDgaUB5qWkDeoNoc62C/QKk93YwDgYDVR0PAQH/BAQDAgbA\r\nMAoGCCqGSM49BAMCA2gAMGUCMAK0/sP+jVQFNFakD4SeVy9xAZovv7T9WuaKfztg\r\ndefdJNMm8gaS9HpAa/wwVvnjqQIxAOU2sPULdJMNC6qw563eDasMq9fRUnAf17+/\r\nI+byednRNGW3SGYtyGWN8IKKBut4lA==\r\n-----END CERTIFICATE-----"
  },
  {
    "path": "common/collector/tsp/tsp.go",
    "content": "/*\nPackage tsp implements a PKIX timestamping client.\nhttps://tools.ietf.org/html/rfc3161\n*/\npackage tsp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"io\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/safereader\"\n)\n\nconst (\n\t// The maximum amount the response GenTime can differ from\n\t// the current time at the time of response validation\n\tmaxAge = 1 * time.Minute\n\n\t// The maximum amount the response GenTime can be set in the future\n\t// allowing for correction for system clock inconsistencies\n\t//\n\t// One second for timestamps with only a second accuracy (i.e., that do\n\t// not contain fractions of seconds in Gentime) and one second for\n\t// actual clock skew.\n\tmaxSkew = 2 * time.Second\n\n\t// Maximum size for the timestamping server response.\n\tmaxResponseSize = 10240 // 10 KiB.\n)\n\n// The CMS content type used for timestamp tokens.\n// https://tools.ietf.org/html/rfc3161#page-8\nvar idCTTSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}\n\n// Conf contains the configurable options for the TSP client. It only contains\n// serialized values such that it can easily be unmarshaled from a file.\ntype Conf struct {\n\t// The URL where the TSP requests are sent.\n\tURL string\n\n\t// The certificates used by the server to sign the token.\n\tSigners []string\n\n\t// The maximum time that GenTime and SignTime can differ in a timestamp\n\tDelayTime int64\n\n\t// Retry is the amount of times a TSP request is retried in case of\n\t// network or server errors.\n\tRetry uint64\n\n\t// MaxSkew in milliseconds, if 0 then defaults to 2 seconds.\n\tMaxSkew uint64\n\n\t// MaxAge in minutes, if 0 then defaults to 1 minute.\n\tMaxAge uint64\n}\n\n// Client is used for performing TSP requests and checking responses.\ntype Client struct {\n\turl     string\n\tsigners []*x509.Certificate\n\tdelay   time.Duration\n\tretry   uint64\n\tmaxSkew time.Duration\n\tmaxAge  time.Duration\n}\n\n// New returns a new TSP client with the provided configuration.\nfunc New(conf *Conf) (c *Client, err error) {\n\tif len(conf.Signers) == 0 {\n\t\treturn nil, UnconfiguredSignersError{Description: _TSP_SIGNERS}\n\t}\n\n\tc = &Client{\n\t\turl:     conf.URL,\n\t\tdelay:   time.Duration(conf.DelayTime) * time.Second,\n\t\tretry:   conf.Retry,\n\t\tmaxSkew: time.Duration(conf.MaxSkew) * time.Second, //nolint:gosec\n\t\tmaxAge:  time.Duration(conf.MaxAge) * time.Minute,  //nolint:gosec\n\t}\n\tif conf.MaxSkew <= 0 {\n\t\tc.maxSkew = maxSkew\n\t}\n\tif conf.MaxAge <= 0 {\n\t\tc.maxAge = maxAge\n\t}\n\tif c.signers, err = cryptoutil.PEMCertificates(conf.Signers...); err != nil {\n\t\treturn nil, SignerParsingError{Err: err, Description: _TSP_SIGNERS_VERIFY}\n\t}\n\treturn\n}\n\n// Create requests and verifies a timestamp on data from the configured TSP\n// server. If nonce is not nil, then that value will be used as the nonce in\n// the request, otherwise a random value is generated. Create returns a\n// DER-encoded timestamp token.\nfunc (c *Client) Create(ctx context.Context, data, nonce []byte) ([]byte, error) {\n\tif len(c.url) == 0 {\n\t\treturn nil, UnconfiguredURLError{Description: _TSP_NO_URI}\n\t}\n\n\t// If there is no nonce, generate one\n\tif nonce == nil {\n\t\tnonce = make([]byte, 20)\n\t\tif _, err := rand.Read(nonce); err != nil {\n\t\t\treturn nil, GenerateNonceError{Err: err, Description: _TSP_NONCE}\n\t\t}\n\t}\n\n\tvar tst timeStampToken\n\tvar err error\nretry:\n\tfor attempt := uint64(0); ; attempt++ {\n\t\tswitch tst, err = c.submitRequest(ctx, data, nonce); {\n\t\tcase err == nil:\n\t\t\tbreak retry\n\t\tcase attempt < c.retry && shouldRetry(err):\n\t\t\tlog.Log(ctx, RetryingRequestSubmission{Attempt: attempt + 1, Err: err,\n\t\t\t\tDescription: _TSP_PROV_CONN})\n\t\t\ttime.Sleep(1 * time.Second)\n\t\tdefault:\n\t\t\treturn nil, RequestSubmissionError{Err: err, Description: _TSP_PROV_ERR}\n\t\t}\n\t}\n\n\tinfo, err := checkTSTInfo(tst, data, nonce)\n\tif err != nil {\n\t\treturn nil, TSTInfoCheckError{Err: err, Description: _TSP_TST_INFO}\n\t}\n\n\tif err = c.checkGenTime(time.Now(), info.GenTime, info.Accuracy); err != nil {\n\t\treturn nil, GenTimeCheckError{Err: err, Description: _TSP_TST_TIME}\n\t}\n\n\tif err = c.checkSignedData(tst, info.GenTime); err != nil {\n\t\treturn nil, SignedDataCheckError{Err: err, Description: _TSP_TST_DATA}\n\t}\n\n\treturn tst.Raw, nil\n}\n\nfunc shouldRetry(err error) bool {\n\treturn errors.Walk(err, func(err error) error {\n\t\tswitch t := err.(type) {\n\n\t\t// Sending the HTTP request or reading the response body failed.\n\t\tcase SendRequestError, ResponseBodyReadError:\n\t\t\treturn err\n\n\t\t// The HTTP status was a 5xx code.\n\t\tcase ResponseStatusNotOK:\n\t\t\tif strings.HasPrefix(t.Status.(string), \"5\") {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}) != nil\n}\n\n// Check checks a stored DER-encoded timestamp token on data and returns the\n// token generation time. If nonce is not nil, then the nonce in the timestamp\n// token must match that value.\nfunc (c *Client) Check(response, data, nonce []byte) (time.Time, error) {\n\tvar tsToken timeStampToken\n\tif err := unmarshalTSToken(response, &tsToken); err != nil {\n\t\treturn time.Time{}, err\n\t}\n\n\tinfo, err := checkTSTInfo(tsToken, data, nonce)\n\tif err != nil {\n\t\treturn time.Time{}, CheckTSTInfoCheckError{Err: err, Description: _TSP_TST_INFO}\n\t}\n\n\tif err = c.checkSignedData(tsToken, info.GenTime); err != nil {\n\t\treturn time.Time{}, CheckSignedDataCheckError{Err: err, Description: _TSP_TST_DATA}\n\t}\n\treturn info.GenTime, nil\n}\n\n// ParseTime parses a DER-encoded timestamp token and returns the time it was\n// generated.\n//\n// Warning! ParseTime does not check the validity of the timestamp, it only\n// returns the generation time value.\nfunc ParseTime(response []byte) (time.Time, error) {\n\tvar tsToken timeStampToken\n\tif err := unmarshalTSToken(response, &tsToken); err != nil {\n\t\treturn time.Time{}, err\n\t}\n\tvar info tstInfo\n\tif err := unmarshalTSTInfo(tsToken, &info); err != nil {\n\t\treturn time.Time{}, err\n\t}\n\treturn info.GenTime, nil\n}\n\nfunc (c *Client) submitRequest(ctx context.Context, data, nonce []byte) (\n\ttst timeStampToken, err error) {\n\n\t// Construct the request.\n\thash := sha256.Sum256(data)\n\n\tvar n *big.Int\n\tif nonce != nil {\n\t\tn = new(big.Int).SetBytes(nonce)\n\t}\n\n\treq := tsRequest{\n\t\tVersion: 1,\n\t\tMessageImprint: messageImprint{\n\t\t\tHashAlgorithm: pkix.AlgorithmIdentifier{\n\t\t\t\t// Algorithm identifier for SHA-256.\n\t\t\t\tAlgorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},\n\t\t\t},\n\t\t\tHashedMessage: hash[:],\n\t\t},\n\t\tNonce:   n,\n\t\tCertReq: true,\n\t}\n\n\treqBytes, err := asn1.Marshal(req)\n\tif err != nil {\n\t\terr = MarshallingRequestError{Err: err, Description: _TSP_REQ}\n\t\treturn\n\t}\n\n\t// Submit the request and read the response.\n\thttpReq, err := http.NewRequest(http.MethodPost, c.url, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\terr = NewRequestError{Err: err, Description: _TSP_REQ_FINAL}\n\t\treturn\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\thttpReq.Header.Set(\"Content-Type\", \"application/timestamp-query\")\n\n\treqDump, err := httputil.DumpRequestOut(httpReq, true)\n\tif err != nil {\n\t\terr = ReqDumpError{Err: err, Description: _TSP_REQ_FINAL}\n\t\treturn\n\t}\n\tlog.Debug(ctx, RequestDump{Request: string(reqDump), Description: _TSP_REQ_FINAL_OK})\n\n\tlog.Log(ctx, SendingRequest{\n\t\tURL:         c.url,\n\t\tHash:        req.MessageImprint.HashedMessage,\n\t\tNonce:       n,\n\t\tDescription: _TSP_REQ_FINAL_OK,\n\t})\n\n\thttpResp, err := http.DefaultClient.Do(httpReq)\n\tif err != nil {\n\t\terr = log.Alert(SendRequestError{Err: err, Description: _TSP_REQ_HTTP})\n\t\treturn\n\t}\n\tlog.Log(ctx, ReceivedResponse{Description: _TSP_RESP_HTTP_OK})\n\n\tdefer func() {\n\t\tif closeErr := httpResp.Body.Close(); closeErr != nil && err == nil {\n\t\t\terr = closeErr\n\t\t}\n\t}()\n\n\trespDump, err := httputil.DumpResponse(httpResp, false)\n\tif err != nil {\n\t\terr = RespDumpError{Err: err, Description: _TSP_RESP_HTTP_PARSE}\n\t\treturn\n\t}\n\tlog.Debug(ctx, ResponseDump{Dump: string(respDump), Description: _TSP_RESP_HTTP_PARSE_OK})\n\n\tif httpResp.StatusCode != http.StatusOK {\n\t\terr = ResponseStatusNotOK{Status: httpResp.Status, Description: _TSP_RESP_HTTP_RESP_NOK}\n\t\treturn\n\t}\n\n\tif ctype := httpResp.Header.Get(\"Content-Type\"); ctype != \"application/timestamp-reply\" {\n\t\terr = UnexpectedResponseContentType{ContentType: ctype, Description: _TSP_RESP_HTTP_HEADER}\n\t\treturn\n\t}\n\n\t// The entire response will be returned (as tst.Raw) so we need to\n\t// allocate a new byte slice using ioutil.ReadAll.\n\tbody, err := io.ReadAll(safereader.New(httpResp.Body, maxResponseSize))\n\tif err != nil {\n\t\terr = ResponseBodyReadError{Err: err, Description: _TSP_RESP_READ}\n\t\treturn\n\t}\n\tlog.Debug(ctx, BodyDump{Body: body, Description: _TSP_RESP_READ_OK})\n\n\t// Parse the response and check TSP status.\n\tvar resp tsResponse\n\trest, err := asn1.Unmarshal(body, &resp)\n\tif err != nil {\n\t\terr = ResponseUnmarshalError{Err: err, Description: _TSP_RESP_READ_ASN1}\n\t\treturn\n\t} else if len(rest) > 0 {\n\t\terr = ResponseUnmarshalExcessBytes{Bytes: rest, Description: _TSP_RESP_READ_ASN1}\n\t\treturn\n\t}\n\n\t// https://tools.ietf.org/html/rfc3161#section-2.4.2\n\t// Status 0 means granted. We do not allow status 1 meaning granted with modifications\n\tif resp.Status.Status != 0 {\n\t\terr = TSStatusError{Status: resp.Status.Status, Description: _TSP_STATUS}\n\t\treturn\n\t}\n\n\ttst = resp.TimeStampToken\n\treturn\n}\n\nfunc unmarshalTSToken(data []byte, tsToken *timeStampToken) error {\n\trest, err := asn1.Unmarshal(data, tsToken)\n\tif err != nil {\n\t\treturn TSTokenUnmarshalError{Err: err, Description: _TSP_TST_ASN1}\n\t}\n\tif len(rest) > 0 {\n\t\treturn TSTokenExcessBytesError{Bytes: rest, Description: _TSP_TST_ASN1}\n\t}\n\treturn nil\n}\n\nfunc unmarshalTSTInfo(tst timeStampToken, info *tstInfo) error {\n\tencap := tst.Content.EncapContentInfo\n\tif !encap.EContentType.Equal(idCTTSTInfo) {\n\t\treturn UnexpectedEcontentType{Type: encap.EContentType, Description: _TSP_TST_CONTENT_TYPE}\n\t}\n\n\trest, err := asn1.Unmarshal(encap.EContent, info)\n\tif err != nil {\n\t\treturn EContentUnmarshalError{Err: err, Description: _TSP_TST_CONTENT_ASN1}\n\t}\n\tif len(rest) > 0 {\n\t\treturn EContentUnmarshalExcessBytesError{Bytes: rest, Description: _TSP_TST_CONTENT_ASN1}\n\t}\n\treturn nil\n}\n\nfunc checkTSTInfo(tst timeStampToken, data, nonce []byte) (info tstInfo, err error) {\n\tif err := unmarshalTSTInfo(tst, &info); err != nil {\n\t\treturn info, err\n\t}\n\n\t// https://tools.ietf.org/html/rfc3161#page-8\n\tif info.Version != 1 {\n\t\treturn info, UnexpectedTSTInfoVersion{Version: info.Version, Description: _TSP_TOKEN_VERSION}\n\t}\n\n\t// https://tools.ietf.org/html/rfc3161#page-8\n\t// info.MessageImprint MUST have the same value as the similar field in\n\t// TimeStampReq. We relax this a little bit: the message imprint must\n\t// be a digest over data. This means that the response can use a\n\t// different algorithm than the request, but since tha TSA does not\n\t// know the data, then they should be unable to construct another\n\t// digest.\n\tchash, ok := digestAlgs[info.MessageImprint.HashAlgorithm.Algorithm.String()]\n\tif !ok {\n\t\treturn info, UnsupportedMessageImprintAlgorithm{\n\t\t\tAlgorithm:   info.MessageImprint.HashAlgorithm,\n\t\t\tDescription: _TSP_ALG_SUP,\n\t\t}\n\t}\n\thash := chash.New()\n\thash.Write(data)\n\tcalculated := hash.Sum(nil)\n\n\tif !bytes.Equal(info.MessageImprint.HashedMessage, calculated) {\n\t\treturn info, MessageImprintMismatch{\n\t\t\tAlgorithm:   info.MessageImprint.HashAlgorithm,\n\t\t\tToken:       info.MessageImprint.HashedMessage,\n\t\t\tData:        calculated,\n\t\t\tDescription: _TSP_DIG_DIFF,\n\t\t}\n\t}\n\n\t// https://tools.ietf.org/html/rfc3161#page-11\n\t// If the nonce field is present in the request it MUST be present in the response.\n\t// Also both nonces MUST have the same value.\n\tif nonce != nil {\n\t\tif info.Nonce == nil {\n\t\t\treturn info, ResponseNonceMissing{Description: _TSP_TST_NONCE}\n\t\t}\n\t\tn := new(big.Int).SetBytes(nonce)\n\t\tif info.Nonce.Cmp(n) != 0 {\n\t\t\treturn info, ResponseNonceMismatch{\n\t\t\t\tExpected:    n,\n\t\t\t\tGot:         info.Nonce,\n\t\t\t\tDescription: _TSP_TST_NONCE_NOK,\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Client) checkGenTime(now, gen time.Time, acc accuracy) error {\n\taccuracy := time.Duration(acc.Seconds)*time.Second +\n\t\ttime.Duration(acc.Millis)*time.Millisecond +\n\t\ttime.Duration(acc.Micros)*time.Microsecond\n\n\tif age := now.Sub(gen) + accuracy; age > c.maxAge {\n\t\treturn GenTimeTooOld{Age: age, GenTime: gen, Description: _TSP_TIME_OFF}\n\t}\n\n\tskewed := now.Add(c.maxSkew - accuracy)\n\tif gen.After(skewed) {\n\t\treturn GenTimeSetInFuture{Skewed: skewed, GenTime: gen, Description: _TSP_TIME_FUTURE}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "common/collector/tsp/tsp_test.go",
    "content": "package tsp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/log\"\n\n\t_ \"crypto/sha256\"\n)\n\nvar client *Client\n\nfunc init() {\n\tpem, err := os.ReadFile(\"testdata/demo_tsu_ecc_2023.pem.crt\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n\tclient, err = New(&Conf{\n\t\tURL:       \"http://demo.sk.ee/tsaecc/\",\n\t\tSigners:   []string{string(pem)},\n\t\tDelayTime: 1,\n\t\tRetry:     2,\n\t\tMaxSkew:   1,\n\t\tMaxAge:    2,\n\t})\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc TestCreate(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Short mode on, skipping TSP test against live responder\")\n\t}\n\n\tctx := log.TestContext(context.Background())\n\n\tdata := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\n\t_, err := client.Create(ctx, data, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestCheck(t *testing.T) {\n\tbytes, err := os.ReadFile(\"testdata/test_response_08-11-2023\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdata := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}\n\tnonce := []byte{0xc8, 0xac, 0x0e, 0x31, 0x01, 0x26, 0x36, 0xd5, 0x1b,\n\t\t0x87, 0x28, 0xb6, 0x18, 0x4f, 0x4e, 0x66, 0xc7, 0x65, 0x2e, 0xcd}\n\tgenTime, err := client.Check(bytes, data, nonce)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttimestamp := time.Date(2023, time.November, 8, 13, 59, 49, 0, time.UTC)\n\tif !genTime.Equal(timestamp) {\n\t\tt.Errorf(\"reported genTime %s does not match expected %s\", genTime, timestamp)\n\t}\n}\n"
  },
  {
    "path": "common/collector/tsp/types.go",
    "content": "package tsp\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\t\"time\"\n)\n\n// REQUEST\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.1\n// Leave out optional fields we don't use\ntype tsRequest struct {\n\tVersion        int `asn1:\"default:1\"`\n\tMessageImprint messageImprint\n\tNonce          *big.Int `asn1:\"optional\"`\n\tCertReq        bool\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.1\ntype messageImprint struct {\n\tHashAlgorithm pkix.AlgorithmIdentifier\n\tHashedMessage []byte\n}\n\n// RESPONSE\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\ntype tsResponse struct {\n\tStatus         pkiStatusInfo\n\tTimeStampToken timeStampToken `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\ntype pkiStatusInfo struct {\n\tStatus       int\n\tStatusString []string       `asn1:\"optional\"`\n\tFailInfo     asn1.BitString `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\n// TimeStampToken ::= ContentInfo\n//\n//\t-- contentType is id-signedData\n//\t-- content is SignedData\n//\n//nolint:fmt\ntype timeStampToken struct {\n\tRaw         asn1.RawContent\n\tContentType asn1.ObjectIdentifier\n\tContent     signedData `asn1:\"explicit,tag:0\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.1\ntype signedData struct {\n\tVersion          int\n\tDigestAlgorithms []pkix.AlgorithmIdentifier `asn1:\"set\"`\n\tEncapContentInfo encapsulatedContentInfo\n\tCertificates     []certificateChoices   `asn1:\"set,tag:0,optional\"`\n\tCrls             []pkix.CertificateList `asn1:\"set,tag:1,optional\"`\n\tSignerInfos      []signerInfo           `asn1:\"set\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.2\ntype certificateChoices struct {\n\tRawContent asn1.RawContent\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.2\ntype encapsulatedContentInfo struct {\n\tRawContent   asn1.RawContent\n\tEContentType asn1.ObjectIdentifier\n\tEContent     []byte `asn1:\"explicit,tag:0,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype signerInfo struct {\n\tVersion int\n\n\t// Golang doesn't support ASN.1 CHOICE, so make 2 optional fields\n\tIssuerAndSerialNumber issuerAndSerialNumber `asn1:\"optional\"`\n\tSubjectKeyIdentifier  []byte                `asn1:\"tag:0,optional\"`\n\n\tDigestAlgorithm    pkix.AlgorithmIdentifier\n\tSignedAttrs        []signedAttribute `asn1:\"set,tag:0,optional\"`\n\tSignatureAlgorithm pkix.AlgorithmIdentifier\n\tSignature          []byte\n\tUnsignedAttrs      []pkix.AttributeTypeAndValue `asn1:\"set,tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype signedAttribute struct {\n\tAttrType  asn1.ObjectIdentifier\n\tAttrValue asn1.RawValue\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.4\ntype issuerAndSerialNumber struct {\n\tIssuer       pkix.RDNSequence\n\tSerialNumber *big.Int\n}\n\n// https://tools.ietf.org/html/rfc3161#page-8\ntype tstInfo struct {\n\tVersion        int\n\tPolicy         asn1.ObjectIdentifier\n\tMessageImprint messageImprint\n\tSerialNumber   *big.Int\n\tGenTime        time.Time\n\tAccuracy       accuracy         `asn1:\"optional\"`\n\tOrdering       bool             `asn1:\"optional\"`\n\tNonce          *big.Int         `asn1:\"optional\"`\n\tExtensions     []pkix.Extension `asn1:\"tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#page-10\ntype accuracy struct {\n\tSeconds int `asn1:\"optional\"`\n\tMillis  int `asn1:\"tag:0,optional\"`\n\tMicros  int `asn1:\"tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5035#section-3\ntype signingCertificateV2 struct {\n\tCerts    []essCertIDv2\n\tPolicies asn1.RawValue `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5035#section-4\ntype essCertIDv2 struct {\n\tHashAlgorithm         pkix.AlgorithmIdentifier `asn1:\"optional\"`\n\tCertHash              []byte\n\tIssuerAndSerialNumber issuerSerial `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5035#section-4\ntype issuerSerial struct {\n\t// Although the RFC says SEQUENCE of generalName we will assume 1 name\n\tIssuer       generalName\n\tSerialNumber *big.Int\n}\n\n// https://tools.ietf.org/html/rfc5280#page-38\ntype generalName struct {\n\tDirectoryName pkix.RDNSequence `asn1:\"explicit,tag:4\"`\n}\n\n// https://tools.ietf.org/html/rfc6211#section-2\ntype cmsAlgorithmProtection struct {\n\tDigestAlgorithm    pkix.AlgorithmIdentifier\n\tSignatureAlgorithm pkix.AlgorithmIdentifier `asn1:\"tag:1\"`\n}\n"
  },
  {
    "path": "common/collector/yaml/ast.go",
    "content": "package yaml\n\nimport (\n\t\"encoding/base64\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\n// Node is a node in the YAML AST. It is of concrete type Scalar, Sequence, or\n// Mapping.\ntype Node interface {\n\t// apply sets v to the value of the node.\n\tapply(v reflect.Value)\n\n\t// equal compares a Node to another Node of the same type, including\n\t// the path. Only used for testing.\n\tequal(other Node) bool\n}\n\n// apply calls n.apply(v), but gracefully handles cases where n is nil.\nfunc apply(n Node, v reflect.Value) {\n\tif n == nil {\n\t\tv = indirect(v)\n\t\tv.Set(reflect.Zero(v.Type()))\n\t\treturn\n\t}\n\tn.apply(v)\n}\n\n// equal calls n.equal(other), but gracefully handles cases where n is nil.\nfunc equal(n, other Node) bool {\n\tif n == nil {\n\t\treturn other == nil\n\t}\n\treturn n.equal(other)\n}\n\n// Scalar represents a YAML scalar.\ntype Scalar struct {\n\tpath  string\n\tvalue string\n}\n\n// apply sets the value of a string, byte slice, int, or bool to the scalar.\nfunc (s Scalar) apply(v reflect.Value) {\n\tv = indirect(v)\n\tswitch v.Kind() {\n\tcase reflect.String:\n\t\tv.SetString(s.String())\n\tcase reflect.Int64:\n\t\ti, err := strconv.ParseInt(s.String(), 10, 64)\n\t\tif err != nil {\n\t\t\ts.error(InvalidInt64Error{Value: s, Err: err, Description: _YAML_INT64})\n\t\t}\n\t\tv.SetInt(i)\n\tcase reflect.Uint64:\n\t\tu, err := strconv.ParseUint(s.String(), 10, 64)\n\t\tif err != nil {\n\t\t\ts.error(InvalidUint64Error{Value: s, Err: err, Description: _YAML_INT64})\n\t\t}\n\t\tv.SetUint(u)\n\tcase reflect.Bool:\n\t\tswitch s.String() {\n\t\tcase \"true\":\n\t\t\tv.SetBool(true)\n\t\tcase \"false\":\n\t\t\tv.SetBool(false)\n\t\tdefault:\n\t\t\ts.error(InvalidBoolError{Value: s, Description: _YAML_BOOL})\n\t\t}\n\tcase reflect.Slice:\n\t\tif v.Type().Elem().Kind() == reflect.Uint8 {\n\t\t\tb, err := base64.StdEncoding.DecodeString(s.String())\n\t\t\tif err != nil {\n\t\t\t\ts.error(InvalidBase64Error{Value: s, Err: err, Description: _YAML_B64})\n\t\t\t}\n\t\t\tv.Set(reflect.ValueOf(b))\n\t\t\tbreak\n\t\t}\n\t\tfallthrough\n\tdefault:\n\t\tif !reflect.TypeOf(s).AssignableTo(v.Type()) {\n\t\t\ts.error(InvalidScalarError{Type: v.Type(), Description: _YAML_SCALAR})\n\t\t}\n\t\tv.Set(reflect.ValueOf(s))\n\t}\n}\n\nfunc (s Scalar) equal(other Node) bool {\n\to, ok := other.(Scalar)\n\treturn ok && s.String() == o.String() && s.path == o.path\n}\n\nfunc (s Scalar) String() string {\n\treturn s.value\n}\n\nfunc (s Scalar) error(err error) {\n\tstop(ApplyScalarError{Path: s.path, Err: err, Description: _YAML_SCALAR_ERR})\n}\n\n// Sequence represents a YAML sequence.\ntype Sequence struct {\n\tpath     string\n\telements []Node\n}\n\n// apply sets the elements of a slice or array.\nfunc (s Sequence) apply(v reflect.Value) {\n\tv = indirect(v)\n\tswitch v.Kind() {\n\tcase reflect.Slice:\n\t\tif v.Cap() >= len(s.elements) {\n\t\t\tv.SetLen(len(s.elements))\n\t\t} else {\n\t\t\tv.Set(reflect.MakeSlice(v.Type(), len(s.elements), len(s.elements)))\n\t\t}\n\tcase reflect.Array:\n\t\tif v.Len() != len(s.elements) {\n\t\t\ts.error(InvalidArrayLengthError{Len: v.Len(), Expected: len(s.elements),\n\t\t\t\tDescription: _YAML_ARRAY})\n\t\t}\n\tdefault:\n\t\tif !reflect.TypeOf(s).AssignableTo(v.Type()) {\n\t\t\ts.error(InvalidSliceError{Type: v.Type(), Description: _YAML_SLICE})\n\t\t}\n\t\tv.Set(reflect.ValueOf(s))\n\t\treturn\n\t}\n\n\tfor i, n := range s.elements {\n\t\te := reflect.New(v.Type().Elem()).Elem()\n\t\tapply(n, e)\n\t\tv.Index(i).Set(e)\n\t}\n}\n\nfunc (s Sequence) equal(other Node) bool {\n\to, ok := other.(Sequence)\n\tif !ok || len(s.elements) != len(o.elements) || s.path != o.path {\n\t\treturn false\n\t}\n\tif s.elements == nil || o.elements == nil {\n\t\treturn s.elements == nil && o.elements == nil\n\t}\n\tfor i, v := range o.elements {\n\t\tif !equal(s.elements[i], v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (s Sequence) error(err error) {\n\tstop(ApplySequenceError{Path: s.path, Err: err, Description: _YAML_SEQUENCE_ERR})\n}\n\n// Mapping represents a YAML mapping.\ntype Mapping struct {\n\tpath  string\n\tpairs map[string]Node\n}\n\n// apply sets the fields of a struct or entries of a map.\nfunc (m Mapping) apply(v reflect.Value) {\n\tv = indirect(v)\n\tswitch v.Kind() {\n\tcase reflect.Struct:\n\t\tfor k, n := range m.pairs {\n\t\t\tvar f reflect.Value\n\n\t\t\t// Try an exact match first.\n\t\t\tif exported(k) {\n\t\t\t\tf = v.FieldByName(k)\n\t\t\t}\n\n\t\t\t// Otherwise try case-insensitive comparison.\n\t\t\tif !f.IsValid() {\n\t\t\t\tlk := strings.ToLower(k)\n\t\t\t\tf = v.FieldByNameFunc(func(name string) bool {\n\t\t\t\t\treturn exported(name) && strings.ToLower(name) == lk\n\t\t\t\t})\n\t\t\t}\n\t\t\tif !f.IsValid() {\n\t\t\t\t// Ignore unspecified fields.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\te := reflect.New(f.Type()).Elem()\n\t\t\tapply(n, e)\n\t\t\tf.Set(e)\n\t\t}\n\tcase reflect.Map:\n\t\tktype := v.Type().Key()\n\t\tif !reflect.TypeOf(\"\").ConvertibleTo(ktype) {\n\t\t\tm.error(KeyNotConvertibleToStringError{Type: ktype,\n\t\t\t\tDescription: _YAML_STRING})\n\t\t}\n\t\tif v.IsNil() {\n\t\t\tv.Set(reflect.MakeMap(v.Type()))\n\t\t}\n\t\tfor k, n := range m.pairs {\n\t\t\te := reflect.New(v.Type().Elem()).Elem()\n\t\t\tapply(n, e)\n\t\t\tv.SetMapIndex(reflect.ValueOf(k).Convert(ktype), e)\n\t\t}\n\tdefault:\n\t\tif !reflect.TypeOf(m).AssignableTo(v.Type()) {\n\t\t\tm.error(InvalidMappingError{Type: v.Type(), Description: _YAML_MAPPING})\n\t\t}\n\t\tv.Set(reflect.ValueOf(m))\n\t}\n}\n\nfunc (m Mapping) equal(other Node) bool {\n\to, ok := other.(Mapping)\n\tif !ok || len(m.pairs) != len(o.pairs) || m.path != o.path {\n\t\treturn false\n\t}\n\tif m.pairs == nil || o.pairs == nil {\n\t\treturn m.pairs == nil && o.pairs == nil\n\t}\n\tfor k, v := range o.pairs {\n\t\tif !equal(m.pairs[k], v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (m Mapping) error(err error) {\n\tstop(ApplyMappingError{Path: m.path, Err: err, Description: _YAML_MAPPING_ERR})\n}\n\n// indirect dereferences p until it finds a non-pointer. If any nil values are\n// encountered on the way, new pointers are allocated.\nfunc indirect(p reflect.Value) (v reflect.Value) {\n\tv = p\n\tfor {\n\t\tif v.Kind() != reflect.Ptr {\n\t\t\treturn\n\t\t}\n\t\tif v.IsNil() {\n\t\t\tv.Set(reflect.New(v.Type().Elem()))\n\t\t}\n\t\tv = v.Elem()\n\t}\n}\n\n// exported checks if the variable name is exported.\nfunc exported(name string) bool {\n\tr, _ := utf8.DecodeRuneInString(name)\n\treturn unicode.IsUpper(r)\n}\n"
  },
  {
    "path": "common/collector/yaml/ast_test.go",
    "content": "package yaml\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\n// Helper functions for creating Nodes without path information.\nfunc scalar(value string) Scalar            { return Scalar{value: value} }\nfunc sequence(elements ...Node) Sequence    { return Sequence{elements: elements} }\nfunc mapping(pairs map[string]Node) Mapping { return Mapping{pairs: pairs} }\n\nfunc expect(t *testing.T, cause error, recovered interface{}) {\n\tif err := stopped(nil, recovered); err == nil || errors.CausedBy(err, cause) == nil {\n\t\tt.Errorf(\"unexpected error: %v; want cause %T\", err, cause)\n\t}\n}\n\nfunc TestScalar_Apply(t *testing.T) {\n\tt.Run(\"bool\", func(t *testing.T) {\n\t\tvar b bool\n\t\tv := reflect.ValueOf(&b).Elem()\n\n\t\tif scalar(\"true\").apply(v); !b {\n\t\t\tt.Error(\"unexpected boolean value: false\")\n\t\t}\n\n\t\tif scalar(\"false\").apply(v); b {\n\t\t\tt.Error(\"unexpected boolean value: true\")\n\t\t}\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidBoolError), recover())\n\t\t}()\n\t\tscalar(\"garbage\").apply(v)\n\t})\n\n\tt.Run(\"byte slice\", func(t *testing.T) {\n\t\tvar b []byte\n\t\tv := reflect.ValueOf(&b).Elem()\n\n\t\tif scalar(\"3q2+7w==\").apply(v); !bytes.Equal(b, []byte{0xde, 0xad, 0xbe, 0xef}) {\n\t\t\tt.Error(\"unexpected byte slice value:\", b)\n\t\t}\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidBase64Error), recover())\n\t\t}()\n\t\tscalar(\"garbage\").apply(v)\n\t})\n\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\tvar s []string\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidScalarError), recover())\n\t\t}()\n\t\tscalar(\"- hello, world!\").apply(v)\n\t})\n\n\tt.Run(\"int64\", func(t *testing.T) {\n\t\tvar i int64\n\t\tv := reflect.ValueOf(&i).Elem()\n\n\t\tif scalar(\"-123\").apply(v); i != -123 {\n\t\t\tt.Error(\"unexpected integer value:\", i)\n\t\t}\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidInt64Error), recover())\n\t\t}()\n\t\tscalar(\"garbage\").apply(v)\n\t})\n\n\tt.Run(\"uint64\", func(t *testing.T) {\n\t\tvar u uint64\n\t\tv := reflect.ValueOf(&u).Elem()\n\n\t\tif scalar(\"456\").apply(v); u != 456 {\n\t\t\tt.Error(\"unexpected unsigned integer value:\", u)\n\t\t}\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidUint64Error), recover())\n\t\t}()\n\t\tscalar(\"-789\").apply(v)\n\t})\n\n\tt.Run(\"string\", func(t *testing.T) {\n\t\tvar s string\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tif scalar(\"hello, world!\").apply(v); s != \"hello, world!\" {\n\t\t\tt.Error(\"unexpected string value:\", s)\n\t\t}\n\t})\n}\n\nfunc TestSequence_Apply(t *testing.T) {\n\tt.Run(\"array\", func(t *testing.T) {\n\t\tvar a [3]string\n\t\tv := reflect.ValueOf(&a).Elem()\n\n\t\tsequence(scalar(\"foo\"), scalar(\"bar\"), scalar(\"baz\")).apply(v)\n\t\tif a[0] != \"foo\" || a[1] != \"bar\" || a[2] != \"baz\" {\n\t\t\tt.Error(\"unexpected array value:\", a)\n\t\t}\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidArrayLengthError), recover())\n\t\t}()\n\t\tsequence(scalar(\"hello\"), scalar(\"world!\")).apply(v)\n\t})\n\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\tvar s string\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidSliceError), recover())\n\t\t}()\n\t\tsequence(scalar(\"hello, world!\")).apply(v)\n\t})\n\n\tt.Run(\"slice\", func(t *testing.T) {\n\t\tvar s []string\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tq := sequence(scalar(\"foo\"), scalar(\"bar\"), scalar(\"baz\"))\n\t\tq.apply(v)\n\t\tif len(s) != len(q.elements) {\n\t\t\tt.Errorf(\"unexpected slice length: %d; want %d\", len(s), len(q.elements))\n\t\t}\n\t\tif s[0] != \"foo\" || s[1] != \"bar\" || s[2] != \"baz\" {\n\t\t\tt.Error(\"unexpected slice value:\", s)\n\t\t}\n\n\t\t// Check that we do not allocate a new slice if we have\n\t\t// enough cap already.\n\t\tc := cap(s)\n\t\tq = sequence(scalar(\"hello\"), scalar(\"world!\"))\n\t\tq.apply(v)\n\t\tif len(s) != len(q.elements) {\n\t\t\tt.Errorf(\"unexpected slice length: %d; want %d\", len(s), len(q.elements))\n\t\t}\n\t\tif cap(s) != c {\n\t\t\tt.Errorf(\"unexpected slice capacity: %d; want %d\", cap(s), c)\n\t\t}\n\t\tif s[0] != \"hello\" || s[1] != \"world!\" {\n\t\t\tt.Error(\"unexpected slice value:\", s)\n\t\t}\n\t})\n}\n\nfunc TestMapping_Apply(t *testing.T) {\n\tt.Run(\"convertible\", func(t *testing.T) {\n\t\ttype convertible string\n\t\tvar m map[convertible]string\n\t\tv := reflect.ValueOf(&m).Elem()\n\n\t\t// Just check if this panics or not.\n\t\tmapping(map[string]Node{\"hello\": scalar(\"world!\")}).apply(v)\n\t})\n\n\tt.Run(\"invalid\", func(t *testing.T) {\n\t\tvar s string\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tdefer func() {\n\t\t\texpect(t, new(InvalidMappingError), recover())\n\t\t}()\n\t\tmapping(map[string]Node{\"hello\": scalar(\"world!\")}).apply(v)\n\t})\n\n\tt.Run(\"map\", func(t *testing.T) {\n\t\tvar m map[string]string\n\t\tv := reflect.ValueOf(&m).Elem()\n\n\t\tg := mapping(map[string]Node{\"hello\": scalar(\"world!\"), \"foo\": scalar(\"bar\")})\n\t\tg.apply(v)\n\t\tif len(m) != len(g.pairs) {\n\t\t\tt.Errorf(\"unexpected map size: %d; want %d\", len(m), len(g.pairs))\n\t\t}\n\t\tfor k, e := range g.pairs {\n\t\t\tif v, ok := m[k]; !ok {\n\t\t\t\tt.Errorf(\"missing key: %q\", k)\n\t\t\t} else if v != e.(Scalar).String() {\n\t\t\t\tt.Errorf(\"unexpected value for key %q: %q; want %q\", k, e, v)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"struct\", func(t *testing.T) {\n\t\tvar s struct {\n\t\t\tFoo     string\n\t\t\tBar     string\n\t\t\tbar     string // To check that the exported value gets set, not this one.\n\t\t\tgarbage string // To check that unexported values do not get set.\n\t\t}\n\n\t\tv := reflect.ValueOf(&s).Elem()\n\n\t\tg := mapping(map[string]Node{\"Foo\": scalar(\"hello\"), \"bar\": scalar(\"world!\")})\n\t\tg.apply(v)\n\t\tif s.Foo != \"hello\" || s.Bar != \"world!\" || len(s.bar) > 0 {\n\t\t\tt.Errorf(\"unexpected struct values: {Foo:%q Bar:%q bar:%q}; \"+\n\t\t\t\t\"want {Foo:%q Bar:%q bar:%q}\",\n\t\t\t\ts.Foo, s.Bar, s.bar, g.pairs[\"Foo\"], g.pairs[\"bar\"], \"\")\n\t\t}\n\n\t\t// Check that extra fields are ignored.\n\t\tmapping(map[string]Node{\"garbage\": scalar(\"hello, world!\")}).apply(v)\n\t\tif len(s.garbage) > 0 {\n\t\t\tt.Errorf(\"value assigned to unexported field: {garbage:%q}\", s.garbage)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "common/collector/yaml/doc.go",
    "content": "/*\nPackage yaml implements a parser for a subset of the YAML data serialization\nstandard (http://yaml.org/). It basically only contains features that are\nuseful for configuration files.\n\nComposite types, in addition to basic scalar values, the yaml package supports sequences and\nmappings.\n\nSequences:\n\n  - one\n  - two\n  - three\n\nMappings:\n\n\tkey: value\n\tfoo: bar\n\nThese composite types can be nested as necessary.\n\nSequences of sequences:\n\n  - - one\n  - two\n  - three\n  - - four\n  - five\n  - six\n\nMappings of mappings:\n\n\tkey:\n\t  foo: bar\n\tbaz:\n\t  hello: world\n\nSequences of mappings:\n\n  - key:   value\n    foo:   bar\n  - baz:   qux\n    hello: world\n\nMappings of sequences:\n\n\tkey:\n\t  - one\n\t  - two\n\t  - three\n\tfoo:\n\t  - four\n\t  - five\n\t  - six\n\n# Multiline values\n\nIn multiline values, all line feeds are converted to spaces and whitespace (not\nonly indenting spaces!) around each line are trimmed. So the mapping\n\n\tkey: value\n\t  goes\n\t    here\n\n\tsecond:\n\nwill set \"value goes here\" as the value of \"key\".\n\nThis behavior can be overridden with literal style, where the value starts with\na single line only containing a vertical line (|). In literal style everything\nis preserved, except indentation up to the first line and trailing empty lines.\nSo the mapping\n\n\tkey: |\n\t  value\n\n\t   goes\n\t  here\n\n\tsecond:\n\nwill set \"value\\n\\n goes\\nhere\\n\" as the value of \"key\".\n\n# Referenced values\n\nThe !container tag can be used for referencing values from external sources.\n\nThe tag is followed by a key whose value is read from a provided container (see\nivxv.ee/common/collector/container). Usually the YAML-serialized data being parsed is included\nin the same container and is referencing values that are inconvenient to\ninline.\n\n\tcertificate: !container cert.pem\n\nIf the key ends in \".yaml\", then the referenced content is parsed as YAML and\nthe resulting root node is used as the value.\n\n# Comments\n\nThe number sign/hash character (#) preceded by whitespace starts a comment,\nwhich will ignore content until the end of the line.\n*/\npackage yaml\n"
  },
  {
    "path": "common/collector/yaml/lexer.go",
    "content": "package yaml\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n)\n\n// lexeme is a syntactic unit with metadata about where it occurred.\ntype lexeme struct {\n\tcontent string\n\tline    int\n\tcolumn  int\n\tfirst   bool // Is this the first lexeme on the line? Simplifies parsing.\n}\n\n// lexer is the lexical analysis state.\ntype lexer struct {\n\tdone <-chan struct{} // Cancellation channel.\n\tc    chan lexeme     // Output channel.\n\n\tb      *bufio.Reader // Buffered input.\n\tline   int           // Current line.\n\tcolumn int           // Current column.\n\n\t// Keep the booleans in the struct together to minimize padding.\n\tpending lexeme // Pending lexeme.\n\tfirst   bool   // Are we reading the first lexeme on the line?\n\n\tbuf bytes.Buffer // Reusable buffer.\n}\n\n// pend pends the given lexeme for a send, but does not send it yet. Any\n// following calls to pend, while the lexeme is not sent, will append content,\n// not modifying metadata. Calling lexer.send will force a flush of the pending\n// lexeme.\nfunc (l *lexer) pend(content string, line, column int) {\n\tif len(l.pending.content) == 0 {\n\t\tl.pending.line = line\n\t\tl.pending.column = column\n\t\tl.pending.first = l.first\n\t\tl.first = false\n\t}\n\tl.pending.content += content\n}\n\nvar canceled = Canceled{Description: _YAML_CTX_DONE}\n\n// send sends the given lexeme, also flushing any pending sends. Empty content\n// will not be sent, only triggering a send of pending content.\nfunc (l *lexer) send(content string, line, column int) {\n\tif len(l.pending.content) > 0 {\n\t\tselect {\n\t\tcase l.c <- l.pending:\n\t\tcase <-l.done:\n\t\t\tl.error(canceled)\n\t\t}\n\t\tl.pending.content = \"\"\n\t\t// No need to reset metadata, they will be set on the first\n\t\t// call to lexer.pend.\n\t}\n\tif len(content) == 0 {\n\t\treturn\n\t}\n\tselect {\n\tcase l.c <- lexeme{content, line, column, l.first}:\n\tcase <-l.done:\n\t\tl.error(canceled)\n\t}\n\tl.first = false\n}\n\n// lex starts a new goroutine that does lexical analysis on the input and\n// writes lexemes into c. If done is closed, then that cancels the analysis.\n// The error return value is written into err.\n//\n// Callers should close done to exit the goroutine when lexing is cancelled\n// without reading all lexemes, so that it does not hang trying to send to c.\n// Consumers should read from c until it is closed and then check if the error\n// received from err is nil.\nfunc lex(done <-chan struct{}, r io.Reader) (c <-chan lexeme, err <-chan error) {\n\tl := lexer{\n\t\tdone:  done,\n\t\tc:     make(chan lexeme),\n\t\tb:     bufio.NewReader(r),\n\t\tline:  1,\n\t\tfirst: true,\n\t}\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer func() {\n\t\t\tclose(l.c)\n\t\t\terrc <- stopped(nil, recover()) // Buffered write, does not block.\n\t\t}()\n\n\t\tl.discardBOM()\n\t\tfor {\n\t\t\t// Skip any spaces between lexemes. We will get\n\t\t\t// indentation information from the column location.\n\t\t\tl.while(\" \")\n\n\t\t\tr, eof := l.rune()\n\t\t\tif eof {\n\t\t\t\tl.send(\"\", 0, 0) // Flush pending content.\n\t\t\t\treturn\n\t\t\t}\n\t\t\tswitch r {\n\t\t\tcase '#':\n\t\t\t\t// Comment: skip until end of line.\n\t\t\t\tl.until(\"\\n\")\n\t\t\t\tcontinue\n\n\t\t\tcase '\\n':\n\t\t\t\t// If we have pending content, then append the\n\t\t\t\t// line feed to it (so the last literal block\n\t\t\t\t// of a document knows if it ends with a line\n\t\t\t\t// feed or not). Otherwise skip line feeds, we\n\t\t\t\t// will get line information from line\n\t\t\t\t// location.\n\t\t\t\tif len(l.pending.content) > 0 {\n\t\t\t\t\tl.pend(\"\\n\", 0, 0)\n\t\t\t\t}\n\t\t\t\tl.send(\"\", 0, 0) // Flush pending content.\n\t\t\t\tl.line++\n\t\t\t\tl.column = 0\n\t\t\t\tl.first = true\n\t\t\t\tcontinue\n\n\t\t\tcase '-', ':':\n\t\t\t\t// If followed by a space or line feed, then\n\t\t\t\t// this is a sequence or mapping indicator.\n\t\t\t\tnext, eof := l.peek()\n\t\t\t\tif eof || next == ' ' || next == '\\n' {\n\t\t\t\t\tl.send(string(r), l.line, l.column)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Not an indicator, the '-' or ':' is part of\n\t\t\t\t// content.\n\t\t\t\tl.pend(string(r), l.line, l.column)\n\n\t\t\tcase '|':\n\t\t\t\t// If followed by nothing but spaces or a\n\t\t\t\t// space-separated comment until end of line,\n\t\t\t\t// then this is a literal style indicator.\n\t\t\t\tpos := l.column\n\t\t\t\tspaces, next, eof := l.while(\" \")\n\t\t\t\tif eof || next == '\\n' || (len(spaces) > 0 && next == '#') {\n\t\t\t\t\tl.send(\"|\", l.line, pos)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Not an indicator, the '|' and spaces are\n\t\t\t\t// part of content.\n\t\t\t\tl.pend(\"|\"+spaces, l.line, pos)\n\n\t\t\tcase '!':\n\t\t\t\t// If \"!container\" or a secondary tag followed\n\t\t\t\t// by a space or line feed, then send this as a\n\t\t\t\t// tag.\n\t\t\t\tpos := l.column\n\t\t\t\ttag, _, _ := l.until(\" \\n\")\n\t\t\t\tif tag == \"container\" || strings.HasPrefix(tag, \"!\") {\n\t\t\t\t\tl.send(\"!\"+tag, l.line, pos)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Not a tag, the '!' and read runes are part\n\t\t\t\t// of content.\n\t\t\t\tl.pend(\"!\"+tag, l.line, pos)\n\n\t\t\tdefault:\n\t\t\t\t// Start of content, pend the read rune.\n\t\t\t\tl.pend(string(r), l.line, l.column)\n\t\t\t}\n\n\t\t\t// By default, unless a case ended with \"continue\",\n\t\t\t// expect content to follow. Read until the next\n\t\t\t// possible mapping marker, comment, or line feed -- if\n\t\t\t// reading content, then a sequence or literal style\n\t\t\t// indicator or tag cannot occur on the same line\n\t\t\t// unless preceded by a mapping indicator.\n\t\t\t//\n\t\t\t// There is no need to track positional information, as\n\t\t\t// all cases have content already pending and we will\n\t\t\t// use that position.\n\t\t\tcontent, next, _ := l.until(\":#\\n\")\n\t\t\tl.pend(content, 0, 0)\n\t\t\tfor next == '#' && !strings.HasSuffix(l.pending.content, \" \") {\n\t\t\t\t// We hit a regular '#' inside content: remove\n\t\t\t\t// it from the buffer, pend it, and repeat\n\t\t\t\t// reading content.\n\t\t\t\tl.rune()\n\t\t\t\tl.pend(\"#\", 0, 0)\n\t\t\t\tcontent, next, _ = l.until(\":#\\n\")\n\t\t\t\tl.pend(content, 0, 0)\n\t\t\t}\n\t\t}\n\t}()\n\treturn l.c, errc\n}\n\n// discardBOM discards an optional UTF-8 byte order mark.\nfunc (l *lexer) discardBOM() {\n\tswitch bom, err := l.b.Peek(3); err {\n\tcase nil:\n\t\tif bytes.Equal(bom, []byte{0xef, 0xbb, 0xbf}) {\n\t\t\t//nolint:errcheck // l.b.Discard(3) is guaranteed to\n\t\t\t// work after l.b.Peek(3), since l.b.Buffered() >= 3.\n\t\t\tl.b.Discard(3)\n\t\t}\n\tcase io.EOF:\n\tdefault:\n\t\tl.error(LexPeekBOMError{Err: err, Description: _YAML_MORE_BYTES_LEFT})\n\t}\n}\n\n// rune reads the next rune from input.\nfunc (l *lexer) rune() (r rune, eof bool) {\n\tl.column++\n\n\t// Peek next two bytes to detect \\r\\n. In that case we discard the \\r\n\t// and only read \\n to normalize the line ending. Must happen before\n\t// ReadRune in order for UnreadRune to work.\n\tswitch rn, err := l.b.Peek(2); err {\n\tcase nil:\n\t\tif rn[0] == '\\r' && rn[1] == '\\n' {\n\t\t\t//nolint:errcheck // l.b.Discard(1) is guaranteed to\n\t\t\t// work after l.b.Peek(2), since l.b.Buffered() >= 1.\n\t\t\tl.b.Discard(1)\n\t\t}\n\tcase io.EOF:\n\tdefault:\n\t\t// Do not rely on the same error happening again on ReadRune.\n\t\t// It might have been a temporary failure, which results in\n\t\t// ReadRune succeeding and bypassing normalization.\n\t\tl.error(LexPeekError{Err: err, Description: _YAML_MORE_BYTES_LEFT})\n\t}\n\n\tr, _, err := l.b.ReadRune()\n\teof = err == io.EOF\n\tif err != nil && !eof {\n\t\tl.error(LexReadError{Err: err, Description: _YAML_EOF})\n\t}\n\tif r == '\\r' { // Also normalize a lone \\r to \\n.\n\t\tr = '\\n'\n\t}\n\treturn\n}\n\n// unread puts the last read rune back into the buffer.\nfunc (l *lexer) unread() {\n\terr := l.b.UnreadRune()\n\tif err != nil {\n\t\tl.error(LexUnreadError{Err: err, Description: _YAML_READ_RUNE})\n\t}\n\tl.column--\n}\n\n// peek reads the next rune and immediately unreads it.\nfunc (l *lexer) peek() (r rune, eof bool) {\n\t// Use rune and unread instead of utf8.DecodeRune(l.b.Peek(4)) to get\n\t// line-ending normalization.\n\tr, eof = l.rune()\n\tif !eof {\n\t\tl.unread()\n\t}\n\treturn\n}\n\n// while reads all following runes from input until one that is not present in\n// set is found.\nfunc (l *lexer) while(set string) (read string, next rune, eof bool) {\n\treturn l.whileEq(set, true)\n}\n\n// until reads all following runes from input until one that is present in set\n// is found.\nfunc (l *lexer) until(set string) (read string, next rune, eof bool) {\n\treturn l.whileEq(set, false)\n}\n\n// whileEq reads all following runes from input while they either are in set or\n// they are not, determined by eq.\nfunc (l *lexer) whileEq(set string, eq bool) (read string, next rune, eof bool) {\n\tl.buf.Reset()\n\tr, eof := l.rune()\n\tfor !eof && strings.ContainsRune(set, r) == eq {\n\t\tl.buf.WriteRune(r)\n\t\tr, eof = l.rune()\n\t}\n\tif !eof {\n\t\t// Put the triggering rune back.\n\t\tl.unread()\n\t}\n\treturn l.buf.String(), r, eof\n}\n\n// error stops the lexical analysis with the provided error, wrapping it with\n// the current line and column.\nfunc (l *lexer) error(err error) {\n\tstop(LexicalAnalysisError{Line: l.line, Column: l.column, Err: err, Description: _YAML_ERR_WRAP})\n}\n"
  },
  {
    "path": "common/collector/yaml/lexer_test.go",
    "content": "package yaml\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc testLex(t *testing.T, yaml string, lexemes []lexeme) {\n\tdone := make(chan struct{})\n\tdefer close(done)\n\n\tvar i int\n\tc, errc := lex(done, strings.NewReader(yaml))\n\tfor l := range c {\n\t\tif i >= len(lexemes) {\n\t\t\tt.Errorf(\"yaml %q, unexpected lexeme: %v\", yaml, l)\n\t\t\tcontinue\n\t\t}\n\t\tx := lexemes[i]\n\t\tif l.content != x.content || l.line != x.line ||\n\t\t\tl.column != x.column || l.first != x.first {\n\t\t\tt.Errorf(\"yaml %q, unexpected lexeme value: %+v; want %+v\", yaml, l, x)\n\t\t}\n\t\ti++\n\t}\n\tif err := <-errc; err != nil {\n\t\tt.Fatal(\"lexical analysis error:\", err)\n\t}\n\tif i != len(lexemes) {\n\t\tt.Errorf(\"yaml %q, %d missing lexeme(s)\", yaml, len(lexemes)-i)\n\t}\n}\n\nfunc TestLex(t *testing.T) {\n\tt.Run(\"comment\", func(t *testing.T) {\n\t\ttestLex(t, \"#comment\", nil)\n\t\ttestLex(t, \"    # comment\\n\", nil)\n\t\ttestLex(t, \"content # comment\\n\", []lexeme{{\"content \\n\", 1, 1, true}})\n\t})\n\n\tt.Run(\"content\", func(t *testing.T) {\n\t\ttestLex(t, \"content\", []lexeme{{\"content\", 1, 1, true}})\n\t\ttestLex(t, \"content\\n\", []lexeme{{\"content\\n\", 1, 1, true}})\n\t\ttestLex(t, \"    content\", []lexeme{{\"content\", 1, 5, true}})\n\t\ttestLex(t, \"content    \", []lexeme{{\"content    \", 1, 1, true}})\n\t\ttestLex(t, \"hello\\nworld\", []lexeme{{\"hello\\n\", 1, 1, true}, {\"world\", 2, 1, true}})\n\t\ttestLex(t, \"url#anchor\", []lexeme{{\"url#anchor\", 1, 1, true}})\n\t\ttestLex(t, \"-123\", []lexeme{{\"-123\", 1, 1, true}})\n\t\ttestLex(t, \"-#\", []lexeme{{\"-#\", 1, 1, true}})\n\t\ttestLex(t, \"foo - bar\", []lexeme{{\"foo - bar\", 1, 1, true}})\n\t\ttestLex(t, \":symbol\", []lexeme{{\":symbol\", 1, 1, true}})\n\t\ttestLex(t, \":#\", []lexeme{{\":#\", 1, 1, true}})\n\t\ttestLex(t, \"http://example.org\", []lexeme{{\"http://example.org\", 1, 1, true}})\n\t\ttestLex(t, \"a |\", []lexeme{{\"a |\", 1, 1, true}})\n\t\ttestLex(t, \"| b\", []lexeme{{\"| b\", 1, 1, true}})\n\t\ttestLex(t, \"|#\", []lexeme{{\"|#\", 1, 1, true}})\n\t\ttestLex(t, \"a | b\", []lexeme{{\"a | b\", 1, 1, true}})\n\t\ttestLex(t, \"!foo bar\", []lexeme{{\"!foo bar\", 1, 1, true}})\n\t})\n\n\tt.Run(\"literal\", func(t *testing.T) {\n\t\ttestLex(t, \"    |\", []lexeme{{\"|\", 1, 5, true}})\n\t\ttestLex(t, \"|    \", []lexeme{{\"|\", 1, 1, true}})\n\t\ttestLex(t, \"| # comment\", []lexeme{{\"|\", 1, 1, true}})\n\t})\n\n\tt.Run(\"mapping\", func(t *testing.T) {\n\t\ttestLex(t, \"key: value\", []lexeme{\n\t\t\t{\"key\", 1, 1, true}, {\":\", 1, 4, false}, {\"value\", 1, 6, false}})\n\t\ttestLex(t, \"key:\\n value\", []lexeme{\n\t\t\t{\"key\", 1, 1, true}, {\":\", 1, 4, false}, {\"value\", 2, 2, true}})\n\t})\n\n\tt.Run(\"sequence\", func(t *testing.T) {\n\t\ttestLex(t, \"- sequence\", []lexeme{{\"-\", 1, 1, true}, {\"sequence\", 1, 3, false}})\n\t\ttestLex(t, \"-\\n sequence\", []lexeme{{\"-\", 1, 1, true}, {\"sequence\", 2, 2, true}})\n\t})\n\n\tt.Run(\"reference\", func(t *testing.T) {\n\t\ttestLex(t, \"!container file.txt\", []lexeme{\n\t\t\t{\"!container\", 1, 1, true}, {\"file.txt\", 1, 12, false}})\n\t})\n\n\tt.Run(\"secondary\", func(t *testing.T) {\n\t\ttestLex(t, \"!!str 123\", []lexeme{\n\t\t\t{\"!!str\", 1, 1, true}, {\"123\", 1, 7, false}})\n\t})\n\n\tt.Run(\"carriage return\", func(t *testing.T) {\n\t\ttestLex(t, \"content\\r\", []lexeme{{\"content\\n\", 1, 1, true}})\n\t\ttestLex(t, \"content\\r\\n\", []lexeme{{\"content\\n\", 1, 1, true}})\n\t\ttestLex(t, \"hello\\rworld\", []lexeme{\n\t\t\t{\"hello\\n\", 1, 1, true}, {\"world\", 2, 1, true}})\n\t\ttestLex(t, \"hello\\r\\nworld\", []lexeme{\n\t\t\t{\"hello\\n\", 1, 1, true}, {\"world\", 2, 1, true}})\n\t})\n\n\tt.Run(\"bom\", func(t *testing.T) {\n\t\ttestLex(t, \"\\xefcontent\", []lexeme{{\"\\ufffdcontent\", 1, 1, true}})\n\t\ttestLex(t, \"\\xef\\xbbcontent\", []lexeme{{\"\\ufffd\\ufffdcontent\", 1, 1, true}})\n\t\ttestLex(t, \"\\xef\\xbb\\xbfcontent\", []lexeme{{\"content\", 1, 1, true}})\n\t})\n}\n"
  },
  {
    "path": "common/collector/yaml/log_desc.go",
    "content": "package yaml\n\nconst (\n\t_YAML_INT64           = \"Failed parsing value to base-10 integer\"\n\t_YAML_BOOL            = \"bool type can only be 'true' or 'false'\"\n\t_YAML_B64             = \"Failed parsing value to base-64 []byte\"\n\t_YAML_SCALAR          = \"Failed to parse value to YAML scalar\"\n\t_YAML_SCALAR_ERR      = \"YAML scalar reports an error\"\n\t_YAML_ARRAY           = \"Invalid array length\"\n\t_YAML_SLICE           = \"Failed to parse value to slice\"\n\t_YAML_SEQUENCE_ERR    = \"YAML sequence reports an error\"\n\t_YAML_STRING          = \"Failed to parse value to string\"\n\t_YAML_MAPPING         = \"Failed to parse value to YAML mapping\"\n\t_YAML_MAPPING_ERR     = \"YAML mapping reports an error\"\n\t_YAML_CTX_DONE        = \"Context is cancelled\"\n\t_YAML_MORE_BYTES_LEFT = \"Expected no bytes or EOF bytes, but got something different\"\n\t_YAML_EOF             = \"Expected EOF bytes, but got something different\"\n\t_YAML_READ_RUNE       = \"This bufio.Reader hasn't called Reader.ReadRune method just before calling Reader.UnreadRune method\"\n\t_YAML_ERR_WRAP        = \"Wrapper for YAML lexer errors\"\n\t_YAML_TRAILING        = \"Trailing (extra) data left after parsing YAML node\"\n\t_YAML_NO_KEY_MAPPING  = \"YAML mapping has no key, i.e. ':value'\"\n\t_YAML_SEQ_EXPECT      = \"Expected YAML sequence, but got something different\"\n\t_YAML_SCALAR_EMPTY    = \"YAML scalar is empty string\"\n\t_YAML_BDOC_TO_YAML    = \"Cannot structure .bdoc container content into YAML format\"\n\t_YAML_BDOC_YAML_PARSE = \"Failed to parse YAML representation of a .bdoc container content\"\n\t_YAML_KEY_UNIQ        = \"YAML key must be unique\"\n\t_YAML_MAPPING_DEPTH   = \"Invalid YAML mapping depth (expected doesn't match actual)\"\n\t_YAML_KEY_MAPPING     = \"YAML mapping has a format of 'key:value'\"\n\t_YAML_MAPPING_EOF     = \"YAML mapping should be the only content in a file row, so EOF is expected at the end of YAML mapping\"\n\t_YAML_KEY_LINE        = \"Unexpected YAML mapping key\"\n\t_YAML_SYN_WRAP        = \"Wrapper for YAML syntactical processing errors\"\n\t_YAML_NOT_PTR_OR_NIL  = \"Value is either nil or not a pointer\"\n)\n"
  },
  {
    "path": "common/collector/yaml/parser.go",
    "content": "package yaml\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// parser is the syntactic analysis state.\ntype parser struct {\n\tc      <-chan lexeme     // Channel of lexemes from lexer.\n\tcont   map[string][]byte // Map of container data.\n\tcrumbs []string          // Breadcrumbs to the current location.\n\tbuf    bytes.Buffer      // Reusable buffer.\n}\n\n// Parse parses the root node of the YAML-serialized data. It is an error for\n// input to contain more than one root.\n//\n// If data contains container references, then the data in container is used.\nfunc Parse(data io.Reader, container map[string][]byte) (root Node, err error) {\n\treturn parse(data, container, \"\")\n}\n\nfunc parse(data io.Reader, container map[string][]byte, path string) (root Node, err error) {\n\tdone := make(chan struct{})\n\tdefer func() {\n\t\tclose(done)\n\t\terr = stopped(err, recover())\n\t}()\n\n\t// Start lexical analysis and create parser.\n\tc, errc := lex(done, data)\n\tp := parser{c: c, cont: container}\n\n\t// If path is not empty, then use as initial breadcrumb.\n\tif len(path) > 0 {\n\t\tp.push(path)\n\t}\n\n\t// Perform syntactic analysis.\n\troot, next := p.node(1)\n\n\t// Check for trailing data before reading the error, because if the\n\t// lexer is still waiting to send something and we attempt to read from\n\t// errc we will get a deadlock. Closing done would also not help,\n\t// because then we would hide trailing data errors with lexical anaysis\n\t// cancelled errors.\n\tif len(next.content) > 0 {\n\t\tp.error(next.line, next.column, TrailingDataError{Trailing: next.content,\n\t\t\tDescription: _YAML_TRAILING})\n\t}\n\terr = <-errc\n\treturn\n}\n\n// node parses a single node from the lexemes in p.c. It stops at and returns\n// the first lexeme that does not belong to the node's block (i.e., is at a\n// lesser indentation level than depth).\nfunc (p *parser) node(depth int) (node Node, next lexeme) {\n\tnext, ok := <-p.c\n\tif !ok || next.column < depth {\n\t\treturn\n\t}\n\tswitch next.content {\n\tcase \"-\":\n\t\treturn p.sequence(next, false)\n\tcase \":\":\n\t\tp.error(next.line, next.column, NodeEmptyMappingKeyError{Description: _YAML_NO_KEY_MAPPING})\n\t\treturn // Never reached.\n\tcase \"|\":\n\t\t// As a special case, a root level literal indicator increases\n\t\t// minimal depth by one.\n\t\tif depth == 1 {\n\t\t\tdepth++\n\t\t}\n\t\treturn p.literal(depth)\n\tcase \"!container\":\n\t\treturn p.reference(depth, next)\n\tdefault:\n\t\treturn p.scalarOrMapping(depth, next)\n\t}\n}\n\n// sequence parses a sequence. l is the first sequence indicator lexeme. The\n// method returns the next lexeme that is not part of the sequence.\n//\n// If mappingDepth is true, then the next lexeme after the sequence can be at\n// the same depth as the first indicator: this is used in cases, where the\n// sequence indicators are at the same depth as the parent mapping's keys. If\n// mappingDepth is false, then it is an error for next to be at the same depth\n// as the first sequence indicator.\nfunc (p *parser) sequence(l lexeme, mappingDepth bool) (s Sequence, next lexeme) {\n\ts.path = p.path()\n\tdepth := l.column\n\tfor {\n\t\t// Read the next element of the sequence.\n\t\tp.push(fmt.Sprint(len(s.elements)))\n\t\tvar element Node\n\t\telement, next = p.node(depth + 1)\n\t\tif element == nil {\n\t\t\treturn // p.c was closed.\n\t\t}\n\t\ts.elements = append(s.elements, element)\n\t\tp.pop()\n\n\t\t// Check if the block ended, otherwise we must have a new\n\t\t// sequence indicator at the same depth.\n\t\tswitch {\n\t\tcase next.column < depth:\n\t\t\treturn\n\t\tcase mappingDepth && next.column == depth && next.content != \"-\":\n\t\t\treturn\n\t\tcase next.column != depth, next.content != \"-\":\n\t\t\tp.error(next.line, next.column,\n\t\t\t\tSequenceIndicatorError{Lexeme: next.content,\n\t\t\t\t\tDescription: _YAML_SEQ_EXPECT})\n\t\t}\n\t}\n}\n\n// literal parses a literal style scalar block with minimal depth. It returns\n// the next lexeme that is not part of the scalar.\nfunc (p *parser) literal(depth int) (s Scalar, next lexeme) {\n\ts.path = p.path()\n\tp.buf.Reset()\n\tvar endofblock bool\n\tvar written int // How many runes have been written on the current line.\n\tvar pline int   // Previous line.\n\tfor next = range p.c {\n\t\t// If next is shallower than depth, then the block ended.\n\t\tif next.column < depth {\n\t\t\tendofblock = true\n\t\t\tbreak\n\t\t}\n\n\t\t// The first lexeme sets the previous line and block depth\n\t\t// (which is already checked to be greater than or equal to the\n\t\t// minimal depth).\n\t\tif pline == 0 {\n\t\t\tpline = next.line\n\t\t\tdepth = next.column\n\t\t}\n\n\t\t// If line has increased, then recreate the intermediate empty\n\t\t// lines. Usually the previous line already had a line feed at\n\t\t// its end, but special characters (e.g., mapping indicators)\n\t\t// do not have them, so check for it.\n\t\tvar lf int\n\t\tif strings.HasSuffix(p.buf.String(), \"\\n\") {\n\t\t\tlf++\n\t\t}\n\t\tfor n := next.line - pline - lf; n > 0; n-- {\n\t\t\tp.buf.WriteByte('\\n')\n\t\t}\n\n\t\t// If this is the first lexeme on the line, then reset written.\n\t\tif next.first {\n\t\t\twritten = 0\n\t\t}\n\n\t\t// Recreate leading spaces.\n\t\tfor n := next.column - written - depth; n > 0; n-- {\n\t\t\tp.buf.WriteByte(' ')\n\t\t\twritten++\n\t\t}\n\n\t\tp.buf.WriteString(next.content)\n\t\twritten += utf8.RuneCountInString(next.content)\n\t\tpline = next.line\n\t}\n\t// p.c was closed, next should be empty.\n\tif !endofblock {\n\t\tnext = lexeme{}\n\t}\n\ts.value = p.buf.String()\n\treturn\n}\n\n// reference parses a referenced value. depths is the block depth and l is the\n// reference tag read by the caller. The method reads content from the external\n// source and returns that as the node.\nfunc (p *parser) reference(depth int, l lexeme) (node Node, next lexeme) {\n\t// The tag has to be followed by a scalar containing the reference. By\n\t// passing an empty scalar, we are avoiding scalarOrMapping returning a\n\t// mapping. However, if there is nothing to read, then it will not\n\t// return a nil node, but a Scalar with the content of the lexeme,\n\t// i.e., \"\". If there is something to read, then it will be prefixed by\n\t// a space meant to separate the empty lexeme and the first read one.\n\trefnode, next := p.scalarOrMapping(depth, lexeme{})\n\tref := strings.TrimSpace(refnode.(Scalar).String())\n\tif ref == \"\" {\n\t\tp.error(l.line, l.column, MissingReferenceError{Description: _YAML_SCALAR_EMPTY})\n\t}\n\n\t// Get the referenced value.\n\tdata, ok := p.cont[ref]\n\tif !ok {\n\t\tp.error(l.line, l.column, InvalidContainerError{Ref: ref, Description: _YAML_BDOC_TO_YAML})\n\t}\n\n\t// If the ref ends in \".yaml\", then parse data as YAML.\n\tif strings.HasSuffix(ref, \".yaml\") {\n\t\tnode, err := parse(bytes.NewReader(data), p.cont, p.path())\n\t\tif err != nil {\n\t\t\tp.error(l.line, l.column,\n\t\t\t\tParseReferencedYAMLError{Reference: ref, Err: err, Description: _YAML_BDOC_YAML_PARSE})\n\t\t}\n\t\treturn node, next\n\t}\n\n\t// Otherwise just return data as a scalar.\n\treturn Scalar{path: p.path(), value: string(bytes.TrimSpace(data))}, next\n}\n\n// scalarOrMapping parses a scalar, which might later turn out to actually be a\n// mapping key. depth is the block depth and l is the first lexeme of the\n// scalar that was already read by the caller. The method returns the next\n// lexeme that is not part of the scalar or mapping.\nfunc (p *parser) scalarOrMapping(depth int, l lexeme) (node Node, next lexeme) {\n\tnext, ok := <-p.c\n\n\tif strings.HasPrefix(l.content, \"!!\") {\n\t\t// Ignore secondary tag handles, which are (most likely) used\n\t\t// for types: all our types depend on the structures being\n\t\t// applied to, not what the tag specifies.\n\t\tl = next\n\t\tnext, ok = <-p.c\n\t}\n\n\t// Use the value first and only then check if the channel was closed.\n\t// This is okay because a lexeme zero value will never satisfy the\n\t// condition. We do this to avoid writing to the buffer if it is never\n\t// used.\n\tif next.line == l.line && next.content == \":\" {\n\t\t// Found a mapping indicator on the same line: l is a key.\n\t\treturn p.mapping(l)\n\t}\n\n\tp.buf.Reset()\n\tp.buf.WriteString(strings.TrimSpace(l.content))\n\n\tfor ok {\n\t\t// If next is shallower than depth, then the block ended.\n\t\tif next.column < depth {\n\t\t\tbreak\n\t\t}\n\n\t\t// If the first lexeme was not a mapping key, than any\n\t\t// following ones cannot be either.\n\t\tif next.content == \":\" {\n\t\t\tp.error(next.line, next.column, UnexpectedMappingError{Description: _YAML_NO_KEY_MAPPING})\n\t\t}\n\n\t\tp.buf.WriteByte(' ')\n\t\tp.buf.WriteString(strings.TrimSpace(next.content))\n\n\t\tnext, ok = <-p.c\n\t}\n\treturn Scalar{path: p.path(), value: p.buf.String()}, next\n}\n\n// mapping parses a mapping. key is the first mapping key lexeme. The method\n// returns the next lexeme that is not part of the mapping.\nfunc (p *parser) mapping(key lexeme) (m Mapping, next lexeme) {\n\tm.path = p.path()\n\tm.pairs = make(map[string]Node)\n\tdepth := key.column\n\tfor {\n\t\t// Remove spaces between key and mapping indicator.\n\t\tkey.content = strings.TrimSpace(key.content)\n\n\t\t// Keys must be unique. Unlike standard YAML, we require the\n\t\t// keys to be case-insensitively unique.\n\t\tfor k := range m.pairs {\n\t\t\tif strings.EqualFold(k, key.content) {\n\t\t\t\tp.error(key.line, key.column,\n\t\t\t\t\tNonUniqueKeyError{Key: key.content, Description: _YAML_KEY_UNIQ})\n\t\t\t}\n\t\t}\n\n\t\t// Read the value mapped to the key.\n\t\tp.push(key.content)\n\t\tvar element Node\n\t\telement, next = p.node(depth + 1)\n\n\t\t// As a special case, mapping may contain sequences at the same\n\t\t// depth as their keys, because the sequence indicator is\n\t\t// considered as part of indentation and therefore the sequence\n\t\t// block is actually deeper than depth.\n\t\t//\n\t\t// Allows for:\n\t\t//\n\t\t//     key:\n\t\t//     - first\n\t\t//     - second\n\t\t//     another key:\n\t\t//\n\t\tif element == nil && next.column == depth && next.content == \"-\" {\n\t\t\telement, next = p.sequence(next, true)\n\t\t}\n\n\t\tm.pairs[key.content] = element // element may be nil.\n\t\tp.pop()\n\n\t\t// Check if the block ended, otherwise we must have a new key\n\t\t// at the same depth and a mapping indicator.\n\t\tswitch {\n\t\tcase next.column < depth:\n\t\t\treturn\n\t\tcase next.column != depth:\n\t\t\tp.error(next.line, next.column,\n\t\t\t\tMappingKeyDepthError{Lexeme: next.content, Description: _YAML_MAPPING_DEPTH})\n\t\tcase next.content == \":\":\n\t\t\tp.error(next.line, next.column, EmptyMappingKeyError{Description: _YAML_NO_KEY_MAPPING})\n\t\t}\n\t\tkey = next // The lexeme returned by p.node is the next key.\n\n\t\t// Read the mapping indicator and check that it is on the same\n\t\t// line as the key.\n\t\tvar ok bool\n\t\tnext, ok = <-p.c\n\t\tswitch {\n\t\tcase !ok:\n\t\t\tp.error(key.line, key.column, EOFMissingMappingIndicatorError{Description: _YAML_MAPPING_EOF})\n\t\tcase next.content != \":\":\n\t\t\tp.error(next.line, next.column,\n\t\t\t\tMappingIndicatorError{Lexeme: next.content, Description: _YAML_KEY_MAPPING})\n\t\tcase next.line != key.line:\n\t\t\tp.error(next.line, next.column,\n\t\t\t\tMappingIndicatorLineError{Want: key.line, Description: _YAML_KEY_LINE})\n\t\t}\n\t}\n}\n\n// error stops the syntactic analysis with the provided error, line, and column\n// number.\nfunc (p *parser) error(line, column int, err error) {\n\tstop(SyntacticAnalysisError{Line: line, Column: column, Err: err, Description: _YAML_SYN_WRAP})\n}\n\n// push pushes an element to the path of the parser.\nfunc (p *parser) push(s string) {\n\tp.crumbs = append(p.crumbs, s)\n}\n\n// pop pops the latest element from the path of the parser.\nfunc (p *parser) pop() {\n\tp.crumbs = p.crumbs[:len(p.crumbs)-1]\n}\n\n// path returns the path to the current location of the parser.\nfunc (p *parser) path() string {\n\treturn strings.Join(p.crumbs, \".\")\n}\n"
  },
  {
    "path": "common/collector/yaml/parser_test.go",
    "content": "package yaml\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/errors\"\n)\n\nvar container = map[string][]byte{\n\t\"foo bar.txt\":  []byte(\"foo bar\"),\n\t\"foo bar.yaml\": []byte(\"foo: bar\\nhello: world\"),\n}\n\nfunc testParse(t *testing.T, yaml string, expected Node) {\n\troot, err := Parse(strings.NewReader(yaml), container)\n\tif err != nil {\n\t\tt.Error(\"syntactic analysis error:\", err)\n\t\treturn\n\t}\n\tif !equal(root, expected) {\n\t\tt.Errorf(\"yaml %q, unexpected root node: %T(%q); want %T(%q)\",\n\t\t\tyaml, root, root, expected, expected)\n\t}\n}\n\nfunc testParseError(t *testing.T, yaml string, line, col int, cause error) {\n\t_, err := Parse(strings.NewReader(yaml), container)\n\tsae, ok := err.(SyntacticAnalysisError)\n\tif !ok || sae.Line.(int) != line || sae.Column.(int) != col ||\n\t\terrors.CausedBy(err, cause) == nil {\n\n\t\tt.Errorf(\"yaml %q, unexpected error: %v; want line %d, col %d, cause %T\",\n\t\t\tyaml, err, line, col, cause)\n\t}\n}\n\nfunc TestParse(t *testing.T) {\n\tt.Run(\"reference\", func(t *testing.T) {\n\t\ttestParse(t, \"!container foo bar.txt\", scalar(\"foo bar\"))\n\t\ttestParse(t, \"!container foo bar.yaml\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"foo\":   Scalar{\"foo\", \"bar\"},\n\t\t\t\t\"hello\": Scalar{\"hello\", \"world\"},\n\t\t\t}))\n\n\t\ttestParse(t, \"foo: !container foo bar.txt\",\n\t\t\tmapping(map[string]Node{\"foo\": Scalar{\"foo\", \"foo bar\"}}))\n\n\t\ttestParse(t, \"foo: !container foo bar.yaml\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"foo\": Mapping{\"foo\", map[string]Node{\n\t\t\t\t\t\"foo\":   Scalar{\"foo.foo\", \"bar\"},\n\t\t\t\t\t\"hello\": Scalar{\"foo.hello\", \"world\"},\n\t\t\t\t}},\n\t\t\t}))\n\n\t\ttestParseError(t, \"!container \", 1, 1, new(MissingReferenceError))\n\t\ttestParseError(t, \"!container x.txt\", 1, 1, new(InvalidContainerError))\n\t})\n\n\tt.Run(\"secondary\", func(t *testing.T) {\n\t\ttestParse(t, \"!!str\", scalar(\"\"))\n\t\ttestParse(t, \"!!str 123\", scalar(\"123\"))\n\t})\n\n\tt.Run(\"indent\", func(t *testing.T) {\n\t\ttestParseError(t, \" - first\\n- second\", 2, 1, new(TrailingDataError))\n\t\ttestParse(t, \"key:\\n- value\\nsuffix:\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"key\":    Sequence{\"key\", []Node{Scalar{\"key.0\", \"value\"}}},\n\t\t\t\t\"suffix\": nil,\n\t\t\t}))\n\n\t\ttestParseError(t, \" key:\\n  value\\nkey: value\", 3, 1, new(TrailingDataError))\n\t\ttestParse(t, \"|\\n literal\", scalar(\"literal\"))\n\t\ttestParse(t, \"  |\\n literal\", scalar(\"literal\"))\n\t\ttestParseError(t, \"|\\nliteral\", 2, 1, new(TrailingDataError))\n\t\ttestParseError(t, \"foo: !container\\nfoo bar.txt\", 1, 6, new(MissingReferenceError))\n\t})\n\n\tt.Run(\"literal\", func(t *testing.T) {\n\t\ttestParse(t, \"|\\n literal\", scalar(\"literal\"))\n\t\ttestParse(t, \"|\\n literal\\n\", scalar(\"literal\\n\"))\n\t\ttestParse(t, \"|\\n literal\\n\\n\\n\", scalar(\"literal\\n\"))\n\t\ttestParse(t, \"|\\n hello\\n\\n world!\\n\\n foobar\", scalar(\"hello\\n\\nworld!\\n\\nfoobar\"))\n\t\ttestParse(t, \"|\\n  hello\\n   world!\", scalar(\"hello\\n world!\"))\n\t\ttestParse(t, \"|\\n õäöü: world!\", scalar(\"õäöü: world!\"))\n\t\ttestParse(t, \"|\\n hello:\\n  world\", scalar(\"hello:\\n world\"))\n\t\ttestParse(t, \"|\\n !file foo.txt\", scalar(\"!file foo.txt\"))\n\t\ttestParse(t, \"|\\n !!str 123\", scalar(\"!!str 123\"))\n\t})\n\n\tt.Run(\"mapping\", func(t *testing.T) {\n\t\ttestParse(t, \"key: value\", mapping(map[string]Node{\"key\": Scalar{\"key\", \"value\"}}))\n\t\ttestParse(t, \"key : value\", mapping(map[string]Node{\"key\": Scalar{\"key\", \"value\"}}))\n\t\ttestParse(t, \"key:\\n value\", mapping(map[string]Node{\"key\": Scalar{\"key\", \"value\"}}))\n\t\ttestParse(t, \"key: value\\nfoo: bar\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"key\": Scalar{\"key\", \"value\"},\n\t\t\t\t\"foo\": Scalar{\"foo\", \"bar\"},\n\t\t\t}))\n\t\ttestParse(t, \"key:\", mapping(map[string]Node{\"key\": nil}))\n\t\ttestParse(t, \"key:\\nfoo:\", mapping(map[string]Node{\"key\": nil, \"foo\": nil}))\n\n\t\ttestParseError(t, \"key: value\\nKey: bar\", 2, 1, new(NonUniqueKeyError))\n\t\ttestParseError(t, \"key: |\\n  literal\\n trailing\", 3, 2, new(MappingKeyDepthError))\n\t\ttestParseError(t, \": value\", 1, 1, new(NodeEmptyMappingKeyError))\n\t\ttestParseError(t, \"key: value\\n: value\", 2, 1, new(EmptyMappingKeyError))\n\t\ttestParseError(t, \"key: value\\nfoo\", 2, 1, new(EOFMissingMappingIndicatorError))\n\t\ttestParseError(t, \"key: value\\nfoo\\nbar\", 3, 1, new(MappingIndicatorError))\n\t\ttestParseError(t, \"key: value\\nkey\\n: value\", 3, 1, new(MappingIndicatorLineError))\n\t})\n\n\tt.Run(\"mixed\", func(t *testing.T) {\n\t\ttestParse(t, \"literal: |\\n hello\\n  world\",\n\t\t\tmapping(map[string]Node{\"literal\": Scalar{\"literal\", \"hello\\n world\"}}))\n\n\t\ttestParse(t, \"- |\\n hello\\n  world\",\n\t\t\tsequence(Scalar{\"0\", \"hello\\n world\"}))\n\n\t\ttestParse(t, \"sequence:\\n - first\\n - second\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"sequence\": Sequence{\"sequence\", []Node{\n\t\t\t\t\tScalar{\"sequence.0\", \"first\"},\n\t\t\t\t\tScalar{\"sequence.1\", \"second\"},\n\t\t\t\t}},\n\t\t\t}))\n\n\t\ttestParse(t, \"- hello: world\\n  foo: bar\",\n\t\t\tsequence(Mapping{\"0\", map[string]Node{\n\t\t\t\t\"hello\": Scalar{\"0.hello\", \"world\"},\n\t\t\t\t\"foo\":   Scalar{\"0.foo\", \"bar\"},\n\t\t\t}}))\n\n\t\ttestParse(t, \"- - first\\n  - second\",\n\t\t\tsequence(Sequence{\"0\", []Node{\n\t\t\t\tScalar{\"0.0\", \"first\"},\n\t\t\t\tScalar{\"0.1\", \"second\"},\n\t\t\t}}))\n\n\t\ttestParse(t, \"mapping:\\n hello: world\\n foo: bar\",\n\t\t\tmapping(map[string]Node{\n\t\t\t\t\"mapping\": Mapping{\"mapping\", map[string]Node{\n\t\t\t\t\t\"hello\": Scalar{\"mapping.hello\", \"world\"},\n\t\t\t\t\t\"foo\":   Scalar{\"mapping.foo\", \"bar\"},\n\t\t\t\t}},\n\t\t\t}))\n\t})\n\n\tt.Run(\"scalar\", func(t *testing.T) {\n\t\ttestParse(t, \"scalar\", scalar(\"scalar\"))\n\t\ttestParse(t, \"hello\\nworld!\", scalar(\"hello world!\"))\n\t\ttestParse(t, \"\\n hello\\n\\tworld!\\r\\n\", scalar(\"hello world!\"))\n\t\ttestParseError(t, \"scalar\\nkey: value\", 2, 4, new(UnexpectedMappingError))\n\t})\n\n\tt.Run(\"sequence\", func(t *testing.T) {\n\t\ttestParse(t, \"- sequence\", sequence(Scalar{\"0\", \"sequence\"}))\n\t\ttestParse(t, \"  - first\\n  - second\",\n\t\t\tsequence(Scalar{\"0\", \"first\"}, Scalar{\"1\", \"second\"}))\n\n\t\ttestParseError(t, \"- sequence\\n: mapping\", 2, 1, new(SequenceIndicatorError))\n\t})\n}\n"
  },
  {
    "path": "common/collector/yaml/testdata/unmarshal.yaml",
    "content": "# Scalars\nstring: hello, world!\nint:    -123\nbool:   true\n\n# Sequences\nslice:\n  - first\n  - second\n  - third\narray:\n  - fourth\n  - fifth\n  - sixth\n\n# Mappings\nmap:\n  key:   value\n  hello: world!\nstruct:\n  foo: bar\n  baz: qux\n\n# Interface\nintrospection:\n  scalar: hello, world!\n  sequence:\n    - first\n    - second\n\n# Nested\nnested:\n  - key:   value\n    hello: world!\n  - key:   foobar\n    hello: maailm!\n"
  },
  {
    "path": "common/collector/yaml/unmarshal.go",
    "content": "package yaml\n\nimport (\n\t\"io\"\n\t\"reflect\"\n)\n\ntype yamlError struct {\n\terr error\n}\n\n// stop panics with a yamlError to stop the current operation.\n//\n// Instead of having to return an error through many layers of recursive\n// function calls, use a panic to unwind the stack until stopped is deferred.\nfunc stop(err error) {\n\tpanic(yamlError{err})\n}\n\n// stopped checks if the current operation was stopped and returns the error if\n// it did. Otherwise returns the provided error, so that it does not get\n// overwritten if no panic happened. Only useful inside a deferred function\n// like this:\n//\n//\tdefer func() {\n//\t  err = stopped(err, recover())\n//\t}()\nfunc stopped(err error, recovered interface{}) error {\n\tif recovered != nil {\n\t\tyerr, ok := recovered.(yamlError)\n\t\tif !ok {\n\t\t\tpanic(recovered)\n\t\t}\n\t\terr = yerr.err\n\t}\n\treturn err\n}\n\n// Apply sets the value of v to that of the YAML Node n. n must be assignable\n// to v, with the following exceptions:\n//\n//   - Scalar nodes can be applied to strings, byte slices (in which case the\n//     content is automatically base64-decoded), int64s, uint64s, and\n//     booleans,\n//   - Sequence nodes can be applied to slices and arrays, and\n//   - Mapping nodes can be applied to maps with string keys and structs (the\n//     mapping keys must must case-insensitively match fields in the struct).\nfunc Apply(n Node, v interface{}) (err error) {\n\tdefer func() {\n\t\terr = stopped(err, recover())\n\t}()\n\n\tr := reflect.ValueOf(v)\n\tif r.Kind() != reflect.Ptr || r.IsNil() {\n\t\treturn ApplyInvalidTargetError{Type: r.Type(), Description: _YAML_NOT_PTR_OR_NIL}\n\t}\n\tapply(n, r)\n\treturn\n}\n\n// Unmarshal calls Parse on the YAML-serialized data and calls Apply to apply\n// the returned root Node to v.\n//\n// If data contains container references, then the data in container is used.\nfunc Unmarshal(data io.Reader, container map[string][]byte, v interface{}) (err error) {\n\troot, err := Parse(data, container)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn Apply(root, v)\n}\n"
  },
  {
    "path": "common/collector/yaml/unmarshal_test.go",
    "content": "package yaml\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n)\n\n// unmarshal is the test struct to unmarshal into.\ntype unmarshal struct {\n\tString string\n\tInt    int64\n\tBool   bool\n\n\tSlice []string\n\tArray [3]string\n\n\tMap    map[string]string\n\tStruct struct {\n\t\tFoo string\n\t\tBaz string\n\t}\n\n\tIntrospection Node\n\n\tNested []struct {\n\t\tKey   string\n\t\tHello string\n\t}\n}\n\nfunc compare(t *testing.T, name string, have, want interface{}) bool {\n\tif have != want {\n\t\tt.Errorf(`unexpected %s: \"%v\", want \"%v\"`, name, have, want)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc gets(m map[string]string, k string) (s string) {\n\ts, ok := m[k]\n\tif !ok {\n\t\ts = \"<no such key>\"\n\t}\n\treturn\n}\n\nfunc TestUnmarshal(t *testing.T) {\n\tf, err := os.Open(\"testdata/unmarshal.yaml\")\n\tif err != nil {\n\t\tt.Fatal(\"failed to open test YAML:\", err)\n\t}\n\n\tvar v unmarshal\n\tif err := Unmarshal(f, nil, &v); err != nil {\n\t\tt.Fatal(\"failed to unmarshal test YAML:\", err)\n\t}\n\n\tcompare(t, \"string\", v.String, \"hello, world!\")\n\tcompare(t, \"int\", v.Int, int64(-123))\n\tcompare(t, \"bool\", v.Bool, true)\n\n\tif compare(t, \"slice length\", len(v.Slice), 3) {\n\t\tcompare(t, \"slice[0]\", v.Slice[0], \"first\")\n\t\tcompare(t, \"slice[1]\", v.Slice[1], \"second\")\n\t\tcompare(t, \"slice[2]\", v.Slice[2], \"third\")\n\t}\n\tcompare(t, \"array[0]\", v.Array[0], \"fourth\")\n\tcompare(t, \"array[1]\", v.Array[1], \"fifth\")\n\tcompare(t, \"array[2]\", v.Array[2], \"sixth\")\n\n\tcompare(t, \"map.key\", gets(v.Map, \"key\"), \"value\")\n\tcompare(t, \"map.hello\", gets(v.Map, \"hello\"), \"world!\")\n\tcompare(t, \"struct.foo\", v.Struct.Foo, \"bar\")\n\tcompare(t, \"struct.baz\", v.Struct.Baz, \"qux\")\n\n\tif compare(t, \"introspection type\", reflect.TypeOf(v.Introspection),\n\t\treflect.TypeOf(Mapping{})) {\n\n\t\tm := v.Introspection.(Mapping)\n\t\tcompare(t, \"introspection.scalar\",\n\t\t\tm.pairs[\"scalar\"].(Scalar).String(), \"hello, world!\")\n\n\t\tsn := m.pairs[\"sequence\"]\n\t\tif compare(t, \"introspection.sequence type\", reflect.TypeOf(sn),\n\t\t\treflect.TypeOf(Sequence{})) {\n\n\t\t\ts := sn.(Sequence)\n\t\t\tif compare(t, \"introspection.sequence length\", len(s.elements), 2) {\n\t\t\t\tcompare(t, \"introspection.sequence[0]\",\n\t\t\t\t\ts.elements[0].(Scalar).String(), \"first\")\n\t\t\t\tcompare(t, \"introspection.sequence[1]\",\n\t\t\t\t\ts.elements[1].(Scalar).String(), \"second\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif compare(t, \"nested length\", len(v.Nested), 2) {\n\t\tcompare(t, \"nested[0].key\", v.Nested[0].Key, \"value\")\n\t\tcompare(t, \"nested[0].hello\", v.Nested[0].Hello, \"world!\")\n\t\tcompare(t, \"nested[1].key\", v.Nested[1].Key, \"foobar\")\n\t\tcompare(t, \"nested[1].hello\", v.Nested[1].Hello, \"maailm!\")\n\t}\n}\n"
  },
  {
    "path": "common/collector/zip/zip.go",
    "content": "// Package zip implements .ZIP File Format Specification, version 6.3.10 as defined in\n// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT.\npackage zip\n\nimport (\n\t\"archive/zip\"\n\t\"crypto/subtle\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n)\n\nconst (\n\t// 6.0 Traditional PKWARE Encryption.\n\ttraditionalPKWAREEncryption = 12\n\n\t// 4.4.4 general purpose bit flag: (2 bytes). Bit 0.\n\tencryptedFlag = 0b00000001\n\n\t// 4.4.4 general purpose bit flag: (2 bytes). Bit 3.\n\tdataDescriptorFlag = 0b00001000\n\n\t// _maxFilesPrealloc is a limit for files count in a zip. If there are more files in a zip, then\n\t// runtime allocation happens for each subsequent file in a list.\n\t_maxFilesPrealloc = 10\n\n\t// _zipFileBuffer is a size in bytes of a buffer to read a zip file content.\n\t_zipFileBuffer = 8192 // 8 KiB\n)\n\nvar (\n\t// APPENDIX E - AE-x encryption marker: https://www.winzip.com/en/support/aes-encryption/. Little-endian value\n\t// of 0x9901.\n\taexEncryption = [2]byte{1, 153}\n\n\t// 4.4.5 compression method: (2 bytes). The file is stored (no compression).\n\tnoCompression = [2]byte{0, 0}\n\n\t// 4.3.7 Local file header. Little-endian of 0x04034b50.\n\tlocalFileHeaderSignature = [4]byte{80, 75, 3, 4}\n\n\t// 8.5.5 The signature value 0x08074b50 is also used by some ZIP implementations as a marker for the\n\t// Data Descriptor record. Little-endian of 0x08074b50.\n\tdataDescriptorSignature = [4]byte{80, 75, 7, 8}\n\n\t// 4.3.11 Archive extra data record. Little-endian of 0x08064b50.\n\tarchiveExtraDataSignature = [4]byte{80, 75, 6, 8}\n\n\t// 4.3.12 Central directory structure. Little-endian value of 0x02014b50.\n\tcentralFileHeaderSignature = [4]byte{80, 75, 1, 2}\n\n\t// 4.3.13 Digital signature. Little-endian value of 0x05054b50.\n\theaderSignature = [4]byte{80, 75, 5, 5}\n\n\t// 4.3.14 Zip64 end of central directory record. Little-endian value of 0x06064b50.\n\tzip64EndOfCentralDirSignature = [4]byte{80, 75, 6, 6}\n\n\t// 4.3.15 Zip64 end of central directory locator. Little-endian value of 0x07064b50.\n\tzip64EndOfCentralDirLocatorSignature = [4]byte{80, 75, 6, 7}\n\n\t// 4.3.16 End of central directory record. Little-endian value of 0x06054b50.\n\tendOfCentralDirSignature = [4]byte{80, 75, 5, 6}\n)\n\nvar (\n\tErrNextSegment      = errors.New(\"attempting to read bytes of a next segment\")\n\tErrFileCountLimit   = errors.New(\"there are more files in a zip than expected\")\n\tErrUncompressedSize = errors.New(\"zip file uncompressed size exceeds a limit\")\n\tErrFileSizeLimit    = errors.New(\"zip file content size exceeds a limit\")\n)\n\n// file is a [file data n] segment of a zip.\ntype file struct {\n\t// name is a filename.\n\tname string\n\n\t// compressedSize is a compressed size of a file in a zip.\n\tcompressedSize uint64\n\n\t// uncompressedSize is an uncompressed size of a file in a zip.\n\tuncompressedSize uint64\n}\n\n// bufferedDecompress decompresses a file f, using a buffer of a size buf.\nfunc bufferedDecompress(f *zip.File, buf []byte, fileSizeLimit int) (file, error) {\n\trc, err := f.Open()\n\tif err != nil {\n\t\treturn file{}, fmt.Errorf(\"failed to read a zip file: %v\", err)\n\t}\n\tdefer rc.Close()\n\n\t// Standard, but unreliable approach to catch a file that exceeds a size limit\n\tif f.UncompressedSize64 > uint64(fileSizeLimit) { //nolint:gosec\n\t\treturn file{}, ErrUncompressedSize\n\t}\n\n\tvar fileSize int\n\nloop:\n\t// https://wiki.sei.cmu.edu/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream\n\tfor { // safe endless loop, since we don't let to read more than limit bytes\n\t\tn, err := rc.Read(buf)\n\t\tswitch err {\n\t\tcase io.EOF:\n\t\t\t// end of file\n\t\t\tbreak loop\n\t\tcase nil:\n\t\t\t// no errors\n\t\tdefault:\n\t\t\treturn file{}, fmt.Errorf(\"unexpected error during zip file reading: %v\", err)\n\t\t}\n\n\t\tfileSize += n\n\n\t\t// Reliable way to catch a file that exceeds a size limit\n\t\tif fileSize > fileSizeLimit {\n\t\t\treturn file{}, ErrFileSizeLimit\n\t\t}\n\t}\n\n\treturn file{\n\t\tname:             f.Name,\n\t\tcompressedSize:   f.CompressedSize64,\n\t\tuncompressedSize: f.UncompressedSize64,\n\t}, nil\n}\n\n// DecompressOptions contain parameters for decompressing a zip file.\ntype DecompressOptions struct {\n\t// FilesLimit represents max files count in a zip.\n\tFilesLimit int\n\n\t// FileSizeLimit represents max size of a single zipped file in bytes.\n\tFileSizeLimit int\n\n\t// ZipSizeLimit represents max size of a zip in bytes.\n\tZipSizeLimit int64\n}\n\n// EndOfCentralDirectory locates the end of [end of central directory record] segment in a zip stream r, which is\n// assumed to have the given size in bytes. For zip files decompression DecompressOptions are used.\nfunc EndOfCentralDirectory(r io.ReaderAt, opts DecompressOptions) (offset int64, err error) {\n\t// Validate r according to zip specification as much as archive/zip allows\n\tstream, err := zip.NewReader(r, opts.ZipSizeLimit)\n\tif err != nil {\n\t\treturn offset, fmt.Errorf(\"failed to read zip stream: %v\", err)\n\t}\n\n\t// We have to locate all zip files beforehand, using archive/zip, in order to obtain compressed/uncompressed\n\t// sizes of each file. The edge case that we are delegating to archive/zip is the situation where general\n\t// purpose flag bit 3 is set, and therefore compressed/uncompressed sizes may not appear in\n\t// [local file header n], but rather in [data descriptor n], which, unfortunately, appears after\n\t// [local file header n] and therefore makes it hard to parse these values in a sequential stream\n\tfiles := make([]file, 0, _maxFilesPrealloc) // no realloc if enough cap\n\n\t// Read uncompressed zip file content step-by-step by a buffer size\n\tbuf := make([]byte, _zipFileBuffer)\n\n\tfor i := range stream.File {\n\t\tif i > opts.FilesLimit {\n\t\t\treturn offset, ErrFileCountLimit\n\t\t}\n\n\t\tf, err := bufferedDecompress(stream.File[i], buf, opts.FileSizeLimit)\n\t\tif err != nil {\n\t\t\treturn offset, err\n\t\t}\n\n\t\tfiles = append(files, f)\n\t}\n\n\tvar fileOffest int64 // offset from [local file header n] ... [file data n]\n\tvar countFiles int\n\nloop2:\n\tfor { // infinite loop is dangerous, but we have validated zip with NewReader and the size, so it is safe here\n\t\tcountFiles++\n\t\tfileOffest, err = readFile(r, fileOffest, files)\n\n\t\tif offset < fileOffest { // filter the largest offset\n\t\t\toffset = fileOffest\n\t\t}\n\n\t\tswitch err {\n\t\tcase nil:\n\t\t\t// More [local file header n] ... [data descriptor n] segments left\n\t\tcase ErrNextSegment:\n\t\t\t// No [local file header n] ... [data descriptor n] segments left\n\t\t\tbreak loop2\n\t\tdefault:\n\t\t\treturn offset, fmt.Errorf(\"failed to offset [local file header n] ... [data descriptor n]: %v\", err) //nolint:lll\n\t\t}\n\t}\n\n\t// [archive decryption header]\n\tif offset, err = readArchiveDecryptionHeader(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [archive extra data record]\n\tif offset, err = readArchiveExtraDataRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [central directory header 1] ... [central directory header n]\n\tif offset, err = readCentralDirectoryHeader(r, countFiles, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [zip64 end of central directory record]\n\tif offset, err = readZip64EndOfCentralDirectoryRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [zip64 end of central directory locator]\n\tif offset, err = readZip64EndOfCentralDirectoryLocator(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [end of central directory record]\n\tif offset, err = readEndOfCentralDirectoryRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// readFile returns the offset of [local file header n] ... [data descriptor n]. Files are [file data n]\n// segments of a stream r, which must be parsed beforehand.\nfunc readFile(r io.ReaderAt, off int64, files []file) (int64, error) { //nolint:gocyclo\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read local file header signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase localFileHeaderSignature:\n\t\t// There is a file in a zip\n\t\toff += 4 // offset local file header signature\n\tcase endOfCentralDirSignature:\n\t\t// Empty zip\n\t\treturn off, ErrNextSegment\n\tdefault:\n\t\treturn off, errors.New(\"expected [local file header n]\")\n\t}\n\n\toff += 2 // offset version needed to extract\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read general purpose bit flag: %v\", err)\n\t}\n\toff += 2 // offset general purpose bit flag\n\n\t// 4.4.4 general purpose bit flag: (2 bytes). Bit 0: If set, indicates that the file is encrypted.\n\tisEncrypted := sig[0]&encryptedFlag == encryptedFlag\n\n\t// 4.3.9.1 This descriptor MUST exist if bit 3 of the general purpose bit flag is set.\n\twithDataDescriptor := sig[0]&dataDescriptorFlag == dataDescriptorFlag\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read compression method: %v\", err)\n\t}\n\toff += 2 // offset compression method\n\n\tisCompressed := subtle.ConstantTimeCompare(sig[:2], noCompression[:]) != 1\n\n\tif isCompressed {\n\t\toff += 8 // offset last mod file time ... crc-32\n\t} else {\n\t\toff += 12 // offset last mod file time ... compressed size\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read compression size: %v\", err)\n\t}\n\n\tsize := int64(binary.LittleEndian.Uint32(sig[:])) // either compressed or uncompressed size\n\n\tif isCompressed {\n\t\toff += 8 // offset compressed size ... uncompressed size\n\t} else {\n\t\toff += 4 // offset uncompressed size\n\t}\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read file name length: %v\", err)\n\t}\n\toff += 2 // offset file name length\n\n\tname := int64(binary.LittleEndian.Uint16(sig[:2]))\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read extra field length: %v\", err)\n\t}\n\toff += 2 // offset extra field length\n\n\textra := int64(binary.LittleEndian.Uint16(sig[:2]))\n\n\tfileName := make([]byte, name)\n\tif _, err := r.ReadAt(fileName, off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read file name (variable size): %v\", err)\n\t}\n\toff += name // offset file name (variable size)\n\n\t// Only use provided files if size doesn't present in [local file header n], which actually means that\n\t// size is stored in [data descriptor n], which we cannot be parsed yet (sequential stream)\n\tif size <= 0 {\n\t\tentryID := -1\n\n\t\tfor i := range files {\n\t\t\tif files[i].name == string(fileName) {\n\t\t\t\tentryID = i\n\t\t\t}\n\t\t}\n\n\t\tif entryID == -1 {\n\t\t\treturn off, fmt.Errorf(\"%s not found in a zip stream\", fileName)\n\t\t}\n\n\t\tsize = int64(files[entryID].uncompressedSize) //nolint:gosec\n\t\tif isCompressed {\n\t\t\tsize = int64(files[entryID].compressedSize) //nolint:gosec\n\t\t}\n\t}\n\n\textraField := make([]byte, extra)\n\tif _, err := r.ReadAt(extraField, off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read extra field (variable size): %v\", err)\n\t}\n\n\tvar isPKWAREEncryption bool // traditional PKWARE Encryption\n\n\t// Gather encryption info from extra field\n\tif isEncrypted {\n\t\t// Header ID - 2 bytes\n\t\tswitch {\n\t\tcase subtle.ConstantTimeCompare(extraField[:2], aexEncryption[:]) == 1:\n\t\t\t// AE-x (e.g. AES) encryption marker, encryption info is stored within extra field,\n\t\t\t// not within [encryption header n]\n\t\tdefault:\n\t\t\t// Encryption info is stored withing [encryption header n]\n\t\t\tisPKWAREEncryption = true\n\t\t}\n\t}\n\n\toff += extra // offset extra field (variable size)\n\n\t// If the file is encrypted, the encryption header for the file SHOULD be placed after the local header and\n\t// before the file data (relevant for traditional PKWARE Encryption only)\n\tif isPKWAREEncryption {\n\t\toff += traditionalPKWAREEncryption // offset [encryption header n]\n\t}\n\n\t// After possibly [encryption header n] there must be [file data n]\n\toff += size // offset [file data n]\n\n\tif withDataDescriptor {\n\t\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read possibly data descriptor signature: %v\", err)\n\t\t}\n\n\t\tif subtle.ConstantTimeCompare(sig[:], dataDescriptorSignature[:]) == 1 { // signature is optional\n\t\t\toff += 4 // offset data descriptor signature\n\t\t}\n\n\t\toff += 12 // offset data descriptor (4+4+4)\n\n\t\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read 4 bytes after [data descriptor n]: %v\", err)\n\t\t}\n\n\t\tswitch sig {\n\t\tcase localFileHeaderSignature:\n\t\t\t// [data descriptor 1] [local file header 2], next file to be parsed\n\t\t\treturn off, nil\n\t\tcase centralFileHeaderSignature:\n\t\t\t// [data descriptor n] [central directory header 1], all files are parsed, start parsing\n\t\t\t// central dir\n\t\t\treturn off, ErrNextSegment\n\t\tdefault:\n\t\t\t// [data descriptor n] is either zip64, or we have encountered [archive decryption header]\n\t\t\t// segment.\n\t\t\t//\n\t\t\t// Assume it is zip64, so possibly offset zip64 data descriptor\n\t\t\toff += 8\n\t\t}\n\n\t\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read 4 bytes after zip64 [data descriptor n]: %v\", err)\n\t\t}\n\n\t\tswitch sig {\n\t\tcase localFileHeaderSignature:\n\t\t\t// zip64 [data descriptor 1] [local file header 2], so next file to be parsed\n\t\t\treturn off, nil\n\t\tcase centralFileHeaderSignature:\n\t\t\t// zip64 [data descriptor n] [central directory header 1], all files are parsed,\n\t\t\t// start parsing central dir\n\t\t\treturn off, ErrNextSegment\n\t\tdefault:\n\t\t\t// Either zip64 [data descriptor n] [archive decryption header] or we have already read\n\t\t\t// 8 bytes of [archive decryption header].\n\t\t\t//\n\t\t\t// Assume we have read 8 bytes of [archive decryption header], so read 4 more to reach\n\t\t\t// [archive extra data record]\n\t\t\toff += 4\n\t\t}\n\n\t\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to read 8 bytes after zip64 [data descriptor n]: %v\", err)\n\t\t}\n\n\t\tswitch sig {\n\t\tcase archiveExtraDataSignature:\n\t\t\t// Reached [archive extra data record], so unread [archive decryption header] to reset stream\n\t\t\t// offset at the beginning of [archive decryption header]. There are no files left in a stream\n\t\t\toff -= traditionalPKWAREEncryption\n\n\t\t\treturn off, ErrNextSegment\n\t\tdefault:\n\t\t\t// We have read 4 bytes of [archive decryption header], so unread it\n\t\t\toff -= 4\n\t\t}\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to read 4 bytes after possibly [data descriptor n]: %v\", err)\n\t}\n\n\tif subtle.ConstantTimeCompare(sig[:], centralFileHeaderSignature[:]) == 1 {\n\t\t// No more zip files\n\t\treturn off, ErrNextSegment\n\t}\n\n\treturn off, nil // read next zip file\n}\n\n// readArchiveDecryptionHeader possibly reads [archive decryption header] segment, where off must point at the\n// beginning of the segment in a r stream. If the segment doesn't present in a stream at the location off, the\n// returned value is the same as off. Only supports traditional PKWARE encryption header.\nfunc readArchiveDecryptionHeader(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\t// Even though [archive decryption header] doesn't have a signature, we try to find out what these\n\t// next 4 bytes are\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read 4 bytes of [arhive decryption header]: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase centralFileHeaderSignature:\n\t\t// Non-empty zip file without [archive decryption header]\n\t\treturn off, nil\n\tcase endOfCentralDirSignature:\n\t\t// Empty zip file without [archive decryption header]\n\t\treturn off, nil\n\tdefault:\n\t\t// 6.1 Traditional PKWARE Encryption\n\t\toff += traditionalPKWAREEncryption\n\t}\n\n\treturn off, nil\n}\n\n// readArchiveExtraDataRecord possibly reads [archive extra data record] segment, where off must point at the\n// beginning of the segment in a r stream. If the segment doesn't present in a stream at the location off, the\n// returned value is the same as off.\nfunc readArchiveExtraDataRecord(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read arhive extra data record signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase archiveExtraDataSignature:\n\t\t// [archive decryption header] is present\n\t\toff += 4 // offset archive extra data signature\n\tcase centralFileHeaderSignature:\n\t\t// Non-empty zip file without [archive decryption header]\n\t\treturn off, nil\n\tcase endOfCentralDirSignature:\n\t\t// Empty zip file without [archive decryption header]\n\t\treturn off, nil\n\tdefault:\n\t\treturn off, errors.New(\"expected [archive extra data record]\")\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read extra field length: %v\", err)\n\t}\n\toff += 4 // offset extra field length\n\n\toff += int64(binary.LittleEndian.Uint32(sig[:])) // offset extra field data (variable size)\n\n\treturn off, nil\n}\n\n// readCentralDirectoryHeader possibly reads [central directory header 1] ... [central directory header n]\n// segment, where off must point at the beginning of the segment in a r stream. If the segment doesn't present\n// in a stream at the location off, the returned value is the same as off.\nfunc readCentralDirectoryHeader(r io.ReaderAt, filesCount int, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read central file header signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase centralFileHeaderSignature:\n\t\t// Do nothing as we offset central file header signature in a 'for range files' anyway\n\tcase endOfCentralDirSignature:\n\t\t// Empty zip\n\t\treturn off, nil\n\tdefault:\n\t\treturn off, errors.New(\"expected [central directory header n]\")\n\t}\n\n\t// All files within [local file header 1] ... [local file header n] must be present in\n\t// [central directory header 1] ... [central directory header n]\n\tfor range filesCount {\n\t\toff += 4 + 24 // offset central file header signature ... uncompressed size\n\n\t\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read file name length: %v\", err)\n\t\t}\n\t\toff += 2 // offset file name length\n\n\t\tname := binary.LittleEndian.Uint16(sig[:2])\n\n\t\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read extra field length: %v\", err)\n\t\t}\n\t\toff += 2 // offset extra field length\n\n\t\textra := binary.LittleEndian.Uint16(sig[:2])\n\n\t\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\t\treturn off, fmt.Errorf(\"failed to read file comment length: %v\", err)\n\t\t}\n\t\toff += 2 // offset file comment length\n\n\t\tcom := binary.LittleEndian.Uint16(sig[:2])\n\n\t\toff += 12 // offset disk number start ... relative offset of local header\n\n\t\toff += int64(name + extra + com) // offset file name (variable size) ... file comment (variable size)\n\t}\n\n\t// 4.3.13 Digital signature, optional\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read central directory digital signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase headerSignature:\n\t\t// Digital signature present\n\t\toff += 4 // offset header signature\n\tcase zip64EndOfCentralDirSignature:\n\t\t// Non-empty zip file without a digital signature\n\t\treturn off, nil\n\tcase endOfCentralDirSignature:\n\t\t// Empty zip file without a digital signature\n\t\treturn off, nil\n\tdefault:\n\t\treturn off, errors.New(\"expected central directory header digital signature\")\n\t}\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read size of data: %v\", err)\n\t}\n\toff += 2 // offset size of data\n\n\toff += int64(binary.LittleEndian.Uint16(sig[:2])) // offset signature data\n\n\treturn off, nil\n}\n\n// readZip64EndOfCentralDirectoryRecord possibly reads [zip64 end of central directory record] segment, where\n// off must point at the beginning of the segment in a r stream. If the segment doesn't present in a stream at\n// the location off, the returned value is the same as off.\nfunc readZip64EndOfCentralDirectoryRecord(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [8]byte{} // allocate space suitable for size of zip64 end of central directory record\n\n\tif _, err := r.ReadAt(sig[:4], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read zip64 end of central directory signature: %v\", err)\n\t}\n\n\tswitch {\n\tcase subtle.ConstantTimeCompare(sig[:4], zip64EndOfCentralDirSignature[:]) == 1:\n\t\t// [zip64 end of central directory record] is present\n\t\toff += 4 // offset zip64 end of central dir signature\n\tcase subtle.ConstantTimeCompare(sig[:4], endOfCentralDirSignature[:]) == 1:\n\t\t// Non-empty/empty zip\n\t\treturn off, nil\n\tdefault:\n\t\treturn off, errors.New(\"expected [zip64 end of central directory record]\")\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read size of zip64 end of central directory record: %v\", err)\n\t}\n\toff += 8 // offset size of zip64 end of central directory record\n\n\t// 4.3.14.1 The value stored into the \"size of zip64 end of central directory record\" SHOULD be the size of\n\t// the remaining record and SHOULD NOT include the leading 12 bytes.\n\toff += int64(binary.LittleEndian.Uint64(sig[:])) //nolint:gosec\n\n\treturn off, nil\n}\n\n// readZip64EndOfCentralDirectoryLocator possibly reads [zip64 end of central directory locator] segment, where\n// off must point at the beginning of the segment in a r stream. If the segment doesn't present in a stream at\n// the location off, the returned value is the same as off.\nfunc readZip64EndOfCentralDirectoryLocator(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read zip64 end of central directory locator signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase zip64EndOfCentralDirLocatorSignature:\n\t\t// [zip64 end of central directory record] [zip64 end of central directory locator] are present\n\t\toff += 4 // offset zip64 end of central dir locator signature\n\tcase endOfCentralDirSignature:\n\t\t// Non-empty/empty zip\n\t\treturn off, nil\n\tdefault:\n\t\treturn off, errors.New(\"expected [zip64 end of central directory locator]\")\n\t}\n\n\toff += 16 // offset [zip64 end of central directory locator]\n\n\treturn off, nil\n}\n\n// readEndOfCentralDirectoryRecord possibly reads [end of central directory record] segment, where off must point\n// at the beginning of the segment in a r stream. If the segment doesn't present in a stream at the location off,\n// the returned value is the same as off.\nfunc readEndOfCentralDirectoryRecord(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read end of central dir signature: %v\", err)\n\t}\n\n\tif subtle.ConstantTimeCompare(sig[:], endOfCentralDirSignature[:]) != 1 {\n\t\treturn off, errors.New(\"expected [end of central directory record]\")\n\t}\n\toff += 4 // offset end of central dir signature\n\n\toff += 16 // offset number of this disk ... starting disk number\n\n\tif _, err := r.ReadAt(sig[:2], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to read .ZIP file comment length\")\n\t}\n\toff += 2 // offset .ZIP file comment length\n\n\toff += int64(binary.LittleEndian.Uint16(sig[:2])) // offset .ZIP file comment (variable size)\n\n\treturn off, nil\n}\n"
  },
  {
    "path": "common/collector/zip/zip_test.go",
    "content": "package zip\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/subtle\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n)\n\n// endOfCentralDirectory locates the end of [end of central directory record] segment in a zip stream r.\n// Input files represent a collection of [local file header 1] [encryption header 1] [file data 1] ...\n// [local file header n] [encryption header n] [file data n] segments of a zip stream.\nfunc endOfCentralDirectory(r io.ReaderAt, files []*zip.File) (offset int64, err error) {\n\tvar off int64 // offset from beginning of [local file header 1] till [file data n] end\n\n\tfor i := range files {\n\t\tif off, err = files[i].DataOffset(); err != nil { // offset [local file header] [encryption header]\n\t\t\treturn offset, fmt.Errorf(\"failed to offset [local file header] [encryption header]: %v\", err)\n\t\t}\n\n\t\toff += int64(files[i].CompressedSize64) // offset [local file header] [encryption header] [file data]\n\n\t\tif offset < off { // filter the largest offset\n\t\t\toffset = off\n\t\t}\n\t}\n\n\t// [data descriptor n]\n\tif offset, err = readDataDescriptorN(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [archive decryption header]\n\tif offset, err = readArchiveDecryptionHeader(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [archive extra data record]\n\tif offset, err = readArchiveExtraDataRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [central directory header 1] ... [central directory header n]\n\tif offset, err = readCentralDirectoryHeader(r, len(files), offset); err != nil {\n\t\treturn\n\t}\n\n\t// [zip64 end of central directory record]\n\tif offset, err = readZip64EndOfCentralDirectoryRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [zip64 end of central directory locator]\n\tif offset, err = readZip64EndOfCentralDirectoryLocator(r, offset); err != nil {\n\t\treturn\n\t}\n\n\t// [end of central directory record]\n\tif offset, err = readEndOfCentralDirectoryRecord(r, offset); err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// readDataDescriptorN possibly reads [data descriptor n] segment, where off must point at the beginning of\n// the segment in a r stream. If the segment doesn't present in a stream at the location off, the returned value\n// is the same as off.\nfunc readDataDescriptorN(r io.ReaderAt, off int64) (int64, error) {\n\tsig := [4]byte{}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read data descriptor signature: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase dataDescriptorSignature:\n\t\toff += 4 // offset data descriptor signature\n\tcase centralFileHeaderSignature: // non-empty zip/zip64, that doesn't have [data descriptor n]\n\t\treturn off, nil\n\tcase endOfCentralDirSignature: // empty zip/zip64, that doesn't have [data descriptor n]\n\t\treturn off, nil\n\tdefault:\n\t\t// Data descriptor crc-32 or we have read 4 bytes of [archive decryption header] that doesn't have\n\t\t// [data descriptor n]\n\t}\n\n\toff += 4 + 4 + 4 // offset 12 bytes, which is the size of [data descriptor n]\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read [data descriptor n]: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase archiveExtraDataSignature:\n\t\t// [data descriptor n] doesn't present and we have offset 12 bytes of [archive decryption header], so\n\t\t// set offset back to the beginning\n\t\toff -= traditionalPKWAREEncryption\n\n\t\treturn off, nil\n\tcase centralFileHeaderSignature: // we have offset 12 bytes of [data description n]\n\t\treturn off, nil\n\tdefault:\n\t\t// We have offset 12 bytes of zip64 [data description n], there are 8 bytes more left to offset, or\n\t\t// we have offset 12 bytes of [data description n] and [archive decryption header] is the next.\n\t\t//\n\t\t// Let's first assume that it is zip64 [data descriptor n]\n\t\toff += 8 // (4+4+4)+8 == 4+8+8\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read zip64 [data descriptor n]: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase centralFileHeaderSignature: // we have offset zip64 [data description n]\n\t\treturn off, nil\n\tdefault:\n\t\t// We have offset zip64 [data description n] and [archive decryption header] is the next, or\n\t\t// we have read 8 bytes of [archive decryption header] and there are left 4 bytes more to reach\n\t\t// [archive extra data record].\n\t\t//\n\t\t// Let's offset 4 bytes to possibly reach [archive extra data record]\n\t\toff += 4\n\t}\n\n\tif _, err := r.ReadAt(sig[:], off); err != nil {\n\t\treturn off, fmt.Errorf(\"failed to possibly read 4 bytes after zip64 [data descriptor n]: %v\", err)\n\t}\n\n\tswitch sig {\n\tcase archiveExtraDataSignature:\n\t\t// We have reached [archive extra data record], so set offset back to the beginning of\n\t\t// [archive decryption header]\n\t\toff -= traditionalPKWAREEncryption\n\tdefault:\n\t\t// We have offset 4 bytes from the beginning of [archive decryption header], so set it back to the\n\t\t// beginning\n\t\toff -= 4\n\t}\n\n\treturn off, nil\n}\n\nfunc TestEndOfCentralDirectory(t *testing.T) {\n\tzipFiles := map[string]int64{\n\t\t\"testdata/zip64.zip\":                            32768,\n\t\t\"testdata/zip64-2.zip\":                          32768,\n\t\t\"testdata/subdir.zip\":                           32768,\n\t\t\"testdata/smartid1.bdoc\":                        32768, // Smart-ID (https://www.smart-id.com/)\n\t\t\"testdata/smartid2.bdoc\":                        32768, // Smart-ID with trailing bytes\n\t\t\"testdata/mid1.bdoc\":                            32768, // Mobile-ID (https://www.id.ee/en/mobile-id/)\n\t\t\"testdata/mid2.bdoc\":                            32768, // Mobile-ID with trailing bytes\n\t\t\"testdata/container1.asice\":                     32768, // ID card (https://www.id.ee/en/rubriik/id-card-en/)\n\t\t\"testdata/container2.asice\":                     32768, // ID card with trailing bytes\n\t\t\"testdata/container3_general_purpose_bit3.bdoc\": 32768,\n\t\t\"testdata/zipcrypto.zip\":                        32768, // for some reason reading with archive/zip fails, but without this read works fine\n\t\t\"testdata/unix.zip\":                             32768,\n\t\t\"testdata/utf8-7zip.zip\":                        32768,\n\t\t\"testdata/utf8-winrar.zip\":                      32768,\n\t\t\"testdata/deflate.zip\":                          32768,\n\t\t\"testdata/uncompressed.zip\":                     32768,\n\t\t\"testdata/deflate_trailing_bytes.zip\":           32768,\n\t\t\"testdata/uncompressed_trailing_bytes.zip\":      32768,\n\t\t\"testdata/empty.zip\":                            32768,\n\t\t\"testdata/aes256.zip\":                           32768, // for some reason reading with archive/zip fails, but without this read works fine\n\t\t\"testdata/symlink.zip\":                          32768,\n\t\t\"testdata/time-7zip.zip\":                        32768,\n\t\t\"testdata/time-osx.zip\":                         32768,\n\t\t\"testdata/time-win7.zip\":                        32768,\n\t\t\"testdata/time-winrar.zip\":                      32768,\n\t\t\"testdata/utf8-osx.zip\":                         32768,\n\t\t\"testdata/utf8-winzip.zip\":                      32768,\n\t\t\"testdata/deleted1.zip\":                         32768,\n\t\t\"testdata/42.zip\":                               42838, // https://unforgettable.dk/. Fails with unsupported compression algorithm by archive/zip\n\t\t\"testdata/zbsm.zip\":                             42374, // https://www.bamsoftware.com/hacks/zipbomb/\n\t}\n\n\t// Tails are not declared in .ZIP file comment length section, so basically all these zips are malformed\n\tzipFilesWithTails := map[string]string{\n\t\t\"testdata/container2.asice\":                \"This is trailing bytes\",\n\t\t\"testdata/mid2.bdoc\":                       \"Mobile-ID trailing bytes\",\n\t\t\"testdata/smartid2.bdoc\":                   \"Smart-ID trailing bytes\",\n\t\t\"testdata/deflate_trailing_bytes.zip\":      \"Unexpected trailing bytes on deflate compression\",\n\t\t\"testdata/uncompressed_trailing_bytes.zip\": \"Unexpected trailing bytes on uncompressed zip\",\n\t}\n\n\t// NB! Never unzip these files!\n\tzipBombs := []string{\n\t\t\"testdata/42.zip\",\n\t\t\"testdata/zbsm.zip\",\n\t}\n\n\tunexpectedErrors := []string{\n\t\t\"testdata/zipcrypto.zip\",\n\t\t\"testdata/aes256.zip\",\n\t}\n\n\tfor zipFile, zipFileSize := range zipFiles {\n\t\tb, err := os.ReadFile(zipFile)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tr := bytes.NewReader(b)\n\t\tzr, err := zip.NewReader(r, zipFileSize)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\topts := DecompressOptions{\n\t\t\tFilesLimit:    30,\n\t\t\tFileSizeLimit: 32768, // BDOC oriented value\n\t\t\tZipSizeLimit:  zipFileSize,\n\t\t}\n\n\t\toffset, err := EndOfCentralDirectory(r, opts)\n\t\tif err != nil {\n\t\t\tvar foundZipBomb bool\n\n\t\t\tfor _, zipBomb := range zipBombs {\n\t\t\t\tif zipBomb == zipFile {\n\t\t\t\t\tfoundZipBomb = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif foundZipBomb { // zip bombs are expected to fail\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar foundUnexpectedErrors bool\n\n\t\t\tfor _, unexpectedError := range unexpectedErrors {\n\t\t\t\tif unexpectedError == zipFile {\n\t\t\t\t\tfoundUnexpectedErrors = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif foundUnexpectedErrors {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif offset <= 0 {\n\t\t\tt.Fatal(\"offset of even empty zip cannot be 0\")\n\t\t}\n\t\tif offset != int64(len(b)) {\n\t\t\ttail, ok := zipFilesWithTails[zipFile]\n\t\t\tif !ok {\n\t\t\t\tt.Fatalf(\"zip content length mismatch, expected=%d, got=%d, and trailing text is: %s\", offset, len(b), b[offset:])\n\t\t\t}\n\n\t\t\tif subtle.ConstantTimeCompare(b[offset:], []byte(tail)) != 1 {\n\t\t\t\tt.Fatalf(\"trailing bytes mismatch, expected=%s, got=%s\", tail, b[offset:])\n\t\t\t}\n\t\t}\n\n\t\t// This is how caller may detect a malformed zip file\n\t\t//\n\t\t// 1. Find expected end of a zip file\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// 2. Try to read at least one byte more, and if succeeds, then there are appended bytes at the end of zip.\n\t\t// Only io.EOF is considered as a valid outcome\n\t\tp := [1]byte{}\n\t\t_, err = r.ReadAt(p[:], offset)\n\t\tif err != io.EOF {\n\t\t\t// Following 'if' block is only for testing purposes, caller must throw an error if non-EOF occurs\n\t\t\tif _, ok := zipFilesWithTails[zipFile]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tt.Fatal(\"appended bytes at the end of zip\")\n\t\t}\n\n\t\t// Compare that outcome of test is the same as real\n\t\toffset2, err := endOfCentralDirectory(r, zr.File)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif offset != offset2 {\n\t\t\tt.Fatalf(\"EndOfCentralDirectory offset=%d, test-endOfCentralDirectory offset=%d\", offset, offset2)\n\t\t}\n\t}\n}\n\nfunc BenchmarkEndOfCentralDirectory(b *testing.B) {\n\tf, err := os.ReadFile(\"testdata/smartid1.bdoc\")\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    30,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  32768,\n\t}\n\n\tvar offset int64\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}\n\tb.StopTimer()\n\t// 98358 ns/op\t49350 B/op\t68 allocs/op\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n\nfunc TestEndOfCentralDirectoryRegression(t *testing.T) {\n\texpected := 68.0\n\n\tf, err := os.ReadFile(\"testdata/smartid1.bdoc\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    30,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  32768,\n\t}\n\n\tvar offset int64\n\n\tif allocs := testing.AllocsPerRun(10, func() {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}); allocs > expected {\n\t\tt.Fatalf(\"EndOfCentralDirectory now requires %0.f heap allocations, while before required %0.f\", allocs, expected)\n\t}\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n\nfunc BenchmarkClassicalZipBombEndOfCentralDirectoryRegression(b *testing.B) {\n\tf, err := os.ReadFile(\"testdata/42.zip\")\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    500,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  42838,\n\t}\n\n\tvar offset int64\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}\n\tb.StopTimer()\n\t// 7491 ns/op\t19031 B/op\t78 allocs/op\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n\nfunc TestClassicalZipBombEndOfCentralDirectoryRegression(t *testing.T) {\n\texpected := 78.0\n\n\tf, err := os.ReadFile(\"testdata/42.zip\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    500,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  42838,\n\t}\n\n\tvar offset int64\n\n\tif allocs := testing.AllocsPerRun(10, func() {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}); allocs > expected {\n\t\tt.Fatalf(\"EndOfCentralDirectory for 42.zip now requires %0.f heap allocations, while before required %0.f\", allocs, expected)\n\t}\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n\nfunc BenchmarkBetterZipBombEndOfCentralDirectoryRegression(b *testing.B) {\n\tf, err := os.ReadFile(\"testdata/zbsm.zip\")\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    500,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  42374,\n\t}\n\n\tvar offset int64\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}\n\tb.StopTimer()\n\t// 60901 ns/op\t73653 B/op\t979 allocs/op\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n\nfunc TestBetterZipBombEndOfCentralDirectoryRegression(t *testing.T) {\n\texpected := 979.0\n\n\tf, err := os.ReadFile(\"testdata/zbsm.zip\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr := bytes.NewReader(f)\n\n\topts := DecompressOptions{\n\t\tFilesLimit:    500,\n\t\tFileSizeLimit: 32768,\n\t\tZipSizeLimit:  42374,\n\t}\n\n\tvar offset int64\n\n\tif allocs := testing.AllocsPerRun(10, func() {\n\t\toffset, err = EndOfCentralDirectory(r, opts)\n\t}); allocs > expected {\n\t\tt.Fatalf(\"EndOfCentralDirectory for zbsm.zip now requires %0.f heap allocations, while before required %0.f\", allocs, expected)\n\t}\n\n\tfmt.Fprint(io.Discard, offset, err)\n}\n"
  },
  {
    "path": "common/external/.gitignore",
    "content": ""
  },
  {
    "path": "common/go/common.mk",
    "content": "# Common Makefile for all Go modules\n# 1. Create new Makefile in a Go module directory\n# 2. Write as first line in a Makefile `include ../common/go/common.mk`\n\nCOMMONDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\nROOTDIR   := $(abspath $(COMMONDIR)../../)/\n\ninclude $(COMMONDIR)govar.mk\n\n.PHONY: all\nall: tidy generate\n\tenv GOBIN=$(or $(IVXV_GOBIN),$(abspath bin)) \\\n\t\t$(GO) install $(if $(DEVELOPMENT),-tags development )./...\n\n# Generate gen_types.go for each package\n.PHONY: gotools\ngotools:\n\t$(MAKE) -C $(ROOTDIR)common/tools/go all ROOTDIR=$(ROOTDIR)\n\n.PHONY: lint\nlint: all\n\tif which golangci-lint > /dev/null; then \\\n\t\tenv PATH=\"$(dir $(GO)):$$PATH\" \\\n\t\t\tgolangci-lint run --config $(COMMONDIR)golangci-lint.yaml; \\\n\tfi\n\n.PHONY: test\ntest: lint\n\tenv GODEBUG=x509sha1=1 $(GO) test -v $(GOTESTFLAGS) ./...\n\n.PHONY: install\ninstall: all\n\tif [ -d bin ]; then mkdir -p $(DESTDIR)/usr && cp -r bin $(DESTDIR)/usr; fi\n\t$(if $(EXTRADATA),mkdir -p $(DESTDIR)/usr/share/ivxv && cp -r $(EXTRADATA) $(DESTDIR)/usr/share/ivxv)\n\n.PHONY: generate\ngenerate: gotools\n\t$(GO) generate ./...\n\t$(ROOTDIR)common/tools/go/bin/gen -base $(ROOTDIR) ./...\n\n.PHONY: clean\nclean:\n\tfind . -not -path \\*/testdata/\\* \\( \\\n\t\t-name gen_types.go -o \\\n\t\t-name gen_types_test.go -o \\\n\t\t-name gen_import.go -o \\\n\t\t-name gen_import_dev.go \\\n\t\t\\) -delete\n\t -$(GO) clean -i ./...\n\trm -rf bin/ pkg/\n\n.PHONY: tidy\ntidy:\n\t$(GO) mod tidy\n\n# Generate gen_import.go for each package where in source files:\n# `import //ivxv:modules <module>`\n# Generate gen_import_dev.go for each package where in source files:\n# `import //ivxv:development <module>`\n.PHONY: goimports\ngoimports:\n\t$(ROOTDIR)scripts/goimports.sh ./...\n"
  },
  {
    "path": "common/go/golangci-lint.yaml",
    "content": "run:\n  timeout: 5m\n\nlinters:\n  enable:\n    - copyloopvar\n    - dupl\n    - errcheck\n    - goconst\n    - gocritic\n    - gocyclo\n    - gofmt\n    - goimports\n    - gosec\n    - gosimple\n    - govet\n    - ineffassign\n    - lll\n    - nolintlint\n    - revive\n    - staticcheck # also checks for deprecated code\n    - stylecheck\n    - typecheck\n    - unconvert\n    - unused\n\nlinters-settings:\n  goimports:\n    local-prefixes: ivxv.ee\n  lll:\n    tab-width: 8\n  nolintlint:\n    allow-unused: false\n    allow-leading-space: false\n    require-specific: true\n\nissues:\n  # Escape all *_test.log and log_desc.go files from linter\n  exclude-files:\n    - '(.+)_test\\.go'\n    - '(.+)log_desc\\.go'\n  # Escape code generator from linter\n  exclude-dirs:\n    - 'log/cmd/gen'\n"
  },
  {
    "path": "common/go/govar.mk",
    "content": "GO := /usr/lib/go-1.23/bin/go\nGOPATHLOCAL := $(ROOTDIR)common/external/go\n\n# Only use go version >= 1.23\nifeq ($(shell which $(GO)),)\n\tfallback := $(shell which go)\n\tifneq ($(fallback),)\n\t\tversion := $(shell $(fallback) version | cut -d' ' -f3)\n\t\tnewer := $(shell echo \"go1.23\\n$(version)\" | sort --version-sort \\\n\t\t| tail --lines=1)\n\t\tifeq ($(version),$(newer))\n\t\t\tGO := $(fallback)\n\t\tendif\n\tendif\nendif\n\nexport GO\n\n# go downloads dependencies into GOMODCACHE\nexport GOMODCACHE=$(GOPATHLOCAL)\n\n# go searches for dependencies declared in go.mod in:\n# a) the Internet if GOPROXY is unset\n# b) local directory if GOPROXY is set\nifndef ONLINE\nexport GOPROXY=file://$(GOPATHLOCAL)/cache/download\nexport GOSUMDB=off\nendif\n"
  },
  {
    "path": "common/java/.gitignore",
    "content": ".classpath\n.gradle/\n.idea/\n.project\nbuild/\n/log/\nbin/\n.settings/\n"
  },
  {
    "path": "common/java/Makefile",
    "content": "include common.mk\n"
  },
  {
    "path": "common/java/build.gradle",
    "content": "buildscript {\n    ext.base = '../../'\n    apply from: \"${base}/common/java/common-buildscript.gradle\", to: buildscript\n}\n\napply from: \"${base}/common/java/common-build.gradle\"\napply plugin: 'java-library'\napply plugin: 'java-test-fixtures'\n\ndependencies {\n    api 'ch.qos.cal10n:cal10n-api:0.8.1'\n    implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5'\n    implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5'\n    api 'com.fasterxml.jackson.core:jackson-databind:2.18.1'\n    api 'org.apache.commons:commons-collections4:4.4'\n    /*\n    Upgrading to 3.0.0, will produce at\n    common/java/src/main/java/ee/ivxv/common/util/PdfDoc.java\n    PDType0Font.load(..., ...);\n    a warning:\n    \"\"\"\n    MM DD, YY HH:MM:SS org.apache.fontbox.ttf.gsub.GlyphSubstitutionDataExtractor putNewSubstitutionEntry\n    WARNING: For the newGlyph: XXX, newValue: [YY] is trying to override the oldValue: ZZZ\n    \"\"\"\n    */\n    implementation 'org.apache.pdfbox:pdfbox:3.0.3'\n    // Only >= v1.70 bouncycastle can validate digidocj containers >= v5.1.0\n    api 'org.bouncycastle:bcprov-jdk15on:1.70'\n    api 'org.bouncycastle:bcpkix-jdk15on:1.70'\n    implementation 'org.digidoc4j:digidoc4j:5.3.1'\n    /*\n    Previous slf4j api 'org.slf4j:slf4j-api:1.7.25' is a bit out-dated, if you\n    upgrade to the current latest api 'org.slf4j:slf4j-api:2.0.9', then you get\n    a warning SLF4J: No SLF4J providers were found. Solution is described here:\n    https://www.slf4j.org/codes.html#ignoredBindings\n    From the list of proposed solutions the logger below is the only one which\n    is the latest current version of slf4j which behaves exactly the same way\n    as old one\n    */\n    api 'ch.qos.logback:logback-classic:1.5.12'\n    implementation 'org.yaml:snakeyaml:2.3'\n    // Legacy Java module 'javax.xml.bind' that is used by org.digidocj\n    // In IVXV code 'javax.xml.bind' is replaced with Java 17 specific java.util.HexFormat\n    api 'org.glassfish.jaxb:jaxb-runtime:4.0.5'\n\n    testFixturesApi 'junit:junit:4.13.2'\n    testFixturesApi 'org.hamcrest:hamcrest-library:3.0'\n    testFixturesApi 'pl.pragmatists:JUnitParams:1.1.1'\n    testFixturesApi 'org.mockito:mockito-core:5.14.2'\n}\n\nsourceSets {\n    main {\n        java {\n            if (project.hasProperty('development')) {\n                srcDir file('src/integration-test/java')\n            }\n        }\n        resources {\n            srcDir \"${projectDir}/translations\"\n        }\n    }\n    testFixtures {\n        java {\n            srcDir file('src/integration-test/java')\n        }\n    }\n}\n\n// Dummy for common Makefile.\ntask installDist\n"
  },
  {
    "path": "common/java/common-build.gradle",
    "content": "/*\n * To be included by the master build scripts.\n * Requires property 'base' to appoint to the repository root.\n *\n * Sets common properties, sets repositories and defines the task for\n * downloading dependencies.\n */\n\napply plugin: 'java'\n\ngroup 'ee.ivxv'\nversion '1.10.3'\n\njava {\n\tsourceCompatibility = JavaVersion.VERSION_21\n}\n\next.external = \"${base}/common/external\"\n\ntest {\n    testLogging {\n        events \"PASSED\", \"STARTED\", \"FAILED\", \"SKIPPED\"\n    }\n}\n\n// Skip subproject tests if build was invoked by the root Makefile\nsubprojects {\n    tasks.withType(Test) {\n        if (System.env.ROOT_BUILD) {\n            enabled = false\n        }\n    }\n}\n\nallprojects {\n    plugins.withType(ApplicationPlugin) {\n        startScripts {\n            doLast {\n                // Set the windows command prompt to support UTF-8 output\n                windowsScript.text = \"@echo off\\r\\nchcp 65001 >NUL 2>&1\\r\\nset JAVA_OPTS=-Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8\\r\\n@echo on\\r\\n\" + windowsScript.text\n            }\n        }\n    }\n}\n\nrepositories {\n    mavenCentral()\n}\n\ntask jarall(type: Jar) {\n  manifest.from jar.manifest\n  archiveClassifier = 'all'\n  duplicatesStrategy = DuplicatesStrategy.EXCLUDE\n  from {\n    configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }\n  } {\n    exclude \"META-INF/DEPENDENCIES\"\n    exclude \"META-INF/*.SF\"\n    exclude \"META-INF/*.DSA\"\n    exclude \"META-INF/*.RSA\"\n    exclude \"log*.xml\"\n    exclude \"log*.properties\"\n  }\n  with jar\n}\n\ntasks.withType(JavaCompile) {\n    options.compilerArgs.add(\"-Xlint:unchecked\")\n    options.encoding = \"UTF-8\"\n    options.deprecation = true\n}\n"
  },
  {
    "path": "common/java/common-buildscript.gradle",
    "content": "/*\n * To be included inside the 'buildscript' block of the master build script as\n * 'apply from: ... to: buildscript'\n *\n * Initializes the build script plugin repositories and dependencies.\n */\n\ndef external = file('../external').absolutePath\n\nrepositories {\n    mavenCentral()\n}\n"
  },
  {
    "path": "common/java/common.mk",
    "content": "COMMONDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\nROOTDIR   := $(abspath $(COMMONDIR)../../)/\n\ninclude $(COMMONDIR)javavar.mk\n\n.PHONY: all\nall: GFLAGS += --warning-mode all\nall:\n\t$(G) build installDist $(if $(DEVELOPMENT),-P development=1) $(GFLAGS)\n\n.PHONY: all-dev\nall-dev:\n\t$(MAKE) DEVELOPMENT=1\n\n.PHONY: test\ntest:\ntest:\n\t$(G) test $(GFLAGS)\n\n.PHONY: install\ninstall: GFLAGS += -x test\ninstall: all\nifndef DESTDIR\n\t$(error $$DESTDIR must be set to install Java applications)\nendif\n\tif [ -d build/distributions ]; then \\\n\t\tmkdir -p $(DESTDIR)/ && \\\n\t\tcp build/distributions/*.zip $(DESTDIR)/; \\\n\tfi\n\n.PHONY: clean\nclean:\nclean:\n\t$(G) clean $(GFLAGS)\n"
  },
  {
    "path": "common/java/javavar.mk",
    "content": "G := $(ROOTDIR)common/external/gradle-8.11/bin/gradle\nG_CACHE := $(ROOTDIR)common/external/java/\nGFLAGS := -g=$(G_CACHE)\n\n# Will download Java dependencies from Web into common/external/java/\n# if you run from project's root `make [...] ONLINE=[...]`, otherwise\n# local Java dependencies from common/external/java/ will be used\nifndef ONLINE\nGFLAGS += --offline\nendif\n\n"
  },
  {
    "path": "common/java/settings.gradle",
    "content": "rootProject.name = 'common'\n\n"
  },
  {
    "path": "common/java/src/integration-test/java/ee/ivxv/common/cli/TestContextFactory.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.impl.BboxHelperImpl;\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.report.CsvReporterImpl;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.smartcard.CardService;\nimport ee.ivxv.common.service.smartcard.dummy.DummyCardService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * This class must extend {@code ContextFactory} and have the same full name as the value of\n * {@code ContextFactory.OVERLOAD_CLASS}.\n * \n * <p>\n * The class can override service creation methods and provide different implementation.\n */\npublic class TestContextFactory extends ContextFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(TestContextFactory.class);\n\n    public TestContextFactory() {\n        super();\n        log.info(\"Creating instance of overloaded context factory {}\", getClass().getName());\n    }\n\n    @Override\n    public Console getConsole() {\n        log.info(\"Creating Console by overloaded context factory {}\", getClass().getName());\n        return super.getConsole();\n    }\n\n    @Override\n    public Reporter getReporter(I18n i18n) {\n        return new CsvReporterImpl(i18n) {\n            @Override\n            public String getCurrentTime() {\n                // Using fixed current time in reports to simplify comparing output with 'diff' tool\n                return \"20170401000000\";\n            }\n        };\n    }\n\n    @Override\n    public CardService getCard(Console console, I18n i18n) {\n        return new DummyCardService(console, i18n, true);\n    }\n\n    @Override\n    public BboxHelper getBbox(Conf conf, ContainerReader container) {\n        log.info(\"Creating Bbox by overloaded context factory {}\", getClass().getName());\n        return new BboxHelperImpl(conf, container);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/integration-test/java/ee/ivxv/common/service/smartcard/dummy/DummyCardService.java",
    "content": "package ee.ivxv.common.service.smartcard.dummy;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.smartcard.Card;\nimport ee.ivxv.common.service.smartcard.CardService;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.HexFormat;\nimport java.util.Map;\n\n/**\n * DummyCardService is a card service which uses JSON storage for content.\n */\npublic class DummyCardService implements CardService {\n    static class DummyFilesystems {\n        private Map<String, Map<String, byte[]>> filesystems;\n\n        @JsonCreator\n        private DummyFilesystems( //\n                @JsonProperty(\"filesystems\") Map<String, Map<String, byte[]>> filesystems) {\n            this.filesystems = filesystems;\n        }\n\n        private DummyFilesystems() {\n            this.filesystems = new HashMap<String, Map<String, byte[]>>();\n        }\n\n        @JsonIgnore\n        private void createFilesystem(String id) {\n            if (filesystems.get(id) == null) {\n                filesystems.put(id, new HashMap<String, byte[]>());\n            }\n        }\n\n        @JsonIgnore\n        Map<String, byte[]> getFilesystem(String id) {\n            return filesystems.get(id);\n        }\n\n        @JsonIgnore\n        byte[] getFile(String id, byte[] path) {\n            Map<String, byte[]> fs = filesystems.get(id);\n            return fs == null ? null : fs.get(hex(path));\n        }\n\n        @JsonIgnore\n        void putFile(String id, byte[] path, byte[] content) {\n            filesystems.get(id).put(hex(path), content);\n        }\n\n        @JsonIgnore\n        boolean removeFilesystem(String id) {\n            boolean res = filesystems.remove(id) != null;\n            return res;\n        }\n\n        @JsonIgnore\n        boolean removeFile(String id, byte[] path) {\n            Map<String, byte[]> fs = getFilesystem(id);\n            if (fs == null) {\n                return false;\n            }\n            boolean res = fs.remove(hex(path)) != null;\n            return res;\n        }\n\n        public Map<String, Map<String, byte[]>> getFilesystems() {\n            return filesystems;\n        }\n\n        @JsonIgnore\n        private static String hex(byte[] in) {\n            return HexFormat.of().formatHex(in).toUpperCase();\n        }\n    }\n\n    DummyFilesystems fses;\n\n    /**\n     * The environment variable to read the dummy card filesystems path.\n     */\n    public static final String ENV_CARD_FS_PATH_VAR = \"DUMMY_CARDS_PATH\";\n    /**\n     * If the environment variable defined by the key {@#ENV_CARD_FS_PATH_VAR} does not hold the\n     * path for dummy cards use the following default location.\n     */\n    public static final String DEFAULT_CARD_FS_PATH = \"dummy_card_filesystems\";\n\n    private final I18nConsole console;\n    private final Path cardFsPath;\n\n    public DummyCardService(Console console, I18n i18n) {\n        this(console, i18n, false);\n    }\n\n    public DummyCardService(Console console, I18n i18n, boolean persistent) {\n        this.console = new I18nConsole(console, i18n);\n        String envpath = System.getenv(ENV_CARD_FS_PATH_VAR);\n        String path = envpath != null ? envpath : DEFAULT_CARD_FS_PATH;\n        cardFsPath = persistent ? Paths.get(path) : null;\n        if (cardFsPath != null && Files.exists(cardFsPath)) {\n            try {\n                fses = Json.read(cardFsPath, DummyFilesystems.class);\n            } catch (Exception e) {\n                throw new RuntimeException(\"Unable to read dummy filesystems\", e);\n            }\n        }\n        if (this.fses == null) {\n            this.fses = new DummyFilesystems();\n        }\n    }\n\n    @Override\n    public Card createCard(String id) {\n        fses.createFilesystem(id);\n        return new DummyPKCS15Card(id, console, this);\n    }\n\n    @Override\n    public Cards createCards() {\n        Cards cards = new Cards(this, console) {\n            @Override\n            public Card getCard(int index) {\n                return cards.get(index);\n            }\n        };\n        return cards;\n    }\n\n    @Override\n    public boolean isPluggableService() {\n        return false;\n    }\n\n    /**\n     * Commit the current storage to file.\n     */\n    protected void writeToFile() {\n        if (cardFsPath == null) {\n            return;\n        }\n        try {\n            Json.write(fses, cardFsPath);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Unable to write dummy filesystems\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/integration-test/java/ee/ivxv/common/service/smartcard/dummy/DummyPKCS15Card.java",
    "content": "package ee.ivxv.common.service.smartcard.dummy;\n\nimport ee.ivxv.common.service.smartcard.SmartCardException;\nimport ee.ivxv.common.service.smartcard.pkcs15.PKCS15Card;\nimport ee.ivxv.common.service.smartcard.pkcs15.PKCS15IndexedBlob;\nimport ee.ivxv.common.util.I18nConsole;\n\npublic class DummyPKCS15Card extends PKCS15Card {\n    private DummyCardService cs;\n\n    public DummyPKCS15Card(String id, I18nConsole console, DummyCardService cs) {\n        super(id, console);\n        this.cs = cs;\n    }\n\n    @Override\n    public boolean isInitialized() {\n        return true;\n    }\n\n    @Override\n    public void eraseFilesystem() {\n        cs.fses.removeFilesystem(getId());\n    }\n\n    @Override\n    public void storeIndexedBlob(byte[] aid, byte[] identifier, byte[] blob, int index)\n            throws SmartCardException {\n        storeBlob(aid, identifier, new PKCS15IndexedBlob(index, blob).encode());\n    }\n\n    @Override\n    public void storeBlob(byte[] aid, byte[] identifier, byte[] blob) {\n        cs.fses.putFile(getId(), identifier, blob);\n        cs.writeToFile();\n    }\n\n    @Override\n    public boolean removeBlob(byte[] aid, byte[] identifier) {\n        boolean res = cs.fses.removeFile(getId(), identifier);\n        cs.writeToFile();\n        return res;\n    }\n\n    @Override\n    public byte[] getBlob(byte[] aid, byte[] identifier) {\n        return cs.fses.getFile(getId(), identifier);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/M.java",
    "content": "package ee.ivxv.common;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.service.i18n.Translatable;\n\n/**\n * Generic re-usable messages for common module and applications.\n */\n@BaseName(\"i18n.common-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum M implements Translatable {\n    // Error messages\n    e_bb_invalid_type, //\n    e_cert_not_found, //\n    e_cert_read_error, //\n    e_checksum_mismatch, //\n    e_cont_signature_expected, //\n    e_cont_single_file_expected, //\n    e_decryption_error, //\n    e_file_not_container, //\n    e_file_not_found, //\n    e_invalid_container, //\n\n    e_dist_read_error, //\n    e_dist_parish_id_not_unique, //\n    e_dist_parish_id_invalid, //\n    e_dist_parish_region_unknown, //\n    e_dist_bb_parish_missing, //\n\n    e_cand_read_error, //\n    e_cand_invalid_dist, //\n    e_cand_duplicate_id, //\n\n    m_election_id, //\n    m_datetime_pattern, //\n    m_progress_bar, //\n    m_relative_progress_bar, //\n\n    m_loading_params, //\n    m_params_arg_for_cont, //\n\n    m_reading_container, //\n    m_files, //\n    m_file_row, //\n    m_signatures, //\n    m_signature_row, //\n\n    m_cont_checking_signature, //\n    m_cont_signer, //\n    m_cont_signature_time, //\n    m_cont_signature_is_valid, //\n\n    m_checksum_loading, //\n    m_checksum_loaded, //\n    m_checksum_arg_for_cont, //\n    m_checksum_calculate, //\n    m_checksum_ok, //\n\n    m_bb_arg_for_checksum, //\n    m_bb_loading, //\n    m_bb_loaded, //\n    m_bb_checking_type, //\n    m_bb_type, //\n    m_bb_numof_collector_ballots, //\n    m_bb_numof_ballots, //\n    m_bb_checking_integrity, //\n    m_bb_data_is_integrous, //\n    m_bb_checking_ballot_sig, //\n    m_bb_total_ballots, //\n    m_bb_numof_ballots_sig_valid, //\n    m_bb_numof_ballots_sig_invalid, //\n    m_bb_all_ballots_sig_valid, //\n    m_bb_compare_with_reg, //\n    m_bb_ballot_missing_reg, //\n    m_bb_reg_missing_ballot, //\n    m_bb_in_compliance_with_reg, //\n    m_reg_in_compliance_with_bb, //\n    m_bb_total_checked_ballots, //\n    m_bb_exporting, //\n    m_bb_exporting_voter, //\n    m_bb_exported, //\n\n    m_reg_arg_for_checksum, //\n    m_reg_loading, //\n    m_reg_loaded, //\n    m_reg_numof_records, //\n    m_reg_checking_integrity, //\n    m_reg_data_is_integrous, //\n\n    m_bb_saving, //\n    m_bb_saved, //\n    m_bb_checksum_saving, //\n    m_bb_checksum_saved, //\n\n    m_cand_loading, //\n    m_cand_arg_for_cont, //\n    m_cand_loaded, //\n    m_cand_count, //\n\n    m_dist_loading, //\n    m_dist_arg_for_cont, //\n    m_dist_loaded, //\n    m_dist_count,\n\n    m_skip_cmd_loading,\n    m_skip_cmd_loaded,\n    m_skip_cmd_arg_for_cont,\n\n    m_proof_loading, //\n    m_proof_loaded, //\n    m_proof_count, //\n\n    m_out_start, //\n    m_out_done, //\n\n    r_ivl_description, //\n    r_ivl_parish_name, //\n    r_ivl_district_name, //\n    ;\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/ASN1Decodable.java",
    "content": "package ee.ivxv.common.asn1;\n\n/**\n * Interface for classes which can decoded as ASN1 structures.\n *\n */\npublic interface ASN1Decodable {\n    /**\n     * Set the instance fields from DER-encoded byte array.\n     * \n     * @param in ASN1-encoded serialization of instance.\n     * @throws ASN1DecodingException When decoding fails.\n     */\n    void readFromBytes(byte[] in) throws ASN1DecodingException;\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/ASN1DecodingException.java",
    "content": "package ee.ivxv.common.asn1;\n\n/**\n * ASN1DecodingException denotes ASN1 decoding exception.\n *\n */\n@SuppressWarnings(\"serial\")\npublic class ASN1DecodingException extends Exception {\n    public ASN1DecodingException(String str) {\n        super(str);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/ASN1Encodable.java",
    "content": "package ee.ivxv.common.asn1;\n\n/**\n * Interface to classes which can be encoded in ASN1 DER format.\n *\n */\npublic interface ASN1Encodable {\n    /**\n     * Serialize the instance using ASN1.\n     * \n     * @return ASN1-encoded serialization of the instance.\n     */\n    byte[] encode();\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/Ciphertext.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport java.io.IOException;\nimport java.util.Arrays;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.DERSequence;\n\n/**\n * Ciphertext is a structure for encoding and decoding ciphertexts to and from ASN1 DER-encoded\n * values. In addition to the raw ciphertext value, it also stores OID representing the cryptosystem\n * used.\n *\n */\npublic class Ciphertext implements ee.ivxv.common.asn1.ASN1Encodable, ASN1Decodable {\n    private ASN1ObjectIdentifier oid;\n    private byte[] ct;\n\n    /**\n     * Initialize empty Ciphertext for decoding.\n     */\n    public Ciphertext() {\n\n    }\n\n    /**\n     * Initialize Ciphertext for encoding.\n     * \n     * @param oid OID representing the used cryptosystem.\n     * @param ct Byte representation of ASN1 encoded ciphertext.\n     */\n    public Ciphertext(String oid, byte[] ct) {\n        this.oid = new ASN1ObjectIdentifier(oid);\n        this.ct = ct;\n    }\n\n    /**\n     * Encode Ciphertext as ASN1 Ciphertext. The following structure is used:\n     * \n     * <pre>\n     * {@code\n     * Ciphertext ::= SEQUENCE {\n     *     SEQUENCE {\n     *        oid OID\n     *        },\n     *     ct EncodedCiphertext -- Encryption-scheme specific structure\n     * }\n     * </pre>\n     * \n     * @return ASN1 encoded byte array.\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            return new DERSequence(\n                    new ASN1Encodable[] {new DERSequence(oid), ASN1Primitive.fromByteArray(ct)})\n                            .getEncoded();\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            return null;\n        }\n    }\n\n    /**\n     * Set the Ciphertext values from a ASN1 encoded byte array. The expected structure is same as\n     * for {@link #encode()}.\n     * \n     * @param in Input byte array.\n     * @throws ASN1DecodingException if the input is not valid or does not correspond to expected\n     *         structure.\n     * \n     * @see #encode()\n     */\n    @Override\n    public void readFromBytes(byte[] in) throws ASN1DecodingException {\n        if (oid != null || ct != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        @SuppressWarnings(\"resource\")\n        ASN1InputStream a = new ASN1InputStream(in);\n        ASN1Primitive p;\n        try {\n            p = a.readObject();\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Invalid ASN1\");\n        }\n        if (!(p instanceof ASN1Sequence)) {\n            throw new ASN1DecodingException(\"Input not ASN1 SEQUENCE\");\n        }\n\n        // ASN1InputStream returns parsers for the objects that it encounters\n        // but does not error if it encounters also unparseable data. Compare the\n        // obtained object with the input to make sure that everything was parsed.\n        byte[] checkBytes;\n        try {\n            checkBytes = p.getEncoded(\"DER\");\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Invalid ASN1 DER\");\n        }\n\n        if (!Arrays.equals(in, checkBytes)) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n\n\n        ASN1Sequence s = (ASN1Sequence) p;\n        if (s.size() != 2) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n        if (!(s.getObjectAt(0) instanceof ASN1Sequence)) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n        ASN1Sequence oidSeq = (ASN1Sequence) s.getObjectAt(0);\n        if (oidSeq.size() != 1) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n        if (!(oidSeq.getObjectAt(0) instanceof ASN1ObjectIdentifier)) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n        oid = (ASN1ObjectIdentifier) oidSeq.getObjectAt(0);\n        try {\n            ct = ((ASN1Primitive) s.getObjectAt(1)).getEncoded(\"DER\");\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Bytes do not correspond to Ciphertext structure\");\n        }\n    }\n\n    /**\n     * Get the object identifier of the Ciphertext.\n     * \n     * @return Object identifier of the Ciphertext.\n     * @throws ASN1DecodingException If OID is not set.\n     */\n    public String getOID() throws ASN1DecodingException {\n        if (oid == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        return oid.toString();\n    }\n\n    /**\n     * Get the underlying ciphertext.\n     * \n     * @return Underlying encoded ciphertext.\n     * @throws ASN1DecodingException If ciphertext is not set.\n     */\n    public byte[] getCiphertext() throws ASN1DecodingException {\n        if (ct == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        return ct.clone();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/Field.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.ASN1Encodable;\nimport ee.ivxv.common.asn1.ASN1Decodable;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.DERGeneralString;\nimport java.math.BigInteger;\nimport java.io.IOException;\n\n/**\n * Field class is used to encode and decode arbitrary types.\n *\n */\npublic class Field implements ASN1Encodable, ASN1Decodable {\n    private ASN1Primitive f;\n\n    /**\n     * Create uninitialized instance for decoding.\n     */\n    public Field() {}\n\n    /**\n     * Initialize using {@link java.math.BigInteger}.\n     * \n     * @param i\n     */\n    public Field(BigInteger i) {\n        f = new ASN1Integer(i);\n    }\n\n    /**\n     * Initialize using string.\n     * \n     * @param s\n     */\n    public Field(String s) {\n        f = new DERGeneralString(s);\n    }\n\n    /**\n     * Initialize using raw byte array.\n     * \n     * @param b\n     */\n    public Field(byte[] b) {\n        f = new DEROctetString(b);\n    }\n\n    /**\n     * Return ASN1 DER-encoded byte arrays representing the value the instance was initialized with.\n     * \n     * @return ASN1 DER-encoded byte array.\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            return f.getEncoded(\"DER\");\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            return null;\n        }\n    }\n\n    /**\n     * Decode the value from ASN1 DER-encoded input.\n     * \n     * @param b ASN1 DER-encoded input.\n     * @throws ASN1DecodingException When instance is already initialized or decoding from byte\n     *         array fails.\n     */\n    @Override\n    public void readFromBytes(byte[] b) throws ASN1DecodingException {\n        if (f != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        try {\n            f = ASN1Primitive.fromByteArray(b);\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Reading from byte array failed: \" + e.toString());\n        }\n    }\n\n    /**\n     * Get the field value as {@link java.math.BigInteger}.\n     * \n     * @return\n     * @throws ASN1DecodingException When not decodable as an integer.\n     */\n    public BigInteger getInteger() throws ASN1DecodingException {\n        if (f == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        if (!(f instanceof ASN1Integer)) {\n            throw new ASN1DecodingException(\"Field is not an integer\");\n        }\n        return ((ASN1Integer) f).getValue();\n    }\n\n    /**\n     * Get the field values as string.\n     * \n     * @return\n     * @throws ASN1DecodingException When not decodable as a string.\n     */\n    public String getString() throws ASN1DecodingException {\n        if (f == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        if (!(f instanceof DERGeneralString)) {\n            throw new ASN1DecodingException(\"Field is not a string\");\n        }\n        return ((DERGeneralString) f).getString();\n    }\n\n    /**\n     * Get the field value as raw bytes.\n     * \n     * @return\n     * @throws ASN1DecodingException When not decodable as raw bytes.\n     */\n    public byte[] getBytes() throws ASN1DecodingException {\n        if (f == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        if (!(f instanceof DEROctetString)) {\n            throw new ASN1DecodingException(\"Field is not a byte array\");\n        }\n        return ((DEROctetString) f).getOctets();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/PKCS8PrivateKey.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport ee.ivxv.common.asn1.ASN1Decodable;\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\nimport org.bouncycastle.asn1.pkcs.PrivateKeyInfo;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.ASN1OctetString;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport java.io.IOException;\n\n/**\n * PKCS8PrivateKey is class for encoding and decoding private keys in PKCS8 structure.\n *\n */\npublic class PKCS8PrivateKey implements ee.ivxv.common.asn1.ASN1Encodable, ASN1Decodable {\n    private ASN1ObjectIdentifier oid;\n    private byte[] key;\n    private byte[] params;\n\n    /**\n     * Create uninitialized PKCS8PrivateKey for decoding.\n     */\n    public PKCS8PrivateKey() {}\n\n    /**\n     * Initialize PKCS8PrivateKey using an encryption scheme OID, raw key bytes and ASN1-encoded key\n     * parameters. If parameters are not set, it may be null.\n     * \n     * @param oid Encryption scheme OID\n     * @param key Raw key bytes\n     * @param params ASN1-encoded key parameters, may be null.\n     */\n    public PKCS8PrivateKey(String oid, byte[] key, byte[] params) {\n        this.oid = new ASN1ObjectIdentifier(oid);\n        this.key = key.clone();\n        this.params = params;\n    }\n\n    /**\n     * Encode the key in PKCS8 structure. The structure is defined as:\n     * \n     * <pre>\n     * {@code\n     * PrivateKeyInfo ::= SEQUENCE {\n     *   version Version,\n     *   privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,\n     *   privateKey PrivateKey,\n     *   attributes [0] IMPLICIT Attributes OPTIONAL\n     *   }\n     * \n     * Version ::= INTEGER\n     * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier\n     * PrivateKey ::= OCTET STRING\n     * Attributes ::= SET OF Attribute\n     * \n     * AlgorithmIdentifier ::= SEQUENCE {\n     *   algorithm OBJECT IDENTIFIER,\n     *   parameters ANY DEFINED BY algorithm OPTIONAL\n     *   }\n     * }\n     * </pre>\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            if (params != null) {\n                return new PrivateKeyInfo(\n                        new AlgorithmIdentifier(oid, ASN1Primitive.fromByteArray(params)),\n                        new DEROctetString(key)).getEncoded(\"DER\");\n            } else {\n                return new PrivateKeyInfo(new AlgorithmIdentifier(oid), new DEROctetString(key))\n                        .getEncoded(\"DER\");\n            }\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            // the exception thrown here from are:\n            // 1. ASN1Primitive.fromByteArray(params)),\n            // 2. return new PrivateKeyInfo(\n            // 3. ).getEncoded(\"DER\");\n            return null;\n        }\n    }\n\n    /**\n     * Use ASN1 DER-encoded bytes to set the fields of PKCS8PrivateKey. The expected input structure\n     * is defined in {@link #encode()}.\n     * \n     * @param in Input ASN1 DER-encoded bytes\n     * @throw ASN1DecodingException When instance is initialized or input bytes do not correspond to\n     *        the PKCS8PrivateKey\n     *\n     * @see #encode()\n     */\n    @Override\n    public void readFromBytes(byte[] in) throws ASN1DecodingException {\n        if (oid != null || key != null || params != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        @SuppressWarnings(\"resource\")\n        ASN1InputStream a = new ASN1InputStream(in);\n        ASN1Primitive p;\n        try {\n            p = a.readObject();\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Invalid ASN1\");\n        }\n        PrivateKeyInfo pki;\n        pki = PrivateKeyInfo.getInstance(p);\n        oid = pki.getPrivateKeyAlgorithm().getAlgorithm();\n        ASN1Encodable k;\n        try {\n            k = pki.parsePrivateKey();\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Input not PKCS8PrivateKey\");\n        }\n        if (!(k instanceof ASN1OctetString)) {\n            throw new ASN1DecodingException(\"Input not PKCS8PrivateKey\");\n        }\n        key = ((ASN1OctetString) k).getOctets();\n        k = pki.getPrivateKeyAlgorithm().getParameters();\n        if (k != null) {\n            try {\n                params = ((ASN1Primitive) k).getEncoded(\"DER\");\n            } catch (IOException e) {\n                throw new ASN1DecodingException(\"Input not PKCS8PrivateKey\");\n            }\n        }\n    }\n\n    /**\n     * Get the encryption scheme OID\n     * \n     * @return Encryption scheme OID as a string.\n     * @throws ASN1DecodingException When not set.\n     */\n    public String getOID() throws ASN1DecodingException {\n        if (oid == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        return oid.toString();\n    }\n\n    /**\n     * Get the raw key bytes.\n     * \n     * @return Raw key bytes.\n     * @throws ASN1DecodingException When not set.\n     */\n    public byte[] getKey() throws ASN1DecodingException {\n        if (key == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        return key;\n    }\n\n    /**\n     * Get the key parameters. May be null.\n     * \n     * @return ASN1 DER-encoded key parameters. May be null.\n     */\n    public byte[] getParameters() {\n        // params can be null\n        return params;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/RSAParams.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.DERSequence;\n\n/**\n * RSAParams is a class for serializing RSA parameters (public and private exponents and modulus) in\n * ASN.1 structure.\n */\npublic class RSAParams implements ee.ivxv.common.asn1.ASN1Encodable, ASN1Decodable {\n\n    private BigInteger publicExponent;\n    private BigInteger privateExponent;\n    private BigInteger modulus;\n\n    /**\n     * Constructor for uninitialized instance for later decoding.\n     */\n    public RSAParams() {}\n\n    /**\n     * Creates a new instance with the specified parameters.\n     * \n     * @param publicExponent the public exponent, not {@code null}.\n     * @param privateExponent the private exponent, not{@code null}.\n     * @param modulus the modulus, not {@code null}.\n     */\n    public RSAParams(BigInteger publicExponent, BigInteger privateExponent, BigInteger modulus) {\n        this.publicExponent = publicExponent;\n        this.privateExponent = privateExponent;\n        this.modulus = modulus;\n    }\n\n    /**\n     * Creates a new partially evaluated instance of RSAParams with private exponent and modulus.\n     * \n     * @param d the private exponent, not {@code null}.\n     * @param n the modulus, not {@code null}.\n     * @return the new RSAParams.\n     */\n    public static RSAParams createPrivateKeyParams(BigInteger d, BigInteger n) {\n        return new RSAParams(BigInteger.ZERO, d, n);\n    }\n\n    public BigInteger getPublicExponent() {\n        return publicExponent;\n    }\n\n    public BigInteger getPrivateExponent() {\n        return privateExponent;\n    }\n\n    public BigInteger getModulus() {\n        return modulus;\n    }\n\n    /**\n     * Encode the fields in ASN1 SEQUENCE.\n     * \n     * @returns ASN1 DER-encoded SEQUENCE of fields.\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            ASN1Encodable[] fields = {new ASN1Integer(publicExponent),\n                    new ASN1Integer(privateExponent), new ASN1Integer(modulus)};\n            return new DERSequence(fields).getEncoded();\n        } catch (IOException ex) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            return null;\n        }\n    }\n\n    /**\n     * Decode the ASN1 DER-encode byte array as an RSAKeyPair object.\n     * \n     * @param in Input ASN1 DER-encoded byte array.\n     * @throws ASN1DecodingException when input is not ASN1 SEQUENCE containing 3 ASN1 INTEGERs.\n     */\n    @Override\n    public void readFromBytes(byte[] in) throws ASN1DecodingException {\n        if (publicExponent != null || privateExponent != null || modulus != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        @SuppressWarnings(\"resource\")\n        ASN1InputStream a = new ASN1InputStream(in);\n        ASN1Primitive p;\n        try {\n            p = a.readObject();\n        } catch (IOException ex) {\n            throw new ASN1DecodingException(\"Invalid ASN1\");\n        }\n        if (!(p instanceof ASN1Sequence)) {\n            throw new ASN1DecodingException(\"Input not ASN1 SEQUENCE\");\n        }\n        ASN1Encodable[] fields = ((ASN1Sequence) p).toArray();\n\n        if (fields.length != 3) {\n            throw new ASN1DecodingException(\"Expected 3 elements, got \" + fields.length);\n        }\n        for (int i = 0; i < fields.length; i++) {\n            if (!(fields[i] instanceof ASN1Integer)) {\n                throw new ASN1DecodingException(\"Sequence field not an integer\");\n            }\n        }\n        publicExponent = ((ASN1Integer) fields[0]).getValue();\n        privateExponent = ((ASN1Integer) fields[1]).getValue();\n        modulus = ((ASN1Integer) fields[2]).getValue();\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/Sequence.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport ee.ivxv.common.asn1.ASN1Decodable;\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport java.math.BigInteger;\nimport java.io.IOException;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.DERSequence;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.DERGeneralString;\n\n/**\n * Sequence can be used to encode and decode a series of values.\n *\n */\npublic class Sequence implements ee.ivxv.common.asn1.ASN1Encodable, ASN1Decodable {\n    private ASN1Encodable[] fields;\n\n    /**\n     * Constructor for uninitialized instance for later decoding.\n     */\n    public Sequence() {}\n\n    /**\n     * Constructor for a sequence of integers.\n     * \n     * @param ints\n     */\n    public Sequence(BigInteger... ints) {\n        fields = new ASN1Integer[ints.length];\n        for (int i = 0; i < ints.length; i++) {\n            fields[i] = new ASN1Integer(ints[i]);\n        }\n    }\n\n    /**\n     * Constructor for a sequence of ASN1-encoded byte arrays.\n     * \n     * @param encoded\n     */\n    public Sequence(byte[]... encoded) {\n        fields = new ASN1Primitive[encoded.length];\n        for (int i = 0; i < encoded.length; i++) {\n            try {\n                fields[i] = ASN1Primitive.fromByteArray(encoded[i]);\n            } catch (IOException e) {\n                // if exception is thrown here then this is a programming error.\n                // these error must be fixed during development phase and thus they\n                // are not thrown in production. so, silence IOExceptions\n            }\n        }\n    }\n\n    /**\n     * Constructor for a sequence of strings. Every string is encoded as GeneralString.\n     * \n     * @param strs\n     */\n    public Sequence(String... strs) {\n        fields = new DERGeneralString[strs.length];\n        for (int i = 0; i < strs.length; i++) {\n            fields[i] = new DERGeneralString(strs[i]);\n        }\n    }\n\n    /**\n     * Encode the fields in ASN1 SEQUENCE.\n     * \n     * @returns ASN1 DER-encoded SEQUENCE of fields.\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            return new DERSequence(fields).getEncoded();\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            return null;\n        }\n    }\n\n    /**\n     * Decode the ASN1 DER-encode byte array as a Sequence.\n     * \n     * @param in Input ASN1 DER-encoded byte array.\n     * @throws ASN1DecodingException when input is not ASN1 SEQUENCE.\n     */\n    @Override\n    public void readFromBytes(byte[] in) throws ASN1DecodingException {\n        if (fields != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        @SuppressWarnings(\"resource\")\n        ASN1InputStream a = new ASN1InputStream(in);\n        ASN1Primitive p;\n        try {\n            p = a.readObject();\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Invalid ASN1\");\n        }\n        if (!(p instanceof ASN1Sequence)) {\n            throw new ASN1DecodingException(\"Input not ASN1 SEQUENCE\");\n        }\n        fields = ((ASN1Sequence) p).toArray();\n    }\n\n    /**\n     * Get the sequence elements as byte arrays.\n     * \n     * @return Sequence elements as byte arrays.\n     * @throws ASN1DecodingException When sequence elements are not byte arrays.\n     */\n    public byte[][] getBytes() throws ASN1DecodingException {\n        if (fields == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        byte[][] ret = new byte[fields.length][];\n        try {\n            for (int i = 0; i < fields.length; i++) {\n                ret[i] = ((ASN1Primitive) fields[i]).getEncoded(\"DER\");\n            }\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Sequence field incorrectly encoded\");\n        }\n        return ret;\n    }\n\n    /**\n     * Get the sequence elements as integers.\n     * \n     * @return Sequence elements as integers.\n     * @throws ASN1DecodingException When sequence elements are not integers.\n     */\n    public BigInteger[] getIntegers() throws ASN1DecodingException {\n        if (fields == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        BigInteger[] ret = new BigInteger[fields.length];\n        for (int i = 0; i < fields.length; i++) {\n            if (!(fields[i] instanceof ASN1Integer)) {\n                throw new ASN1DecodingException(\"Sequence field not an integer\");\n            }\n            ret[i] = ((ASN1Integer) fields[i]).getValue();\n        }\n        return ret;\n    }\n\n    /**\n     * Get the sequence elements as strings.\n     * \n     * @return Sequence elements as strings.\n     * @throws ASN1DecodingException When sequence elements are not strings.\n     */\n    public String[] getStrings() throws ASN1DecodingException {\n        if (fields == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        String[] ret = new String[fields.length];\n        for (int i = 0; i < fields.length; i++) {\n            if (!(fields[i] instanceof DERGeneralString)) {\n                throw new ASN1DecodingException(\"Sequence field not a string\");\n            }\n            ret[i] = ((DERGeneralString) fields[i]).getString();\n        }\n        return ret;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/asn1/X509PublicKey.java",
    "content": "package ee.ivxv.common.asn1;\n\nimport ee.ivxv.common.asn1.ASN1Decodable;\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Sequence;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\nimport org.bouncycastle.asn1.ASN1InputStream;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport java.io.IOException;\n\n/**\n * X509PublicKey is a class for encoding and decoding X509 SubjectPublicKeyInfo structure.\n *\n */\npublic class X509PublicKey implements ee.ivxv.common.asn1.ASN1Encodable, ASN1Decodable {\n    private ASN1ObjectIdentifier oid;\n    public byte[] key;\n    public byte[] params;\n\n    /**\n     * Initialize empty X509PublicKey for decoding from encoded data.\n     */\n    public X509PublicKey() {}\n\n    /**\n     * Initialize X509PublicKey using encryption scheme OID, raw key bytes and encoded parameters.\n     * If the parameters are not set, then it may be null.\n     * \n     * @param oid OID of the encryption scheme, given as a String.\n     * @param key Raw public key bytes.\n     * @param params ASN1 DER-encoded key parameters.\n     */\n    public X509PublicKey(String oid, byte[] key, byte[] params) {\n        this.oid = new ASN1ObjectIdentifier(oid);\n        this.key = new Sequence(key).encode();\n        this.params = params;\n    }\n\n    /**\n     * Encode the X509PublicKey into following ASN1 structure:\n     * \n     * <pre>\n     * {@code\n     *  SubjectPublicKeyInfo ::= SEQUENCE {\n     *    algorithm   AlgorithmIdentifier,\n     *    publicKey   BIT STRING\n     *    }\n     *    \n     *  AlgorithmIdentifier  ::=  SEQUENCE {\n     *    algorithm   OBJECT IDENTIFIER,\n     *    parameters  ANY DEFINED BY algorithm OPTIONAL\n     *    }\n     * }\n     * </pre>\n     */\n    @Override\n    public byte[] encode() {\n        try {\n            if (params != null) {\n                return new SubjectPublicKeyInfo(\n                        new AlgorithmIdentifier(oid, ASN1Primitive.fromByteArray(params)), key)\n                                .getEncoded(\"DER\");\n            } else {\n                return new SubjectPublicKeyInfo(new AlgorithmIdentifier(oid), key)\n                        .getEncoded(\"DER\");\n            }\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production. so, silence IOExceptions\n            // the exception thrown here from are:\n            // 1. ASN1Primitive.fromByteArray(params)),\n            // 2. return new PrivateKeyInfo(\n            // 3. ).getEncoded(\"DER\");\n            return null;\n        }\n    }\n\n    /**\n     * Decode input into uninitialized X509PublicKey instance. The input must be formatted according\n     * to the structure defined in {@link #encode()}.\n     * \n     * @param in ASN1 DER-encoded byte array to be decoded.\n     * @throws ASN1DecodingException When instance is already initialized or input does not\n     *         correspond to the expected structure.\n     */\n    @Override\n    public void readFromBytes(byte[] in) throws ASN1DecodingException {\n        if (oid != null || key != null || params != null) {\n            throw new ASN1DecodingException(\"Instance already initialized\");\n        }\n        @SuppressWarnings(\"resource\")\n        ASN1InputStream a = new ASN1InputStream(in);\n        ASN1Primitive p;\n        try {\n            p = a.readObject();\n        } catch (IOException e) {\n            throw new ASN1DecodingException(\"Invalid ASN1\");\n        }\n        SubjectPublicKeyInfo spki;\n        spki = SubjectPublicKeyInfo.getInstance(p);\n        oid = spki.getAlgorithm().getAlgorithm();\n        key = spki.getPublicKeyData().getBytes();\n        ASN1Encodable k;\n        k = spki.getAlgorithm().getParameters();\n        if (k != null) {\n            try {\n                params = ((ASN1Primitive) k).getEncoded(\"DER\");\n            } catch (IOException e) {\n                throw new ASN1DecodingException(\"Input not X509PrivateKey\");\n            }\n        }\n    }\n\n    /**\n     * Get the OID of the encryption scheme.\n     * \n     * @return OID\n     * @throws ASN1DecodingException When OID value is not set.\n     */\n    public String getOID() throws ASN1DecodingException {\n        if (oid == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        return oid.toString();\n    }\n\n    /**\n     * Get the raw key bytes.\n     * \n     * @return Key as raw bytes.\n     * @throws ASN1DecodingException When key is not set..\n     */\n    public byte[] getKey() throws ASN1DecodingException {\n        if (key == null) {\n            throw new ASN1DecodingException(\"Instance not initialized\");\n        }\n        Sequence keys = new Sequence();\n        try {\n            keys.readFromBytes(this.key);\n        } catch (ASN1DecodingException ex) {\n            // suppress this exception as the key is constructed during\n            // instance initialization\n        }\n        try {\n            return keys.getBytes()[0];\n        } catch (ASN1DecodingException ex) {\n            // suppress this exception as the key is constructed during\n            // instance initialization\n            return null;\n        }\n    }\n\n    /**\n     * Get the key parameters. Key parameters may be null if parameters are not needed or set.\n     * \n     * @return Encoded key parameters.\n     */\n    public byte[] getParameters() {\n        // params can be null\n        return params;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/App.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.util.NameHolder;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Command line application. An application has one or more tools that define applications\n * functionality.\n */\npublic abstract class App<T extends AppContext<?>> {\n\n    public final NameHolder name;\n    public final Map<String, Tool<T, ?>> tools;\n\n    public App(NameHolder name, List<Tool<T, ?>> tools) {\n        this.name = name;\n        this.tools = createToolsMap(tools);\n    }\n\n    public abstract T createContext(InitialContext ctx, Conf conf, CommonArgs args);\n\n    private Map<String, Tool<T, ?>> createToolsMap(List<Tool<T, ?>> toolList) {\n        Map<String, Tool<T, ?>> tmpTools = new LinkedHashMap<>();\n\n        for (Tool<T, ?> tool : toolList) {\n            tmpTools.put(tool.name.getName(), tool);\n        }\n\n        return Collections.unmodifiableMap(tmpTools);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/AppContext.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.smartcard.CardService;\n\n/**\n * AppContext is a collection of all dependencies an application or it's tools may need during their\n * work. Instances of AppContext are created after loading the configuration. Instances of\n * AppContext can be application-specific.\n * \n * @param <T> The exact type of configuration, to support application-specific configuration.\n */\npublic class AppContext<T extends Conf> {\n\n    public final InitialContext i;\n    public final T conf;\n    public final CommonArgs args;\n    public final Reporter reporter;\n    public final CardService card;\n    public final ContainerReader container;\n    public final BboxHelper bbox;\n\n    public AppContext(InitialContext i, T conf, CommonArgs args) {\n        this(i, conf, args, ContextFactory.get().getReporter(i.i18n),\n                ContextFactory.get().getCard(i.console, i.i18n),\n                ContextFactory.get().getContainer(conf, args.ct.value()));\n    }\n\n    public AppContext(InitialContext i, T conf, CommonArgs args, Reporter reporter,\n            CardService card, ContainerReader container) {\n        this(i, conf, args, reporter, card, container,\n                ContextFactory.get().getBbox(conf, container));\n    }\n\n    public AppContext(InitialContext i, T conf, CommonArgs args, Reporter reporter,\n            CardService card, ContainerReader container, BboxHelper bbox) {\n        this.i = i;\n        this.conf = conf;\n        this.args = args;\n        this.reporter = reporter;\n        this.card = card;\n        this.container = container;\n        this.bbox = bbox;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/AppHelper.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.util.I18nConsole;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.function.BiConsumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nclass AppHelper {\n\n    private static final String LIST_EL = \"-\";\n    private static final String LIST_SHIFT = \" \";\n\n    private final App<?> app;\n    private final CommonArgs cargs;\n    private final I18nConsole console;\n\n    AppHelper(App<?> app, CommonArgs cargs, I18nConsole console) {\n        this.app = app;\n        this.cargs = cargs;\n        this.console = console;\n    }\n\n    static void walk(Args args, int level, BiConsumer<Arg<?>, Integer> consumer) {\n        for (Arg<?> arg : args.args) {\n            consumer.accept(arg, level);\n            if (arg instanceof Arg.Tree) {\n                walk(((Arg.Tree) arg).value, level + 1, consumer);\n            }\n            if (arg instanceof Arg.TreeList) {\n                ((Arg.TreeList<?>) arg).value.forEach(a -> walk(a, level + 1, consumer));\n            }\n        }\n    }\n\n    void showHelp() {\n        console.println(Msg.app, app.name.getName(), app.name);\n        console.println();\n        showUsage();\n        console.println();\n        showTools();\n        console.println();\n        showArgs(cargs);\n    }\n\n    void showToolHelp(Tool<?, ?> tool) {\n        console.println(Msg.app, app.name.getName(), app.name);\n        console.println(Msg.tool, tool.name.getName(), tool.name);\n        console.println();\n        showUsage();\n        console.println();\n        showArgs(cargs);\n        console.println();\n        showParams(tool.createArgs());\n    }\n\n    private void showUsage() {\n        console.println(Msg.usage);\n        Object[] args = cargs.args.stream().filter(a -> a != cargs.help).map(this::formatUsageArg)\n                .toArray();\n        console.println(Msg.row_indent,\n                Arrays.asList(app.name.getName(), Msg.tool_placeholder, args));\n\n        Object helpArg = Optional.ofNullable(Option.formatShortName(cargs.help)).map(\n                sn -> (Object) new Message(Msg.usage_arg_names, sn, Option.formatName(cargs.help)))\n                .orElse(Option.formatName(cargs.help));\n\n        console.println(Msg.row_indent,\n                Arrays.asList(app.name.getName(), Msg.tool_placeholder, helpArg));\n        console.println(Msg.row_indent, Arrays.asList(app.name.getName(), helpArg));\n    }\n\n    private Object formatUsageArg(Arg<?> arg) {\n        boolean isFlag = arg.value != null && (arg.value() instanceof Boolean);\n        Object a = isFlag ? Option.formatName(arg)\n                : new Message(Msg.usage_arg_w_value, Option.formatName(arg), arg.name.getName());\n        return arg.isRequired() ? a : new Message(Msg.usage_arg_optional, a);\n    }\n\n    private void showTools() {\n        console.println(Msg.tools);\n        for (Tool<?, ?> tool : app.tools.values()) {\n            console.println(Msg.row_indent,\n                    new Message(Msg.tool_row, tool.name.getName(), tool.name));\n        }\n    }\n\n    private void showArgs(Args args) {\n        console.println(Msg.args);\n        walk(args, 0, (arg, level) -> {\n            String names = Stream.of(Option.formatShortName(arg), Option.formatName(arg))\n                    .filter(s -> !s.isEmpty()).collect(Collectors.joining(\" \"));\n            Object name = required(Msg.param_name_required, names, arg.isRequired());\n            Message indented = indent(level, Msg.arg_row, name, arg.name);\n            console.println(indented);\n            if (arg instanceof Arg.TreeList) {\n                // Add a single child node for which to show arguments\n                ((Arg.TreeList<?>) arg).addNew();\n            }\n        });\n    }\n\n    private void showParams(Args args) {\n        console.println(Msg.params);\n        AtomicBoolean isFirstTreeElem = new AtomicBoolean();\n        // In case of YAML list, the additional '- ' is added to indentation. Keep track of those.\n        List<Integer> levelShift = new ArrayList<>();\n\n        walk(args, 0, (arg, level) -> {\n            // Discard the unused tail of 'levelShift' and inherit the value from parent level\n            levelShift.subList(level, levelShift.size()).clear();\n            levelShift.add(level > 0 ? levelShift.get(level - 1) : 0);\n            // Consider that a \" \" is added between array elements by the i18n service.\n            String shift = String.join(\" \", Collections.nCopies(levelShift.get(level), LIST_SHIFT));\n\n            Object name = required(Msg.param_name_required, arg.name.getName(), arg.isRequired());\n            Message paramRow = new Message(Msg.param_row, name, arg.name);\n            Message indented;\n            if (isFirstTreeElem.getAndSet(false)) {\n                indented = indent(level, new Object[] {shift, LIST_EL, paramRow});\n                // Other elements of the same level are shifted by LIST_EL\n                levelShift.set(level - 1, levelShift.get(level) + 1);\n            } else {\n                indented = indent(level, new Object[] {shift, paramRow});\n            }\n\n            console.println(indented);\n\n            if (arg instanceof Arg.MultiValueArg<?>) {\n                // Show a single element placeholder\n                console.println(\n                        indent(level + 1, new Object[] {shift, LIST_EL, Msg.value_placeholder}));\n            }\n            if (arg instanceof Arg.TreeList) {\n                // Add a single child node for which to show parameters\n                ((Arg.TreeList<?>) arg).addNew();\n                isFirstTreeElem.set(true);\n            }\n        });\n    }\n\n    private Message indent(int level, Enum<?> key, Object... args) {\n        return indent(level, new Message(key, args));\n    }\n\n    private Message indent(int level, Object arg) {\n        Object indentedArg = arg;\n        for (int i = 0; i++ < level; indentedArg = new Message(Msg.row_indent, indentedArg));\n        return new Message(Msg.row_indent, indentedArg);\n    }\n\n    private Object required(Enum<?> key, String name, boolean isRequired) {\n        return isRequired ? new Message(key, name) : name;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/AppRunner.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.conf.ConfLoader;\nimport ee.ivxv.common.conf.ConfVerifier;\nimport ee.ivxv.common.conf.LocaleConfLoader;\nimport ee.ivxv.common.service.container.InvalidContainerException;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.ContainerHelper;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.log.PerformanceLog;\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.MDC;\n\n/**\n * General logic for running IVXV command line application - taking care of common aspects, like\n * checking arguments, showing help, handling errors, generating session id for log, etc.\n * \n * <p>\n * The expected execution pattern is one of:\n * \n * <pre>\n * $ &lt;app&gt; [-h|--help]\n * $ &lt;app&gt; &lt;tool&gt; [-h|--help]\n * $ &lt;app&gt; &lt;tool&gt; [tool-arguments]\n * </pre>\n * \n * @param <T> The type of the application context of the instance.\n */\npublic class AppRunner<T extends AppContext<?>> {\n\n    static final Logger log = LoggerFactory.getLogger(AppRunner.class);\n\n    private static final String SESSION_ID = \"SID\";\n\n    private final InitialContext ictx;\n    final App<T> app;\n    private final CommonArgs cargs;\n    final I18nConsole console;\n    final AppHelper appHelper;\n\n    // Error handler - different handling for application and tool level errors\n    private ErrorHandler errorHandler;\n    // The parsed command line, once arguments are provided\n    private CommandLine cl;\n    private final List<Runnable> finalizers = new ArrayList<>();\n\n    /**\n     * Calls the 2-parameter constructor with {@code ContextFactory.createInitialContext()}.\n     * \n     * @param app The application to run.\n     */\n    public AppRunner(App<T> app) {\n        this(ContextFactory.get().createInitialContext(), app);\n    }\n\n    /**\n     * @param ctx The <i>initial</i> context to be used until the configuration is loaded and new\n     *        application context created from it.\n     * @param app The application to run.\n     */\n    public AppRunner(InitialContext ictx, App<T> app) {\n        this.ictx = ictx;\n        this.app = app;\n        cargs = new CommonArgs();\n        console = new I18nConsole(ictx.console, ictx.i18n);\n        appHelper = new AppHelper(app, cargs, console);\n\n        errorHandler = new AppErrorHandler();\n        finalizers.add(() -> ictx.console.shutdown());\n    }\n\n    public boolean run(String[] args) {\n        boolean result = false;\n        long t = System.currentTimeMillis();\n\n        // For performance reasons we want to ensure that the application is run with\n        // 64-bit JVM. For OpenJDK and Oracle Java the property sun.arch.data.model gives\n        // the architecture of the JVM quite reliably. Other JVMs may, but do not have to\n        // have the property set.\n        // We also want to have OpenJDK or Oracle Java as we are using javax.smartcardio,\n        // which is not widely implemented (e.g.\n        // https://android.googlesource.com/platform/libcore/+/jb-mr2-release/expectations/brokentests.txt#522)\n\n        String data_model = System.getProperty(\"sun.arch.data.model\", \"N/A\");\n        switch(data_model) {\n            case \"N/A\":\n                // unknown Java runtime\n                console.println(Msg.w_unknown_java_runtime);\n                break;\n            case \"64\":\n                // supported model\n                break;\n            default:\n                // unknown data model\n                console.println(Msg.w_java_data_model);\n                break;\n        }\n\n        try {\n            MDC.put(SESSION_ID, ictx.sessionId);\n            PerformanceLog.log.info(\"STARTING application '{}'\", app.name.getName());\n            LocaleConfLoader.load(ictx.locale);\n\n            if (ContextFactory.get().isTestMode()) {\n                console.println(Msg.w_test_mode);\n            }\n\n            cl = CommandLine.parse(args);\n\n            log.info(\"Executing: {}\", Stream.of(args).collect(Collectors.joining(\" \")));\n\n            result = runInternal();\n        } catch (InvalidContainerException e) {\n            errorHandler.handleInvalidContainerException(e);\n        } catch (ParseException e) {\n            errorHandler.handleParseException(e);\n        } catch (MessageException e) {\n            errorHandler.handleMessageException(e);\n        } catch (Throwable e) {\n            // Safety-net exception handling\n            errorHandler.handleThrowable(e);\n        } finally {\n            Msg msg = result ? Msg.app_result_success : Msg.app_result_failure;\n            console.println(msg, app.name);\n            try {\n                finalizers.forEach(f -> f.run());\n            } catch (Throwable e) {\n                errorHandler.handleThrowable(e);\n            }\n            PerformanceLog.log.info(\"FINISHED application '{}', TIME: {} ms, SUCCESS: {}\",\n                    app.name.getName(), (System.currentTimeMillis() - t), result);\n            MDC.remove(SESSION_ID);\n        }\n        return result;\n    }\n\n    private boolean runInternal() throws Exception {\n        cl.set(cargs);\n\n        log.debug(\"Application '{}' arguments:\", app.name.getName());\n        logArgValues(cargs);\n\n        // Set language, if requested\n        if (cargs.lang.isSet()) {\n            ictx.locale.setLocale(Locale.of(cargs.lang.value()));\n        }\n\n        // Show application help, if requested (tool not selected)\n        if (cl.getCommands().isEmpty() && cargs.help.value()) {\n            appHelper.showHelp();\n            return true;\n        }\n\n        // Proceed with selected tool\n        Tool<T, ?> tool = selectTool();\n\n        // Show tool help, if requested\n        if (cargs.help.value()) {\n            appHelper.showToolHelp(tool);\n            return true;\n        }\n\n        PerformanceLog.log.info(\"Starting tool '{}', app-threads: {}, container-threads: {}\",\n                tool.name.getName(), cargs.threads.value(), cargs.ct.value());\n\n        return runTool(tool);\n    }\n\n    private Tool<T, ?> selectTool() {\n        List<String> commands = cl.getCommands();\n\n        if (commands.isEmpty()) {\n            throw new ParseException(Msg.e_tool_missing);\n        }\n\n        if (commands.size() > 1) {\n            throw new ParseException(Msg.e_multiple_tools, commands);\n        }\n\n        String first = commands.get(0);\n        if (!app.tools.containsKey(first)) {\n            throw new ParseException(Msg.e_unknown_tool, first);\n        }\n\n        return app.tools.get(first);\n    }\n\n    private <U extends Tool<T, A>, A extends Args> boolean runTool(U tool) throws Exception {\n        A args = tool.createArgs();\n\n        errorHandler = new ToolErrorHandler<>(tool);\n\n        cl.set(args);\n\n        // Command line is processed - check unknown options\n        checkUnknownOptions();\n\n        if (!cargs.isValid()) {\n            cargs.validate().getErrors().forEach(e -> console.println(e.error));\n            throw new ParseException(Msg.e_common_args_invalid);\n        }\n\n        // Create application-specific application context and run the tool\n        T ctx = createContext();\n        console.println();\n\n        readArgsFromYaml(ctx, tool.name.getName(), args);\n\n        log.debug(\"Tool '{}' arguments:\", tool.name.getName());\n        logArgValues(args);\n\n        if (!args.isValid()) {\n            args.validate().getErrors().forEach(e -> console.println(e.error));\n            throw new ParseException(Msg.e_tool_args_invalid);\n        }\n\n        return tool.prepare(ctx).run(args);\n    }\n\n    private void checkUnknownOptions() {\n        boolean hasUnused = false;\n\n        for (Option o : cl.getUnusedOptions()) {\n            log.debug(\"Unknown option '{}'\", o.getName());\n            console.println(Msg.e_unknown_arg, o.getName());\n            hasUnused = true;\n        }\n\n        if (hasUnused) {\n            throw new ParseException(Msg.e_unknown_args_present);\n        }\n    }\n\n    private T createContext() {\n        // Load conf (conf as a mandatory field is set)\n        Conf conf = ConfLoader.load(cargs.conf.value(), console);\n\n        ConfVerifier.verify(conf);\n\n        // Create application-specific application context with the loaded conf\n        T ctx = app.createContext(ictx, conf, cargs);\n\n        ConfVerifier.verifySignature(ctx, cargs.conf.value());\n\n        // Must shut down the container service, otherwise the application may hang\n        finalizers.add(() -> ctx.container.shutdown());\n\n        return ctx;\n    }\n\n    private void readArgsFromYaml(AppContext<?> ctx, String toolName, Args args) throws Exception {\n        if (!cargs.params.isSet()) {\n            return;\n        }\n        Path path = cargs.params.value();\n        console.println(M.m_loading_params, path);\n        ctx.container.requireContainer(path);\n        ContainerHelper ch = new ContainerHelper(console, ctx.container.read(path.toString()));\n        try (InputStream in = ch.getSingleFileAndReport(M.m_params_arg_for_cont).getStream()) {\n            YamlData yaml = YamlData.parse(path, in);\n            yaml.set(toolName, args);\n        }\n    }\n\n    private void logArgValues(Args args) {\n        AppHelper.walk(args, 1, (arg, level) -> {\n            String indent = String.join(\"\", Collections.nCopies(level, \"  \"));\n            if ((arg instanceof Arg.Tree) || (arg instanceof Arg.TreeList<?>)) {\n                log.debug(\"{}{}:\", indent, arg.name.getName());\n            } else {\n                log.debug(\"{}{} = {}\", indent, arg.name.getName(), arg.value);\n            }\n        });\n    }\n\n    /**\n     * Interface for local error handler - either application level or tool level.\n     */\n    interface ErrorHandler {\n        void handleInvalidContainerException(InvalidContainerException e);\n\n        void handleParseException(ParseException e);\n\n        void handleMessageException(MessageException e);\n\n        void handleThrowable(Throwable e);\n    }\n\n    class AppErrorHandler implements ErrorHandler {\n\n        private final String appName;\n\n        public AppErrorHandler() {\n            appName = app.name.getName();\n        }\n\n        @Override\n        public void handleInvalidContainerException(InvalidContainerException e) {\n            log.warn(\"A container validation error occurred running app '{}'\", appName, e);\n            console.println(M.e_invalid_container, e.path);\n        }\n\n        @Override\n        public void handleParseException(ParseException e) {\n            log.debug(\"Parsing error occurred running app '{}'\", appName, e);\n            console.println(e.getKey(), e.getArgs());\n            console.println();\n            appHelper.showHelp();\n        }\n\n        @Override\n        public void handleMessageException(MessageException e) {\n            log.warn(\"An error occurred running app '{}'\", appName, e);\n            console.println(e.getKey(), e.getArgs());\n        }\n\n        @Override\n        public void handleThrowable(Throwable e) {\n            log.warn(\"An error occurred running app '{}'\", appName, e);\n            console.println(Msg.e_app_error, appName, e);\n        }\n    }\n\n    class ToolErrorHandler<U extends Tool<T, ?>> implements ErrorHandler {\n\n        private final U tool;\n        private final String appName;\n        private final String toolName;\n\n        public ToolErrorHandler(U tool) {\n            this.tool = tool;\n            appName = app.name.getName();\n            toolName = tool.name.getName();\n        }\n\n        @Override\n        public void handleInvalidContainerException(InvalidContainerException e) {\n            log.warn(\"A container validation error occurred running app '{}'\", appName, e);\n            console.println(M.e_invalid_container, e.path);\n        }\n\n        @Override\n        public void handleParseException(ParseException e) {\n            log.debug(\"Parsing error occurred running app '{}' tool '{}'\", appName, toolName, e);\n            console.println(e.getKey(), e.getArgs());\n            console.println();\n            appHelper.showToolHelp(tool);\n        }\n\n        @Override\n        public void handleMessageException(MessageException e) {\n            log.warn(\"An error occurred running app '{}' tool '{}'\", appName, toolName, e);\n            console.println(e.getKey(), e.getArgs());\n        }\n\n        @Override\n        public void handleThrowable(Throwable e) {\n            log.warn(\"An error occurred running app '{}' tool '{}'\", appName, toolName, e);\n            console.println(Msg.e_tool_error, toolName, e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/Arg.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.crypto.CryptoUtil;\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.util.NameHolder;\nimport ee.ivxv.common.util.Util;\nimport java.math.BigInteger;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.time.LocalTime;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeParseException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Instances of <code>Arg</code> are the building blocks of the application (and tool) arguments'\n * data model. The argument values can be acquired from either command line ({@link Option}) or a\n * configuration file.\n *\n * @param <T> The type of the argument value.\n */\npublic abstract class Arg<T> {\n\n    static final Logger log = LoggerFactory.getLogger(Arg.class);\n\n    /*\n     * Re-used parser instances. Since parsers are stateless, there is no need for creating new one\n     * for each argument.\n     */\n    private static final Parser<String> STRING_PARSER = new StringParser();\n    private static final Parser<Boolean> BOOLEAN_PARSER = new BooleanParser();\n    private static final Parser<byte[]> BYTE_ARRAY_PARSER = new ByteArrayParser();\n    private static final Parser<Integer> INT_PARSER = new IntParser();\n    private static final Parser<Long> LONG_PARSER = new LongParser();\n    private static final Parser<BigInteger> BIGINT_PARSER = new BigIntParser();\n    private static final Parser<Instant> INSTANT_PARSER = new InstantParser();\n    private static final Parser<LocalDate> LOCAL_DATE_PARSER = new LocalDateParser();\n    private static final Parser<Path> PATH_PARSER = new PathParser();\n    static final Parser<Path> EXISTING_FILE_PARSER = new PathParser(true, false);\n\n    static final Resolver IDENTITY_RESOLVER = new Resolver();\n\n    public final NameHolder name;\n    T value;\n    private boolean isRequired = true;\n    private T def;\n\n    public Arg(NameHolder name) {\n        this.name = name;\n    }\n\n    // ******************************** STATIC CONVENIENCE METHODS ****************************** //\n\n    public static Arg<String> aString(NameHolder n) {\n        return new SingleValueArg<>(n, STRING_PARSER);\n    }\n\n    public static Arg<Boolean> aFlag(NameHolder n) {\n        return new SingleValueArg<Boolean>(n, BOOLEAN_PARSER) {\n\n            @Override\n            public void parse(List<String> v, Resolver r) throws ParseException {\n                // Allow empty argument list, which means 'true'\n                if (v.isEmpty()) {\n                    value = Boolean.TRUE;\n                    return;\n                }\n                super.parse(v, r);\n            }\n\n        }.setDefault(Boolean.FALSE).setOptional();\n\n    }\n\n    public static Arg<byte[]> aByteArray(NameHolder n) {\n        return new SingleValueArg<>(n, BYTE_ARRAY_PARSER);\n    }\n\n    public static Arg<Integer> anInt(NameHolder n) {\n        return new SingleValueArg<>(n, INT_PARSER);\n    }\n\n    public static Arg<Long> aLong(NameHolder n) {\n        return new SingleValueArg<>(n, LONG_PARSER);\n    }\n\n    public static Arg<BigInteger> aBigInt(NameHolder n) {\n        return new SingleValueArg<>(n, BIGINT_PARSER);\n    }\n\n    public static Arg<Instant> anInstant(NameHolder n) {\n        return new SingleValueArg<>(n, INSTANT_PARSER);\n    }\n\n    public static Arg<LocalDate> aLocalDate(NameHolder n) {\n        return new SingleValueArg<>(n, LOCAL_DATE_PARSER);\n    }\n\n    public static Arg<Path> aPath(NameHolder n) {\n        return new SingleValueArg<>(n, PATH_PARSER);\n    }\n\n    public static Arg<Path> aPath(NameHolder n, Boolean exists, Boolean isDir) {\n        return new SingleValueArg<>(n, new PathParser(exists, isDir));\n    }\n\n    public static Arg<PublicKeyHolder> aPublicKey(NameHolder n, ASN1ObjectIdentifier alg) {\n        return new SingleValueArg<>(n, new PublicKeyParser(alg));\n    }\n\n    /**\n     * Creates an argument instance for a value from the specified choices, which is found by\n     * matching the result of <tt>toString()</tt> of a choice with the string being parsed.\n     *\n     * @param n\n     * @param values\n     * @return\n     */\n    @SafeVarargs\n    public static <U> Arg<U> aChoice(NameHolder n, U... values) {\n        return new SingleValueArg<>(n, new ChoiceParser<>(values));\n    }\n\n    @SafeVarargs\n    public static <U> Arg<U> aChoice(NameHolder n, Function<String, U> mapper, U... values) {\n        return new SingleValueArg<>(n, new ChoiceParser<>(mapper, values));\n    }\n\n    public static Arg<List<String>> listOfStrings(NameHolder n) {\n        return new MultiValueArg<>(n, STRING_PARSER);\n    }\n\n    public static Arg<List<Boolean>> listOfFlags(NameHolder n) {\n        return new MultiValueArg<>(n, BOOLEAN_PARSER);\n    }\n\n    public static Arg<List<byte[]>> listOfByteArrays(NameHolder n) {\n        return new MultiValueArg<>(n, BYTE_ARRAY_PARSER);\n    }\n\n    public static Arg<List<Integer>> listOfInts(NameHolder n) {\n        return new MultiValueArg<>(n, INT_PARSER);\n    }\n\n    public static Arg<List<BigInteger>> listOfBigInts(NameHolder n) {\n        return new MultiValueArg<>(n, BIGINT_PARSER);\n    }\n\n    public static Arg<List<Instant>> listOfInstants(NameHolder n) {\n        return new MultiValueArg<>(n, INSTANT_PARSER);\n    }\n\n    public static Arg<List<Path>> listOfPaths(NameHolder n) {\n        return new MultiValueArg<>(n, new PathParser());\n    }\n\n    public static Arg<List<Path>> listOfPaths(NameHolder n, Boolean exists, Boolean isDir) {\n        return new MultiValueArg<>(n, new PathParser(exists, isDir));\n    }\n\n    public static Arg<List<PublicKeyHolder>> listOfPublicKeys(NameHolder n,\n            ASN1ObjectIdentifier alg) {\n        return new MultiValueArg<>(n, new PublicKeyParser(alg));\n    }\n\n    /**\n     * Creates an argument instance for a list of values from the specified choices, which are found\n     * by matching the result of <tt>toString()</tt> of a choice with the string being parsed.\n     *\n     * @param n\n     * @param values\n     * @return\n     */\n    @SafeVarargs\n    public static <U> Arg<List<U>> listOfChoices(NameHolder n, U... values) {\n        return new MultiValueArg<>(n, new ChoiceParser<>(values));\n    }\n\n    @SafeVarargs\n    public static <U> Arg<List<U>> listOfChoices(NameHolder n, Function<String, U> mapper,\n            U... values) {\n        return new MultiValueArg<>(n, new ChoiceParser<>(mapper, values));\n    }\n\n    // ******************************** INSTANCE METHODS ******************************** //\n\n    /**\n     * Sets the argument optional. By default arguments are required.\n     *\n     * @return This\n     */\n    public Arg<T> setOptional() {\n        isRequired = false;\n        return this;\n    }\n\n    /**\n     * Set the default value of the argument. Note that argument with a not-null default has always\n     * a value.\n     *\n     * @return This\n     */\n    public Arg<T> setDefault(T v) {\n        def = v;\n        return this;\n    }\n\n    /**\n     * @return Whether the argument is required in the parent context - parent is set or required.\n     */\n    public boolean isRequired() {\n        return isRequired;\n    }\n\n    /**\n     * @param s Not-null list of strings\n     * @throws ParseException\n     */\n    public final void parse(List<String> s) throws ParseException {\n        parse(s, IDENTITY_RESOLVER);\n    }\n\n    public abstract void parse(List<String> s, Resolver r) throws ParseException;\n\n    /**\n     * @return The value of the argument: either the parsed or the default value.\n     */\n    public T value() {\n        return value != null ? value : def;\n    }\n\n    /**\n     * @return Whether the argument has any value.\n     */\n    public boolean isSet() {\n        return value() != null;\n    }\n\n    /**\n     * @return Whether the argument value is valid. Only checks that required argument is set.\n     */\n    public final boolean isValid() {\n        return validate().isValid();\n    }\n\n    /**\n     * Validates the argument and possibly it's children and returns the validation result.\n     *\n     * @return Returns the validation result.\n     */\n    public final ValidationResult validate() {\n        return validate(new ValidationResult());\n    }\n\n    ValidationResult validate(ValidationResult result) {\n        if (isRequired() && !isSet()) {\n            result.addError(this, new Message(Msg.e_arg_required, name.getName()));\n        }\n        return result;\n    }\n\n    // ******************************** IMPLEMENTATIONS ******************************** //\n\n    /**\n     * Argument with single value.\n     *\n     * @param <T> The value type.\n     */\n    public static class SingleValueArg<T> extends Arg<T> {\n\n        private final Parser<T> parser;\n\n        public SingleValueArg(NameHolder name, Parser<T> parser) {\n            super(name);\n            this.parser = parser;\n        }\n\n        @Override\n        public void parse(List<String> v, Resolver r) throws ParseException {\n            if (v.size() != 1) {\n                throw new ParseException(Msg.e_requires_single_value, name.getName(), v);\n            }\n            try {\n                value = parser.parse(v.get(0), r);\n            } catch (Exception e) {\n                throw new ParseException(Msg.e_arg_parse_error, name.getName(), v, e);\n            }\n        }\n    }\n\n    /**\n     * Argument with multiple values, i.e list of values.\n     *\n     * @param <T> The value type.\n     */\n    public static class MultiValueArg<T> extends Arg<List<T>> {\n\n        private final Parser<T> parser;\n\n        public MultiValueArg(NameHolder name, Parser<T> parser) {\n            super(name);\n            this.parser = parser;\n        }\n\n        @Override\n        public void parse(List<String> v, Resolver r) throws ParseException {\n            try {\n                value = v.stream().map(s -> parser.parse(s, r)).collect(Collectors.toList());\n            } catch (Exception e) {\n                throw new ParseException(Msg.e_arg_parse_error, name.getName(), v, e);\n            }\n        }\n\n        @Override\n        public boolean isSet() {\n            return value != null && !value.isEmpty();\n        }\n    }\n\n    /**\n     * Represents hierarchical argument tree. Tree value is set if any of the sub-arguments is set.\n     * If tree is required, it must be set even if all sub-arguments are optional.\n     */\n    public static class Tree extends Arg<Args> {\n\n        private boolean isExclusive;\n\n        public Tree(NameHolder name, Arg<?>... args) {\n            super(name);\n            this.value = new Args(args);\n        }\n\n        /**\n         * If <tt>true</tt> at most one branch can be set. It does not make sense to have tree\n         * exclusive and sub-arguments required.\n         */\n        public boolean isExclusive() {\n            return isExclusive;\n        }\n\n        public Tree setExclusive() {\n            isExclusive = true;\n            return this;\n        }\n\n        @Override\n        public void parse(List<String> v, Resolver r) throws ParseException {\n            if (!v.isEmpty()) {\n                throw new ParseException(Msg.e_value_not_allowed, name.getName(), v);\n            }\n        }\n\n        @Override\n        public boolean isSet() {\n            return value.isSet();\n        }\n\n        @Override\n        public final ValidationResult validate(ValidationResult result) {\n            long setCount = value.args.stream().filter(a -> a.isSet()).count();\n            if (isExclusive && setCount > 1) {\n                result.addError(this,\n                        new Message(Msg.e_requires_single_value, name.getName(), setCount));\n            }\n            super.validate(result);\n            value.validate(isRequired(), result);\n            return result;\n        }\n    }\n\n    /**\n     * TreeList is a arbitrary-size list with structural content, i.e. list of <tt>Args</tt>.\n     *\n     * @param <T> The actual type of <tt>Args</tt> contained in this list.\n     */\n    public static class TreeList<T extends Args> extends Arg<List<T>> {\n\n        private final Supplier<T> factory;\n\n        public TreeList(NameHolder name, Supplier<T> factory) {\n            super(name);\n            this.factory = factory;\n            value = new ArrayList<>();\n        }\n\n        @Override\n        public void parse(List<String> v, Resolver r) throws ParseException {\n            if (!v.isEmpty()) {\n                throw new ParseException(Msg.e_value_not_allowed, name.getName(), v);\n            }\n        }\n\n        @Override\n        public boolean isSet() {\n            return value.stream().anyMatch(Args::isSet);\n        }\n\n        @Override\n        public final ValidationResult validate(ValidationResult result) {\n            super.validate(result);\n            // Use 'true' as 'shouldExist' in Args.validate(), because node obviously exists\n            value.forEach(a -> a.validate(true, result));\n            return result;\n        }\n\n        /**\n         * @return Adds new instance of <tt>T</tt> to the value (list of <tt>T</tt>) and returns it.\n         */\n        public T addNew() {\n            T e = factory.get();\n            value.add(e);\n            return e;\n        }\n    }\n\n    // ******************************** PARSERS ******************************** //\n\n    /**\n     * Parser is an interface for parsing a string into another type.\n     *\n     * @param <T> The target type to parse into.\n     */\n    @FunctionalInterface\n    interface Parser<T> {\n        default T parse(String s) throws ParseException {\n            return parse(s, IDENTITY_RESOLVER);\n        }\n\n        T parse(String s, Resolver r) throws ParseException;\n    }\n\n    static class StringParser implements Parser<String> {\n        @Override\n        public String parse(String s, Resolver r) {\n            return s;\n        }\n    }\n\n    static class BooleanParser implements Parser<Boolean> {\n        static final List<String> TRUE_STRINGS = Arrays.asList(\"true\", \"TRUE\");\n        static final List<String> FALSE_STRINGS = Arrays.asList(\"false\", \"FALSE\");\n\n        @Override\n        public Boolean parse(String s, Resolver r) {\n            if (TRUE_STRINGS.contains(s)) {\n                return Boolean.TRUE;\n            } else if (FALSE_STRINGS.contains(s)) {\n                return Boolean.FALSE;\n            } else {\n                throw new ParseException(Msg.e_invalid_boolean, s);\n            }\n        }\n    }\n\n    static class ByteArrayParser implements Parser<byte[]> {\n        @Override\n        public byte[] parse(String s, Resolver r) {\n            return Util.toBytes(s);\n        }\n    }\n\n    static class IntParser implements Parser<Integer> {\n        @Override\n        public Integer parse(String s, Resolver r) {\n            try {\n                return Integer.parseInt(s);\n            } catch (NumberFormatException e) {\n                throw new ParseException(Msg.e_invalid_int, s);\n            }\n        }\n    }\n\n    static class LongParser implements Parser<Long> {\n        @Override\n        public Long parse(String s, Resolver r) {\n            try {\n                return Long.parseLong(s);\n            } catch (NumberFormatException e) {\n                throw new ParseException(Msg.e_invalid_long, s);\n            }\n        }\n    }\n\n    static class BigIntParser implements Parser<BigInteger> {\n        @Override\n        public BigInteger parse(String s, Resolver r) {\n            try {\n                return new BigInteger(s, 10);\n            } catch (NumberFormatException e) {\n                throw new ParseException(Msg.e_invalid_number, s);\n            }\n        }\n    }\n\n    static class InstantParser implements Parser<Instant> {\n        @Override\n        public Instant parse(String s, Resolver r) {\n            try {\n                // Use OffsetDateTime parser to allow \"+/-zz:zz\" format time zone\n                return OffsetDateTime.parse(s).toInstant();\n            } catch (DateTimeParseException e) {\n                throw new ParseException(Msg.e_invalid_instant, s);\n            }\n        }\n    }\n\n    static class LocalDateParser implements Parser<LocalDate> {\n        @Override\n        public LocalDate parse(String s, Resolver r) {\n            try {\n                return LocalDate.parse(s);\n            } catch (DateTimeParseException e) {\n                // Dates parsed via YAML are reformatted with time and zone offset, so also\n                // check for that format, but ensure that time and offset are both zero.\n                try {\n                    OffsetDateTime full = OffsetDateTime.parse(s);\n                    if (!full.toLocalTime().equals(LocalTime.MIDNIGHT)\n                            || !full.getOffset().equals(ZoneOffset.UTC)) {\n                        throw new ParseException(Msg.e_invalid_local_date, s);\n                    }\n                    return full.toLocalDate();\n                } catch (DateTimeParseException e2) {\n                    throw new ParseException(Msg.e_invalid_local_date, s);\n                }\n            }\n        }\n    }\n\n    static class PathParser implements Parser<Path> {\n        private final Boolean exists;\n        private final Boolean isDir;\n\n        PathParser() {\n            this(null, null);\n        }\n\n        /**\n         * @param exists Must the path exist.\n         * @param isDir If <tt>exists</tt>, must the path be a directory.\n         */\n        PathParser(Boolean exists, Boolean isDir) {\n            this.exists = exists;\n            this.isDir = exists != null && exists ? isDir : null;\n        }\n\n        @Override\n        public Path parse(String s, Resolver r) {\n            Path p = r.resolve(Paths.get(s));\n\n            if (exists != null && Files.exists(p) != exists) {\n                Msg msg = exists ? Msg.e_invalid_path_not_exists : Msg.e_invalid_path_exists;\n                throw new ParseException(msg, s);\n            }\n            if (isDir != null && Files.isDirectory(p) != isDir) {\n                Msg msg = isDir ? Msg.e_invalid_path_not_dir : Msg.e_invalid_path_not_file;\n                throw new ParseException(msg, s);\n            }\n\n            return p;\n        }\n    }\n\n    static class PublicKeyParser implements Parser<PublicKeyHolder> {\n        private final ASN1ObjectIdentifier alg;\n\n        PublicKeyParser(ASN1ObjectIdentifier alg) {\n            this.alg = alg;\n        }\n\n        @Override\n        public PublicKeyHolder parse(String s, Resolver r) throws ParseException {\n            Path path = EXISTING_FILE_PARSER.parse(s, r);\n\n            try {\n                return CryptoUtil.loadPublicKey(path, alg);\n            } catch (Exception e) {\n                throw new ParseException(e, Msg.e_invalid_public_key, path.toString(), alg, e);\n            }\n        }\n    }\n\n    static class ChoiceParser<T> implements Parser<T> {\n        private final List<T> values;\n        private final Function<String, T> mapper;\n\n        ChoiceParser(T[] values) {\n            this(s -> defaultMapper(s, values), values);\n        }\n\n        ChoiceParser(Function<String, T> mapper, T[] values) {\n            this.values = Arrays.asList(values);\n            this.mapper = mapper;\n        }\n\n        private static <T> T defaultMapper(String s, T[] values) {\n            for (T v : values) {\n                if (v.toString().equals(s)) {\n                    return v;\n                }\n            }\n            return null;\n        }\n\n        @Override\n        public T parse(String s, Resolver r) {\n            try {\n                T value = mapper.apply(s);\n                if (value != null) {\n                    return value;\n                }\n            } catch (Exception e) {\n                log.debug(\"parse({}), mapper error occurred: {}\", s, e.getMessage(), e);\n            }\n            throw new ParseException(Msg.e_invalid_choice, s, values);\n        }\n    }\n\n    // ******************************** OTHER ******************************** //\n\n    /**\n     * Resolver represents the context in which an argument is being parsed and helps to resolve or\n     * translate the parsed value according to the context.\n     */\n    static class Resolver {\n\n        private final Function<Path, Path> pathResolver;\n\n        Resolver() {\n            this(p -> p);\n        }\n\n        Resolver(Function<Path, Path> pathResolver) {\n            this.pathResolver = pathResolver;\n        }\n\n        Path resolve(Path p) {\n            return pathResolver.apply(p);\n        }\n\n        // Add support for more types as necessary\n\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/Args.java",
    "content": "package ee.ivxv.common.cli;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * List of command line arguments and some convenience methods.\n */\npublic class Args {\n\n    public final List<Arg<?>> args = new ArrayList<>();\n\n    public Args(Arg<?>... args) {\n        this(Arrays.asList(args));\n    }\n\n    public Args(List<Arg<?>> args) {\n        super();\n        this.args.addAll(args);\n    }\n\n    /**\n     * @return Whether any argument is required.\n     */\n    public boolean isRequired() {\n        return args.stream().anyMatch(Arg::isRequired);\n    }\n\n    /**\n     * @return Whether any argument is set.\n     */\n    public boolean isSet() {\n        return args.stream().anyMatch(Arg::isSet);\n    }\n\n    /**\n     * @return Whether all arguments are valid.\n     */\n    public boolean isValid() {\n        return validate().isValid();\n    }\n\n    /**\n     * Validates the arguments and returns the validation result.\n     * \n     * @return Returns the validation result.\n     */\n    public final ValidationResult validate() {\n        return validate(true, new ValidationResult());\n    }\n\n    /**\n     * Validates the argument list.\n     * \n     * @param shouldExist Indicates whether the node containing the argument list should exist,\n     *        hence should check the arguments.\n     * @param r The result where to accumulate the results.\n     * @return\n     */\n    final ValidationResult validate(boolean shouldExist, ValidationResult r) {\n        boolean check = isSet() || shouldExist;\n        args.stream().filter(a -> a.isSet() || check).forEach(a -> a.validate(r));\n        return r;\n    }\n\n    public Arg<?> find(String name) {\n        return args.stream().filter(a -> a.name.getName().equals(name)).findFirst().orElse(null);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/CommandLine.java",
    "content": "package ee.ivxv.common.cli;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * CommandLine is the logical data model of command line arguments Command line consists of\n * <i>commands</i> and <i>options</i>. Commands are non-option arguments starting from the beginning\n * of the command line. Options are command line arguments that start with <tt>-</tt>. Options can\n * have a value, that is the list of non-option arguments that immediately follow the option. Both\n * commands and options are ordered.\n */\npublic class CommandLine {\n\n    /** List of non-option arguments starting from the beginning of the command line arguments. */\n    private final List<String> commands;\n    /** List of options that have not bee matched with any argument so far. */\n    private final List<Option> options;\n\n    private CommandLine(List<String> commands, List<Option> options) {\n        this.commands = Collections.unmodifiableList(commands);\n        this.options = options;\n    }\n\n    /**\n     * Parses an instance of <tt>CommandLine</tt> out of the provided command line arguments.\n     *\n     * @param args the command line arguments\n     * @return Returns a <tt>CommandLine</tt> instance that corresponds to the provided arguments.\n     */\n    public static CommandLine parse(String[] args) {\n        List<String> commands = new ArrayList<>();\n        List<Option> options = new ArrayList<>();\n\n        for (String arg : args) {\n            if (Option.isOption(arg)) {\n                break;\n            }\n            commands.add(arg);\n        }\n\n        for (int i = commands.size(), size = args.length; i < size;) {\n            // The loop always starts with an option name\n            String name = args[i];\n            List<String> value = new ArrayList<>();\n\n            // Leap over and process the option arguments\n            for (i++; i < size && !Option.isOption(args[i]); i++) {\n                value.add(args[i]);\n            }\n\n            Option o = new Option(name, value);\n\n            options.add(o);\n        }\n\n        return new CommandLine(commands, options);\n    }\n\n    public void set(Args args) throws ParseException {\n        Map<Arg<?>, Arg.Tree> branches = new HashMap<>();\n        Set<Arg<?>> processedArgs = new HashSet<>();\n\n        for (Iterator<Option> i = options.iterator(); i.hasNext();) {\n            Option o = i.next();\n            Arg<?> arg = find(o, args, branches);\n\n            if (arg == null) {\n                continue;\n            }\n\n            i.remove();\n\n            if (!processedArgs.add(arg)) {\n                throw new ParseException(Msg.e_multiple_assignments, o.getName());\n            }\n\n            if (arg instanceof Arg.Tree) {\n                Arg.Tree tree = (Arg.Tree) arg;\n\n                if (!tree.isExclusive()) {\n                    throw new ParseException(Msg.e_value_not_allowed, o.getName(), o.getValue());\n                }\n                if (o.getValue().size() != 1) {\n                    throw new ParseException(Msg.e_requires_single_value, o.getName(),\n                            o.getValue());\n                }\n\n                String first = o.getValue().get(0);\n                Arg<?> branch = tree.value.find(first);\n\n                // If the found branch is not a tree, it does not make any sense\n                if (branch == null || !(branch instanceof Arg.Tree)) {\n                    throw new ParseException(Msg.e_branch_expected, o.getName(), first);\n                }\n                branches.put(tree, (Arg.Tree) branch);\n            } else {\n                arg.parse(o.getValue());\n            }\n        }\n    }\n\n    /**\n     * @return Returns an unmodifiable list of commands.\n     */\n    public List<String> getCommands() {\n        return commands;\n    }\n\n    /**\n     * @return Returns the copy of the list of options that are unused so far.\n     */\n    public List<Option> getUnusedOptions() {\n        return new ArrayList<>(options);\n    }\n\n    private Arg<?> find(Option o, Args args, Map<Arg<?>, Arg.Tree> branches) {\n        for (Arg<?> arg : args.args) {\n            if (o.matches(arg)) {\n                if (!o.hasChild()) {\n                    return arg;\n                }\n                if (arg instanceof Arg.Tree) {\n                    return find(o.child(), branches.getOrDefault(arg, (Arg.Tree) arg).value,\n                            branches);\n                }\n                // Referring to sub-argument of a non-tree argument\n                return null;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/CommonArgs.java",
    "content": "package ee.ivxv.common.cli;\n\nimport java.nio.file.Path;\n\n/**\n * CommonArgs is the list of common arguments that are processed on every run for every application.\n * Arguments 'conf' and 'force' are application-level arguments, but 'help' works both on\n * application or tool level - depending on whether tool is specified.\n */\npublic class CommonArgs extends Args {\n\n    private final int p = Runtime.getRuntime().availableProcessors();\n\n    public final Arg<Boolean> help = Arg.aFlag(Msg.arg_help).setOptional();\n    public final Arg<Path> conf = Arg.aPath(Msg.arg_conf, true, null);\n    public final Arg<Path> params = Arg.aPath(Msg.arg_params, true, false).setOptional();\n    public final Arg<Boolean> force = Arg.aFlag(Msg.arg_force).setOptional();\n    public final Arg<Boolean> quiet = Arg.aFlag(Msg.arg_quiet).setOptional();\n    public final Arg<String> lang = Arg.aString(Msg.arg_lang).setOptional();\n    public final Arg<Integer> ct = Arg.anInt(Msg.arg_container_threads).setDefault(0).setOptional();\n    public final Arg<Integer> threads = Arg.anInt(Msg.arg_threads).setDefault(p + 1).setOptional();\n\n    public CommonArgs() {\n        args.add(help);\n        args.add(conf);\n        args.add(params);\n        args.add(force);\n        args.add(quiet);\n        args.add(lang);\n        args.add(ct);\n        args.add(threads);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/ContextFactory.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.conf.LocaleConf;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.impl.BboxHelperImpl;\nimport ee.ivxv.common.service.console.BlockingQueueConsole;\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.bdoc.BdocContainerReader;\nimport ee.ivxv.common.service.i18n.Cal10nI18nImpl;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.report.CsvReporterImpl;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.smartcard.CardService;\nimport ee.ivxv.common.service.smartcard.pkcs15.PKCS15CardService;\nimport java.math.BigInteger;\nimport java.security.SecureRandom;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * ContextFactory class is the \"single source of truth\" for creating services. The class is used\n * with the singleton pattern and the single instance of this class chooses the implementation for\n * each and every service used in an IVXV application.\n * \n * <p>\n * It is possible to have an alternative context factory for \"development mode\" by extending this\n * class by a class with the name of the value of {@code OVERLOAD_CLASS}. If such class is found on\n * the classpath and creating an instance with it's default constructor succeeds, that instance is\n * used as the singleton instance. Hence it forces the behaviour of excluding the context factory\n * for development mode from the production build.\n */\npublic class ContextFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(ContextFactory.class);\n\n    private static final String OVERLOAD_CLASS = \"ee.ivxv.common.cli.TestContextFactory\";\n    private static final ContextFactory INSTANCE = createInstance();\n\n    private final boolean testMode;\n\n    /**\n     * Creates a service factory in <u>test mode</u>.\n     */\n    public ContextFactory() {\n        this(true);\n    }\n\n    private ContextFactory(boolean testMode) {\n        this.testMode = testMode;\n    }\n\n    private static ContextFactory createInstance() {\n        try {\n            Class<?> type = Class.forName(OVERLOAD_CLASS);\n            Object instance = type.getDeclaredConstructor().newInstance();\n            ContextFactory result = (ContextFactory) instance;\n            log.info(\"!!!Using overladed context factory {}!!!\", OVERLOAD_CLASS);\n            return result;\n        } catch (ClassNotFoundException e) {\n            log.info(\"Overloaded context factory class {} not found\", OVERLOAD_CLASS);\n        } catch (Exception e) {\n            log.error(\"Exception occurred while trying to create overloaded context factory {}: {}\",\n                    OVERLOAD_CLASS, e.getMessage(), e);\n        }\n        log.info(\"Using standard context factory {}\", ContextFactory.class.getName());\n        return new ContextFactory(false);\n    }\n\n    public static ContextFactory get() {\n        return INSTANCE;\n    }\n\n    private static String createSessionId() {\n        SecureRandom random = new SecureRandom();\n        return new BigInteger(130, random).toString(32);\n    }\n\n    /**\n     * @return Returns {@code false} if and only if {@code this} is an instance of the class\n     *         {@code ContextFactory} and not a overloading class.\n     */\n    public final boolean isTestMode() {\n        return testMode;\n    }\n\n    public LocaleConf getLocaleConf() {\n        return new LocaleConf();\n    }\n\n    public Console getConsole() {\n        return new BlockingQueueConsole();\n    }\n\n    public I18n getI18n(LocaleConf locale) {\n        return new Cal10nI18nImpl(locale);\n    }\n\n    public Reporter getReporter(I18n i18n) {\n        return new CsvReporterImpl(i18n);\n    }\n\n    public CardService getCard(Console console, I18n i18n) {\n        return new PKCS15CardService(console, i18n);\n    }\n\n    public ContainerReader getContainer(Conf conf, int nThreads) {\n        return new BdocContainerReader(conf, nThreads);\n    }\n\n    public BboxHelper getBbox(Conf conf, ContainerReader container) {\n        return new BboxHelperImpl(conf, container);\n    }\n\n    public InitialContext createInitialContext() {\n        String sessionId = createSessionId();\n        LocaleConf locale = getLocaleConf();\n        Console console = getConsole();\n        I18n i18n = getI18n(locale);\n        return new InitialContext(sessionId, locale, console, i18n);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/InitialContext.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.conf.LocaleConf;\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.i18n.I18n;\n\n/**\n * InitialContext is a collection of essential non-configurable dependencies for applications. The\n * dependencies in this class do not require instances of {@code Conf} neither for creation nor\n * running.\n */\npublic final class InitialContext {\n\n    public final String sessionId;\n    public final LocaleConf locale;\n    public final Console console;\n    public final I18n i18n;\n\n    public InitialContext(String sessionId, LocaleConf locale, Console console, I18n i18n) {\n        this.locale = locale;\n        this.sessionId = sessionId;\n        this.console = console;\n        this.i18n = i18n;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/Msg.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.util.NameHolder;\n\n@BaseName(\"i18n.common-cli-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg implements NameHolder {\n    // Used by AppRunner\n    e_app_error, e_common_args_invalid, e_multiple_tools, e_tool_args_invalid, e_tool_error, //\n    e_tool_missing, e_unknown_arg, e_unknown_args_present, e_unknown_tool, //\n\n    w_test_mode, w_java_data_model, w_unknown_java_runtime, //\n\n    app, tool, row_indent, tool_placeholder, value_placeholder, //\n    usage, usage_arg_w_value, usage_arg_names, usage_arg_optional, //\n    tools, tool_row, //\n    args, arg_row, arg_name_required, //\n    params, param_row, param_name_required, //\n\n    app_result_success, //\n    app_result_failure, //\n\n    // Used by Arg, CommandLine and YamlData\n    e_arg_parse_error, //\n    e_invalid_boolean, e_invalid_choice, e_invalid_int, e_invalid_long, e_invalid_number, //\n    e_invalid_instant, e_invalid_local_date, //\n    e_invalid_path_exists, e_invalid_path_not_exists, e_invalid_path_not_dir, //\n    e_invalid_path_not_file, e_invalid_public_key, //\n    e_multiple_assignments, e_requires_single_value, e_value_not_allowed, e_branch_expected, //\n    e_yaml_invalid_file, e_yaml_invalid_key, e_yaml_list_expected, e_yaml_map_expected, //\n    e_yaml_scalar_expected, //\n    e_arg_required, //\n\n    // Common tools\n    tool_verify,\n\n    // Common arguments\n    arg_help(\"h\"), arg_conf(\"c\"), arg_params(\"p\"), arg_force(\"f\"), arg_quiet(\"q\"), arg_lang, //\n    arg_container_threads(\"ct\"), arg_threads(\"t\"),\n\n    // Verify tool arguments\n    arg_file;\n\n    private final String shortName;\n\n    Msg() {\n        this(null);\n    }\n\n    Msg(String shortName) {\n        this.shortName = shortName;\n    }\n\n    @Override\n    public String getShortName() {\n        return shortName;\n    }\n\n    @Override\n    public String getName() {\n        return extractName(name());\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/Option.java",
    "content": "package ee.ivxv.common.cli;\n\nimport java.util.List;\n\n/**\n * An instance of <code>Option</code> represents a single command line option name that should\n * correspond to a valid command line argument (instance of {@link Arg}). The class provides helper\n * methods to match the option with an argument.\n * \n * Option may represent sub-arguments. In that case the option is either in the form\n * '--parent-child' (long argument names) or '-p-c' (short argument names).\n */\nclass Option {\n\n    private static final String SEP = \"-\";\n    private static final String SHORT_PREFIX = SEP;\n    private static final String LONG_PREFIX = SEP + SEP;\n\n    private final String fullName;\n    private final boolean isShort;\n    private final String[] names;\n    private final int i;\n    private final List<String> value;\n\n    Option(String name, List<String> value) {\n        this(name, isShortName(name), name.substring(name.lastIndexOf(SEP, 1) + 1).split(SEP), 0,\n                value);\n    }\n\n    private Option(String fullName, boolean isShort, String[] names, int i, List<String> value) {\n        this.fullName = fullName;\n        this.isShort = isShort;\n        this.names = names;\n        this.i = i;\n        this.value = value;\n    }\n\n    static boolean isShortName(String name) {\n        return !name.startsWith(LONG_PREFIX);\n    }\n\n    static boolean isOption(String s) {\n        return s != null && (s.startsWith(SHORT_PREFIX) || s.startsWith(LONG_PREFIX));\n    }\n\n    static String formatName(Arg<?> arg) {\n        return arg.name.getName() != null ? LONG_PREFIX + arg.name.getName() : \"\";\n    }\n\n    static String formatShortName(Arg<?> arg) {\n        return arg.name.getShortName() != null ? SHORT_PREFIX + arg.name.getShortName() : \"\";\n    }\n\n    public String getName() {\n        return fullName;\n    }\n\n    String getLevelName() {\n        return names[i];\n    }\n\n    List<String> getValue() {\n        return value;\n    }\n\n    boolean hasChild() {\n        return i < names.length - 1;\n    }\n\n    Option child() {\n        return new Option(fullName, isShort, names, i + 1, value);\n    }\n\n    boolean matches(Arg<?> arg) {\n        return getLevelName().equals(isShort ? arg.name.getShortName() : arg.name.getName());\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n\n        sb.append(fullName);\n        sb.append(\" = \");\n        sb.append(value);\n\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/ParseException.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.service.i18n.MessageException;\n\npublic class ParseException extends MessageException {\n\n    private static final long serialVersionUID = -5672721085726710014L;\n\n    public ParseException(Enum<?> key, Object... args) {\n        super(key, args);\n    }\n\n    public ParseException(Throwable cause, Enum<?> key, Object... args) {\n        super(cause, key, args);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/Tool.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.util.NameHolder;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * Implements single functionality/command of a command line application.\n * \n * @param <T> The type of the application context of this tool.\n * @param <U> The type of the arguments of this tool.\n */\npublic class Tool<T extends AppContext<?>, U extends Args> {\n\n    public final NameHolder name;\n    public final Supplier<U> argsSupplier;\n    public final Function<T, Runner<U>> runnerSupplier;\n\n    public Tool(NameHolder name, Supplier<U> argsSupplier, Function<T, Runner<U>> runnerSupplier) {\n        this.name = name;\n        this.argsSupplier = argsSupplier;\n        this.runnerSupplier = runnerSupplier;\n    }\n\n    /** @return New instance of arguments for this particular type of tool. */\n    protected U createArgs() {\n        return argsSupplier.get();\n    }\n\n    /**\n     * @param ctx The correct application context.\n     * @return Returns the functional part of the application.\n     */\n    protected Runner<U> prepare(T ctx) {\n        return runnerSupplier.apply(ctx);\n    }\n\n    /**\n     * Separate interface to ensure that tools are run only after proper preparation.\n     * \n     * @param <U> The type of the arguments of this tool.\n     */\n    public interface Runner<U extends Args> {\n        /**\n         * Runs the prepared tool with the specified arguments.\n         * \n         * @param args Evaluated and valid arguments of the tool.\n         * @return Whether the execution was successful.\n         * @throws Exception any exception thrown here is handled by central error handling logic.\n         */\n        boolean run(U args) throws Exception;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/ValidationResult.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.service.i18n.Translatable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class ValidationResult {\n\n    private final List<Error> errors = new ArrayList<>();\n\n    public List<Error> getErrors() {\n        return Collections.unmodifiableList(errors);\n    }\n\n    public void addError(Arg<?> arg, Translatable msg) {\n        errors.add(new Error(arg, msg));\n    }\n\n    public boolean isValid() {\n        return errors.isEmpty();\n    }\n\n    public static class Error {\n        public final Arg<?> arg;\n        public final Translatable error;\n\n        public Error(Arg<?> arg, Translatable error) {\n            this.arg = arg;\n            this.error = error;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/VerifyTool.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.VerifyTool.VerifyArgs;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.util.I18nConsole;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class VerifyTool implements Tool.Runner<VerifyArgs> {\n\n    private final I18nConsole console;\n    private final ContainerReader container;\n\n    public VerifyTool(AppContext<?> ctx) {\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        container = ctx.container;\n    }\n\n    @Override\n    public boolean run(VerifyArgs args) throws Exception {\n        verify(args.file.value());\n\n        return true;\n    }\n\n    private void verify(Path path) throws Exception {\n        console.println();\n        console.println(M.m_reading_container, path);\n        container.requireContainer(path);\n        Container c = container.read(path.toString());\n\n        console.println(M.m_signatures);\n        c.getSignatures().forEach(s -> console.printlnraw(M.m_signature_row,\n                s.getSigner().getSerialNumber(), s.getSigner().getName(), s.getSigningTime()));\n\n        console.println(M.m_files);\n        List<DataFile> contents = c.getFiles().stream().sorted(\n                (f1, f2) -> f1.getName().compareToIgnoreCase(f2.getName())\n                ).toList();\n        contents.forEach(f -> console.println(M.m_file_row, f.getName()));\n    }\n\n    public static class VerifyArgs extends Args {\n\n        Arg<Path> file = Arg.aPath(Msg.arg_file, true, false);\n\n        public VerifyArgs() {\n            args.add(file);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/YamlData.java",
    "content": "package ee.ivxv.common.cli;\n\nimport ee.ivxv.common.cli.Arg.Resolver;\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.yaml.snakeyaml.Yaml;\n\npublic class YamlData {\n\n    static final Logger log = LoggerFactory.getLogger(YamlData.class);\n\n    private final Resolver resolver;\n    private final Map<?, ?> map;\n\n    private YamlData(Path path, Map<?, ?> map) throws ParseException {\n        this.map = map;\n        resolver = new Resolver(path::resolveSibling);\n    }\n\n    public static YamlData parse(Path path, InputStream in) {\n        try {\n            Yaml yaml = new Yaml();\n            Object o = yaml.load(in);\n\n            return new YamlData(path, expectMap(o, \"\"));\n        } catch (Exception e) {\n            throw new ParseException(e, Msg.e_yaml_invalid_file, e.getMessage());\n        }\n    }\n\n    private static Map<?, ?> expectMap(Object o, String path) {\n        if (o == null) {\n            return new HashMap<>();\n        }\n        if (!(o instanceof Map<?, ?>)) {\n            throw new ParseException(Msg.e_yaml_map_expected, path, o);\n        }\n\n        return (Map<?, ?>) o;\n    }\n\n    public void set(String toolName, Args args) throws ParseException {\n        Map<?, ?> toolMap = expectMap(map.get(toolName), toolName);\n        set(args, toolMap, toolName, \"\"); // Default for optionalValuePrefix is \"\"\n    }\n\n    private void set(Args args, Map<?, ?> m, String argPath, String optionalValuePrefix) {\n        for (Map.Entry<?, ?> entry : m.entrySet()) {\n            String name = entry.getKey().toString();\n            String subPath = getPath(argPath, name);\n            Object value = entry.getValue();\n\n            if (YamlDataExtension.isVoterListsDirTag(name)) {\n                YamlDataExtension.setVoterListsDirName(value);\n                continue;\n            }\n\n            if (!optionalValuePrefix.equals(\"\")) {\n                value = optionalValuePrefix + (String) value;\n            }\n\n            log.debug(\"Path: {}, name: {}, value: {} ({})\", argPath, name, value,\n                    value != null ? value.getClass() : null);\n\n            Arg<?> arg = args.find(name);\n\n            if (arg == null) {\n                throw new ParseException(Msg.e_yaml_invalid_key, argPath, name);\n            }\n\n            if (arg instanceof Arg.TreeList) {\n                if (value == null) {\n                    continue;\n                }\n\n                if (((List<?>) value).isEmpty()) {\n                    throw new ParseException(Msg.e_yaml_list_expected, subPath, value);\n                }\n                if (!(value instanceof List)) {\n                    throw new ParseException(Msg.e_yaml_list_expected, subPath, value);\n                }\n\n                Arg.TreeList<?> tl = (Arg.TreeList<?>) arg;\n\n                ((List<?>) value).forEach(o -> set(tl.addNew(), expectMap(o, argPath), argPath, YamlDataExtension.getVoterlistsDirName()));\n\n                continue;\n            }\n\n            if (arg instanceof Arg.Tree) {\n                Arg.Tree tree = (Arg.Tree) arg;\n                // Default for optionalValuePrefix is \"\"\n                set(tree.value(), expectMap(value, subPath), subPath, \"\");\n                continue;\n            } else if (value instanceof Map) {\n                throw new ParseException(Msg.e_yaml_scalar_expected, subPath, value);\n            }\n\n            List<String> values = new ArrayList<>();\n\n            if (value instanceof List) {\n                ((List<?>) value).forEach(o -> values.add(format(o)));\n            } else if (value != null) {\n                values.add(format(value));\n            }\n\n            arg.parse(values, resolver);\n        }\n    }\n\n    private String format(Object o) {\n        if (o instanceof Date) {\n            return DateTimeFormatter.ISO_INSTANT.format(((Date) o).toInstant());\n        }\n        return o.toString();\n    }\n\n    private String getPath(String parent, String child) {\n        return String.format(\"%s.%s\", parent, child);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/cli/YamlDataExtension.java",
    "content": "package ee.ivxv.common.cli;\n\nimport java.util.List;\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\n\nclass YamlDataExtension {\n\n    private static final String VOTERLISTS_DIR_TAG = \"voterlists_dir\"; // const\n    private static final String DIRECTORY_TRAILING_SLASH = \"/\"; // const\n    private static String voterlists_dir_name = \"\"; // var\n\n\n    /**\n     * Get private variable \"voterlists_dir_name\". Default value for \"voterlists_dir_name\" = \"\"\n     *\n     * @return private variable \"voterlists_dir_name\" or \"\" as a default value\n     */\n    static String getVoterlistsDirName() {\n        return voterlists_dir_name;\n    }\n\n    /**\n     * Check whether \"tagName\" matches \"VOTERLISTS_DIR_TAG\"\n     *\n     * @param tagName Yaml tag name\n     * @return true if matches, otherwise - false\n     */\n    static boolean isVoterListsDirTag(String tagName) {\n        return tagName.equals((VOTERLISTS_DIR_TAG));\n    }\n\n    /**\n     * Set private variable \"voterlists_dir_name\". Default value for \"voterlists_dir_name\" = \"\".\n     * If Exception is thrown - do nothing and let \"processor\" application handle that case\n     *\n     * @param tagValue Yaml tag value\n     */\n    static void setVoterListsDirName(Object tagValue) {\n        try {\n            voterlists_dir_name = (String) tagValue + DIRECTORY_TRAILING_SLASH;\n        } catch (Exception ex) {\n            YamlData.log.error(getExceptionStackAsString(ex));\n        }\n    }\n\n    /**\n     * Get Exception's stack trace as a human-readable String\n     *\n     * @param ex Exception\n     * @return Exception's stack trace as a String\n     */\n    public static String getExceptionStackAsString(Exception ex) {\n        List<?> exStack = (Arrays.stream(ex.getStackTrace()).collect(Collectors.toList()));\n        return exStack.stream().map(x -> x.toString() + \"\\n\").toString();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/Conf.java",
    "content": "package ee.ivxv.common.conf;\n\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Conf is common runtime configuration of any IVXV application.\n */\npublic class Conf {\n\n    private final List<X509Certificate> caCerts;\n    private final List<X509Certificate> ocspCerts;\n    private final List<X509Certificate> tsaCerts;\n\n    public Conf() {\n        this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());\n    }\n\n    public Conf(List<X509Certificate> caCerts, List<X509Certificate> ocspCerts,\n            List<X509Certificate> tsaCerts) {\n        this.caCerts = Collections.unmodifiableList(caCerts);\n        this.ocspCerts = Collections.unmodifiableList(ocspCerts);\n        this.tsaCerts = Collections.unmodifiableList(tsaCerts);\n    }\n\n    public List<X509Certificate> getCaCerts() {\n        return caCerts;\n    }\n\n    public List<X509Certificate> getOcspCerts() {\n        return ocspCerts;\n    }\n\n    public List<X509Certificate> getTsaCerts() {\n        return tsaCerts;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/ConfBuilder.java",
    "content": "package ee.ivxv.common.conf;\n\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ConfBuilder {\n\n    private final List<X509Certificate> caCerts = new ArrayList<>();\n    private final List<X509Certificate> ocspCerts = new ArrayList<>();\n    private final List<X509Certificate> tsaCerts = new ArrayList<>();\n\n    public static ConfBuilder aConf() {\n        return new ConfBuilder();\n    }\n\n    public ConfBuilder withCaCerts(X509Certificate... certs) {\n        return addCerts(caCerts, certs);\n    }\n\n    public ConfBuilder withOcspCerts(X509Certificate... certs) {\n        return addCerts(ocspCerts, certs);\n    }\n\n    public ConfBuilder withTsaCerts(X509Certificate... certs) {\n        return addCerts(tsaCerts, certs);\n    }\n\n    public Conf build() {\n        return new Conf(caCerts, ocspCerts, tsaCerts);\n    }\n\n    private ConfBuilder addCerts(List<X509Certificate> dest, X509Certificate... certs) {\n        for (X509Certificate cert : certs) {\n            dest.add(cert);\n        }\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/ConfLoader.java",
    "content": "package ee.ivxv.common.conf;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.container.bdoc.BdocContainerReader;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.security.cert.X509Certificate;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * ConfLoader creates a new Conf instance with the data loaded from the specified location.\n */\npublic class ConfLoader {\n\n    static final Logger log = LoggerFactory.getLogger(ConfLoader.class);\n\n    public static final String CONF_FILE_NAME = \"ivxv.properties\";\n    private static final int MAX_REPORTED_UNUSED_FILES = 20;\n\n    static ContainerReader NOT_VALIDATING_CONTAINER_READER = new NotValidatingContainerReader();\n\n    public static final String CA_KEY = \"ca\";\n    public static final String OCSP_KEY = \"ocsp\";\n    public static final String TSA_KEY = \"tsa\";\n\n    private final I18nConsole console;\n    private final ResourceLoader loader;\n\n    // Fields for processing\n    private final Set<String> usedFiles = new HashSet<>();\n\n    ConfLoader(I18nConsole console, ResourceLoader loader) {\n        this.console = console;\n        this.loader = loader;\n    }\n\n    public static Conf load(Path path, I18nConsole console) {\n        if (!path.toFile().exists()) {\n            throw new MessageException(M.e_file_not_found, path);\n        }\n\n        ConfLoader loader = new ConfLoader(console, getLoader(path));\n\n        console.println(Msg.m_loading_conf, path);\n\n        return loader.load();\n    }\n\n    private static ResourceLoader getLoader(Path path) {\n        if (isDir(path)) {\n            return new DirLoader(path);\n        } else if (NOT_VALIDATING_CONTAINER_READER.isContainer(path)) {\n            return new ContainerLoader(path);\n        }\n\n        throw new MessageException(Msg.e_unsupported_conf_type, path);\n    }\n\n    private static boolean isDir(Path path) {\n        return path.toFile().isDirectory();\n    }\n\n    Conf load() {\n        ConfBuilder cb = new ConfBuilder();\n\n        Properties props = loadProperties();\n\n        loadCerts(props.getProperty(CA_KEY, \"\"), cb::withCaCerts);\n        loadCerts(props.getProperty(OCSP_KEY, \"\"), cb::withOcspCerts);\n        loadCerts(props.getProperty(TSA_KEY, \"\"), cb::withTsaCerts);\n\n        // Report unknown properties\n        props.keySet().stream()\n                .filter(k -> !k.equals(CA_KEY) && !k.equals(OCSP_KEY) && !k.equals(TSA_KEY))\n                .forEach(k -> console.println(Msg.w_unknown_property, k));\n\n        try (Stream<String> files = loader.getAllFiles()) {\n            files.filter(f -> !usedFiles.contains(f)).limit(MAX_REPORTED_UNUSED_FILES)\n                    .forEach(this::reportUnusedFile);\n        }\n\n        return cb.build();\n    }\n\n    private Properties loadProperties() {\n        InputStream in = load(CONF_FILE_NAME);\n        if (in == null) {\n            throw new MessageException(Msg.e_conf_file_not_found, CONF_FILE_NAME);\n        }\n\n        Properties props = new Properties();\n        try (InputStreamReader reader = new InputStreamReader(in, Util.CHARSET)) {\n            props.load(reader);\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_conf_file_open_error, CONF_FILE_NAME,\n                    e.getMessage());\n        }\n\n        return props;\n    }\n\n    private void loadCerts(String paths, Consumer<X509Certificate> consumer) {\n        for (String p : paths.split(\",\")) {\n            p = p.trim();\n            if (p.isEmpty()) {\n                continue;\n            }\n            log.debug(\"Loading certificate {}\", p);\n            try (InputStream in = load(p)) {\n                if (in == null) {\n                    throw new MessageException(ee.ivxv.common.M.e_cert_not_found, p);\n                }\n                X509Certificate cert = Util.readCertAsPem(in);\n                consumer.accept(cert);\n            } catch (MessageException e) {\n                throw e;\n            } catch (Exception e) {\n                throw new MessageException(e, ee.ivxv.common.M.e_cert_read_error, p,\n                        e.getMessage());\n            }\n        }\n    }\n\n    private InputStream load(String name) {\n        usedFiles.add(name);\n        return loader.getResource(name);\n    }\n\n    private void reportUnusedFile(String name) {\n        log.warn(\"Unused file in configuration: {}\", name);\n        console.println(Msg.e_unused_file, name);\n    }\n\n    /**\n     * ResourceLoader is an interface for loading resources from inside configuration location.\n     */\n    private interface ResourceLoader {\n        InputStream getResource(String name);\n\n        Stream<String> getAllFiles();\n    }\n\n    private static class DirLoader implements ResourceLoader {\n\n        private final Path path;\n\n        DirLoader(Path path) {\n            this.path = path;\n        }\n\n        @Override\n        public InputStream getResource(String name) {\n            log.debug(\"Loading resource {} from directory {}\", name, path);\n            return openFileInputStream(name);\n        }\n\n        private InputStream openFileInputStream(String s) {\n            try {\n                return new FileInputStream(Paths.get(path.toString(), s).toFile());\n            } catch (FileNotFoundException e) {\n                // To be consistent with ContainerLoader. Handled in load(2).\n                return null;\n            }\n        }\n\n        @Override\n        public Stream<String> getAllFiles() {\n            try {\n                // Stream is closed where it is used - super.load()\n                @SuppressWarnings(\"resource\")\n                Stream<Path> paths = Files.walk(path);\n\n                return paths.filter(p -> !p.equals(path)).map(p -> path.relativize(p).toString());\n            } catch (IOException e) {\n                throw new RuntimeException(\"Error occurred getting files of directory \" + path, e);\n            }\n        }\n    } // class DirLoader\n\n    private static class ContainerLoader implements ResourceLoader {\n\n        private final Path path;\n        Map<String, DataFile> files = new HashMap<>();\n\n        ContainerLoader(Path path) {\n            this.path = path;\n            readFiles();\n        }\n\n        private void readFiles() {\n            Container c = NOT_VALIDATING_CONTAINER_READER.read(path.toString());\n\n            for (DataFile file : c.getFiles()) {\n                files.put(file.getName(), file);\n            }\n        }\n\n        @Override\n        public InputStream getResource(String name) {\n            log.debug(\"Loading resource {} from container {}\", name, path);\n            return Optional.ofNullable(files.get(name)).map(f -> f.getStream()).orElse(null);\n        }\n\n        @Override\n        public Stream<String> getAllFiles() {\n            return files.keySet().stream();\n        }\n    } // class ContainerLoader\n\n    private static class NotValidatingContainerReader extends BdocContainerReader {\n\n        NotValidatingContainerReader() {\n            super(ConfBuilder.aConf().build(), 1);\n        }\n\n        @Override\n        protected void validate(org.digidoc4j.Container c, String ref) {\n            // Don't validate\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/ConfVerifier.java",
    "content": "package ee.ivxv.common.conf;\n\nimport ee.ivxv.common.cli.AppContext;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.ContainerHelper;\nimport ee.ivxv.common.util.I18nConsole;\nimport java.nio.file.Path;\nimport java.security.cert.X509Certificate;\nimport java.util.List;\n\n/**\n * ConfVerifier verifies the configuration integrity and correctness.\n */\npublic class ConfVerifier {\n\n    private static final String OCSP_EXT = \"1.3.6.1.5.5.7.3.9\";\n    private static final String TSA_EXT = \"1.3.6.1.5.5.7.3.8\";\n\n    public static void verify(Conf conf) {\n        conf.getCaCerts().forEach(c -> requireCa(c));\n        conf.getOcspCerts().forEach(c -> requireExt(c, OCSP_EXT, Msg.e_ocsp_not_ocsp_cert));\n        conf.getTsaCerts().forEach(c -> requireExt(c, TSA_EXT, Msg.e_tsp_not_tsp_cert));\n    }\n\n    private static void requireCa(X509Certificate cert) {\n        if (!isCa(cert)) {\n            throw new MessageException(Msg.e_ca_not_ca_cert, cert);\n        }\n    }\n\n    private static boolean isCa(X509Certificate cert) {\n        return cert.getBasicConstraints() >= 0;\n    }\n\n    private static void requireExt(X509Certificate cert, String ext, Enum<?> key) {\n        if (!hasExt(cert, ext)) {\n            throw new MessageException(key, cert, ext);\n        }\n    }\n\n    private static boolean hasExt(X509Certificate cert, String ext) {\n        try {\n            List<String> exts = cert.getExtendedKeyUsage();\n            return exts != null && exts.contains(ext);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static void verifySignature(AppContext<?> ctx, Path path) {\n        ctx.container.requireContainer(path);\n\n        I18nConsole console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        Container c = ctx.container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n\n        ch.reportSignatures(Msg.m_conf_arg_for_cont);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/LocaleConf.java",
    "content": "package ee.ivxv.common.conf;\n\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.WeakHashMap;\nimport java.util.function.Consumer;\n\n/**\n * LocaleConf is a class that contains the current set of locale configuration. Note that the locale\n * configuration can be modified - both the list of available locales loaded and the current locale\n * changed. Locale configuration is initialized (loaded) separately from other configuration.\n */\npublic class LocaleConf {\n\n    public static final Locale DEFAULT_LOCALE = Locale.of(\"et\");\n\n    private List<Locale> allLocales;\n    private Locale locale;\n    private final WeakHashMap<Object, Consumer<Locale>> localeChangeListeners = new WeakHashMap<>();\n\n    /**\n     * Uses the default locale as the only available locale.\n     */\n    public LocaleConf() {\n        setAllLocales(Arrays.asList(DEFAULT_LOCALE));\n    }\n\n    public Locale getLocale() {\n        return locale;\n    }\n\n    public void setLocale(Locale locale) {\n        if (!allLocales.contains(locale)) {\n            throw new MessageException(Msg.e_unsupported_locale, locale, allLocales);\n        }\n        Locale oldLocale = this.locale;\n        this.locale = locale;\n        if (oldLocale == null || !oldLocale.equals(locale)) {\n            localeChangeListeners.values().forEach(l -> l.accept(locale));\n        }\n    }\n\n    /**\n     * @return Returns unmodifiable list of all supported locales.\n     */\n    public List<Locale> getAllLocales() {\n        return allLocales;\n    }\n\n    /**\n     * Sets the list of all available locales.\n     * \n     * @param allLocales The list of all available locales. NB! Assumed to be not empty. If the list\n     *        does not contain the current locale, the first element of the list is set as the\n     *        current locale.\n     */\n    void setAllLocales(List<Locale> allLocales) {\n        this.allLocales = Collections.unmodifiableList(allLocales);\n\n        if (!allLocales.contains(locale)) {\n            setLocale(allLocales.get(0));\n        }\n    }\n\n    public void addLocaleChangeListener(Object key, Consumer<Locale> listener) {\n        localeChangeListeners.put(key, listener);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/LocaleConfLoader.java",
    "content": "package ee.ivxv.common.conf;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Properties;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class LocaleConfLoader {\n\n    private static final Logger log = LoggerFactory.getLogger(LocaleConfLoader.class);\n\n    private static final String LANG_PROPERTIES = \"lang.properties\";\n    private static final String LANGS_KEY = \"languages\";\n\n    /**\n     * Loads locale configuration into the specified instance and returns it.\n     * \n     * @param localeConf The destination instance to load the locale configuration into.\n     * @return Returns <tt>localeConf</tt>.\n     */\n    public static LocaleConf load(LocaleConf localeConf) {\n        log.info(\"Loading language properties from {}\", LANG_PROPERTIES);\n        try (InputStream in = Util.getResource(LANG_PROPERTIES)) {\n            load(in, localeConf);\n            return localeConf;\n        } catch (MessageException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_lang_conf_not_found, LANG_PROPERTIES);\n        }\n    }\n\n    static LocaleConf load(InputStream langProperties, LocaleConf localeConf) throws IOException {\n        Properties langProps = loadLangProperties(langProperties);\n        List<Locale> locales = new ArrayList<>();\n\n        if (!langProps.containsKey(LANGS_KEY)) {\n            throw new MessageException(Msg.e_langs_property_missing, LANG_PROPERTIES, LANGS_KEY);\n        }\n        String[] langs = langProps.getProperty(LANGS_KEY).split(\",\");\n        log.info(\"Loaded languages: {}\", Arrays.asList(langs));\n        Stream.of(langs).map(String::trim).filter(s -> !s.isEmpty())\n                .forEach(s -> locales.add(Locale.of(s)));\n\n        if (locales.isEmpty()) {\n            throw new MessageException(Msg.e_langs_empty, LANG_PROPERTIES, LANGS_KEY);\n        }\n\n        localeConf.setAllLocales(locales);\n\n        return localeConf;\n    }\n\n    private static Properties loadLangProperties(InputStream langProperties) throws IOException {\n        if (langProperties == null) {\n            throw new MessageException(Msg.e_lang_conf_not_found, LANG_PROPERTIES);\n        }\n\n        Properties props = new Properties();\n\n        props.load(langProperties);\n\n        return props;\n    }\n\n    @BaseName(\"bootstrap-i18n.common-conf-localeconfloader-msg\")\n    @LocaleData(defaultCharset = \"UTF-8\", value = {})\n    public enum Msg {\n        e_lang_conf_not_found, e_langs_property_missing, e_langs_empty\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/conf/Msg.java",
    "content": "package ee.ivxv.common.conf;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.service.i18n.Translatable;\n\n@BaseName(\"i18n.common-conf-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg implements Translatable {\n    // ConfLoader errors\n    e_conf_file_not_found, e_conf_file_open_error, e_unsupported_conf_type, e_unused_file, //\n    e_ca_not_ca_cert, e_ocsp_not_ocsp_cert, e_tsp_not_tsp_cert,\n\n    w_unknown_property,\n\n    // LocaleConf errors\n    e_unsupported_locale, //\n\n    m_loading_conf, //\n    m_conf_arg_for_cont,\n\n    ;\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/CorrectnessUtil.java",
    "content": "package ee.ivxv.common.crypto;\n\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.math.GroupElement;\n\n/**\n * CorrectnessUtil is a utility class for checking the correctness of ElGamal ciphertexts.\n */\npublic class CorrectnessUtil {\n    /**\n     * Ciphertext correctness results.\n     */\n    public enum CiphertextCorrectness {\n        // if adding enum values, update\n        // ee.ivxv.processor.util.ReportHelper#translate(CiphertextCorrectness) method.\n        /**\n         * Valid ciphertext\n         */\n        VALID,\n        /**\n         * Byte array not decodable as ElGamalCiphertext\n         */\n        INVALID_BYTES,\n        /**\n         * Value is not element of expected group\n         */\n        INVALID_GROUP,\n        /**\n         * Value is out of range.\n         */\n        INVALID_RANGE,\n        /**\n         * Value is not quadratic residue.\n         */\n        INVALID_QR,\n        /**\n         * Value is not a point on the curve.\n         */\n        INVALID_POINT,\n        /**\n         * Invalid ciphertext.\n         */\n        INVALID;\n    }\n\n    /**\n     * Verify the correctness of the serialized ciphertext.\n     * <p>\n     * Verify that the ciphertext is correct and safe to use.\n     * \n     * @param pk The expected public key which was used to encode the ciphertext\n     * @param ctb Serialized ciphertext to check for correctness.\n     * @return Correctness check value.\n     */\n    public static CiphertextCorrectness isValidCiphertext(ElGamalPublicKey pk, byte[] ctb) {\n        ElGamalCiphertext ciphertext;\n        try {\n            ciphertext = new ElGamalCiphertext(pk.getParameters(), ctb);\n        } catch (IllegalArgumentException e) {\n            return CiphertextCorrectness.INVALID_BYTES;\n        } catch (Throwable t) {\n            return CiphertextCorrectness.INVALID;\n        }\n        for (GroupElement el : new GroupElement[] {ciphertext.getBlind(),\n                ciphertext.getBlindedMessage()}) {\n            switch (pk.getParameters().getGroup().isGroupElement(el)) {\n                case VALID:\n                    continue;\n                case INVALID_GROUP:\n                    return CiphertextCorrectness.INVALID_GROUP;\n                case INVALID_RANGE:\n                    return CiphertextCorrectness.INVALID_RANGE;\n                case INVALID_QR:\n                    return CiphertextCorrectness.INVALID_QR;\n                case INVALID_POINT:\n                    return CiphertextCorrectness.INVALID_POINT;\n                default:\n                    return CiphertextCorrectness.INVALID;\n            }\n        }\n        return CiphertextCorrectness.VALID;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/CryptoUtil.java",
    "content": "package ee.ivxv.common.crypto;\n\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.InvalidKeyException;\nimport java.security.KeyFactory;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.PublicKey;\nimport java.security.Security;\nimport java.security.Signature;\nimport java.security.SignatureException;\nimport java.security.cert.CertificateEncodingException;\nimport java.security.cert.X509Certificate;\nimport java.security.spec.X509EncodedKeySpec;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.bouncycastle.cert.ocsp.BasicOCSPResp;\nimport org.bouncycastle.cert.ocsp.CertificateID;\nimport org.bouncycastle.cert.ocsp.OCSPException;\nimport org.bouncycastle.jce.provider.BouncyCastleProvider;\nimport org.bouncycastle.operator.AlgorithmNameFinder;\nimport org.bouncycastle.operator.DefaultAlgorithmNameFinder;\nimport org.bouncycastle.operator.bc.BcDigestCalculatorProvider;\n\npublic class CryptoUtil {\n\n    static {\n        Security.addProvider(new BouncyCastleProvider());\n    }\n\n    static final AlgorithmNameFinder ALGORITHM_FINDER = new DefaultAlgorithmNameFinder();\n\n    public static PublicKeyHolder loadPublicKey(Path path, ASN1ObjectIdentifier alg)\n            throws Exception {\n        byte[] keyBytes = Util.decodePublicKey(Util.toString(Files.readAllBytes(path)));\n        return withPublicKey(loadPublicKey(keyBytes, ALGORITHM_FINDER.getAlgorithmName(alg)));\n    }\n\n    public static PublicKey loadPublicKey(byte[] bytes, String algorithm) throws Exception {\n        KeyFactory kf = KeyFactory.getInstance(algorithm);\n        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);\n        PublicKey key = kf.generatePublic(keySpec);\n\n        return key;\n    }\n\n    public static PublicKeyHolder withPublicKey(PublicKey key) {\n        return new PublicKeyHolder(key);\n    }\n\n    public static CertificateHolder withCertificate(X509Certificate cert) {\n        return new CertificateHolder(cert);\n    }\n\n    /**\n     * PublicKeyHolder is a helper class for common operations with public key.\n     */\n    public static class PublicKeyHolder {\n\n        private final PublicKey key;\n\n        PublicKeyHolder(PublicKey key) {\n            this.key = key;\n        }\n\n        public boolean verify(BasicOCSPResp res) {\n            return verify(res.getTBSResponseData(), res.getSignature(), res.getSignatureAlgOID());\n        }\n\n        public boolean verify(byte[] data, byte[] signature, ASN1ObjectIdentifier algId) {\n            return verify(data, signature, ALGORITHM_FINDER.getAlgorithmName(algId));\n        }\n\n        public boolean verify(byte[] data, byte[] signature, String algorithm) {\n            try {\n                Signature dsa = createSignature(algorithm);\n\n                dsa.initVerify(key);\n                dsa.update(data);\n\n                return dsa.verify(signature);\n            } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {\n                throw new RuntimeException(e.getMessage(), e);\n            }\n        }\n\n        Signature createSignature(String algorithm) throws NoSuchAlgorithmException {\n            return Signature.getInstance(algorithm);\n        }\n\n    }\n\n    /**\n     * CertificateHolder is a helper class for common operations with X509 certificates.\n     */\n    public static class CertificateHolder {\n\n        private static final BcDigestCalculatorProvider DCP = new BcDigestCalculatorProvider();\n\n        private final X509CertificateHolder cert;\n\n        CertificateHolder(X509Certificate cert) {\n            try {\n                this.cert = new X509CertificateHolder(cert.getEncoded());\n            } catch (CertificateEncodingException | IOException e) {\n                throw new RuntimeException(e.getMessage(), e);\n            }\n        }\n\n        public boolean matchesIssuer(CertificateID certId) {\n            try {\n                return certId.matchesIssuer(cert, DCP);\n            } catch (OCSPException e) {\n                throw new RuntimeException(e.getMessage(), e);\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/Plaintext.java",
    "content": "package ee.ivxv.common.crypto;\n\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.util.Util;\n\nimport java.math.BigInteger;\nimport java.util.Arrays;\nimport java.util.HexFormat;\nimport java.util.Optional;\nimport java.util.function.Function;\n\n/**\n * Plaintext instance represents an immutable plaintext suitable for encryption. It includes\n * additional methods for padding, encoding and decoding the messages.\n *\n */\npublic class Plaintext {\n    private final byte[] msg;\n    public final boolean padded;\n\n    /**\n     * Initialize the Plaintext instance from byte array.\n     * \n     * @param msg Byte array to set message.\n     * @param padded A boolean indicating if the message is already padded.\n     */\n    public Plaintext(byte[] msg, boolean padded) {\n        this.msg = msg;\n        this.padded = padded;\n    }\n\n    /**\n     * Initialize Plaintext from unpadded byte array.\n     * \n     * @param msg Byte array to set message.\n     */\n    public Plaintext(byte[] msg) {\n        this(msg, false);\n    }\n\n    /**\n     * Initialize Plaintext from unpadded UTF-8 encoded String.\n     * \n     * @param msg Unpadded UTF-8 encoded message.\n     */\n    public Plaintext(String msg) {\n        this(Util.toBytes(msg), false);\n    }\n\n    /**\n     * Initialize empty Plaintext.\n     */\n    public Plaintext() {\n        this(new byte[] {}, false);\n    }\n\n    /**\n     * Initialize Plaintext from BigInteger from its byte representation. Truncating or padding with\n     * zeros the byte representation if necessary.\n     * \n     * @param msg BigInteger to initialize from.\n     * @param totalBits The number of bits to use. Lower bits are truncated if necessary.\n     * @param padded Indicate if the BigInteger denotes a padded message.\n     */\n    // the user must be sure that the msg fits into this number of bits. we\n    // truncate the lower bits if they do not fit\n    public Plaintext(BigInteger msg, int totalBits, boolean padded) {\n        byte[] bib = msg.toByteArray();\n        this.msg = new byte[(totalBits + 7) / 8];\n        System.arraycopy(bib, 0, this.msg,\n                this.msg.length - bib.length >= 0 ? this.msg.length - bib.length : 0, bib.length);\n        this.padded = padded;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"Plaintext(%s)\", HexFormat.of().formatHex(msg).toUpperCase());\n    }\n\n    /**\n     * Get byte-encoded representation of the Plaintext, embedded in a ASN1 OCTETSTRING.\n     * \n     * @return ASN1 encoded message in bytes.\n     */\n    public byte[] getBytes() {\n        return new Field(this.msg).encode();\n    }\n\n    /**\n     * Get byte-encoded representation of the Plaintext.\n     * \n     * @return Message in bytes.\n     */\n    public byte[] getMessage() {\n        return this.msg.clone();\n    }\n\n    /**\n     * Get UTF-8 decoded representation of the message. If the message is not valid UTF-8, then the\n     * result is undefined.\n     * \n     * @return\n     */\n    public String getUTF8DecodedMessage() {\n        return Util.toString(this.msg);\n    }\n\n    /**\n     * Return a BigInteger representation of the message.\n     * \n     * @return BigInteger representation of the message.\n     */\n    public BigInteger toBigInteger() {\n        return new BigInteger(1, this.msg);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o)\n            return true;\n        if (o == null || getClass() != o.getClass())\n            return false;\n\n        Plaintext plaintext = (Plaintext) o;\n\n        return Arrays.equals(this.msg, plaintext.msg);\n\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(msg);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/SignatureUtil.java",
    "content": "package ee.ivxv.common.crypto;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.RSAParams;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.security.KeyFactory;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.NoSuchProviderException;\nimport java.security.SignatureException;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.RSAPublicKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1Integer;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.DERSequence;\nimport org.bouncycastle.asn1.nist.NISTObjectIdentifiers;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.pkcs.RSASSAPSSparams;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\nimport org.bouncycastle.crypto.CryptoException;\nimport org.bouncycastle.crypto.Digest;\nimport org.bouncycastle.crypto.digests.SHA256Digest;\nimport org.bouncycastle.crypto.engines.RSABlindedEngine;\nimport org.bouncycastle.crypto.params.RSAKeyParameters;\nimport org.bouncycastle.crypto.signers.PSSSigner;\n\n/**\n * Helper functions for operating with signatures.\n */\npublic class SignatureUtil {\n    /**\n     * Helper functions for operating with RSA signatures.\n     */\n    public static class RSA {\n        /**\n         * Helper functions for operating with RSA signatures using PSS padding scheme.\n         */\n        public static class RSA_PSS {\n            public final static String HASH_FUN = \"SHA-256\";\n            public final static String MFG1_FUN = \"SHA-256\";\n            public final static int HASH_LENGTH = 32;\n            public final static byte TRAILER_BYTE = (byte) 0xbc;\n\n            /**\n             * Generate RSA signature with PSS padding.\n             * <p>\n             * For the padding, the following parameters are used: SHA-256 hash, SHA-256 hash for\n             * MGF, hash length 32 bytes, trailer byte 0xbc.\n             *\n             * @param msg Message to be signed\n             * @param key Key to use for signing\n             * @param salt Salt to use for signing.\n             * @return\n             * @throws SignatureException\n             */\n            public static byte[] generateSignature(byte[] msg, RSAParams key, byte[] salt)\n                    throws SignatureException {\n                if (msg == null) {\n                    return null;\n                }\n                byte[] ret;\n                RSAKeyParameters rsapar =\n                        new RSAKeyParameters(true, key.getModulus(), key.getPrivateExponent());\n                RSABlindedEngine rsaeng = new RSABlindedEngine();\n                rsaeng.init(false, rsapar);\n                Digest msgDigest = getDigest();\n                Digest mgfDigest = getDigest();\n                PSSSigner pss = new PSSSigner(rsaeng, msgDigest, mgfDigest, salt, TRAILER_BYTE);\n                pss.init(true, rsapar);\n                pss.update(msg, 0, msg.length);\n                try {\n                    ret = pss.generateSignature();\n                } catch (CryptoException ex) {\n                    throw new SignatureException(ex);\n                }\n                return ret;\n            }\n\n            /**\n             * Verify RSA signature with PSS padding\n             * <p>\n             * For the padding, the following parameters are used: SHA-256 hash, SHA-256 hash for\n             * MGF, hash length 32 bytes, trailer byte 0xbc.\n             *\n             * @param msg Message to be verified\n             * @param key Key for verifying the message\n             * @param sig Signature\n             * @return Boolean indicating if verification succeeded.\n             */\n            public static boolean verifySignature(byte[] msg, RSAPublicKey key, byte[] sig) {\n                RSAKeyParameters rsapar =\n                        new RSAKeyParameters(false, key.getModulus(), key.getPublicExponent());\n                RSABlindedEngine rsaeng = new RSABlindedEngine();\n                rsaeng.init(false, rsapar);\n                Digest msgDigest = getDigest();\n                Digest mgfDigest = getDigest();\n                PSSSigner pss =\n                        new PSSSigner(rsaeng, msgDigest, mgfDigest, HASH_LENGTH, TRAILER_BYTE);\n                pss.init(false, rsapar);\n                pss.update(msg, 0, msg.length);\n                boolean ret = pss.verifySignature(sig);\n                return ret;\n            }\n\n            /**\n             * Add PSS padding to the message.\n             *\n             * @param msg Message to be padded.\n             * @param modulus Modulus of the private key.\n             * @param salt Salt to use for padding.\n             * @return Padded message.\n             * @throws SignatureException When padding fails\n             */\n            public static byte[] encode(byte[] msg, BigInteger modulus, byte[] salt)\n                    throws SignatureException {\n                RSAParams sk = RSAParams.createPrivateKeyParams(BigInteger.ONE, modulus);\n                return generateSignature(msg, sk, salt);\n            }\n\n            /**\n             * Get hash function to use for PSS padding. Returns SHA-256\n             *\n             * @return SHA-256 hash function\n             */\n            private static Digest getDigest() {\n                return new SHA256Digest();\n            }\n\n            /**\n             * Get the AlgorithmIdentifier for a RSA key usable with PSS padding.\n             *\n             * @return\n             */\n            public static ASN1Primitive getDefaultAlgorithmIdentifier() {\n                AlgorithmIdentifier hashAID =\n                        new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256);\n                RSASSAPSSparams params = new RSASSAPSSparams(hashAID,\n                        new AlgorithmIdentifier(PKCSObjectIdentifiers.id_mgf1, hashAID),\n                        new ASN1Integer(32), new ASN1Integer(1));\n                return params.toASN1Primitive();\n            }\n\n            private static ASN1Primitive getSubjectPublicKey(BigInteger e, BigInteger n) {\n                return new DERSequence(\n                        new ASN1Encodable[] {new ASN1Integer(n), new ASN1Integer(e)});\n            }\n\n            private static SubjectPublicKeyInfo getSubjectPublicKeyInfo(BigInteger e,\n                    BigInteger n) {\n                SubjectPublicKeyInfo spki = null;\n                try {\n                    spki = new SubjectPublicKeyInfo(\n                            new AlgorithmIdentifier(PKCSObjectIdentifiers.id_RSASSA_PSS),\n                            getSubjectPublicKey(e, n));\n                } catch (IOException e1) {\n                    // the exception is checked\n                }\n                return spki;\n            }\n\n            /**\n             * Get SubjectPublicKeyInfo of the given RSA public key.\n             *\n             * @param pub RSA public key\n             * @return\n             */\n            public static SubjectPublicKeyInfo getSubjectPublicKeyInfo(RSAPublicKey pub) {\n                return getSubjectPublicKeyInfo(pub.getPublicExponent(), pub.getModulus());\n            }\n        }\n\n        /**\n         * Create an encodable RSA parameters holder object.\n         *\n         * @param e Public exponent\n         * @param d Private exponent\n         * @param n Modulus\n         * @return\n         */\n        public static RSAParams paramsToRSAParams(BigInteger e, BigInteger d, BigInteger n) {\n            return new RSAParams(e, d, n);\n        }\n\n        /**\n         * Create RSA public key using values\n         *\n         * @param e Public exponent\n         * @param n Modulus\n         * @return\n         */\n        public static RSAPublicKey paramsToRSAPublicKey(BigInteger e, BigInteger n) {\n            RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);\n            KeyFactory factory = getRsaKeyFactory();\n            RSAPublicKey pk;\n            try {\n                pk = (RSAPublicKey) factory.generatePublic(spec);\n            } catch (InvalidKeySpecException ex) {\n                // in practice we don't get exception as RSA is supported\n                throw new RuntimeException(ex);\n            }\n            return pk;\n        }\n\n        /**\n         * Create RSA private key from PKCS8 serialized value\n         *\n         * @param blob PKCS8 serialized RSA private key value\n         * @return\n         */\n        public static RSAParams bytesToRSAParams(byte[] blob) {\n            if (blob == null) {\n                return null;\n            }\n            RSAParams sk = new RSAParams();\n            try {\n                sk.readFromBytes(blob);\n            } catch (ASN1DecodingException e) {\n                throw new RuntimeException(e);\n            }\n\n            return sk;\n        }\n\n        /**\n         * Create RSA public key from X509 serialized value\n         *\n         * @param blob X509 serialized RSA public key\n         * @return\n         */\n        public static RSAPublicKey bytesToRSAPublicKey(byte[] blob) {\n            if (blob == null) {\n                return null;\n            }\n            X509EncodedKeySpec pkspec = new X509EncodedKeySpec(blob);\n            KeyFactory factory = getRsaKeyFactory();\n            RSAPublicKey pk;\n            try {\n                pk = (RSAPublicKey) factory.generatePublic(pkspec);\n            } catch (InvalidKeySpecException ex) {\n                // in practice we don't get exception as RSA is supported\n                throw new RuntimeException(ex);\n            }\n            return pk;\n        }\n\n        private static KeyFactory getRsaKeyFactory() {\n            try {\n                // Must use provider name to prevent using BouncyCastle\n                return KeyFactory.getInstance(\"RSA\", \"SunRsaSign\");\n            } catch (NoSuchAlgorithmException | NoSuchProviderException ex) {\n                // in practice we don't get exception as RSA is supported\n                throw new RuntimeException(ex);\n            }\n        }\n    }\n\n    /**\n     * Strip excessive bytes from the beginning of the signature.\n     *\n     * @deprecated due to logic error. Do not start using as this method will be removed.\n     *\n     * @param sig\n     * @return\n     */\n    public static byte[] stripSignature(byte[] sig) {\n        byte[] ret;\n        if (sig[0] == (byte) 0) {\n            ret = new byte[sig.length - 1];\n            System.arraycopy(sig, 1, ret, 0, sig.length - 1);\n        } else {\n            ret = sig;\n        }\n        return ret;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/elgamal/ElGamalCiphertext.java",
    "content": "package ee.ivxv.common.crypto.elgamal;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Ciphertext;\nimport ee.ivxv.common.asn1.Sequence;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\n\n/**\n * ElGamalCiphertext holds ElGamal ciphertext information.\n * <p>\n * Given the group {@code G}, the message {@code m}, public key {@code h}, randomness {@code r} and\n * generator {@code g}, the ElGamal ciphertext is a tuple {@code c = (c1, c2) = (g^r, m * h^r)}.\n * <p>\n * We call the part {@code c1} of the ciphertext as {@literal blind} as it is using a blinding\n * factor to hide the message.\n * <p>\n * We call the variable {@code c2} of the ciphertext as {@literal blinded message} as it is a\n * message which is blinded by the blinding factor.\n *\n */\npublic class ElGamalCiphertext {\n    private final GroupElement blind;\n    private final GroupElement blindedMessage;\n    private final String oid;\n\n    /**\n     * Create the ElGamalCiphertext using blind, blinded message and OID of the encryption scheme.\n     * \n     * @param blind\n     * @param blindedMessage\n     * @param oid\n     */\n    public ElGamalCiphertext(GroupElement blind, GroupElement blindedMessage, String oid) {\n        this.blind = blind;\n        this.blindedMessage = blindedMessage;\n        this.oid = oid;\n    }\n\n    /**\n     * Initialize using blind and blinded message encoded as a\n     * {@link ee.ivxv.common.math.ProductGroupElement} and OID of the encryption scheme.\n     * \n     * @param el\n     * @param oid\n     * @throws IllegalArgumentException\n     */\n    public ElGamalCiphertext(ProductGroupElement el, String oid) throws IllegalArgumentException {\n        if (el.getElements().length != 2) {\n            throw new IllegalArgumentException(\"Invalid dimension of group element\");\n        }\n        this.blind = el.getElements()[0];\n        this.blindedMessage = el.getElements()[1];\n        this.oid = oid;\n    }\n\n    /**\n     * Create the ElGamalCiphertext using {@link ElGamalParameters} and ASN1 DER-encoded ciphertext.\n     * \n     * @param params\n     * @param data\n     * @throws IllegalArgumentException When can not decode ciphertext from input.\n     */\n    public ElGamalCiphertext(ElGamalParameters params, byte[] data)\n            throws IllegalArgumentException {\n        Ciphertext p = new Ciphertext();\n        try {\n            p.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Unpacking ElGamalCiphertext failed: \" + e);\n        }\n        String poid = \"\";\n        try {\n            poid = p.getOID();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        } finally {\n            oid = poid;\n        }\n        if (!params.getOID().equals(oid)) {\n            throw new IllegalArgumentException(\"Invalid key parameters.\");\n        }\n        byte[] ct = null;\n        try {\n            ct = p.getCiphertext();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        }\n        Sequence s = new Sequence();\n        try {\n            s.readFromBytes(ct);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Unpacking ElGamalCiphertext failed: \" + e);\n        }\n        byte[][] parts;\n        try {\n            parts = s.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Unpacking ElGamalCiphertext failed: \" + e);\n        }\n        if (parts.length != 2) {\n            throw new IllegalArgumentException(\n                    \"Unpacking ElGamalCiphertext failed: invalid ciphertext length: \"\n                            + parts.length);\n        }\n        blind = params.getGroup().getElement(parts[0]);\n        blindedMessage = params.getGroup().getElement(parts[1]);\n    }\n\n    /**\n     * Get the blind of the ciphertext.\n     * \n     * @return Blind of the ciphertext.\n     */\n    public GroupElement getBlind() {\n        return blind;\n    }\n\n    /**\n     * Get the blinded message of the ciphertext.\n     * \n     * @return Blinded message of the ciphertext.\n     */\n    public GroupElement getBlindedMessage() {\n        return blindedMessage;\n    }\n\n    /**\n     * Get the OID of encryption scheme.\n     * \n     * @return OID of encryption scheme.\n     */\n    public String getOID() {\n        return oid;\n    }\n\n    /**\n     * Encode the ciphertext as ASN1 DER-encoded structure.\n     * <p>\n     * Algorithm-specific structure:\n     * \n     * {@code\n     * SEQUENCE (\n     *   blind          GroupElement\n     *   blindedMessage GroupElement\n     *   )\n     * }\n     * \n     * @see ee.ivxv.common.asn1.Ciphertext\n     * \n     * @return ASN1 DER-encoded bytes representing ciphertext.\n     */\n    public byte[] getBytes() {\n        return new Ciphertext(getOID(),\n                new Sequence(getBlind().getBytes(), getBlindedMessage().getBytes()).encode())\n                        .encode();\n    }\n\n    /**\n     * Represent the abstract ElGamalCiphertext structure as an element {@code (c1, c2)} in a\n     * {@link ee.ivxv.common.math.ProductGroup}.\n     * \n     * @return {@link ee.ivxv.common.math.ProductGroup} element representing ciphertext.\n     */\n    public ProductGroupElement getAsProductGroupElement() {\n        ProductGroup pgroup =\n                new ProductGroup(getBlind().getGroup(), getBlindedMessage().getGroup());\n        ProductGroupElement ret = new ProductGroupElement(pgroup, getBlind(), getBlindedMessage());\n        return ret;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ElGamalCipherText(%s,%s)\", blind, blindedMessage);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/elgamal/ElGamalDecryptionProof.java",
    "content": "package ee.ivxv.common.crypto.elgamal;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.asn1.Sequence;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.rnd.DPRNG;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.IntegerConstructor;\nimport ee.ivxv.common.math.MathException;\nimport java.io.IOException;\nimport java.math.BigInteger;\n\n/**\n * ElGamalDecryptionProof holds the decrypted message and proof of correct decryption.\n * <p>\n * To prove the correctness of decryption, the prover has to prove two aspects - that it knows the\n * private key and that the private key was used for decrypting the value. To give the simultaneous\n * proof of both, the prover at first commits to the private key and message, publishes the\n * commitments. Then, the verifier sends a challenge to the prover and the prover computes the\n * response. Then, the verifier verifies the response.\n * <p>\n * Futhermore, as the proof of decryption must be non-interactive, then the challenge is computed\n * from the previous messages in the protocol.\n * <p>\n * For more detailed description of the proof protocol, see the framework documentation.\n */\npublic class ElGamalDecryptionProof {\n    private static final byte[] NI_PROOF_DOMAIN = new Field(\"DECRYPTION\").encode();\n    public final ElGamalPublicKey publickey;\n    public ElGamalCiphertext ciphertext;\n    public GroupElement decrypted;\n    // this is the a in the whitepaper\n    public GroupElement msgCommitment;\n    // this is the b in the whitepaper\n    public GroupElement keyCommitment;\n    // this is the s in the whitepaper\n    public BigInteger response;\n\n    /**\n     * Initialize using all proof components.\n     * <p>\n     * Use this constructor if the proof is already constructed and the goal is to verify the\n     * correctness of decryption.\n     *\n     * @param ciphertext\n     * @param decrypted\n     * @param publickey\n     * @param msgCommitment\n     * @param keyCommitment\n     * @param response\n     */\n    ElGamalDecryptionProof(ElGamalCiphertext ciphertext, GroupElement decrypted,\n            ElGamalPublicKey publickey, GroupElement msgCommitment, GroupElement keyCommitment,\n            BigInteger response) {\n        this.ciphertext = ciphertext;\n        this.decrypted = decrypted;\n        this.publickey = publickey;\n        this.msgCommitment = msgCommitment;\n        this.keyCommitment = keyCommitment;\n        this.response = response;\n    }\n\n    /**\n     * Initialize using all proof components where commitments are given as an array.\n     * <p>\n     * Use this constructor if the proof is already constructed and the goal is to verify the\n     * correctness of decryption.\n     *\n     * @param ciphertext\n     * @param decrypted\n     * @param publickey\n     * @param commitments\n     * @param response\n     * @throws IllegalArgumentException\n     */\n    ElGamalDecryptionProof(ElGamalCiphertext ciphertext, GroupElement decrypted,\n            ElGamalPublicKey publickey, GroupElement[] commitments, BigInteger response)\n            throws IllegalArgumentException {\n        this(ciphertext, decrypted, publickey);\n        if (commitments.length != 2) {\n            throw new IllegalArgumentException(\"Commitments must consist of two elements\");\n        }\n        this.msgCommitment = commitments[0];\n        this.keyCommitment = commitments[1];\n        this.response = response;\n    }\n\n    /**\n     * Initialize using serialized value.\n     * <p>\n     * Use this constructor if the proof is already constructed and serialized.\n     *\n     * @see #getBytes()\n     *\n     * @param ciphertext\n     * @param decrypted\n     * @param publickey\n     * @param packed\n     */\n    public ElGamalDecryptionProof(ElGamalCiphertext ciphertext, GroupElement decrypted,\n            ElGamalPublicKey publickey, byte[] packed) {\n        this(ciphertext, decrypted, publickey);\n        Sequence seq = new Sequence();\n        byte[][] parts;\n        try {\n            seq.readFromBytes(packed);\n            parts = seq.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing proof failed: \" + e.toString());\n        }\n        if (parts.length != 3) {\n            throw new IllegalArgumentException(\"Parsing proof failed: invalid proof length\");\n        }\n        this.msgCommitment = publickey.getParameters().getGroup().getElement(parts[0]);\n        this.keyCommitment = publickey.getParameters().getGroup().getElement(parts[1]);\n        Field respField = new Field();\n        try {\n            respField.readFromBytes(parts[2]);\n            this.response = respField.getInteger();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing proof failed: \" + e.toString());\n        }\n    }\n\n    /**\n     * Initialize using components for creating a proof.\n     *\n     * @param ciphertext\n     * @param decrypted\n     * @param publickey\n     */\n    public ElGamalDecryptionProof(ElGamalCiphertext ciphertext, GroupElement decrypted,\n            ElGamalPublicKey publickey) {\n        this.ciphertext = ciphertext;\n        this.decrypted = decrypted;\n        this.publickey = publickey;\n    }\n\n    /**\n     * Initialize using components for creating a proof.\n     *\n     * @param publickey\n     */\n    ElGamalDecryptionProof(ElGamalPublicKey publickey) {\n        this.publickey = publickey;\n    }\n\n    /**\n     * Set the ciphertext.\n     *\n     * @param ciphertext\n     */\n    public void setCiphertext(ElGamalCiphertext ciphertext) {\n        this.ciphertext = ciphertext;\n    }\n\n    /**\n     * Get the decrypted value.\n     *\n     * @return\n     */\n    public GroupElement getDecrypted() {\n        return this.decrypted;\n    }\n\n    /**\n     * Set the decrypted value.\n     *\n     * @param decrypted\n     */\n    public void setDecrypted(GroupElement decrypted) {\n        this.decrypted = decrypted;\n    }\n\n    /**\n     * Set the message commitment.\n     * <p>\n     * The message commitment should be the value {@literal blind^r}\n     *\n     * @param com\n     */\n    public void setMessageCommitment(GroupElement com) {\n        this.msgCommitment = com;\n    }\n\n    /**\n     * Set the key commitment.\n     * <p>\n     * The key commitment should be the value {@literal g^r}.\n     *\n     * @param com\n     */\n    public void setKeyCommitment(GroupElement com) {\n        this.keyCommitment = com;\n    }\n\n    /**\n     * Set the prover response.\n     * <p>\n     * The prover response should be the value {@literal k*x+r}, where {@literal k} is the\n     * challenge.\n     *\n     * @param resp\n     */\n    public void setResponse(BigInteger resp) {\n        this.response = resp;\n    }\n\n    /**\n     * Verify the correctness of proofs.\n     *\n     * @see #verifyDecryptionProof(BigInteger)\n     * @see #verifySecretKeyProof(BigInteger)\n     *\n     * @return Boolean indicating if both proofs verified.\n     * @throws MathException\n     */\n    public boolean verifyProof() throws MathException {\n        BigInteger k = computeChallenge();\n        return verifySecretKeyProof(k) && verifyDecryptionProof(k);\n    }\n\n    /**\n     * Verify the proof of knowledge of the secret key\n     *\n     * @param challenge\n     * @return\n     * @throws MathException\n     */\n    public boolean verifySecretKeyProof(BigInteger challenge) throws MathException {\n        // verify that c1^s = a * (c2/d)^k\n        GroupElement left = ciphertext.getBlind().scale(response);\n        GroupElement right =\n                decrypted.inverse().op(ciphertext.getBlindedMessage()).scale(challenge).op(msgCommitment);\n        return left.equals(right);\n    }\n\n    /**\n     * Verify the proof of correct decryption.\n     *\n     * @param challenge\n     * @return\n     * @throws MathException\n     */\n    public boolean verifyDecryptionProof(BigInteger challenge) throws MathException {\n        // verify that g^s = b*y^k\n        GroupElement left = publickey.getParameters().getGenerator().scale(response);\n        GroupElement right = publickey.getKey().scale(challenge).op(keyCommitment);\n        return left.equals(right);\n    }\n\n    /**\n     * Compute the challenge for the prover.\n     * <p>\n     * The challenge is constructed non-interactively from the preceding messages between the\n     * prover and verifier.\n     * <p>\n     * An ASN.1 sequence {@code\n     * SEQUENCE (\n     *   NI_PROOF_DOMAIN GENERAL STRING\n     *   pubkey        SubjectPublicKeyInfo\n     *   ciphertext    encryptedBallot\n     *   decrypted     OCTET STRING\n     *   msgCommitment GroupElement\n     *   keyCommitment GroupElement\n     *   )\n     * } is DER-encoded and the encoded value is used to initialize an instance of\n     * {@link ee.ivxv.common.crypto.rnd.DPRNG} for using with\n     * {@link ee.ivxv.common.math.IntegerConstructor } to generate the challenge not larger than the\n     * order of the generator in the corresponding group.\n     *\n     * @return Challenge encoded as a positive BigInteger\n     */\n    public BigInteger computeChallenge() {\n        byte[] hashSeed = new Sequence(\n                // NI_PROOF_DOMAIN is a bytes representation of word\n                // \"DECRYPTION\" to separate NI instances\n                NI_PROOF_DOMAIN,\n                // public key parameters bytes representation consists of:\n                // - key type OID (mod p or EC)\n                // - key parameters\n                // * order\n                // * generator\n                // * election identifier\n                // - public key byte representation (depends on group\n                // implementation)\n                publickey.getBytes(),\n                // ciphertext byte representation consists of:\n                // - key type OID (mod p or EC)\n                // - blind byte representation (group dependable)\n                // - blinded message byte representation (group dependable)\n                ciphertext.getBytes(),\n                // decrypted bytes representation consists of:\n                // - asn1 encoding of the plaintext\n                decrypted.getBytes(),\n                // msg and key commitments consists of bytes representation\n                // of group elements (group specific)\n                msgCommitment.getBytes(), keyCommitment.getBytes()).encode();\n        DPRNG rnd = new DPRNG(hashSeed);\n        BigInteger k = null;\n        try {\n            k = IntegerConstructor.construct(rnd, publickey.getParameters().getGeneratorOrder());\n        } catch (IOException e) {\n            // The exception is checked. IntegerConstructor passes the IOException thrown by the\n            // underlying Rnd implementation. DPRNG does not depend on IO and thus does not throw\n            // IOException.\n        }\n        return k;\n    }\n\n    /**\n     * Serialize the proof.\n     * <p>\n     * Structure:\n     * {@code\n     * SEQUENCE (\n     *   msgCommitment GroupElement\n     *   keyCommitment GroupElement\n     *   response      INTEGER\n     *   )\n     * }\n     *\n     * @return\n     */\n    public byte[] getBytes() {\n        return new Sequence(msgCommitment.getBytes(), keyCommitment.getBytes(),\n                new Field(response).encode()).encode();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/elgamal/ElGamalParameters.java",
    "content": "package ee.ivxv.common.crypto.elgamal;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.asn1.Sequence;\nimport ee.ivxv.common.math.ECGroup;\nimport ee.ivxv.common.math.ECGroupElement;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.ModPGroup;\nimport ee.ivxv.common.math.ModPGroupElement;\nimport java.math.BigInteger;\n\n/**\n * ElGamalParameters holds parameters for ElGamal crypto system encryption and decryption.\n */\npublic class ElGamalParameters {\n    // http://tools.ietf.org/html/draft-rfced-info-pgutmann-00#section-2\n    private final static String OID_MODP = \"1.3.6.1.4.1.3029.2.1\";\n    // unassigned by IANA\n    private final static String OID_EC = \"1.3.6.1.4.1.99999.1\";\n    private final static String P224 = \"P-224\";\n    private final static String P384 = \"P-384\";\n\n    private String oid;\n    private String ec;\n    private Group group;\n    private GroupElement generator;\n    private String electionIdentifier;\n\n    /**\n     * Initialize the parameters using the group and generator.\n     *\n     * @param group\n     * @param generator\n     * @throws IllegalArgumentException\n     */\n    public ElGamalParameters(Group group, GroupElement generator) throws IllegalArgumentException {\n        if (group.isGroupElement(generator) != Group.Decodable.VALID) {\n            throw new IllegalArgumentException(\"Generator is not group element\");\n        }\n        if (group instanceof ModPGroup && generator instanceof ModPGroupElement) {\n            this.group = group;\n            this.generator = generator;\n            this.oid = OID_MODP;\n        } else if (group instanceof ECGroup && generator instanceof ECGroupElement) {\n            this.group = group;\n            this.generator = generator;\n            this.oid = OID_EC;\n            this.ec = ((ECGroup) group).getCurveName();\n        } else {\n            throw new IllegalArgumentException(\"Group and generator values are unknown\");\n        }\n    }\n\n    /**\n     * Initialize the parameters using algorithm identifier and serialized parameter values.\n     * <p>\n     * When decoding from a X.509 certificate, it is possible to obtain both the algorithm\n     * identifier and the key parameters.\n     *\n     * @see #getBytes()\n     * @see #getOID()\n     *\n     * @param algOID\n     * @param in\n     * @throws IllegalArgumentException\n     */\n    public ElGamalParameters(String algOID, byte[] in) throws IllegalArgumentException {\n        Sequence seq = new Sequence();\n        try {\n            seq.readFromBytes(in);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing ElGamal parameters failed: \" + e);\n        }\n        initGroupAndGenerator(algOID, seq);\n    }\n\n    /**\n     * Initialize the parameters using the group, generator and election identifier.\n     *\n     * @param group\n     * @param generator\n     * @throws IllegalArgumentException\n     */\n    public ElGamalParameters(Group group, GroupElement generator, String electionIdentifier)\n            throws IllegalArgumentException {\n        this(group, generator);\n        this.electionIdentifier = electionIdentifier;\n    }\n\n    private void initGroupAndGenerator(String algOID, Sequence seq)\n            throws IllegalArgumentException {\n        try {\n            switch (algOID) {\n                case OID_MODP:\n                    initModPGroupAndGenerator(seq.getBytes());\n                    break;\n                case OID_EC:\n                    initECGroupAndGenerator(seq.getStrings());\n                    break;\n                default:\n                    throw new IllegalArgumentException(\"Unknown algorithm OID: \" + algOID);\n            }\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing ElGamal parameters failed: \" + e);\n        }\n        oid = algOID;\n    }\n\n    private void initModPGroupAndGenerator(byte[][] fields) throws IllegalArgumentException {\n        if (fields.length != 3) {\n            throw new IllegalArgumentException(\"Invalid ElGamal parameters\");\n        }\n        ModPGroup G = new ModPGroup(fields[0]);\n        generator = new ModPGroupElement(G, fields[1]);\n        group = G;\n        Field f = new Field();\n        String elId;\n        try {\n            f.readFromBytes(fields[2]);\n            elId = f.getString();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Can not decode election identifier: \" + e);\n        }\n        if (!elId.equals(\"\")) {\n            electionIdentifier = elId;\n        }\n    }\n\n    private void initECGroupAndGenerator(String[] fields) throws IllegalArgumentException {\n        if (fields.length != 2) {\n            throw new IllegalArgumentException(\"Invalid elliptic curve ElGamal parameters\");\n        }\n        switch (fields[0]) {\n            case P384:\n                {\n                ec = P384;\n                ECGroup G = new ECGroup(P384);\n                generator = G.getBasePoint();\n                group = G;\n                }\n                break;\n            case P224:\n                {\n                ec = P224;\n                ECGroup G = new ECGroup(P224);\n                generator = G.getBasePoint();\n                group = G;\n                }\n                break;\n            default:\n                throw new IllegalArgumentException(\"Unknown elliptic curve ElGamal curve\");\n        }\n        if (!fields[1].equals(\"\")) {\n            electionIdentifier = fields[1];\n        }\n    }\n\n    /**\n     * Serialize the parameters using ASN1 DER encoding.\n     * <p>\n     * Implementation is algorithm-specific.\n     *\n     * @see #getBytesModP()\n     * @see #getBytesEC()\n     *\n     * @return\n     */\n    public byte[] getBytes() {\n        switch (oid) {\n            case OID_MODP:\n                return getBytesModP();\n            case OID_EC:\n                return getBytesEC();\n            default:\n                // does not happen\n                return null;\n        }\n    }\n\n    /**\n     * Serialize parameters for group of integers modulo a prime.\n     * <p>\n     * The parameters are stored in the following structure:\n     *\n     * {@code\n     * SEQUENCE (\n     *   group      ModPGroup,\n     *   generator  ModPGroupElement,\n     *   electionID GeneralString\n     *   )\n     * }\n     *\n     * @see ee.ivxv.common.math.ModPGroup#getBytes()\n     * @see ee.ivxv.common.math.ModPGroupElement#getBytes()\n     * @return\n     */\n    private byte[] getBytesModP() {\n        return new Sequence(group.getBytes(), generator.getBytes(),\n                new Field(getElectionIdentifier()).encode()).encode();\n    }\n\n    private byte[] getBytesEC() {\n        switch (ec) {\n            case P224:\n                return new Sequence(\"P-224\", getElectionIdentifier()).encode();\n            case P384:\n                return new Sequence(\"P-384\", getElectionIdentifier()).encode();\n            default:\n                // does not happen\n                return null;\n        }\n    }\n\n    // return human-readable parameters\n    @Override\n    public String toString() {\n        return String.format(\"ElGamalParameters(%s, %s)\", getGroup(), getGenerator());\n    }\n\n    public GroupElement getGenerator() {\n        return this.generator;\n    }\n\n    public BigInteger getOrder() {\n        return this.group.getOrder();\n    }\n\n    public BigInteger getGeneratorOrder() {\n        return this.generator.getOrder();\n    }\n\n    public String getElectionIdentifier() {\n        return electionIdentifier == null ? \"\" : electionIdentifier;\n    }\n\n    public Group getGroup() {\n        return this.group;\n    }\n\n    public String getOID() {\n        // the oid depends on the group\n        return this.oid;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (this == other)\n            return true;\n        if (other == null)\n            return false;\n        if (this.getClass() != other.getClass())\n            return false;\n        ElGamalParameters opar = (ElGamalParameters) other;\n        return this.getElectionIdentifier().equals(opar.getElectionIdentifier())\n                && this.getGroup().equals(opar.getGroup())\n                && this.getGenerator().equals(opar.getGenerator());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.getElectionIdentifier().hashCode() ^ this.getOrder().hashCode()\n                ^ this.getGenerator().hashCode() ^ this.getGeneratorOrder().hashCode();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/elgamal/ElGamalPrivateKey.java",
    "content": "package ee.ivxv.common.crypto.elgamal;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.PKCS8PrivateKey;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.rnd.NativeRnd;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.IntegerConstructor;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.math.Group.Decodable;\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.nio.file.Path;\n\n/**\n * ElGamalPrivateKey is the private key for decrypting ElGamal ciphertexts.\n */\npublic class ElGamalPrivateKey {\n    private final ElGamalParameters parameters;\n    private final BigInteger key;\n    private ElGamalPublicKey pub;\n    private Rnd rnd = new NativeRnd();\n\n    /**\n     * Initialize using ElGamal parameters and the private key.\n     *\n     * @param parameters\n     * @param key\n     * @throws IllegalArgumentException\n     */\n    public ElGamalPrivateKey(ElGamalParameters parameters, BigInteger key)\n            throws IllegalArgumentException {\n        this.parameters = parameters;\n        this.key = key;\n    }\n\n    /**\n     * Initialize using serialized private key.\n     *\n     * @see #getBytes()\n     *\n     * @param packed\n     * @throws IllegalArgumentException When parsing fails\n     */\n    public ElGamalPrivateKey(byte[] packed) throws IllegalArgumentException {\n        PKCS8PrivateKey p = new PKCS8PrivateKey();\n        try {\n            p.readFromBytes(packed);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Unpacking ElGamalPrivateKey failed: \" + e);\n        }\n        byte[] paramsb = p.getParameters();\n        if (paramsb == null) {\n            throw new IllegalArgumentException(\"Key parameters not defined\");\n        }\n        String oids = \"\";\n        try {\n            oids = p.getOID();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        }\n        byte[] keyb = null;\n        try {\n            keyb = p.getKey();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        }\n        parameters = new ElGamalParameters(oids, paramsb);\n        key = new BigInteger(keyb);\n    }\n\n    /**\n     * Initialize using armored private key file.\n     * <p>\n     * The input key file should be MIME-encoded private key file with private key headers.\n     *\n     * @param p\n     * @throws IllegalArgumentException When decoding or parsing fails.\n     * @throws IOException When reading fails\n     */\n    public ElGamalPrivateKey(Path p) throws IllegalArgumentException, IOException {\n        this(Util.decodePrivateKey(p));\n    }\n\n    /**\n     * Compute the public key which corresponds to the private key.\n     *\n     * @return\n     */\n    public ElGamalPublicKey getPublicKey() {\n        if (pub == null) {\n            pub = new ElGamalPublicKey(getParameters(), computePublicPart());\n        }\n        return pub;\n    }\n\n    private GroupElement computePublicPart() {\n        return getParameters().getGenerator().scale(key);\n    }\n\n    /**\n     * Get the key parameters.\n     *\n     * @return\n     */\n    public ElGamalParameters getParameters() {\n        return this.parameters;\n    }\n\n    /**\n     * Get the key value.\n     *\n     * @return\n     */\n    public BigInteger getSecretPart() {\n        return this.key;\n    }\n\n    /**\n     * Serialize the private as PKCS8 private key\n     * <p>\n     * The key is represented as ASN1 INTEGER and parameters as ElGamalParameters.\n     *\n     * @see ee.ivxv.common.asn1.PKCS8PrivateKey\n     * @see ee.ivxv.common.crypto.elgamal.ElGamalParameters#getBytes()\n     *\n     * @return\n     */\n    public byte[] getBytes() {\n        PKCS8PrivateKey der = new PKCS8PrivateKey(getParameters().getOID(), key.toByteArray(),\n                getParameters().getBytes());\n        return der.encode();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ElGamalPrivateKey(%s, %s)\", parameters, key);\n    }\n\n    /**\n     * Decrypt a ciphertext using the private key.\n     * <p>\n     * This method performs checks to ensure that the ciphertext is correct.\n     *\n     * @param ct Ciphertext.\n     * @return Decrypted message\n     * @throws MathException When decryption fails.\n     */\n    public GroupElement decrypt(ElGamalCiphertext ct) throws MathException {\n        return decrypt(ct, false);\n    }\n\n    /**\n     * Decrypt a ciphertext using the private key, possibly omitting checks.\n     * <p>\n     * If the user is sure that the ciphertext is correct, then it is possible to omit correctness\n     * checking, gaining a 3x speedup during decryption. If the ciphertext is identified falsely as\n     * correct, then the outcome is undefined.\n     * <p>\n     * Padding is not removed from the decrypted message as a decryption proof must be generated\n     * on the exact decrypted data. This is crucial for auditing purposes, otherwise it is not\n     * possible to prove that a ballot was malformed (e.g., wrong padding) but correctly decrypted.\n     *\n     * @param ct Ciphertext.\n     * @param assumeDecodable If true, omit decodability checks.\n     * @return Decrypted message\n     * @throws MathException When decryption fails.\n     */\n    public GroupElement decrypt(ElGamalCiphertext ct, boolean assumeDecodable) throws MathException {\n        if (!assumeDecodable\n                && getParameters().getGroup().isGroupElement(ct.getBlind()) != Decodable.VALID) {\n            throw new MathException(\"Blind is not decodable\");\n        }\n        GroupElement decrypted = ct.getBlindedMessage().op(ct.getBlind().scale(key).inverse());\n        if (!assumeDecodable && getParameters().getGroup().isDecodable(decrypted) != Decodable.VALID) {\n            throw new MathException(\"Message is not decodable\");\n        }\n\n        return decrypted;\n    }\n\n    /**\n     * Decrypt a ciphertext and provide a proof of correct decryption.\n     * <p>\n     * See the documentation of decryption proof for the explanation of the values of the proof.\n     * This method does not check that the message is actually decodable.\n     *\n     * @see ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof\n     *\n     * @param ct Ciphertext\n     * @return Proof of correct decryption (includes decrypted message)\n     * @throws MathException When computation fails.\n     * @throws IOException When can not read entropy for constructing commitment.\n     */\n    public ElGamalDecryptionProof provableDecrypt(ElGamalCiphertext ct)\n            throws MathException, IOException {\n        // provable decryption must perform all checks - but currently we still\n        // assume that the ciphertext is decodable\n        GroupElement decrypted = decrypt(ct, true);\n        ElGamalDecryptionProof proof = new ElGamalDecryptionProof(ct, decrypted, getPublicKey());\n        BigInteger r = IntegerConstructor.construct(rnd, getParameters().getGeneratorOrder());\n        GroupElement a = ct.getBlind().scale(r);\n        GroupElement b = getParameters().getGenerator().scale(r);\n        proof.setMessageCommitment(a);\n        proof.setKeyCommitment(b);\n        BigInteger k = proof.computeChallenge();\n        BigInteger s = k.multiply(key).add(r).mod(getParameters().getGeneratorOrder());\n        proof.setResponse(s);\n        return proof;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/elgamal/ElGamalPublicKey.java",
    "content": "package ee.ivxv.common.crypto.elgamal;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.X509PublicKey;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.rnd.NativeRnd;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.IntegerConstructor;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.math.ProductGroup;\nimport ee.ivxv.common.math.ProductGroupElement;\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.nio.file.Path;\nimport java.security.KeyException;\n\n/**\n * ElGamalPublicKey allows encrypting messages.\n */\npublic class ElGamalPublicKey {\n    private final ElGamalParameters parameters;\n    private final GroupElement key;\n\n    /**\n     * Initialize using parameters and a public key instance.\n     * \n     * @param parameters\n     * @param key A group element which represents the ElGamal public key.\n     * @throws IllegalArgumentException\n     */\n    public ElGamalPublicKey(ElGamalParameters parameters, GroupElement key)\n            throws IllegalArgumentException {\n        this.parameters = parameters;\n        this.key = key;\n    }\n\n    /**\n     * Initialize using serialized value.\n     * \n     * @see #getBytes()\n     * \n     * @param packed\n     * @throws IllegalArgumentException\n     */\n    public ElGamalPublicKey(byte[] packed) throws IllegalArgumentException {\n        X509PublicKey p = new X509PublicKey();\n        try {\n            p.readFromBytes(packed);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Unpacking ElGamalPublicKey failed: \" + e);\n        }\n        byte[] paramsb = p.getParameters();\n        if (paramsb == null) {\n            throw new IllegalArgumentException(\"Key parameters not defined\");\n        }\n        String oids = \"\";\n        try {\n            oids = p.getOID();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        }\n        byte[] keyb = null;\n        try {\n            keyb = p.getKey();\n        } catch (ASN1DecodingException e) {\n            // does not happen but silence compilator warning\n        }\n        parameters = new ElGamalParameters(oids, paramsb);\n        key = parameters.getGroup().getElement(keyb);\n    }\n\n    /**\n     * Initialize using armored public key file.\n     * <p>\n     * The input key file should be MIME-encoded public key file with public key headers.\n     * \n     * @param p\n     * @throws IllegalArgumentException When decoding or parsing fails.\n     * @throws IOException When reading fails\n     */\n    public ElGamalPublicKey(Path p) throws IllegalArgumentException, IOException {\n        this(Util.decodePublicKey(p));\n    }\n\n    /**\n     * Get the parameters.\n     * \n     * @return\n     */\n    public ElGamalParameters getParameters() {\n        return parameters;\n    }\n\n    /**\n     * Get the public key instance.\n     * \n     * @return\n     */\n    public GroupElement getKey() {\n        return key;\n    }\n\n    /**\n     * Serialize as X509 public key.\n     * \n     * @see ee.ixvx.common.asn1.X509PublicKey\n     * @see ee.ivxv.common.math.GroupElement#getBytes()\n     * @see ee.ivxv.common.crypto.elgamal.ElGamalParameters#getBytes()\n     * \n     * @return\n     */\n    public byte[] getBytes() {\n        X509PublicKey der = new X509PublicKey(getParameters().getOID(), key.getBytes(),\n                getParameters().getBytes());\n        return der.encode();\n    }\n\n    /**\n     * Represent the instance as a product group element.\n     * <p>\n     * Returns the value {@literal (g, y)}, where {@literal g} is the generator of the parameters\n     * and {@literal y} is the value of the public key.\n     * \n     * @return\n     */\n    public ProductGroupElement getAsProductGroupElement() {\n        ProductGroup pgroup = new ProductGroup(getKey().getGroup(), 2);\n        ProductGroupElement ret =\n                new ProductGroupElement(pgroup, getParameters().getGenerator(), getKey());\n        return ret;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ElGamalPublicKey(%s, %s)\", parameters, key);\n    }\n\n    public BigInteger generateCompatibleRandomness() throws IOException {\n        return generateCompatibleRandomness(new NativeRnd());\n    }\n\n    public BigInteger generateCompatibleRandomness(Rnd rnd) throws IOException {\n        return IntegerConstructor.construct(rnd, getParameters().getGeneratorOrder());\n    }\n\n    /**\n     * Encrypt the message using system random source.\n     * \n     * @param msg\n     * @return\n     * @throws KeyException When encryption fails.\n     * @throws IOException When reading randomness fails.\n     */\n    public ElGamalCiphertext encrypt(Plaintext msg) throws KeyException, IOException {\n        return encrypt(msg, generateCompatibleRandomness());\n    }\n\n    /**\n     * Encrypt the message using specified random source.\n     * \n     * @param msg\n     * @param rnd\n     * @return\n     * @throws KeyException When encryption fails.\n     * @throws IOException When reading randomness fails.\n     */\n    public ElGamalCiphertext encrypt(Plaintext msg, Rnd rnd) throws KeyException, IOException {\n        return encrypt(msg, generateCompatibleRandomness(rnd));\n    }\n\n    /**\n     * Encrypt the message using specified randomness\n     * The r must be properly generated, no error checking here\n     *\n     * @param msg\n     * @param rnd\n     * @return\n     * @throws KeyException When encryption fails.\n     * @throws IOException When reading randomness fails.\n     */\n    public ElGamalCiphertext encrypt(Plaintext msg, BigInteger r) throws KeyException, IOException {\n        Plaintext padded = getParameters().getGroup().pad(msg);\n        GroupElement el;\n        try {\n            el = getParameters().getGroup().encode(padded);\n        } catch (MathException e) {\n            throw new KeyException(\"Encoding for key parameters failed: \" + e);\n        }\n        try {\n            return new ElGamalCiphertext(getParameters().getGenerator().scale(r),\n                    getKey().scale(r).op(el), getParameters().getOID());\n        } catch (MathException e) {\n            throw new KeyException(\"Key initialization error\");\n        }\n    }\n\n\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/hash/HashFunction.java",
    "content": "package ee.ivxv.common.crypto.hash;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * Interface for hash functions.\n *\n */\npublic interface HashFunction {\n    /**\n     * Construct digest from byte array.\n     * \n     * @param input\n     * @return Digest of the input.\n     */\n    byte[] digest(byte[] input);\n\n    /**\n     * Construct digest from input stream.\n     * \n     * @param input\n     * @return Digest of the input.\n     * @throws IOException When stream read fails.\n     */\n    byte[] digest(InputStream input) throws IOException;\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/hash/HashType.java",
    "content": "package ee.ivxv.common.crypto.hash;\n\nimport java.util.function.Supplier;\n\npublic enum HashType {\n    SHA256(Sha256::new);\n\n    private final Supplier<HashFunction> supplier;\n\n    HashType(Supplier<HashFunction> supplier) {\n        this.supplier = supplier;\n    }\n\n    public static HashType map(String s) {\n        return HashType.valueOf(s.toUpperCase().replace(\"-\", \"\"));\n    }\n\n    public HashFunction getFunction() {\n        return supplier.get();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/hash/Sha256.java",
    "content": "package ee.ivxv.common.crypto.hash;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\npublic class Sha256 implements HashFunction {\n    private MessageDigest md;\n\n    public Sha256() {\n        try {\n            md = MessageDigest.getInstance(\"SHA-256\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"SHA-256 algorithm not found: \" + e.getMessage(), e);\n        }\n    }\n\n    @Override\n    public byte[] digest(byte[] input) {\n        return md.digest(input);\n    }\n\n    @Override\n    public byte[] digest(InputStream input) throws IOException {\n        byte[] buffer = new byte[2048];\n\n        for (int read; (read = input.read(buffer)) != -1;) {\n            md.update(buffer, 0, read);\n        }\n\n        return md.digest();\n    }\n\n    @Override\n    public String toString() {\n        return \"SHA-256\";\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/CombineRnd.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport org.bouncycastle.crypto.digests.SHAKEDigest;\n\n/**\n * CombineRnd reads bytes from the source and mixes them securely. The output is as predictable as\n * is the least predictable source.\n */\npublic class CombineRnd implements Rnd {\n    private List<Rnd> sources;\n    private SHAKEDigest combiner;\n    private byte[] buf = new byte[FileRnd.BUF_SIZE];\n    private int cwritten;\n    private final int RATE;\n\n    /**\n     * Initialize the instance. The instance is not yet seeded. To add sources and seed the\n     * instance, call {@link #addSource(Rnd)} method with actual entropy sources.\n     */\n    public CombineRnd() {\n        sources = new ArrayList<Rnd>();\n        combiner = new SHAKEDigest(256);\n        cwritten = 0;\n        RATE = combiner.getByteLength();\n    }\n\n    /**\n     * @return false.\n     */\n    @Override\n    public boolean isFinite() {\n        return false;\n    }\n\n    /**\n     * Add a entropy source into the pool. During reading, every source is asked for entropy.\n     * \n     * @param rnd The configured entropy source. The {@link CombineRnd} instances can be chained.\n     * @throws IOException On IOException while reading from finite source.\n     */\n    public void addSource(Rnd rnd) throws IOException {\n        if (rnd.isFinite()) {\n            addFiniteSource(rnd);\n        } else {\n            addInfiniteSource(rnd);\n        }\n    }\n\n    /**\n     * Add infinite source to the sources. The source is read only when invoking\n     * {@link #read(byte[], int, int)}.\n     * \n     * @param rnd The infinite entropy source.\n     */\n    private void addInfiniteSource(Rnd rnd) {\n        sources.add(rnd);\n    }\n\n    /**\n     * Add finite source. The whole source is read and mixed into the entropy pool. It is not read\n     * during invoking {@link #read(byte[], int, int)}.\n     * \n     * @param rnd The finite entropy source.\n     * @throws IOException On exception during reading the source.\n     */\n    private void addFiniteSource(Rnd rnd) throws IOException {\n        int p;\n        while ((p = rnd.read(buf, 0, buf.length)) > 0) {\n            write(buf, 0, p);\n        }\n    }\n\n    /**\n     * Fill the buffer with random bytes. Before every read, available bytes from every infinite\n     * entropy source is read and mixed into the pool. This method is synchronized to allow\n     * concurrent calls from several threads.\n     * \n     * @param out The output buffer to store the random bytes in.\n     * @param offset The offset at which to start storing the bytes in output buffer.\n     * @param len The amount of bytes to store in the output buffer.\n     * @return The requested amount {@code len}.\n     * @throws IOException On exception during reading the entropy source.\n     */\n    @Override\n    public int read(byte[] out, int offset, int len) throws IOException {\n        return read(out, offset, len, false);\n    }\n\n    /**\n     * Fill the buffer with random bytes. Before every read, exactly requested number of bytes from\n     * every infinite entropy source is read and mixed into the pool. This method is synchronized to\n     * allow concurrent calls from several threads.\n     * \n     * @param out The output buffer to store the random bytes in.\n     * @param offset The offset at which to start storing the bytes in output buffer.\n     * @param len The amount of bytes to store in the output buffer.\n     * @return The requested amount {@code len}.\n     * @throws IOException On exception during reading the entropy source.\n     */\n    @Override\n    public int mustRead(byte[] out, int offset, int len) throws IOException {\n        return read(out, offset, len, true);\n    }\n\n    /**\n     * Read from sources and mix into the pool. The number of bytes read from every soure depends on\n     * the {@code must} parameter.\n     * <p>\n     * This method is synchronized.\n     * \n     * @param out The output buffer to store the random bytes in.\n     * @param offset The offset at which to start storing the bytes in output buffer.\n     * @param len The amount of bytes to store in the output buffer.\n     * @param must Denote if must read exactly the amount of bytes from every source.\n     * @return The requested amount {@code len}.\n     * @throws IOException On exception during reading the entropy source.\n     */\n    private synchronized int read(byte[] out, int offset, int len, boolean must)\n            throws IOException {\n        updateFromAllSources(len, must);\n        computeDigest(out, offset, len);\n        padAndFill();\n        writeZeros(len);\n        return len;\n    }\n\n    /**\n     * Write bytes into the entropy pool.\n     * \n     * @param buf The source buffer to read bytes from.\n     * @param offset The source offset in the source buffer to start reading the bytes.\n     * @param len The length of bytes to read from the source buffer.\n     */\n    public void write(byte[] buf, int offset, int len) {\n        combiner.update(buf, offset, len);\n        cwritten = (cwritten + len - offset) % RATE;\n    }\n\n    /**\n     * Convenience method for {@link #write(byte[], int, int)} with {@literal offset} 0 and\n     * {@literal lenbuf.length}.\n     * \n     * @param buf The source buffer to read bytes from.\n     */\n    public void write(byte[] buf) {\n        write(buf, 0, buf.length);\n    }\n\n    /**\n     * Read from the entropy source and mix the read bytes into the entropy pool.\n     * \n     * @param source The entropy source to read from.\n     * @param len The maximum amount of bytes to read. If bytes are not available, then actual\n     *        number of read bytes may be smaller.\n     * @param must If must read until obtain the {@code len} amount of bytes.\n     * @throws IOException On exception while reading from the source.\n     */\n    private void updateFromSource(Rnd source, int len, boolean must) throws IOException {\n        int read = 0;\n        int p;\n        do {\n            if (must) {\n                p = source.mustRead(buf, 0, len - read < buf.length ? len - read : buf.length);\n\n            } else {\n                p = source.read(buf, 0, len - read < buf.length ? len - read : buf.length);\n            }\n            read += p;\n            write(buf, 0, p);\n        } while (p > 0 && read < len);\n    }\n\n    /**\n     * Read from all sources.\n     * \n     * @param len The maximum amount of bytes to read from every source. If bytes are not available,\n     *        then actual number of read bytes may be smaller.\n     * @param must If must read until obtain the {@code len} amount of bytes.\n     * @throws IOException On exception while reading from the source.\n     */\n    private void updateFromAllSources(int len, boolean must) throws IOException {\n        for (Rnd source : sources) {\n            updateFromSource(source, len, must);\n        }\n    }\n\n    /**\n     * Add the SHAKE-256 padding to the entropy pool and fill the rest of the block with zeros. The\n     * padding separates the SHAKE-256 absorbing and squeezing states.\n     */\n    private void padAndFill() {\n        Arrays.fill(buf, (byte) 0);\n        buf[cwritten] = 0x1f;\n        buf[RATE - 1] |= (byte) 0x80;\n        combiner.update(buf, cwritten, RATE - cwritten);\n        cwritten = 0;\n    }\n\n    /**\n     * Write zeros to entropy pool.\n     * \n     * @param len The amount of zeros to write.\n     */\n    private void writeZeros(int len) {\n        // we write as much zeros to vanilla copy as we are reading\n        int written = 0;\n        int to_write;\n        Arrays.fill(buf, (byte) 0);\n        while (written < len) {\n            to_write = len - written < buf.length ? len - written : buf.length;\n            combiner.update(buf, 0, to_write);\n            written += to_write;\n            cwritten = (cwritten + to_write) % RATE;\n        }\n    }\n\n    /**\n     * Copy the entropy pool and use the copy to compute the digest.\n     * \n     * @param out The output buffer to write the digest to.\n     * @param offset The start location in the output buffer to start writing the digest.\n     * @param len The length of requested digest.\n     */\n    private void computeDigest(byte[] out, int offset, int len) {\n        // copy the vanilla digest and use the copy to compute the output\n        SHAKEDigest tmpd = new SHAKEDigest(combiner);\n        tmpd.doOutput(out, offset, len);\n    }\n\n    /**\n     * Close all underlying sources.\n     */\n    @Override\n    public void close() {\n        sources.forEach(source -> source.close());\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/DPRNG.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.security.MessageDigest;\n\n/**\n * DPRNG is a deterministic pseudorandom number generator. It is an entropy source which extends the\n * input seed.\n * <p>\n * It is intended to be used together with {@link CombineRnd}, adding this as a source to it.\n */\npublic class DPRNG implements Rnd {\n    private MessageDigest dgst;\n    private int dgstLen;\n    private byte[] seed;\n    private long counter;\n    private byte[] buffer;\n    private int bufferptr;\n\n    /**\n     * Initialize the DPRNG from a seed.\n     * \n     * @param seed The seed.\n     */\n    public DPRNG(byte[] seed) {\n        seedInit(seed);\n    }\n\n    /**\n     * Initialize the DPRNG from the seed read from a file. A digest of the file is computed and the\n     * digest is used as seed.\n     * \n     * @param rndSource The path for the seed file.\n     * @throws IOException On exception while reading the file.\n     */\n    public DPRNG(Path rndSource) throws IOException {\n        byte[] seed = computeFileDigest(rndSource);\n        seedInit(seed);\n    }\n\n    /**\n     * @return false\n     */\n    @Override\n    public boolean isFinite() {\n        return false;\n    }\n\n    /**\n     * Store the seed and set internals.\n     * \n     * @param seed The seed value.\n     */\n    private void seedInit(byte[] seed) {\n        this.dgst = getDigestInstance();\n        this.dgstLen = dgst.getDigestLength();\n        this.counter = 1;\n        this.seed = seed.clone();\n        this.buffer = new byte[dgstLen];\n        this.bufferptr = Integer.MAX_VALUE;\n    }\n\n    /**\n     * Compute digest of a file.\n     * \n     * @param path Path leading to file.\n     * @return The digest value.\n     * @throws IOException On exception during reading the file.\n     */\n    private byte[] computeFileDigest(Path path) throws IOException {\n        MessageDigest d = getDigestInstance();\n        ByteBuffer b = ByteBuffer.allocate(1024);\n        FileChannel f = FileChannel.open(path, StandardOpenOption.READ);\n        while (f.read(b) >= 0) {\n            b.flip();\n            d.update(b);\n            b.clear();\n        }\n        return d.digest();\n    }\n\n    /**\n     * Read bytes from DPRNG. Given the {@code seed}, the output is obtained sequentially from the\n     * infinite byte array <code>H(1||seed) || H(2||seed) || ..</code>.\n     * <p>\n     * The hash function <code>H</code> used is given in {@link #getDigestInstance()}.\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer to store the value in.\n     * @param off The offset in output buffer for storing the value.\n     * @param len The amount of bytes to store in the output buffer.\n     * @return The requested amount {@code len}.\n     */\n    @Override\n    public int read(byte[] buf, int off, int len) {\n        int read = 0;\n        int to_read;\n        while (read < len) {\n            refill();\n            to_read = Math.min(len - read, dgstLen - bufferptr);\n            System.arraycopy(buffer, bufferptr, buf, off + read, to_read);\n            bufferptr = bufferptr + to_read;\n            read += to_read;\n        }\n        return len;\n    }\n\n    /**\n     * Returns {@link #read(byte[], int, int)}\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer to store the value in.\n     * @param off The offset in output buffer for storing the value.\n     * @param len The amount of bytes to store in the output buffer.\n     * @return The requested amount {@code len}.\n     */\n    @Override\n    public int mustRead(byte[] buf, int off, int len) {\n        return read(buf, off, len);\n    }\n\n    /**\n     * @return SHA-256 digest instance.\n     */\n    private MessageDigest getDigestInstance() {\n        try {\n            return MessageDigest.getInstance(\"SHA-256\");\n        } catch (java.security.NoSuchAlgorithmException e) {\n            // this should not happen, SHA-256 is widespread\n            // we throw a generic exception so that we wouldn't need to add\n            // exception checking mess everywhere\n            throw new RuntimeException(\"SHA-256 digest algorithm not found\");\n        }\n    }\n\n    private void refill() {\n        if (bufferptr < dgstLen) {\n            return;\n        }\n        byte[] rndSeed = getRoundInput();\n        buffer = dgst.digest(rndSeed);\n        bufferptr = 0;\n        counter += 1;\n    }\n\n    private byte[] getRoundCounter() {\n        byte[] ret = new byte[8];\n        ret[0] = (byte) ((counter >>> 56) & 0xff);\n        ret[1] = (byte) ((counter >>> 48) & 0xff);\n        ret[2] = (byte) ((counter >>> 40) & 0xff);\n        ret[3] = (byte) ((counter >>> 32) & 0xff);\n        ret[4] = (byte) ((counter >>> 24) & 0xff);\n        ret[5] = (byte) ((counter >>> 16) & 0xff);\n        ret[6] = (byte) ((counter >>> 8) & 0xff);\n        ret[7] = (byte) ((counter >>> 0) & 0xff);\n        return ret;\n    }\n\n    private byte[] getRoundInput() {\n        byte[] packedCounter = getRoundCounter();\n        byte[] ret = new byte[packedCounter.length + seed.length];\n        System.arraycopy(packedCounter, 0, ret, 0, packedCounter.length);\n        System.arraycopy(seed, 0, ret, packedCounter.length, seed.length);\n        return ret;\n    }\n\n    /**\n     * Close random source.\n     */\n    @Override\n    public void close() {\n        // no-op\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/FileRnd.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.FileChannel;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\n/**\n * Entropy source which reads directly from a file or a stream. Direct use in application is\n * strongly discouraged, as the number of read bytes may be less than requested. Also, the input is\n * not conditioned in any way.\n * <p>\n * It is intended to be used together with {@link CombineRnd}, adding this as a source to it.\n */\npublic class FileRnd implements Rnd {\n    protected static int BUF_SIZE = 1024;\n    private final FileChannel fileChannel;\n    private final ByteBuffer fileBuf = ByteBuffer.allocate(BUF_SIZE);\n    private final boolean finite;\n\n    /**\n     * Initialize the instance using a path.\n     * \n     * @param rndSource Path to file or stream.\n     * @param finite Boolean indicating whether the path target should be considered as a finite\n     *        file or a infinite stream. False positive (i.e. indicating that a infinite source is\n     *        finite) leads to infinite loop during initialization of {@link CombineRnd}. If not\n     *        sure, set {@code false}.\n     * @throws IOException On exception while opening the source.\n     */\n    public FileRnd(Path rndSource, boolean finite) throws IOException {\n        fileChannel = FileChannel.open(rndSource, StandardOpenOption.READ);\n        fileBuf.flip();\n        this.finite = finite;\n    }\n\n    /**\n     * Read the source and write the result to output buffer.\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start storing the read bytes.\n     * @param len The number of bytes to read.\n     * @return The actual number of bytes read.\n     * @throws IOException On exception while reading the source.\n     */\n    @Override\n    public int read(byte[] buf, int offset, int len) throws IOException {\n        int read = 0;\n        int to_read, refilled;\n        while (read < len) {\n            refilled = refill();\n            if (refilled == -1) {\n                // we have reached end of file. fileBuf is empty\n                return read;\n            }\n            to_read = Math.min(len - read, fileBuf.remaining());\n            fileBuf.get(buf, offset + read, to_read);\n            read += to_read;\n            if (refilled < fileBuf.capacity()) {\n                // we have reached end of available stream. do not go further for now.\n                return read;\n            }\n        }\n        return read;\n    }\n\n    /**\n     * Read the source and write exactly the number of bytes requested into output buffer.\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start storing the read bytes.\n     * @param len The number of bytes to read.\n     * @return The number of bytes requested.\n     * @throws IOException On exception while reading the source.\n     */\n    @Override\n    public int mustRead(byte[] buf, int offset, int len) throws IOException {\n        int read = 0;\n        int to_read, refilled;\n        while (read < len) {\n            refilled = refill();\n            if (refilled == -1) {\n                // we have reached end of file. fileBuf is empty\n                throw new IOException(\"End of file\");\n            }\n            to_read = Math.min(len - read, fileBuf.remaining());\n            fileBuf.get(buf, offset + read, to_read);\n            read += to_read;\n        }\n        return read;\n    }\n\n    /**\n     * @return The finite argument given during initialization.\n     */\n    @Override\n    public boolean isFinite() {\n        return finite;\n    }\n\n    private int refill() throws IOException {\n        int read = fileBuf.remaining();\n        if (!fileBuf.hasRemaining()) {\n            fileBuf.clear();\n            read = fileChannel.read(fileBuf);\n            fileBuf.flip();\n        }\n        return read;\n    }\n\n    /**\n     * Try to close the source file.\n     */\n    @Override\n    public void close() {\n        try {\n            fileChannel.close();\n        } catch (IOException e) {\n            // unhandleable exception\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/NativeRnd.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport java.security.NoSuchAlgorithmException;\nimport java.security.NoSuchProviderException;\nimport java.security.SecureRandom;\n\n/**\n * NativeRnd is a entropy source which reads the random bytes directly from system.\n * <p>\n * It is intended to be used together with {@link CombineRnd}, adding this as a source to it.\n */\npublic class NativeRnd implements Rnd {\n    private SecureRandom src;\n\n    /**\n     * Initialize the source. Internally, different interfaces used depending on the operating\n     * system. For Windows systems, {@link #initWin()} is called. For other systems (Linux, BSD,\n     * OSX), {@link #initLinux()} is called.\n     * \n     * @throws RuntimeException On exception during initializing the system source.\n     */\n    public NativeRnd() {\n        String os_name = System.getProperty(\"os.name\").toLowerCase();\n        try {\n            if (os_name.contains(\"windows\")) {\n                initWin();\n            } else {\n                initLinux();\n            }\n        } catch (NoSuchAlgorithmException | NoSuchProviderException ex) {\n            throw new RuntimeException(\"Failed initializing native entropy source\", ex);\n        }\n    }\n\n    /**\n     * Initialize {@code Windows-PRNG} instance. The instance uses {@code CryptGenRandom} from\n     * Windows cryptographic API to obtain the random bytes.\n     * \n     * @throws NoSuchAlgorithmException.\n     * @throws NoSuchProviderException.\n     */\n    private void initWin() throws NoSuchAlgorithmException, NoSuchProviderException {\n        src = SecureRandom.getInstance(\"Windows-PRNG\");\n    }\n\n    /**\n     * Initializes {@code NativePRNGNonBlocking} instance. The instance uses the non-blocking kernel\n     * interface to obtain the random bytes.\n     * \n     * @throws NoSuchAlgorithmException.\n     * @throws NoSuchProviderException.\n     */\n    private void initLinux() throws NoSuchAlgorithmException, NoSuchProviderException {\n        src = SecureRandom.getInstance(\"NativePRNGNonBlocking\");\n    }\n\n    /**\n     * Read the bytes from the system random provider.\n     * \n     * @param buf The output buffer to write the bytes into.\n     * @param offset The offset at the output buffer to start writing the random bytes into.\n     * @param len The amount of bytes to be read.\n     * @return The requested amount {@code len}.\n     */\n    @Override\n    public int read(byte[] buf, int offset, int len) {\n        byte[] tmp = new byte[len];\n        src.nextBytes(tmp);\n        System.arraycopy(tmp, 0, buf, offset, len);\n        return len;\n    }\n\n    /**\n     * Returns {@link #read(byte[], int, int)}.\n     * \n     * @param buf The output buffer to write the bytes into.\n     * @param offset The offset at the output buffer to start writing the random bytes into.\n     * @param len The amount of bytes to be read.\n     * @return The requested amount {@code len}.\n     */\n    @Override\n    public int mustRead(byte[] buf, int offset, int len) {\n        return read(buf, offset, len);\n    }\n\n    /**\n     * @return false.\n     */\n    @Override\n    public boolean isFinite() {\n        return false;\n    }\n\n    /**\n     * Close random source.\n     */\n    @Override\n    public void close() {\n        // no op\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/Rnd.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport java.io.IOException;\n\n/**\n * Define the methods required from any random bytes source.\n */\npublic interface Rnd {\n    /**\n     * Write the random bytes into output buffer.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start writing at.\n     * @param len The amount of random bytes requested.\n     * @return The number of actual bytes written.\n     * @throws IOException\n     */\n    int read(byte[] buf, int offset, int len) throws IOException;\n\n    /**\n     * Write the random bytes into output buffer, blocking if the requested amount is not available\n     * in full.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start writing at.\n     * @param len The amount of random bytes requested.\n     * @return The number of bytes requested.\n     * @throws IOException\n     */\n    int mustRead(byte[] buf, int offset, int len) throws IOException;\n\n    /**\n     * @return Boolean indicating if the total number of bytes the instance can return is bounded.\n     */\n    boolean isFinite();\n\n    /**\n     * Close the random source.\n     */\n    void close();\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/crypto/rnd/UserRnd.java",
    "content": "package ee.ivxv.common.crypto.rnd;\n\nimport ee.ivxv.common.util.Util;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.lang.ProcessBuilder;\nimport java.net.InetAddress;\nimport java.net.Socket;\nimport java.nio.file.Path;\n\n/**\n * Entropy source which calls external program to obtain entropy. Communication with external\n * program is done using sockets by sending the amount of bytes requested (encoded as C int). The\n * external program is supposed to reply with the exact amount of requested bytes.\n *<p>\n * It is intended to be used together with {@link CombineRnd}, adding this as a source to it.\n */\npublic class UserRnd implements Rnd {\n    private Process process;\n    private final boolean cont;\n    private final int port;\n    private final Path externalProg;\n\n    /**\n     * Initialize the external entropy random source. The entropy will be provided by the program\n     * defined by {@literal externalProg}. The communication is performed over local network, where\n     * the program should listen on {@literal port}. The parameter {@literal cont} defines, if the\n     * program is started once or started for read.\n     * \n     * @param externalProg Location of external program to ask for entropy.\n     * @param cont Define if the program should not be killed after every entropy query.\n     * @param port The port to communicate with the external program.\n     */\n    public UserRnd(Path externalProg, boolean cont, int port) throws IOException {\n        if (cont) {\n            this.process = executeExternal(externalProg, port);\n        }\n        this.cont = cont;\n        this.port = port;\n        this.externalProg = externalProg;\n    }\n\n    @SuppressWarnings(\"serial\")\n    private static class QueryException extends IOException {\n        public QueryException(String string) {\n            super(string);\n        }\n    }\n\n    private Process executeExternal(Path location, int port) throws IOException {\n        ProcessBuilder pp = new ProcessBuilder(location.toString(), Integer.toString(port));\n        pp.redirectOutput(ProcessBuilder.Redirect.INHERIT);\n        pp.redirectError(ProcessBuilder.Redirect.INHERIT);\n        Process p = pp.start();\n        return p;\n    }\n\n    private void forceShutdown() {\n        process.destroy();\n        process = null;\n    }\n\n    private byte[] queryExternal(Socket s, int amount, boolean must) throws IOException {\n        OutputStream out = s.getOutputStream();\n        InputStream in = s.getInputStream();\n        byte[] sendValue = Util.toBytes(amount);\n        byte[] ret;\n        int available, read;\n        while (true) {\n            out.write(sendValue);\n            available = in.read();\n            if (available == 0) {\n                if (must) {\n                    try {\n                        Thread.sleep(100);\n                    } catch (InterruptedException e) {\n                        shutdown();\n                        s.close();\n                        throw new QueryException(\"Unexpected interrupt\");\n                    }\n                    continue;\n                } else {\n                    ret = new byte[0];\n                    break;\n                }\n            } else if (available == 0xff) {\n                ret = new byte[amount];\n                read = in.read(ret);\n                if (read != amount) {\n                    shutdown();\n                    s.close();\n                    throw new QueryException(\"Short response\");\n                }\n                break;\n            } else {\n                shutdown();\n                s.close();\n                throw new QueryException(\"Corrupted response\");\n            }\n        }\n        shutdown();\n        s.close();\n        return ret;\n    }\n\n    private byte[] queryProg(int amount, boolean must) throws IOException {\n        Socket s = null;\n        byte[] ret = null;\n        for (int i = 0; i < 5; i++) {\n            start();\n            try {\n                s = new Socket(InetAddress.getLocalHost(), port);\n                ret = queryExternal(s, amount, must);\n                s.close();\n            } catch (QueryException e) {\n                throw new IOException(e.getMessage());\n            } catch (IOException e) {\n                if (i == 4) {\n                    throw e;\n                }\n            } finally {\n                shutdown();\n            }\n            if (ret != null) {\n                return ret;\n            }\n        }\n        return null;\n    }\n\n    private void start() throws IOException {\n        if (!cont || (process != null && !process.isAlive())) {\n            process = executeExternal(externalProg, port);\n        }\n    }\n\n    private void shutdown() {\n        if (!cont) {\n            forceShutdown();\n        }\n    }\n\n    /**\n     * Ask the external program for entropy and write the result to output buffer.\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start storing the read bytes.\n     * @param len The number of bytes to ask.\n     * @return The actual number of bytes read.\n     * @throws IOException On exception while communicating with the source.\n     */\n    @Override\n    public int read(byte[] buf, int offset, int len) throws IOException {\n        byte[] ent = queryProg(len, false);\n        int to_copy = ent.length < len ? ent.length : len;\n        System.arraycopy(ent, 0, buf, offset, to_copy);\n        return to_copy;\n    }\n\n    /**\n     * Ask the external program for entropy and write the result to output buffer.\n     * <p>\n     * This method is not thread-safe and synchronization must be handled by the caller.\n     * \n     * @param buf The output buffer.\n     * @param offset The offset to start storing the read bytes.\n     * @param len The number of bytes to ask.\n     * @return The number of bytes requested.\n     * @throws IOException On exception while communicating with the source.\n     */\n    @Override\n    public int mustRead(byte[] buf, int offset, int len) throws IOException {\n        byte[] ent = queryProg(len, true);\n        System.arraycopy(ent, 0, buf, offset, len);\n        return len;\n    }\n\n    /**\n     * @return false\n     */\n    @Override\n    public boolean isFinite() {\n        return false;\n    }\n\n\n    /**\n     * Shut down the external entropy source if it is running.\n     */\n    @Override\n    public void close() {\n        if (cont) {\n            forceShutdown();\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ECGroup.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.crypto.Plaintext;\n\nimport java.math.BigInteger;\nimport java.util.*;\nimport java.util.function.Supplier;\nimport java.util.stream.IntStream;\n\nimport org.bouncycastle.math.ec.ECCurve;\nimport org.bouncycastle.math.ec.ECFieldElement;\nimport org.bouncycastle.math.ec.ECPoint;\nimport org.bouncycastle.math.ec.custom.sec.SecP224R1Curve;\nimport org.bouncycastle.math.ec.custom.sec.SecP384R1Curve;\n\n/**\n * Elliptic curve group\n */\npublic class ECGroup extends Group {\n    public final static String P224 = \"P-224\";\n    public final static String P384 = \"P-384\";\n\n    // there is no objective reasoning for this value. It seems to give\n    // reasonable success probability (i.e. failure rate 1/2^1024)\n    // the probability of encoding failure is 2^-(2^ENCODING_SUCCESS)\n    private final static int ENCODING_SUCCESS = 10;\n    private final String name;\n    private final ECCurve curve;\n    private final ECGroupElement inf;\n    private final ECGroupElement base;\n\n    // initialize a P-384 group\n    /**\n     * Initialize an elliptic curve P-384\n     */\n    public ECGroup() {\n        this(P384);\n    }\n\n    /**\n     * Initialize an elliptic curve using a serialized value.\n     *\n     * @see #getBytes()\n     * @param data\n     * @throws IllegalArgumentException When parsing fails\n     */\n    public ECGroup(byte[] data) throws IllegalArgumentException {\n        this(getASN1CurveName(data));\n    }\n\n    /**\n     * Initialize an elliptic curve using a standard name for the curve.\n     *\n     * @param curvename\n     * @throws IllegalArgumentException When unknown curve name is used.\n     */\n    public ECGroup(String curvename) throws IllegalArgumentException {\n        name = curvename;\n        switch (curvename) {\n            case P224:\n                {\n                curve = new SecP224R1Curve();\n                BigInteger x = new BigInteger(\n                        \"b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21\",\n                        16);\n                BigInteger y = new BigInteger(\n                        \"bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34\",\n                        16);\n                base = new ECGroupElement(this, getPoint(x, y));\n                }\n                break;\n            case P384:\n                {\n                curve = new SecP384R1Curve();\n                BigInteger x = new BigInteger(\n                        \"aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7\",\n                        16);\n                BigInteger y = new BigInteger(\n                        \"3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f\",\n                        16);\n                base = new ECGroupElement(this, getPoint(x, y));\n                }\n                break;\n            default:\n                throw new IllegalArgumentException(\"Unknown curve: \" + curvename);\n        }\n        inf = new ECGroupElement(this);\n    }\n\n    /**\n     * Initialize an elliptic curve over a field with specified bitlength.\n     *\n     * @param len\n     */\n    public ECGroup(int len) {\n        this(getIntCurveName(len));\n    }\n\n    private static String getIntCurveName(int len) {\n        switch (len) {\n            case 224:\n                return P224;\n            case 384:\n                return P384;\n            default:\n                throw new IllegalArgumentException(\"Invalid curve length\");\n        }\n    }\n\n    private static String getASN1CurveName(byte[] data) throws IllegalArgumentException {\n        Field f = new Field();\n        try {\n            f.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing byte array failed: \" + e.toString());\n        }\n        String curvename;\n        try {\n            curvename = f.getString();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing string failed: \" + e.toString());\n        }\n        return curvename;\n    }\n\n    @Override\n    public String getName() {\n        return this.getCurveName();\n    }\n\n    @Override\n    public GroupElement getElement(byte[] data) throws IllegalArgumentException {\n        return new ECGroupElement(this, data);\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        return curve.getOrder();\n    }\n\n    @Override\n    public BigInteger getFieldOrder() {\n        return curve.getField().getCharacteristic()\n                .multiply(BigInteger.valueOf(curve.getField().getDimension()));\n    }\n\n    /**\n     * Return point at infinity.\n     *\n     * @return Point at infinity\n     */\n    @Override\n    public GroupElement getIdentity() {\n        return inf;\n    }\n\n    /**\n     * Encode a plaintext as an elliptic curve point.\n     * <p>\n     * We perform deterministic hashing. The algorithm for encoding the message is: {@code\n     * * if the msg is larger than the point size + padding size, then the message is too large\n     * 1. we allocate the room for second padding\n     * 2. compute f(x) = x^3 - 3x + b\n     * 3. set i = 0\n     * 4. we take x as x = m || i,\n     * 5. then we check if f(x) is quadratic residue, allowing us to take the square root of f(x).\n     * 5.1 if not, let i += 1 and got to 4.\n     * 5. the square root of f(x) will then become the y-coordinate of the point.\n     * }\n     *\n     * @param msg\n     * @return\n     */\n    @Override\n    public GroupElement encode(Plaintext msg) {\n        int limit = getFieldOrder().bitLength() - ENCODING_SUCCESS;\n\n        BigInteger X = msg.toBigInteger();\n\n        if (X.bitLength() > limit) {\n            throw new IllegalArgumentException(\"Message is too large\");\n        }\n\n        // We allocate room for encoding msg into a curve point\n        X = X.shiftLeft(ENCODING_SUCCESS);\n\n        return weierstrassQuadraticResidue(X);\n    }\n\n    /**\n     * Returns result of Weierstrass equation y^2 = x^3 + ax + b\n     * as modSqrt(y^2). Resulting group element is guaranteed to be\n     * a quadratic residue modulo EC field order with a probability\n     * of 99,9(9)%.\n     *\n     * @param x X coordinate of a curve\n     * @return probably EC group element that is quadratic residue modulo EC field order\n     */\n    public ECGroupElement weierstrassQuadraticResidue(final BigInteger x) {\n        int i = 0;\n        int limit = BigInteger.valueOf(2).pow(ENCODING_SUCCESS).intValue();\n\n        BigInteger X = x;\n        BigInteger Y;\n\n        // Loop until Y is a quadratic residue or limit is exceeded\n        do {\n            Y = weierstrassEquation(X);\n            X = Y == null ? X.add(BigInteger.ONE) : X;\n            i++;\n        } while (Y == null && i < limit);\n\n        Objects.requireNonNull(Y, \"Y coordinate is null\");\n\n        // Y is guaranteed to be a quadratic residue\n        return new ECGroupElement(this, X, Y);\n    }\n\n    /**\n     * Returns smallest solution of Weierstrass equation y^2 = x^3 + ax + b, e.g. if Weierstrass solved\n     * to (3, 5) and (3, 2) then since 5 > 2, 2 is returned.\n     * Returns null if y^2 is not a perfect root.\n     * @param x X coordinate of an elliptic curve point\n     * @return smallest solution of Weierstrass equation or null if y^2 is not a perfect root\n     */\n    private BigInteger weierstrassEquation(final BigInteger x) {\n        ECFieldElement XCoord = this.curve.fromBigInteger(x);\n        ECFieldElement YCoord = XCoord\n                    .square() // x^2\n                    .add(this.curve.getA()) // (x^2 + a)\n                    .multiply(XCoord) // x * (x^2 + a) == x^3 + ax\n                    .add(this.curve.getB()) // x^3 + ax + b\n                    .sqrt(); // SQRT(x^3 + ax + b) mod P\n\n        // null means not a perfect square. Point may lie on a curve, but definitely not suitable for encoding\n        // plaintexts, as only quadratic residues should be used\n        if (YCoord == null)\n            return null;\n\n        // First solution of Weierstrass equation (X,Y)\n        BigInteger Y = YCoord.toBigInteger();\n\n        // Second solution of Weierstrass equation (X,-Y)\n        BigInteger minusY = YCoord.negate().toBigInteger();\n\n        // Take smallest solution\n        // https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ECqPGroup.magic#L485\n        if (minusY.compareTo(Y) < 0) {\n            return minusY;\n        } else {\n            return Y;\n        }\n    }\n\n    @Override\n    public Decodable isDecodable(GroupElement el) {\n        // Is valid group element\n        Decodable isValid = isGroupElement(el);\n        if (isValid != Decodable.VALID) {\n            return Decodable.INVALID_GROUP;\n        }\n\n        // Padded plaintext should be exactly of (group field bits length - 1),\n        // \"-1\" because Java strips leading 0 bit in BigInteger\n        if(decode((ECGroupElement) el) == null)\n            return Decodable.INVALID_RANGE;\n\n        return Decodable.VALID;\n    }\n\n    /**\n     * Returns X coordinate of an elliptic curve if it is of correct bytes length, otherwise null.\n     * @param msg\n     * @return\n     */\n    private BigInteger decode(ECGroupElement msg) {\n        ECPoint XY = msg.getPoint();\n\n        BigInteger X = XY.getAffineXCoord().toBigInteger();\n\n        if ((X.bitLength()) != this.getFieldOrder().bitLength() - 1) {\n            return null;\n        }\n\n        return X;\n    }\n\n    @Override\n    public Plaintext decode(GroupElement msg) {\n        BigInteger X = decode((ECGroupElement) msg);\n        if (X == null) {\n            throw new IllegalArgumentException(\"Element is not decodable\");\n        }\n\n        X = X.shiftRight(ENCODING_SUCCESS);\n\n        return new Plaintext(X.toByteArray(), true);\n    }\n\n    @Override\n    public Plaintext pad(Plaintext msg) {\n        if (msg.padded) {\n            return msg;\n        }\n\n        int fieldOrderBitLen = this.getFieldOrder().bitLength();\n\n        // Padded plaintext looks like 0b01111111 0b11111111 ... 0b1...0PLAINTEXT_BITS00 0b00000000,\n        // \"-2\" are first 0,1 bits and \"-1\" is last \"0\" that separates 0b1... and PLAINTEXT_BITS,\n        // last 00 0b00000000 are encoding bits reserved to encode plaintext into EC point\n        int maxPlaintextBitsLen = fieldOrderBitLen - 2 - 1 - ENCODING_SUCCESS;\n\n        int plaintextBitLen = msg.getMessage().length * 8;\n\n        // right now reserve 1 extra byte\n        if (plaintextBitLen + 8> maxPlaintextBitsLen) {\n            throw new IllegalArgumentException(\"Message is too large\");\n        }\n\n        // Padding is 0b01111111 0b11111111 ... 0b1...0, see comment above\n        int paddingBitLen = this.getFieldOrder().bitLength() - 1 - plaintextBitLen - ENCODING_SUCCESS;\n\n        BigInteger padding = BigInteger.ONE;\n\n        // 0b10000000 ... 0b00000000\n        padding = padding.shiftLeft(paddingBitLen);\n\n        // 0b01111111 ... 0b11111110\n        padding = padding.subtract(BigInteger.TWO);\n\n        byte[] paddingB = padding.toByteArray();\n\n        byte[] paddedPlaintext = new byte[msg.getMessage().length + paddingB.length];\n\n        System.arraycopy(paddingB, 0, paddedPlaintext, 0, paddingB.length);\n        System.arraycopy(msg.getMessage(), 0, paddedPlaintext, paddingB.length, msg.getMessage().length);\n\n        return new Plaintext(paddedPlaintext, true);\n    }\n\n    @Override\n    public Plaintext unpad(Plaintext msg) {\n        if (!msg.padded) {\n            return msg;\n        }\n\n        byte[] XBytes = msg.getMessage();\n\n        int mostSignificantByte = XBytes[0] >> 1;\n\n        int zerosCount = Byte.SIZE - (Integer.SIZE - Integer.numberOfLeadingZeros(mostSignificantByte));\n        int onesCount = Integer.bitCount(mostSignificantByte);\n\n        // After leading zeros there should only be continuous stream of ones up until\n        // padding end bit (0) is met, e.g. 0b00000111 0b11111111 ... 0b1110\n        //\n        // Therefore we assume that inside a single byte (8 bits), once we subtract all 1 bits,\n        // there would only be leading 0 bits, so such combinations like 0b00000101 are not possible\n        // since 1 bits count is indeed 2, leading 0 bits are 5 and therefore 8 - 2 = 6 which makes it\n        // 6 != 5\n        if (8 - onesCount != zerosCount) {\n            throw new IllegalArgumentException(\"Message not padded correctly by inspecting first byte\");\n        }\n\n        // Strip padding bytes, starting from second byte as first byte is already checked\n        for (int i = 1; i < XBytes.length; i++) {\n            switch (XBytes[i]) {\n                case (byte) 0xFF: // padding byte\n                    break;\n                case (byte) 0xFE: // end of padding\n                    // encoded plaintext contains only padding bytes, so decoded plaintext is \"\"\n                    if (i + 1 == XBytes.length) {\n                        return new Plaintext(\"\");\n                    }\n                    byte[] unpaddedPlaintextB = Arrays.copyOfRange(XBytes, i + 1, XBytes.length);\n                    BigInteger unpaddedPlaintext = new BigInteger(unpaddedPlaintextB);\n                    return new Plaintext(unpaddedPlaintext, unpaddedPlaintext.bitLength(), false);\n                default:\n                    throw new IllegalArgumentException(\"Message has incorrect padding byte\");\n            }\n        }\n\n        throw new IllegalArgumentException(\"Unexpected padding\");\n    }\n\n    @Override\n    public Decodable isGroupElement(GroupElement el) {\n        // Element belongs to group\n        if (!this.equals(el.getGroup())) {\n            return Decodable.INVALID_GROUP;\n        }\n\n        // Element is valid EC point\n        ECGroupElement m = (ECGroupElement) el;\n        ECPoint M = m.getPoint();\n        if (!M.isValid()) {\n            return Decodable.INVALID_POINT;\n        }\n\n        return Decodable.VALID;\n    }\n\n    /**\n     * Serialize the group.\n     * <p>\n     * Returns GENERALSTRING of the curve name.\n     *\n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        return new Field(name).encode();\n    }\n\n    /**\n     * Get the curve name.\n     *\n     * @return\n     */\n    public String getCurveName() {\n        return name;\n    }\n\n    ECPoint getPoint(BigInteger x, BigInteger y) {\n        if (x == null || y == null || x.intValue() == -1 && y.intValue() == -1) {\n            return curve.getInfinity();\n        }\n        return curve.createPoint(x, y);\n    }\n\n    ECPoint getInfinitePoint() {\n        return curve.getInfinity();\n    }\n\n    ECCurve getCurve() {\n        return this.curve;\n    }\n\n    /**\n     * Get the base point of the group.\n     *\n     * @return\n     */\n    public ECGroupElement getBasePoint() {\n        return base;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ECGroup o = (ECGroup) other;\n        return this.getCurve().equals(o.getCurve());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.curve.hashCode() ^ 0x0002;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ECGroup(\\\"%s\\\")\", getCurveName());\n    }\n\n    /**\n     * Returns exactly \"count\" amount of EC group elements, which are not guaranteed to be quadratic residues.\n     * EC group elements are derived using curve base point and random X coordinates.\n     *\n     * @param count amount of EC group elements to create\n     * @param rng random X coordinates generator\n     * @return array of EC group elements, which are not guaranteed to be quadratic residues\n     */\n    public ECGroupElement[] elementsOfRandomScalars(final int count, final Supplier<BigInteger> rng) {\n        return IntStream\n                .range(0, count)\n                .mapToObj(i -> (ECGroupElement) getBasePoint().scale(rng.get()))\n                .toArray(ECGroupElement[]::new);\n    }\n\n    /**\n     * Returns exactly \"count\" amount of EC group elements, which are all guaranteed to be quadratic residues.\n     * X coordinates are derived using deterministic pseudo random bytes generator.\n     *\n     * @param count amount of EC group elements to create\n     * @param dprng deterministic pseudo random bytes generator\n     * @return array of EC group elements which are guaranteed to be quadratic residues\n     */\n    public ECGroupElement[] pseudoRandomElements(final int count, final Supplier<byte[]> dprng) {\n        List<ECGroupElement> ec = new ArrayList<>();\n\n        // Loop until exactly count elements are generated\n        while (ec.size() < count) {\n            // Get X coordinate from DPRNG and apply modulo operation on returned value\n            BigInteger X = new BigInteger(1, dprng.get()).mod(this.getFieldOrder());\n\n            // Solve Weierstrass\n            BigInteger Y = weierstrassEquation(X);\n\n            if (Objects.isNull(Y)) {\n                // don't collect Y coordinates that are not quadratic residues\n                continue;\n            }\n\n            // Y is guaranteed to be quadratic residue\n            ec.add(new ECGroupElement(this, X, Y));\n        }\n\n        // From List to array\n        return ec.toArray(ECGroupElement[]::new);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ECGroupElement.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport java.math.BigInteger;\nimport java.util.Arrays;\nimport org.bouncycastle.math.ec.ECFieldElement;\nimport org.bouncycastle.math.ec.ECPoint;\n\n/**\n * ECGroupElement holds elliptic curve points.\n */\npublic class ECGroupElement extends GroupElement {\n    private final ECGroup group;\n    private final ECPoint point;\n\n    /**\n     * Initializie using a group and point.\n     * \n     * @param group\n     * @param point\n     */\n    public ECGroupElement(ECGroup group, ECPoint point) {\n        this.group = group;\n        this.point = point.normalize();\n        checkPoint();\n    }\n\n    /**\n     * Initialize a point at infinity.\n     * \n     * @param group\n     */\n    public ECGroupElement(ECGroup group) {\n        this(group, group.getInfinitePoint());\n    }\n\n    /**\n     * Initialize a point field element integer representation as coordinates.\n     * \n     * @param group\n     * @param x\n     * @param y\n     * @throws IllegalArgumentException\n     */\n    public ECGroupElement(ECGroup group, BigInteger x, BigInteger y)\n            throws IllegalArgumentException {\n        this.group = group;\n        this.point = this.group.getPoint(x, y).normalize();\n        checkPoint();\n    }\n\n    /**\n     * Initialize using field elements as coordinates.\n     * \n     * @param group\n     * @param X\n     * @param Y\n     * @throws IllegalArgumentException\n     */\n    public ECGroupElement(ECGroup group, ECFieldElement X, ECFieldElement Y)\n            throws IllegalArgumentException {\n        this(group, X.toBigInteger(), Y.toBigInteger());\n    }\n\n    /**\n     * Initialize using serialized data.\n     * \n     * @param group\n     * @param data\n     * @throws IllegalArgumentException\n     */\n    public ECGroupElement(ECGroup group, byte[] data) throws IllegalArgumentException {\n        this.group = group;\n        BigInteger[] coords = getANSICoords(data);\n        if (coords == null) {\n            this.point = this.group.getInfinitePoint();\n        } else {\n            this.point = this.group.getPoint(coords[0], coords[1]);\n        }\n        checkPoint();\n    }\n\n    private void checkPoint() throws IllegalArgumentException {\n        if (!point.isValid()) {\n            throw new IllegalArgumentException(\"Point coordinates are not valid\");\n        }\n    }\n\n    private static BigInteger[] getANSICoords(byte[] encoded) throws IllegalArgumentException {\n        byte[] data;\n        Field f = new Field();\n        try {\n            f.readFromBytes(encoded);\n        } catch (ASN1DecodingException ex) {\n            throw new IllegalArgumentException(\"Illegal point encoding\");\n        }\n        try {\n            data = f.getBytes();\n        } catch (ASN1DecodingException ex) {\n            throw new IllegalArgumentException(\"Illegal data in point encoding\");\n        }\n\n        if (data.length == 1 && data[0] == 0x00) {\n            return null;\n        }\n        if (data.length % 2 != 1) {\n            throw new IllegalArgumentException(\n                    \"ANSI coordinates must consist of mode byte and coordinates\");\n        }\n        if (data[0] != 0x04) {\n            throw new IllegalArgumentException(\"Invalid point representation method\");\n        }\n        int coordLen = (data.length - 1) / 2;\n        BigInteger[] coords = new BigInteger[2];\n        coords[0] = new BigInteger(1, Arrays.copyOfRange(data, 1, coordLen + 1));\n        coords[1] = new BigInteger(1, Arrays.copyOfRange(data, coordLen + 1, 2 * coordLen + 1));\n        return coords;\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        return group.getOrder();\n    }\n\n    /**\n     * Add another point to this point and return the result.\n     * \n     * @param other\n     * @return\n     */\n    @Override\n    public GroupElement op(GroupElement other) throws MathException {\n        if (!this.group.equals(other.getGroup())) {\n            throw new MathException(\"Group elements from mismatching groups\");\n        }\n        ECGroupElement e = (ECGroupElement) other;\n        return new ECGroupElement(this.group, this.getPoint().add(e.getPoint()));\n    }\n\n    /**\n     * Scale the point with factor.\n     * \n     * @param factor\n     * @return\n     */\n    @Override\n    public GroupElement scale(BigInteger factor) {\n        return new ECGroupElement(this.group, this.getPoint().multiply(factor));\n    }\n\n    /**\n     * Return the inverse of the point.\n     * \n     * @return\n     */\n    @Override\n    public GroupElement inverse() {\n        return new ECGroupElement(this.group, this.getPoint().negate());\n    }\n\n    /**\n     * Serialize the point.\n     * <p>\n     * See X9-62 section 4.3.6 for EC point serialization algorithm. The point is then encoded as\n     * ASN1 OCTETSTRING\n     * \n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        // ref: https://www.security-audit.com/files/x9-62-09-20-98.pdf section 4.3.6\n        if (this.getPoint().isInfinity()) {\n            return new byte[] {0x00};\n        }\n        byte[] res = this.getPoint().getEncoded(false);\n        return new Field(res).encode();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ECGroupElement(%s, %s)\",\n                this.getPoint().getAffineXCoord().toBigInteger(),\n                this.getPoint().getAffineYCoord().toBigInteger());\n    }\n\n    @Override\n    public Group getGroup() {\n        return this.group;\n    }\n\n    /**\n     * Get the actual point of the element.\n     * \n     * @return\n     */\n    public ECPoint getPoint() {\n        return point;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ECGroupElement el = (ECGroupElement) other;\n        return this.group.equals(el.getGroup()) && this.getPoint().equals(el.getPoint());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.getPoint().hashCode() ^ this.getGroup().hashCode();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/Group.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.crypto.Plaintext;\nimport java.math.BigInteger;\n\n/**\n * Interface for defining a group.\n */\npublic abstract class Group {\n    /**\n     *\n     * Get name of a group.\n     * @return name of a group\n     */\n    public abstract String getName();\n\n    /**\n     * Get a group element from serialized representation.\n     * \n     * @param data\n     * @return\n     * @throws IllegalArgumentException\n     */\n    public abstract GroupElement getElement(byte[] data) throws IllegalArgumentException;\n\n    /**\n     * Get the order of the group.\n     * \n     * @return\n     */\n    public abstract BigInteger getOrder();\n\n    /**\n     * Get the order of the underlying field of the group.\n     * \n     * @return\n     */\n    public abstract BigInteger getFieldOrder();\n\n    /**\n     * Get the identity element of the group.\n     * \n     * @return\n     */\n    public abstract GroupElement getIdentity();\n\n    /**\n     * Encode a message as a group element.\n     * \n     * @param msg\n     * @return\n     * @throws MathException\n     */\n    public abstract GroupElement encode(Plaintext msg) throws MathException;\n\n    /**\n     * Pad the message to the full length.\n     * \n     * @param msg\n     * @return\n     */\n    public abstract Plaintext pad(Plaintext msg);\n\n    /**\n     * Unpad the message.\n     *\n     * @param msg\n     * @return\n     */\n    public abstract Plaintext unpad(Plaintext msg);\n\n    /**\n     * Check that group element can be decoded into a plaintext.\n     * \n     * @see Decodable\n     * \n     * @param el\n     * @return\n     */\n    public abstract Decodable isDecodable(GroupElement el);\n\n    /**\n     * Decode a group element as a plaintext message.\n     * \n     * @param msg\n     * @return\n     */\n    public abstract Plaintext decode(GroupElement msg);\n\n    /**\n     * Check that the element is a valid group element.\n     * \n     * @param el\n     * @return\n     */\n    public abstract Decodable isGroupElement(GroupElement el);\n\n    /**\n     * Serialize the group representation.\n     * \n     * @return\n     */\n    public abstract byte[] getBytes();\n\n    /**\n     * Values for decodability check result.\n     */\n    public enum Decodable {\n        VALID, INVALID_GROUP, INVALID_RANGE, INVALID_QR, INVALID_POINT;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/GroupElement.java",
    "content": "package ee.ivxv.common.math;\n\nimport java.math.BigInteger;\n\n/**\n * Interface for defining a group element and required supported operations.\n */\npublic abstract class GroupElement {\n    /**\n     * Get the order of the element.\n     * \n     * @return\n     */\n    public abstract BigInteger getOrder();\n\n    /**\n     * Perform a group operation with other group element.\n     * \n     * @param other\n     * @return\n     * @throws MathException\n     */\n    public abstract GroupElement op(GroupElement other) throws MathException;\n\n    /**\n     * Scale the element. Equivalent to invoking {@link #op(GroupElement)} factor times.\n     * \n     * @param factor\n     * @return\n     */\n    public abstract GroupElement scale(BigInteger factor);\n\n    /**\n     * Return the inverse element.\n     * \n     * @return\n     */\n    public abstract GroupElement inverse();\n\n    /**\n     * Get the group of the element.\n     * \n     * @return\n     */\n    public abstract Group getGroup();\n\n    /**\n     * Serialize the element.\n     * \n     * @return\n     */\n    public abstract byte[] getBytes();\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/IntegerConstructor.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport java.io.IOException;\nimport java.math.BigInteger;\n\n/**\n * Helper functions for constructing random integers.\n */\npublic class IntegerConstructor {\n    /**\n     * Return a uniformly distributed integer less than the limit.\n     * \n     * @param rnd\n     * @param upper\n     * @return\n     * @throws IllegalArgumentException\n     * @throws IOException\n     */\n    public static BigInteger construct(Rnd rnd, BigInteger upper)\n            throws IllegalArgumentException, IOException {\n        if (upper.signum() != 1) {\n            throw new IllegalArgumentException(\"Nonpositive limit\");\n        }\n        int noBytes = (upper.bitLength() + 7) / 8;\n        int maskLen = upper.bitLength() % 8;\n        maskLen = maskLen == 0 ? 8 : maskLen;\n\n        byte[] bytes = new byte[noBytes];\n        BigInteger n;\n        while (true) {\n            rnd.read(bytes, 0, bytes.length);\n            bytes[0] &= (byte) ((1 << maskLen) - 1);\n            n = new BigInteger(1, bytes);\n            if (n.compareTo(upper) < 0) {\n                return n;\n            }\n        }\n    }\n\n    /**\n     * Return a uniformly distributed prime less than the limit.\n     * \n     * @param rnd\n     * @param upper\n     * @return\n     * @throws IllegalArgumentException\n     * @throws IOException\n     */\n    public static BigInteger constructPrime(Rnd rnd, BigInteger upper)\n            throws IllegalArgumentException, IOException {\n        BigInteger res;\n        while (true) {\n            while (true) {\n                res = construct(rnd, upper);\n                if (res.isProbablePrime(20)) {\n                    // quickcheck\n                    break;\n                }\n            }\n            if (res.isProbablePrime(100)) {\n                // slow check\n                return res;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/LagrangeInterpolation.java",
    "content": "package ee.ivxv.common.math;\n\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Helper methods for interpolating a polynomial from evaluation points.\n */\npublic class LagrangeInterpolation {\n    /*\n     * interpolation is not required in the current context, so we omit the complexity of computing\n     * the coeficcients public static Polynomial interpolate() { }\n     */\n\n    /**\n     * Find the Lagrange basis polynomial.\n     * <p>\n     * Represent the list of points as {@literal (1, f(1)), ..., (n, f(n))}. Then compute the value\n     * {@literal prod_{i=1, i != base}^n (-1)^n f(i)/(base-f(i)) mod modulus}\n     * \n     * @param modulus\n     * @param points\n     * @param base\n     * @return\n     */\n    public static BigInteger basisPolynomial(BigInteger modulus, Object[] points, BigInteger base) {\n        List<BigInteger> realPoints = new ArrayList<>();\n        for (int i = 0; i < points.length; i++) {\n            if (points[i] != null) {\n                realPoints.add(BigInteger.valueOf(i + 1));\n            }\n        }\n        return genericBasisPolynomial(modulus,\n                realPoints.toArray(new BigInteger[realPoints.size()]), base);\n    }\n\n    private static BigInteger genericBasisPolynomial(BigInteger modulus, BigInteger[] points,\n            BigInteger base) throws IllegalArgumentException {\n        BigInteger nominator = new BigInteger(\"1\");\n        BigInteger denominator = new BigInteger(\"1\");\n        for (int i = 0; i < points.length; i++) {\n            if (base.compareTo(points[i]) != 0) {\n                nominator = nominator.multiply(points[i]).mod(modulus);\n                denominator = denominator.multiply(base.subtract(points[i])).mod(modulus);\n            }\n        }\n        if (points.length % 2 == 0) {\n            nominator = nominator.negate().mod(modulus);\n        }\n        return denominator.modInverse(modulus).multiply(nominator).mod(modulus);\n    }\n\n    /**\n     * Find the Lagrange basis polynomial.\n     * <p>\n     * Represent the list of points as {@literal (1, f(1)), ..., (n, f(n))}. Then compute the value\n     * {@literal prod_{i=1, i != base}^n (-1)^n f(i)/(base-f(i)) n!}\n     * \n     * @param points\n     * @param base\n     * @return\n     */\n    public static BigInteger basisInverselessPolynomial(Object[] points, BigInteger base) {\n        List<BigInteger> realPoints = new ArrayList<>();\n        for (int i = 0; i < points.length; i++) {\n            if (points[i] != null) {\n                realPoints.add(BigInteger.valueOf(i + 1));\n            }\n        }\n        return genericBasisInverselessPolynomial(points.length,\n                realPoints.toArray(new BigInteger[realPoints.size()]), base);\n    }\n\n    private static BigInteger genericBasisInverselessPolynomial(int pointLimit, BigInteger[] points,\n            BigInteger base) throws IllegalArgumentException {\n        BigInteger nominator = new BigInteger(\"1\");\n        BigInteger denominator = new BigInteger(\"1\");\n        for (int i = 0; i < points.length; i++) {\n            if (base.compareTo(points[i]) != 0) {\n                nominator = nominator.multiply(points[i]);\n                denominator = denominator.multiply(base.subtract(points[i]));\n            }\n        }\n        if (points.length % 2 == 0) {\n            nominator = nominator.negate();\n        }\n        // instead of finding inverse (which may not even exist), we multiply\n        // the nominator with factorial(pointLimit). The evaluation points are\n        // 1, ..., pointLimit, then as\n        // - |x_i - x_j| < pointLimit\n        // - |x_i - x_j| are distinct for j=1, ..., n\n        // then we can do integer division without losing information.\n        nominator = nominator.multiply(MathUtil.factorial(pointLimit));\n        return nominator.divide(denominator);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/MathException.java",
    "content": "package ee.ivxv.common.math;\n\n@SuppressWarnings(\"serial\")\npublic class MathException extends Exception {\n    public MathException(String string) {\n        super(string);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/MathUtil.java",
    "content": "package ee.ivxv.common.math;\n\nimport java.math.BigInteger;\n\n/**\n * Math helper methods.\n */\npublic class MathUtil {\n    /**\n     * Compute factorial\n     * \n     * @param f\n     * @return f!\n     */\n    public static BigInteger factorial(int f) {\n        return factorial(BigInteger.valueOf(f));\n    }\n\n    /**\n     * Compute factorial.\n     * \n     * @param f\n     * @return f!\n     */\n    public static BigInteger factorial(BigInteger f) {\n        if (f.compareTo(BigInteger.ONE) <= 0) {\n            return BigInteger.ONE;\n        }\n        BigInteger res = BigInteger.ONE;\n        while (f.compareTo(BigInteger.ONE) > 0) {\n            res = res.multiply(f);\n            f = f.subtract(BigInteger.ONE);\n        }\n        return res;\n    }\n\n    /**\n     * Find greatest common denominator of values\n     * \n     * @param v\n     * @return\n     */\n    public static BigInteger gcd(BigInteger... v) {\n        if (v.length == 0) {\n            return BigInteger.ZERO;\n        } else if (v.length == 1) {\n            return v[0];\n        }\n        BigInteger res = v[0];\n        for (int i = 1; i < v.length; i++) {\n            res = res.gcd(v[i]);\n        }\n        return res;\n    }\n\n    /**\n     * Find least common multiple of values.\n     * \n     * @param v\n     * @return\n     */\n    public static BigInteger lcm(BigInteger... v) {\n        if (v.length == 0) {\n            return BigInteger.ZERO;\n        } else if (v.length == 1) {\n            return v[0];\n        }\n        BigInteger p = v[0];\n        for (int i = 1; i < v.length; i++) {\n            p = p.multiply(v[i]);\n        }\n        BigInteger res = p.divide(gcd(v));\n        return res;\n    }\n\n    /**\n     * Perform extended Euclidian algorithm for a and b and output Bezout coefficients\n     * <p>\n     * See {@link https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode}. Finds s\n     * and t such that a*s + b*t = gcd(a,b).\n     * \n     * @param a\n     * @param b\n     * @return Array of two integers\n     */\n    public static BigInteger[] extendedEuclidean(BigInteger a, BigInteger b) {\n        /*-\n         * https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Pseudocode\n         * function extended_gcd(a, b)\n         * s := 0;    old_s := 1\n         * t := 1;    old_t := 0\n         * r := b;    old_r := a\n         * while r ≠ 0\n         *    quotient := old_r div r\n         *    (old_r, r) := (r, old_r - quotient * r)\n         *    (old_s, s) := (s, old_s - quotient * s)\n         *    (old_t, t) := (t, old_t - quotient * t)\n         * output \"Bézout coefficients:\", (old_s, old_t)\n         * output \"greatest common divisor:\", old_r\n         * output \"quotients by the gcd:\", (t, s)\n         */\n        BigInteger q, s, t, r, old_s, old_r, old_t, temp;\n        s = BigInteger.ZERO;\n        t = BigInteger.ONE;\n        r = b;\n        old_s = BigInteger.ONE;\n        old_t = BigInteger.ZERO;\n        old_r = a;\n        while (r.compareTo(BigInteger.ZERO) != 0) {\n            q = old_r.divide(r);\n\n            temp = r;\n            r = old_r.subtract(q.multiply(r));\n            old_r = temp;\n\n            temp = s;\n            s = old_s.subtract(q.multiply(s));\n            old_s = temp;\n\n            temp = t;\n            t = old_t.subtract(q.multiply(t));\n            old_t = temp;\n        }\n        return new BigInteger[] {old_s, old_t};\n    }\n\n    /**\n     * Compute Euler's phi for product of prime arguments.\n     * \n     * @param p\n     * @param q\n     * @return\n     */\n    public static BigInteger phiSemiprime(BigInteger p, BigInteger q) {\n        // compute Euler's phi for p*q. We assume that p, q prime.\n        return p.subtract(BigInteger.ONE).multiply(q.subtract(BigInteger.ONE));\n    }\n\n    /**\n     * Compute the Legendre symbol of of modulo p\n     * \n     * @param e\n     * @param p\n     * @return\n     */\n    public static int legendre(BigInteger e, BigInteger p) {\n        BigInteger q = safePrimeOrder(p);\n        BigInteger big = e.modPow(q, p);\n        if (big.compareTo(BigInteger.ONE) == 0) {\n            return 1;\n        } else if (big.compareTo(BigInteger.ZERO) == 0) {\n            return 0;\n        } else if (big.compareTo(p.subtract(BigInteger.ONE)) == 0) {\n            return -1;\n        }\n\n        // should not reach here, but return something to silence compiler\n        // warning\n        return 1;\n    }\n\n    /**\n     * Compute short Weierstrass curve equation f(x) = x^3 - 3x + b\n     * \n     * @param a\n     * @param b\n     * @param x\n     * @param p\n     * @return\n     */\n    public BigInteger weierstrass(BigInteger a, BigInteger b, BigInteger x, BigInteger p) {\n        BigInteger res = x.modPow(BigInteger.valueOf(2), p).add(a).multiply(x).mod(p).add(b).mod(p);\n        return res;\n    }\n\n    /**\n     * Compute the value (p-1)/2\n     * @param p\n     * @return\n     */\n    public static BigInteger safePrimeOrder(BigInteger p) {\n        return p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2));\n    }\n\n    /**\n     * Returns exact amount of bytes needed to fit bitsLen\n     * @param bitsLen\n     * @return\n     */\n    public static int toBytesLen(int bitsLen) {\n        // Loose fraction, we only interested in the whole part of a number\n        return (int) Math.ceil((double) bitsLen / 8);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ModPGroup.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport org.bouncycastle.math.raw.Mod;\nimport org.bouncycastle.pqc.math.linearalgebra.IntegerFunctions;\n\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.Arrays;\n\n/**\n * Group of integers modulo a safe prime.\n */\npublic class ModPGroup extends Group {\n    private final BigInteger p;\n    private BigInteger q;\n    private final ModPGroupElement one;\n\n    /**\n     * Initialize group using a safe prime modulus.\n     * <p>\n     * This constructor does not verify that modulus is a safe prime.\n     * \n     * @param p\n     */\n    public ModPGroup(BigInteger p) {\n        // we assume p=2*q+1 with q prime\n        this(p, false);\n    }\n\n    /**\n     * Initialize group using a safe prime modulus and verify that the modulus is a safe prime.\n     * \n     * @param p\n     * @param verify\n     * @throws IllegalArgumentException When modulus is not safe prime.\n     */\n    public ModPGroup(BigInteger p, boolean verify) throws IllegalArgumentException {\n        if (verify) {\n            if (!p.isProbablePrime(80)) {\n                throw new IllegalArgumentException(\n                        \"Multiplicative order is not p=2*1+1 with q prime\");\n            }\n        }\n        this.p = p;\n        this.one = new ModPGroupElement(this, BigInteger.ONE);\n    }\n\n    /**\n     * Initialize group using serialized group.\n     * \n     * @see #getBytes()\n     * \n     * @param data\n     * @throws IllegalArgumentException When can not parse\n     */\n    public ModPGroup(byte[] data) throws IllegalArgumentException {\n        Field f = new Field();\n        try {\n            f.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing byte array failed: \" + e);\n        }\n        try {\n            this.p = f.getInteger();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing integer failed: \" + e);\n        }\n        this.one = new ModPGroupElement(this, BigInteger.ONE);\n    }\n\n    /**\n     * Generate a safe prime and initialize the group.\n     * <p>\n     * Use the random source for obtaining modulus candidates. The tries argument defines the number\n     * of tries to repeat. When tries is set to 0, then test candidate values until a suitable\n     * modulus is found.\n     * \n     * @param len\n     * @param rnd\n     * @param tries\n     * @throws IllegalArgumentException When len is not a positive value.\n     * @throws IOException When exception during a read from random source.\n     * @throws MathException When couldn't find a safe prime within tries\n     */\n    public ModPGroup(int len, Rnd rnd, int tries)\n            throws IllegalArgumentException, IOException, MathException {\n        int sglen = len - 1;\n        BigInteger bigTwo = new BigInteger(\"2\");\n        BigInteger genp, genq;\n\n        // For performance reasons, we first test primes lightly.\n        // If both pass, test them thoroughly.\n        for (int i = 0; tries == 0 || i < tries; i++) {\n            genq = IntegerConstructor.construct(rnd, bigTwo.pow(sglen).subtract(BigInteger.ONE));\n            if (!genq.isProbablePrime(2)) {\n                continue;\n            }\n            genp = genq.multiply(bigTwo).add(BigInteger.ONE);\n            if (genp.bitLength() != len || !genp.isProbablePrime(2)) {\n                continue;\n            }\n            if (genp.isProbablePrime(80) && genq.isProbablePrime(80)) {\n                p = genp;\n                q = genq;\n                one = new ModPGroupElement(this, BigInteger.ONE);\n                return;\n            }\n        }\n        throw new MathException(\"Could not generate group parameters during tries\");\n    }\n\n    @Override\n    public String getName() {\n        return \"ModSafePrime\";\n    }\n\n    @Override\n    public GroupElement getElement(byte[] data) throws IllegalArgumentException {\n        return new ModPGroupElement(this, data);\n    }\n\n    /**\n     * Sample a random element from the group.\n     * \n     * @param rnd\n     * @return\n     * @throws IOException\n     */\n    public ModPGroupElement getRandomElement(Rnd rnd) throws IOException {\n        BigInteger template = IntegerConstructor.construct(rnd, getOrder());\n        // square the value to make sure it is quadratic residue\n        template = template.modPow(BigInteger.valueOf(2), getOrder());\n        return new ModPGroupElement(this, template);\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        return this.p;\n    }\n\n    @Override\n    public BigInteger getFieldOrder() {\n        return this.p;\n    }\n\n    @Override\n    public GroupElement getIdentity() {\n        return this.one;\n    }\n\n    private BigInteger getMultiplicativeGroupOrder() {\n        if (this.q == null) {\n            this.q = MathUtil.safePrimeOrder(this.p);\n        }\n        return this.q;\n    }\n\n    private int msgBits() {\n        return getMultiplicativeGroupOrder().bitLength();\n    }\n\n    private int msgBytes() {\n        return (msgBits() + 7) / 8;\n    }\n\n    @Override\n    public Plaintext pad(Plaintext msg) {\n        if (msg.padded) {\n            return msg;\n        }\n\n        byte[] padded = msg.getMessage();\n        int totalBytes = msgBytes();\n\n        if (padded.length > totalBytes - 3) {\n            throw new IllegalArgumentException(\"Padded message length too long\");\n        }\n        byte[] padded2 = new byte[totalBytes];\n        int i;\n        padded2[0] = 0x00;\n        padded2[1] = 0x01;\n        padded2[totalBytes - padded.length - 1] = 0x00;\n        for (i = 2; i < totalBytes - padded.length - 1; i++) {\n            padded2[i] = (byte) 0xff;\n        }\n        System.arraycopy(padded, 0, padded2, i + 1, padded.length);\n        return new Plaintext(padded2, true);\n    }\n\n    @Override\n    public Plaintext unpad(Plaintext msg) {\n        if (!msg.padded) {\n            return msg;\n        }\n\n        byte[] padded = msg.getMessage();\n\n        if (padded.length < 3) {\n            throw new IllegalArgumentException(\"Source message can not contain padding\");\n        }\n        if (padded[0] != 0x00 || padded[1] != 0x01) {\n            throw new IllegalArgumentException(\"Incorrect padding head\");\n        }\n        for (int i = 2; i < padded.length; i++) {\n            switch (padded[i]) {\n                case 0:\n                    // found padding end\n                    return new Plaintext(Arrays.copyOfRange(padded, i + 1, padded.length),\n                            false);\n                case (byte) 0xff:\n                    continue;\n                default:\n                    // incorrect padding byte\n                    throw new IllegalArgumentException(\"Incorrect padding byte\");\n            }\n        }\n        throw new IllegalArgumentException(\"Padding unexpected\");\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ModPGroup o = (ModPGroup) other;\n\n        return this.getOrder().equals(o.getOrder());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.getOrder().hashCode() ^ 0x0002;\n    }\n\n    private int legendre(BigInteger e) {\n        return MathUtil.legendre(e, getOrder());\n    }\n\n    /**\n     * Encode the message as a quadratic residue.\n     * <p>\n     * If the value is quadratic residue, then return the message encoded as integer. Otherwise,\n     * return modulus-msg encoded as integer.\n     * \n     * @param msg\n     * @return\n     * @throws IllegalArgumentException is value is not less than half the order size or greater\n     *         than 0\n     * @throws MathException When computation fails.\n     */\n    @Override\n    public GroupElement encode(Plaintext msg) throws IllegalArgumentException, MathException {\n        BigInteger m = msg.toBigInteger();\n        if (m.compareTo(BigInteger.ZERO) <= 0) {\n            throw new IllegalArgumentException(\"Value to be encoded is less than 0\");\n        }\n        if (m.compareTo(getMultiplicativeGroupOrder()) > 0) {\n            throw new IllegalArgumentException(\"Value to be encoded is larger equal than order/2\");\n        }\n        switch (legendre(m)) {\n            case -1:\n                return new ModPGroupElement(this, getOrder().subtract(m));\n            case 1:\n                return new ModPGroupElement(this, m);\n            default:\n                // Legendre's function only takes values -1,0,1\n                throw new IllegalArgumentException(\"Can not encode 0 as quadratic residue\");\n        }\n    }\n\n    @Override\n    public Decodable isDecodable(GroupElement el) {\n        // Is valid group element\n        Decodable isValid = isGroupElement(el);\n        if (isValid != Decodable.VALID)\n            return isValid;\n\n        // Padded plaintext should be exactly of a group generator order bytes length - 1\n        if (decode((ModPGroupElement) el) == null) {\n            return Decodable.INVALID_RANGE;\n        }\n\n        return Decodable.VALID;\n    }\n\n    /**\n     * Decodes group element into BigInteger equivalent, or null if group element is not decodable.\n     * @param encoded\n     * @return\n     */\n    private BigInteger decode(ModPGroupElement encoded) {\n        BigInteger e = encoded.getValue();\n\n        BigInteger decoded =\n                e.compareTo(getMultiplicativeGroupOrder()) > 0 ? getOrder().subtract(e) : e;\n\n        // Java strips leading 0x00 padding byte that's why \"-1\"\n        if (MathUtil.toBytesLen(decoded.bitLength()) != MathUtil.toBytesLen(this.getMultiplicativeGroupOrder().bitLength()) - 1) {\n            return null;\n        }\n        return decoded;\n    }\n    /**\n     * Decode group element as a message.\n     * <p>\n     * If the value is larger than order/2, then return order-el.\n     * \n     * @param el\n     * @return\n     */\n    @Override\n    public Plaintext decode(GroupElement el) {\n        BigInteger decoded = decode((ModPGroupElement) el);\n        if (decoded == null) {\n            throw new IllegalArgumentException(\"Element is not decodable\");\n        }\n\n        Plaintext padded = new Plaintext(decoded, msgBits(), true);\n        if ((padded.getMessage().length) != MathUtil.toBytesLen(this.getFieldOrder().bitLength())) {\n            throw new IllegalArgumentException(\"Message not padded to correct length\");\n        }\n        return padded;\n\n    }\n\n    @Override\n    public Decodable isGroupElement(GroupElement el) {\n        // Element belongs to group\n        if (!this.equals(el.getGroup())) {\n            return Decodable.INVALID_GROUP;\n        }\n\n        // Element value > 0\n        BigInteger e = ((ModPGroupElement) el).getValue();\n        if (e.compareTo(BigInteger.ZERO) <= 0) {\n            return Decodable.INVALID_RANGE;\n        }\n\n        // Element value < group field order\n        if (e.compareTo(getOrder()) > 0) {\n            return Decodable.INVALID_RANGE;\n        }\n\n        // Is quadratic residue\n        if (IntegerFunctions.jacobi(((ModPGroupElement)el).getValue(), this.getFieldOrder()) != 1) {\n            return Decodable.INVALID_QR;\n        }\n\n        return Decodable.VALID;\n    }\n\n    /**\n     * Serialize group.\n     * <p>\n     * Returns group modulus as ASN1 INTEGER\n     * \n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        return new Field(getOrder()).encode();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ModPGroup(%s)\", getOrder());\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ModPGroupElement.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport java.math.BigInteger;\n\n/**\n * ModPGroupElement is an integer modulo a safe prime.\n */\npublic class ModPGroupElement extends GroupElement {\n    private final ModPGroup group;\n    private final BigInteger value;\n    private BigInteger order;\n\n    /**\n     * Initialize using group and integer value.\n     * \n     * @param group\n     * @param value\n     * @throws IllegalArgumentException\n     */\n    public ModPGroupElement(ModPGroup group, BigInteger value) throws IllegalArgumentException {\n        this.group = group;\n        this.value = value;\n    }\n\n    /**\n     * Initialize using group and serialized value.\n     * \n     * @see #getBytes()\n     * \n     * @param group\n     * @param data\n     * @throws IllegalArgumentException\n     */\n    public ModPGroupElement(ModPGroup group, byte[] data) throws IllegalArgumentException {\n        Field f = new Field();\n        try {\n            f.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing byte array failed: \" + e);\n        }\n        this.group = group;\n        try {\n            this.value = f.getInteger();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing integer failed: \" + e);\n        }\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        if (order == null) {\n            try {\n                order = computeOrder();\n            } catch (MathException e) {\n                throw new IllegalArgumentException(\"Computing multiplicative order failed: \" + e);\n            }\n        }\n        return this.order;\n    }\n\n    private BigInteger computeOrder() throws MathException {\n        // we assume a group with p = 2*q + 1. The order divides p - 1, thus it\n        // is either 1, 2, q or p-1.\n        BigInteger[] possible = new BigInteger[] {BigInteger.ONE, BigInteger.valueOf(2),\n                group.getOrder().subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)),\n                group.getOrder().subtract(BigInteger.ONE)};\n        for (BigInteger p : possible) {\n            if (value.modPow(p, group.getOrder()).equals(BigInteger.ONE)) {\n                return p;\n            }\n        }\n        throw new MathException(\"Invalid group parameters\");\n    }\n\n    /**\n     * Multiply this value with other.\n     * \n     * @param other\n     * @return\n     * @throws MathException When other element is from different group.\n     */\n    @Override\n    public GroupElement op(GroupElement other) throws MathException {\n        if (!this.group.equals(other.getGroup())) {\n            throw new MathException(\"Group elements from mismatching groups\");\n        }\n        ModPGroupElement o = (ModPGroupElement) other;\n        return new ModPGroupElement(this.group,\n                this.getValue().multiply(o.getValue()).mod(this.group.getOrder()));\n    }\n\n    /**\n     * Exponentiate the value.\n     * \n     * @param factor\n     * @return\n     */\n    @Override\n    public GroupElement scale(BigInteger factor) {\n        try {\n            return new ModPGroupElement(this.group,\n                    this.getValue().modPow(factor, this.group.getOrder()));\n        } catch (IllegalArgumentException e) {\n            // this can not happen as the exception is only thrown when\n            // computing element order fails. this happens only if group\n            // parameters are incorrect. however, the parameters are\n            // instantiated from this element's parameters.\n        }\n        return null;\n    }\n\n    /**\n     * Find the modular inverse of the value.\n     * \n     * @return\n     */\n    @Override\n    public GroupElement inverse() {\n        try {\n            return new ModPGroupElement(this.group,\n                    this.getValue().modInverse(this.group.getOrder()));\n        } catch (IllegalArgumentException e) {\n            // this can not happen as the exception is only thrown when\n            // computing element order fails. this happens only if group\n            // parameters are incorrect. however, the parameters are\n            // instantiated from this element's parameters.\n        }\n        return null;\n    }\n\n    /**\n     * @return P - this\n     */\n    public GroupElement negate() {\n        return new ModPGroupElement(this.group, this.group.getFieldOrder().subtract(this.value));\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ModPGroupElement o = (ModPGroupElement) other;\n        return this.group.equals(o.group) && this.getValue().equals(o.getValue());\n    }\n\n    @Override\n    public int hashCode() {\n        return this.group.hashCode() ^ this.getValue().hashCode();\n    }\n\n    /**\n     * Get the integer value of the element.\n     * \n     * @return\n     */\n    public BigInteger getValue() {\n        return this.value;\n    }\n\n    /**\n     * Serialize the value\n     * <p>\n     * Returns the element as ASN1 INTEGER\n     *\n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        return new Field(value).encode();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"ModPGroupElement(%s)\", value);\n    }\n\n    @Override\n    public Group getGroup() {\n        return this.group;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/Polynomial.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport java.io.IOException;\nimport java.math.BigInteger;\n\n/**\n * Polynomial holds and evaluates polynomials over integers modulo a value.\n */\npublic class Polynomial {\n    private final BigInteger[] coefs;\n    private final BigInteger modulus;\n\n    /**\n     * Initialize using a modulus and coefficients.\n     * \n     * @param modulus\n     * @param coefs\n     * @throws IllegalArgumentException\n     */\n    public Polynomial(BigInteger modulus, BigInteger[] coefs) throws IllegalArgumentException {\n        verifyModulus(modulus);\n        this.modulus = modulus;\n        this.coefs = coefs;\n    }\n\n    /**\n     * Generate a random polynomial.\n     * \n     * @param degree\n     * @param modulus\n     * @param rnd\n     * @throws IllegalArgumentException When modulus is not positive\n     * @throws IOException When reading from random source fails.\n     */\n    public Polynomial(int degree, BigInteger modulus, Rnd rnd)\n            throws IllegalArgumentException, IOException {\n        if (degree < 0) {\n            throw new IllegalArgumentException(\"Degree of a polynomial must be non-negative\");\n        }\n        verifyModulus(modulus);\n        this.modulus = modulus;\n        this.coefs = new BigInteger[degree + 1];\n        for (int i = 0; i <= degree; i++) {\n            this.coefs[i] = IntegerConstructor.construct(rnd, modulus);\n        }\n    }\n\n    /**\n     * Generate a random polynomial with defined free coefficient.\n     * \n     * @param degree\n     * @param modulus\n     * @param free\n     * @param rnd\n     * @throws IllegalArgumentException\n     * @throws IOException\n     */\n    public Polynomial(int degree, BigInteger modulus, BigInteger free, Rnd rnd)\n            throws IllegalArgumentException, IOException {\n        if (degree < 0) {\n            throw new IllegalArgumentException(\"Degree of a polynomial must be non-negative\");\n        }\n        verifyModulus(modulus);\n        this.modulus = modulus;\n        this.coefs = new BigInteger[degree + 1];\n        this.coefs[0] = free;\n        for (int i = 1; i <= degree; i++) {\n            this.coefs[i] = IntegerConstructor.construct(rnd, modulus);\n        }\n    }\n\n    // throw an exception if the modulus is not permitted\n    static void verifyModulus(BigInteger modulus) throws IllegalArgumentException {\n        if (modulus.compareTo(BigInteger.ZERO) <= 0) {\n            throw new IllegalArgumentException(\"Modulus must be positive\");\n        }\n    }\n\n    /**\n     * Evaluate the polynomial at a value and return the result\n     * \n     * @param arg\n     * @return\n     */\n    public BigInteger evaluate(BigInteger arg) {\n        BigInteger res = BigInteger.ZERO;\n        for (int i = 0; i < this.coefs.length; i++) {\n            res = res.add(evaluateMonomial(arg, i)).mod(this.modulus);\n        }\n        return res;\n    }\n\n    /**\n     * Evaluate the polynomial at a value and return the result.\n     * \n     * @param arg\n     * @return\n     */\n    public BigInteger evaluate(int arg) {\n        return evaluate(BigInteger.valueOf(arg));\n    }\n\n    private BigInteger evaluateMonomial(BigInteger arg, int degree) {\n        if (degree >= this.coefs.length) {\n            return BigInteger.ZERO;\n        }\n        return arg.modPow(BigInteger.valueOf(degree), this.modulus).multiply(this.coefs[degree])\n                .mod(this.modulus);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ProductGroup.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.asn1.Sequence;\nimport ee.ivxv.common.crypto.Plaintext;\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * ProductGroup is a direct product of several groups.\n */\npublic class ProductGroup extends Group {\n    private static final String NAME_MODP = \"ModPGroup\";\n    private static final String NAME_EC = \"EllipticCurve\";\n\n    private final Group[] groups;\n\n    /**\n     * Initialize using groups.\n     * \n     * @param groups\n     */\n    public ProductGroup(Group... groups) {\n        this.groups = groups;\n    }\n\n    /**\n     * Initialize using a group with multiplicity.\n     * \n     * @param group\n     * @param multiplicity\n     */\n    public ProductGroup(Group group, int multiplicity) {\n        this.groups = new Group[multiplicity];\n        for (int i = 0; i < multiplicity; i++) {\n            this.groups[i] = group;\n        }\n    }\n\n    /**\n     * Initialize from serialized value.\n     * \n     * @param data\n     * @throws IllegalArgumentException When can not parse.\n     */\n    public ProductGroup(byte[] data) throws IllegalArgumentException {\n        Sequence s = new Sequence();\n        try {\n            s.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing data failed\", e);\n        }\n        byte[][] encoded;\n        try {\n            encoded = s.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing values failed\", e);\n        }\n        Group[] groups = new Group[encoded.length];\n        for (int i = 0; i < encoded.length; i++) {\n            Sequence ss = new Sequence();\n            try {\n                ss.readFromBytes(encoded[i]);\n            } catch (ASN1DecodingException e) {\n                throw new IllegalArgumentException(\"Parsing single value failed\", e);\n            }\n            byte[][] value;\n            try {\n                value = ss.getBytes();\n            } catch (ASN1DecodingException e) {\n                throw new IllegalArgumentException(\"Invalid single value\", e);\n            }\n            if (value.length != 2) {\n                throw new IllegalArgumentException(\"Invalid value length\");\n            }\n            Field f = new Field();\n            try {\n                f.readFromBytes(value[0]);\n            } catch (ASN1DecodingException e) {\n                throw new IllegalArgumentException(\"Can not read group type\", e);\n            }\n            String type;\n            try {\n                type = f.getString();\n            } catch (ASN1DecodingException e) {\n                throw new IllegalArgumentException(\"Can not decode group type as string\", e);\n            }\n            if (type.equals(NAME_MODP)) {\n                groups[i] = new ModPGroup(value[1]);\n            } else if (type.equals(NAME_EC)) {\n                groups[i] = new ECGroup(value[1]);\n            } else {\n                throw new IllegalArgumentException(\"Unknown group type\");\n            }\n        }\n        this.groups = groups;\n    }\n\n    @Override\n    public String getName() {\n        return \"ProductGroup\";\n    }\n\n    @Override\n    public GroupElement getElement(byte[] data) throws IllegalArgumentException {\n        return new ProductGroupElement(this, data);\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        BigInteger[] orders = new BigInteger[groups.length];\n        for (int i = 0; i < orders.length; i++) {\n            orders[i] = groups[i].getOrder();\n        }\n        BigInteger order = MathUtil.lcm(orders);\n        return order;\n    }\n\n    @Override\n    public BigInteger getFieldOrder() {\n        BigInteger[] orders = new BigInteger[groups.length];\n        for (int i = 0; i < orders.length; i++) {\n            orders[i] = groups[i].getFieldOrder();\n        }\n        BigInteger order = MathUtil.lcm(orders);\n        return order;\n    }\n\n    @Override\n    public GroupElement getIdentity() {\n        GroupElement[] identities = new GroupElement[groups.length];\n        for (int i = 0; i < identities.length; i++) {\n            identities[i] = groups[i].getIdentity();\n        }\n        ProductGroupElement identity = new ProductGroupElement(this, identities);\n        return identity;\n    }\n\n    @Override\n    public GroupElement encode(Plaintext msg) throws MathException {\n        // this method does not make sense\n        throw new RuntimeException(\"Invalid use of ProductGroup\");\n    }\n\n    @Override\n    public Plaintext pad(Plaintext msg) {\n        // this method does not make sense\n        throw new RuntimeException(\"Invalid use of ProductGroup\");\n    }\n\n    @Override\n    public Plaintext unpad(Plaintext msg) {\n        // this method does not make sense\n        throw new RuntimeException(\"Invalid use of ProductGroup\");\n    }\n\n    @Override\n    public Decodable isDecodable(GroupElement el) {\n        // this method does not make sense\n        throw new RuntimeException(\"Invalid use of ProductGroup\");\n    }\n\n    @Override\n    public Plaintext decode(GroupElement msg) {\n        // this method does not make sense\n        throw new RuntimeException(\"Invalid use of ProductGroup\");\n    }\n\n    @Override\n    public Decodable isGroupElement(GroupElement el) {\n        if (this.equals(el.getGroup()))\n            return Decodable.VALID;\n        return Decodable.INVALID_GROUP;\n    }\n\n    /**\n     * Serialize the value\n     * <p>\n     * Returns ASN1 SEQUENCE of serialized values of the underlying groups.\n     * \n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        byte[][] encoded = new byte[groups.length][];\n        for (int i = 0; i < groups.length; i++) {\n            byte[] groupBytes = groups[i].getBytes();\n            Field f;\n            if (groups[i] instanceof ModPGroup) {\n                f = new Field(NAME_MODP);\n            } else if (groups[i] instanceof ECGroup) {\n                f = new Field(NAME_EC);\n            } else {\n                throw new RuntimeException(\"Invalid group\");\n            }\n            Sequence s = new Sequence(f.encode(), groupBytes);\n            encoded[i] = s.encode();\n        }\n        Sequence ret = new Sequence(encoded);\n        return ret.encode();\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ProductGroup og = (ProductGroup) other;\n        if (groups.length != og.groups.length) {\n            return false;\n        }\n        for (int i = 0; i < groups.length; i++) {\n            if (!groups[i].equals(og.groups[i])) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    @Override\n    public String toString() {\n        List<String> strings = new ArrayList<String>(groups.length);\n        for (int i = 0; i < groups.length; i++) {\n            strings.add(i, groups[i].toString());\n        }\n        String ret = String.format(\"ProductGroup(%s)\", String.join(\",\", strings));\n        return ret;\n    }\n\n    /**\n     * Get the number of groups in the ProductGroup.\n     * \n     * @return\n     */\n    public int getLength() {\n        return groups.length;\n    }\n\n    /**\n     * Get the groups used to construct the ProductGroup.\n     * \n     * @return\n     */\n    public Group[] getGroups() {\n        return this.groups;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/math/ProductGroupElement.java",
    "content": "package ee.ivxv.common.math;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Sequence;\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * ProductGroupElement is a direct product of group elements\n */\npublic class ProductGroupElement extends GroupElement {\n    private final ProductGroup group;\n    private GroupElement[] elements;\n\n    /**\n     * Initialize using a ProductGroup and corresponding group elements.\n     * \n     * @param group\n     * @param elements\n     */\n    public ProductGroupElement(ProductGroup group, GroupElement... elements) {\n        this.group = group;\n        this.elements = elements;\n    }\n\n    /**\n     * Initialize using a ProductGroup and uninitalized group elements.\n     * \n     * @param group\n     */\n    public ProductGroupElement(ProductGroup group) {\n        this.group = group;\n        this.elements = new GroupElement[group.getGroups().length];\n    }\n\n    /**\n     * Initialize using a ProductGroup and serialized data.\n     * \n     * @param group\n     * @param data\n     * @throws IllegalArgumentException When parsing fails\n     */\n    public ProductGroupElement(ProductGroup group, byte[] data) throws IllegalArgumentException {\n        this.group = group;\n        Sequence s = new Sequence();\n        try {\n            s.readFromBytes(data);\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Parsing byte array failed\", e);\n        }\n        byte[][] bs;\n        try {\n            bs = s.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new IllegalArgumentException(\"Invalid input byte array\", e);\n        }\n        if (bs.length != group.getLength()) {\n            throw new IllegalArgumentException(\"Input data does not correspond to groups\");\n        }\n        elements = new GroupElement[bs.length];\n        for (int i = 0; i < bs.length; i++) {\n            elements[i] = group.getGroups()[i].getElement(bs[i]);\n        }\n    }\n\n    @Override\n    public BigInteger getOrder() {\n        BigInteger[] orders = new BigInteger[elements.length];\n        for (int i = 0; i < orders.length; i++) {\n            orders[i] = elements[i].getOrder();\n        }\n        BigInteger order = MathUtil.lcm(orders);\n        return order;\n    }\n\n    /**\n     * Perform pointwise operation with other element and return the result.\n     * \n     * @param other\n     * @return\n     * @throws MatchException When group elements are from different groups.\n     */\n    @Override\n    public GroupElement op(GroupElement other) throws MathException {\n        if (!this.group.equals(other.getGroup())) {\n            throw new MathException(\"Group elements from mismatching groups\");\n        }\n        ProductGroupElement el = (ProductGroupElement) other;\n        GroupElement[] reselements = new GroupElement[elements.length];\n        for (int i = 0; i < reselements.length; i++) {\n            reselements[i] = elements[i].op(el.elements[i]);\n        }\n        ProductGroupElement res = new ProductGroupElement(this.group, reselements);\n        return res;\n    }\n\n    /**\n     * Scale all direct product elements with a scale factor and return the result.\n     * \n     * @param factor\n     * @return\n     */\n    @Override\n    public GroupElement scale(BigInteger factor) {\n        GroupElement[] reselements = new GroupElement[elements.length];\n        for (int i = 0; i < reselements.length; i++) {\n            reselements[i] = elements[i].scale(factor);\n        }\n        ProductGroupElement res = new ProductGroupElement(this.group, reselements);\n        return res;\n    }\n\n    /**\n     * Scale all direct product elements with corresponding scale factor and return the result.\n     * \n     * @param factors\n     * @return\n     */\n    public ProductGroupElement scale(BigInteger[] factors) {\n        GroupElement[] reselements = new GroupElement[elements.length];\n        for (int i = 0; i < reselements.length; i++) {\n            reselements[i] = elements[i].scale(factors[i]);\n        }\n        ProductGroupElement res = new ProductGroupElement(this.group, reselements);\n        return res;\n    }\n\n    /**\n     * Inverse all direct product elements and return the result.\n     * \n     * @return\n     */\n    @Override\n    public GroupElement inverse() {\n        GroupElement[] inverses = new GroupElement[elements.length];\n        for (int i = 0; i < inverses.length; i++) {\n            inverses[i] = elements[i].inverse();\n        }\n        ProductGroupElement ret = new ProductGroupElement(this.group, inverses);\n        return ret;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n        if (other == this) {\n            return true;\n        }\n        ProductGroupElement o = (ProductGroupElement) other;\n        if (getElements().length != o.getElements().length) {\n            return false;\n        }\n        for (int i = 0; i < getElements().length; i++) {\n            if (!getElements()[i].equals(o.getElements()[i]))\n                return false;\n        }\n        return true;\n    }\n\n    @Override\n    public Group getGroup() {\n        return this.group;\n    }\n\n    /**\n     * Serialize element\n     * <p>\n     * Serializes all direct product elements and returns ASN1 SEQUENCE of them.\n     * \n     * @return\n     */\n    @Override\n    public byte[] getBytes() {\n        byte[][] encoded = new byte[elements.length][];\n        for (int i = 0; i < encoded.length; i++) {\n            encoded[i] = elements[i].getBytes();\n        }\n        Sequence s = new Sequence(encoded);\n        byte[] ret = s.encode();\n        return ret;\n    }\n\n    @Override\n    public String toString() {\n        List<String> strings = new ArrayList<String>(elements.length);\n        for (int i = 0; i < elements.length; i++) {\n            strings.add(i, elements[i].toString());\n        }\n        String ret = String.format(\"ProductGroupElement(%s)\", String.join(\",\", strings));\n        return ret;\n    }\n\n    /**\n     * Get the corresponding direct product elements.\n     * \n     * @return\n     */\n    public GroupElement[] getElements() {\n        return this.elements;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/AnonymousBallotBox.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class AnonymousBallotBox implements IBallotBox {\n\n    private final String election;\n    private final Map<String, Map<String, Map<String, List<byte[]>>>> districts;\n\n    @JsonCreator\n    public AnonymousBallotBox( //\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"districts\") Map<String, Map<String, Map<String, List<byte[]>>>> districts) {\n        this.election = election;\n        this.districts = Collections.unmodifiableMap(districts);\n    }\n\n    @Override\n    public String getElection() {\n        return election;\n    }\n\n    @Override\n    @JsonIgnore\n    public Type getType() {\n        return Type.ANONYMIZED;\n    }\n\n    /**\n     * @return Returns the number of votes, because ballots are unknown quantity here.\n     */\n    @Override\n    @JsonIgnore\n    public int getNumberOfBallots() {\n        return districts.values().stream().mapToInt( //\n                s -> s.values().stream()\n                        .mapToInt(q -> q.values().stream().mapToInt(b -> b.size()).sum()).sum())\n                .sum();\n    }\n\n    @JsonIgnore\n    public int getNumberOfQuestions() {\n        Map<String, Boolean> res = new HashMap<>();\n        districts.forEach((d, sMap) -> sMap\n                .forEach((s, qMap) -> qMap.keySet().forEach((q) -> res.putIfAbsent(q, true))));\n        return res.size();\n    }\n\n    /**\n     * @return Returns a map from district id to a map from station id to a map from question id to\n     *         list of encrypted votes.\n     */\n    public Map<String, Map<String, Map<String, List<byte[]>>>> getDistricts() {\n        return districts;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/Ballot.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.time.Instant;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Ballot represents the choices on a single ballot.\n */\npublic class Ballot {\n\n    private final String id;\n    private final Instant time;\n    private final String version;\n    private final Voter voter;\n    private final Map<String, byte[]> votes;\n    private boolean isInvalid;\n\n    @JsonCreator\n    public Ballot( //\n            @JsonProperty(\"id\") String id, //\n            @JsonProperty(\"time\") String time, //\n            @JsonProperty(\"version\") String version, //\n            @JsonProperty(\"name\") String name, //\n            @JsonProperty(\"districtId\") String districtId, //\n            @JsonProperty(\"parish\") String parishId, //\n            @JsonProperty(\"votes\") Map<String, byte[]> votes) {\n        this(id, Instant.parse(time), version,\n                new Voter(null, name, null, parishId, new LName(districtId)), votes);\n    }\n\n    public Ballot(String id, Instant time, String version, Voter voter, Map<String, byte[]> votes) {\n        this.id = id;\n        this.time = time;\n        this.version = version;\n        this.voter = voter;\n        this.votes = Collections.unmodifiableMap(new LinkedHashMap<>(votes));\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public Instant getTime() {\n        return time;\n    }\n\n    public String getVersion() {\n        return version;\n    }\n\n    public String getName() {\n        return voter.getName();\n    }\n\n    @JsonIgnore\n    public LName getDistrict() {\n        return voter.getDistrict();\n    }\n\n    public String getDistrictId() {\n        return voter.getDistrict().getId();\n    }\n\n    public String getParish() {\n        return voter.getParish();\n    }\n\n    /**\n     * @return Returns the map from question id to the encrypted vote\n     */\n    public Map<String, byte[]> getVotes() {\n        return votes;\n    }\n\n    boolean isInvalid() {\n        return isInvalid;\n    }\n\n    /**\n     * Sets the invalid status of the ballot if the status is not already the same as the parameter.\n     *\n     * @param isInvalid\n     * @return Returns whether the operation was successful.\n     */\n    boolean setInvalidState(boolean isInvalid) {\n        if (this.isInvalid == isInvalid) {\n            return false;\n        }\n        this.isInvalid = isInvalid;\n        return true;\n    }\n\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/BallotBox.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n *\n */\npublic class BallotBox implements IBallotBox {\n\n    private final String election;\n    private Type type;\n    private final Map<String, VoterBallots> ballots = new LinkedHashMap<>();\n\n    public BallotBox(String election, Map<String, VoterBallots> ballots) {\n        this(election, Type.INTEGRITY_CONTROLLED, ballots);\n    }\n\n    @JsonCreator\n    public BallotBox( //\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"type\") Type type, //\n            @JsonProperty(\"ballots\") Map<String, VoterBallots> ballots) {\n        this.election = election;\n        this.type = type;\n        this.ballots.putAll(ballots);\n    }\n\n    @Override\n    public String getElection() {\n        return election;\n    }\n\n    @Override\n    public Type getType() {\n        return type;\n    }\n\n    /**\n     * @return Returns unmodifiable map of ballots.\n     */\n    public Map<String, VoterBallots> getBallots() {\n        return Collections.unmodifiableMap(ballots);\n    }\n\n    @Override\n    @JsonIgnore\n    public int getNumberOfBallots() {\n        return ballots.values().stream().mapToInt(vb -> vb.getBallots().size()).sum();\n    }\n\n\n    /**\n     * Removes invalid ciphertexts\n     *\n     * @param filter The filter to apply to all votes using parallel processing.\n     */\n    public void removeInvalidCiphertexts(VoteFilter filter) {\n        requireType(Type.RECURRENT_VOTES_REMOVED);\n        ballots.entrySet().parallelStream()\n                .flatMap(ve -> Vote.streamOf(ve.getKey(), ve.getValue().getLatest())) // All votes\n                .forEach(v -> v.ballot.setInvalidState(!filter.accept(v.voterId, v.ballot, v.questionId, v.vote)));\n\n        // Remove ballots marked invalid by the filter\n        ballots.values().removeIf(vb -> vb.getLatest().isInvalid());\n\n        type = Type.INVALID_CIPHERTEXTS_REMOVED;\n    }\n\n\n    /**\n     * Removes recurrent ballots, i.e. retains only the latest ballot for each voter.\n     *\n     * @param cb Callback to be called on every removal of recurrent ballot.\n     */\n    public void removeRecurrentVotes(BiConsumer<String, Ballot> cb) {\n        requireType(Type.INTEGRITY_CONTROLLED);\n        ballots.forEach((vid, vb) -> vb.removeOldBallots(cb));\n        type = Type.RECURRENT_VOTES_REMOVED;\n    }\n\n    /**\n     * Revokes double ballots according to the revocation lists. The provided callback is called on\n     * every revocation activity (revoke or restore). Revocation activity can be either successful\n     * or unsuccessful. The latter case happens when voter or ballot does not exist or the ballot\n     * has unexpected state - revoking a revoked ballot or restoring a non-revoked ballot.\n     *\n     * @param rls Revocation lists\n     * @param cb Callback to be called on every revocation activity.\n     */\n    public void revokeDoubleVotes(Stream<Supplier<RevocationList>> rls, RevokeCallback cb) {\n        // Check type\n        requireType(Type.INVALID_CIPHERTEXTS_REMOVED);\n        // Mark voters' latest ballot revoked/restored according to revocation lists and report\n        rls.forEach(rlSupplier -> {\n            RevocationList rl = rlSupplier.get();\n            rl.getPersons().forEach(vid -> {\n                VoterBallots vb = ballots.containsKey(vid) ? ballots.get(vid) : null;\n                boolean success = vb != null && vb.setRevokedState(rl.isRevoke());\n                Ballot ballot = null;\n                if (vb != null) {\n                    ballot = vb.getLatest();\n                }\n                cb.call(vid, ballot, rl.isRevoke(), success);\n            });\n        });\n        // Remove voters who were marked revoked in the revocation list\n        ballots.values().removeIf(vb -> vb.isRevoked());\n        // Change type\n        type = Type.DOUBLE_VOTERS_REMOVED;\n    }\n\n    /**\n     * @return Returns an anonymous ballot box with the latest votes of this ballot box.\n     */\n    public AnonymousBallotBox anonymize() {\n        requireType(Type.DOUBLE_VOTERS_REMOVED);\n\n        Map<String, Map<String, Map<String, List<byte[]>>>> anonymous = new LinkedHashMap<>();\n\n        ballots.entrySet().parallelStream()\n                .flatMap(ve -> Vote.streamOf(ve.getKey(), ve.getValue().getLatest())) // All votes\n                .collect(Collectors.toList()).stream() // Join threads, restore initial order\n                .forEach(v -> anonymous\n                        .computeIfAbsent(v.ballot.getDistrictId(), d -> new LinkedHashMap<>())\n                        .computeIfAbsent(v.ballot.getParish(), p -> new LinkedHashMap<>())\n                        .computeIfAbsent(v.questionId, q -> new ArrayList<>()) //\n                        .add(v.vote));\n\n        return new AnonymousBallotBox(election, anonymous);\n    }\n\n    public interface RevokeCallback {\n        void call(String voterId, Ballot b, boolean revoke, boolean success);\n    }\n\n    public interface VoteFilter {\n        boolean accept(String voterId, Ballot b, String qid, byte[] vote);\n    }\n\n    private static class Vote {\n        final String voterId;\n        final Ballot ballot;\n        final String questionId;\n        final byte[] vote;\n\n        Vote(String voterId, Ballot ballot, String questionId, byte[] vote) {\n            this.voterId = voterId;\n            this.ballot = ballot;\n            this.questionId = questionId;\n            this.vote = vote;\n        }\n\n        static Stream<Vote> streamOf(String voterId, Ballot ballot) {\n            return ballot.getVotes().entrySet().stream()\n                    .map(e -> new Vote(voterId, ballot, e.getKey(), e.getValue()));\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/CandidateList.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class CandidateList {\n    private final String election;\n    private final Map<String, Map<String, Map<String, String>>> candidates;\n\n    @JsonCreator\n    public CandidateList( //\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"choices\") Map<String, Map<String, Map<String, String>>> districts) {\n        this.election = election;\n        this.candidates = Collections.unmodifiableMap(districts);\n    }\n\n    public String getElection() {\n        return election;\n    }\n\n    /**\n     * @return Returns a map from district id to a map from party name to a map from candidate id to\n     *         candidate name.\n     */\n    public Map<String, Map<String, Map<String, String>>> getCandidates() {\n        return candidates;\n    }\n\n    @JsonIgnore\n    public int getCount() {\n        return candidates.values().stream()\n                .mapToInt(d -> d.values().stream().mapToInt(p -> p.size()).sum()).sum();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/District.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class District {\n\n    private final String name;\n    private final List<String> parish;\n\n    @JsonCreator\n    public District( //\n            @JsonProperty(\"name\") String name, //\n            @JsonProperty(\"parish\") List<String> parish) {\n        this.name = name;\n        this.parish = Collections.unmodifiableList(parish);\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public List<String> getParish() {\n        return parish;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/DistrictList.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DistrictList {\n\n    private final String election;\n    private final Map<String, District> districts;\n    private final Map<String, Region> regions;\n    private final Map<String, List<String>> counties;\n\n    @JsonCreator\n    public DistrictList( //\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"districts\") Map<String, District> districts, //\n            @JsonProperty(\"regions\") Map<String, Region> regions,\n            @JsonProperty(\"counties\") Map<String, List<String>> counties) {\n        this.election = election;\n        this.districts = Collections.unmodifiableMap(new LinkedHashMap<>(districts));\n        this.regions = Collections.unmodifiableMap(new LinkedHashMap<>(regions));\n        this.counties = counties;\n    }\n\n    public String getElection() {\n        return election;\n    }\n\n    public Map<String, District> getDistricts() {\n        return districts;\n    }\n\n    public Map<String, Region> getRegions() {\n        return regions;\n    }\n\n    @JsonIgnore\n    public int getCount() {\n        return districts.size();\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/IBallotBox.java",
    "content": "package ee.ivxv.common.model;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.i18n.Translatable;\n\npublic interface IBallotBox {\n\n    String getElection();\n\n    Type getType();\n\n    int getNumberOfBallots();\n\n    default void requireType(Type t) {\n        if (getType() != t) {\n            throw new MessageException(M.e_bb_invalid_type, t, getType());\n        }\n    }\n\n    @BaseName(\"i18n.common-model-bb-type\")\n    @LocaleData(defaultCharset = \"UTF-8\", value = {})\n    enum Type implements Translatable {\n        UNORGANIZED, //\n        BACKUP, //\n        INTEGRITY_CONTROLLED, //\n        INVALID_CIPHERTEXTS_REMOVED, //\n        RECURRENT_VOTES_REMOVED, //\n        DOUBLE_VOTERS_REMOVED, //\n        ANONYMIZED;\n\n        @Override\n        public Enum<?> getKey() {\n            return this;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/LName.java",
    "content": "package ee.ivxv.common.model;\n\n/**\n * LName (location name) represents a voting district or voting station name. It consist of 2 parts:\n * {@code regionCode} and {@code number} and has the id of the form {@code <regionCode>.<number>}.\n */\npublic class LName {\n\n    private final String id;\n    private final String regionCode;\n    private final String number;\n\n    public LName(String id) {\n        this.id = id;\n        int i = id.indexOf('.');\n        this.regionCode = id.substring(0, i < 0 ? id.length() : i);\n        this.number = i < 0 ? null : id.substring(i + 1);\n    }\n\n    public LName(String regionCode, String number) {\n        StringBuilder sb = new StringBuilder(regionCode);\n        if (number != null) {\n            sb.append('.').append(number);\n        }\n        this.id = sb.toString();\n        this.regionCode = regionCode;\n        this.number = number;\n    }\n\n    public String getId() {\n        return id;\n    }\n\n    public String getRegionCode() {\n        return regionCode;\n    }\n\n    public String getNumber() {\n        return number;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/Proof.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * JSON serializable list of proof of correct decryptions.\n */\npublic class Proof {\n    private final String election;\n    private final List<ProofJson> proofs;\n\n    /**\n     * Initialize using election identifier.\n     *\n     * @param election\n     */\n    public Proof(String election) {\n        this.election = election;\n        this.proofs = new ArrayList<>();\n    }\n\n    /**\n     * Initialize using serialized values.\n     *\n     * @param election\n     * @param proofs\n     */\n    @JsonCreator\n    public Proof( //\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"proofs\") List<ProofJson> proofs) {\n        this.election = election;\n        this.proofs = Collections.unmodifiableList(proofs);\n    }\n\n    /**\n     * Get election identifier.\n     *\n     * @return\n     */\n    public String getElection() {\n        return election;\n    }\n\n    /**\n     * Get list of serializable decryption proofs.\n     *\n     * @return\n     */\n    public List<ProofJson> getProofs() {\n        return proofs;\n    }\n\n    /**\n     * Add decryption proof.\n     *\n     * @param proof\n     */\n    public void addProof(ElGamalDecryptionProof proof) {\n        proofs.add(new ProofJson(proof.ciphertext.getBytes(),\n                proof.decrypted.getBytes(), proof.getBytes()));\n    }\n\n    /**\n     * Get the number of decryption proofs.\n     *\n     * @return\n     */\n    @JsonIgnore\n    public int getCount() {\n        return proofs.size();\n    }\n\n    /**\n     * JSON serializable representation of single decryption proof.\n     */\n    public static class ProofJson {\n        private final byte[] ciphertext;\n        private final byte[] message;\n        private final byte[] proof;\n\n        /**\n         * Initialize using serialized values.\n         *\n         * @param ciphertext\n         * @param message\n         * @param proof\n         */\n        @JsonCreator\n        private ProofJson( //\n                @JsonProperty(\"ciphertext\") byte[] ciphertext, //\n                @JsonProperty(\"message\") byte[] message, //\n                @JsonProperty(\"proof\") byte[] proof) {\n            this.ciphertext = ciphertext;\n            this.message = message;\n            this.proof = proof;\n        }\n\n        /**\n         * Get the ciphertext for which the proof is for.\n         *\n         * @return\n         */\n        public byte[] getCiphertext() {\n            return ciphertext;\n        }\n\n        /**\n         * Get the message for which the proof is for.\n         *\n         * @return\n         */\n        public byte[] getMessage() {\n            return message;\n        }\n\n        /**\n         * Get serialized proof.\n         *\n         * @return\n         */\n        public byte[] getProof() {\n            return proof;\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/Region.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * Region represents a municipal region.\n * \n * <p>\n * Example: <code>\n *     {\n *       \"county\": \"Viljandi maakond\", \n *       \"parish\": \"Abja vald\", \n *       \"state\": \"Eesti Vabariik\"\n *     }\n * </code>\n */\npublic class Region {\n\n    private final String county;\n    private final String parish;\n    private final String state;\n\n    @JsonCreator\n    public Region( //\n            @JsonProperty(\"county\") String county, //\n            @JsonProperty(\"parish\") String parish, //\n            @JsonProperty(\"state\") String state) {\n        this.county = county;\n        this.parish = parish;\n        this.state = state;\n    }\n\n    public String getCounty() {\n        return county;\n    }\n\n    public String getParish() {\n        return parish;\n    }\n\n    public String getState() {\n        return state;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/RevocationList.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class RevocationList {\n\n    static final String TYPE_REVOKE = \"revoke\";\n\n    private final String election;\n    private final String type;\n    private final List<String> persons;\n\n    @JsonCreator\n    public RevocationList(//\n            @JsonProperty(\"election\") String election, //\n            @JsonProperty(\"type\") String type, //\n            @JsonProperty(\"persons\") List<String> persons) {\n        this.election = election;\n        this.type = type;\n        this.persons = new ArrayList<>(persons);\n    }\n\n    public String getElection() {\n        return election;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public List<String> getPersons() {\n        return persons;\n    }\n\n    public boolean isRevoke() {\n        return TYPE_REVOKE.equals(type);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/SkipCommand.java",
    "content": "package ee.ivxv.common.model;\n\npublic class SkipCommand {\n\n    private final String changeset;\n    private final String election;\n    private final String skip_voter_list;\n\n    public SkipCommand(\n        String changeset, String election, String skip_voter_list) {\n        this.changeset = changeset;\n        this.election = election;\n        this.skip_voter_list = skip_voter_list;\n    }\n\n    public String getElection() {\n        return election;\n    }\n\n    public String getChangeset() {\n        return changeset;\n    }\n\n    public String getSkipVoterList() {\n        return skip_voter_list;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/Voter.java",
    "content": "package ee.ivxv.common.model;\n\npublic class Voter {\n\n    static final String ACTIVITY_ADDITION = \"lisamine\";\n\n    private final String code;\n    private final String name;\n    private final String parish;\n    private final LName district;\n    private final boolean isAddition;\n\n    Voter(String code, String name, String activity, String parish, String district) {\n        this(code, name, activity, parish, new LName(district));\n    }\n\n    public Voter(String code, String name, String activity, String parish, LName district) {\n        this.code = code;\n        this.name = name;\n        this.parish = parish;\n        this.district = district;\n        isAddition = ACTIVITY_ADDITION.equals(activity);\n    }\n\n    public String getCode() {\n        return code;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public LName getDistrict() {\n        return district;\n    }\n\n    public String getParish() {\n        return parish;\n    }\n\n    public boolean isAddition() {\n        return isAddition;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/VoterBallots.java",
    "content": "package ee.ivxv.common.model;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.function.BiConsumer;\n\npublic class VoterBallots {\n\n    private final String voterId;\n    private final List<Ballot> ballots;\n    private final Ballot latest;\n    private boolean isRevoked;\n\n    @JsonCreator\n    public VoterBallots( //\n            @JsonProperty(\"voterId\") String voterId, //\n            @JsonProperty(\"ballots\") List<Ballot> ballots) {\n        this.voterId = voterId;\n        this.ballots = createSortedList(ballots);\n        latest = this.ballots.isEmpty() ? null : this.ballots.get(this.ballots.size() - 1);\n    }\n\n    private static List<Ballot> createSortedList(List<Ballot> ballots) {\n        List<Ballot> result = new ArrayList<>(ballots);\n        Collections.sort(result, (b1, b2) -> b1.getTime().compareTo(b2.getTime()));\n        return result;\n    }\n\n    public String getVoterId() {\n        return voterId;\n    }\n\n    /**\n     * @return Returns chronologically ordered unmodifiable list of ballots.\n     */\n    public List<Ballot> getBallots() {\n        return Collections.unmodifiableList(ballots);\n    }\n\n    @JsonIgnore\n    public Ballot getLatest() {\n        return latest;\n    }\n\n    void removeOldBallots(BiConsumer<String, Ballot> cb) {\n        while (ballots.size() > 1) {\n            cb.accept(voterId, ballots.remove(0));\n        }\n    }\n\n    boolean isRevoked() {\n        return isRevoked;\n    }\n\n    /**\n     * Sets the revoked status of the voterballot if the status is not already the same as the parameter.\n     *\n     * @param isRevoked\n     * @return Returns whether the operation was successful.\n     */\n    boolean setRevokedState(boolean isRevoked) {\n        if (this.isRevoked == isRevoked) {\n            return false;\n        }\n        this.isRevoked = isRevoked;\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/model/VoterList.java",
    "content": "package ee.ivxv.common.model;\n\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.ResultException;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class VoterList {\n\n    public static final String TYPE_INITIAL = \"0\";\n\n    private final VoterList parent;\n    private final String name;\n    private final String versionNumber;\n    private final String electionId;\n    private final String changeset;\n    private final Map<String, Voter> added;\n    private final Map<String, Voter> removed;\n\n    public VoterList(VoterList parent, String name, String versionNumber,\n            String electionId, String changeset, List<Voter> voters) {\n        this.parent = parent;\n        this.name = name;\n        this.versionNumber = versionNumber;\n        this.electionId = electionId;\n        this.changeset = changeset;\n        Map<String, Voter> a = new LinkedHashMap<>();\n        Map<String, Voter> r = new LinkedHashMap<>();\n        voters.forEach(v -> {\n            if (a.containsKey(v.getCode()) && r.containsKey(v.getCode())) {\n                a.remove(v.getCode());\n                r.remove(v.getCode());\n            }\n\n            if (v.isAddition()) {\n                a.put(v.getCode(), v);\n            } else {\n                r.put(v.getCode(), v);\n            }\n        });\n        added = Collections.unmodifiableMap(a);\n        removed = Collections.unmodifiableMap(r);\n    }\n\n    public VoterList getParent() {\n        return parent;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getVersionNumber() {\n        return versionNumber;\n    }\n\n    public String getElectionId() {\n        return electionId;\n    }\n\n    public String getChangeset() {\n        return changeset;\n    }\n\n    public Map<String, Voter> getAdded() {\n        return added;\n    }\n\n    public Map<String, Voter> getRemoved() {\n        return removed;\n    }\n\n    public boolean isInitial() {\n        return TYPE_INITIAL.equals(changeset);\n    }\n\n    /**\n     * Tries to find the active voter from a voter list that is valid in the specified voter list.\n     *\n     * @param voterId\n     * @param changeSet the change number of the desired voter list\n     * @return Returns the voter with the specified code that is active in the specified voter list\n     * \tor <tt>null</tt> if either such voter list does not exist of the voter is not active.\n     */\n    public Voter find(String voterId, String changeSet) throws ResultException {\n        if (getChangeset().equals(changeSet)) {\n            return find(voterId);\n        }\n        if (parent == null) {\n            throw new ResultException(Result.VOTERLIST_NOT_FOUND, changeSet);\n        }\n        return parent.find(voterId, changeSet);\n    }\n\n    /**\n     * Tries to find the voter from this voter list or it's parent.\n     *\n     * @param voterId\n     * @return The voter instance that is active in this voter list or <tt>null</tt>.\n     */\n    public Voter find(String voterId) {\n\n        Voter prev = Optional.ofNullable(parent).map(p -> p.find(voterId)).orElse(null);\n        Voter current_add = added.get(voterId);\n        Voter current_del = removed.get(voterId);\n\n        // no change\n        if (current_add == null && current_del == null) {\n            return prev;\n        }\n\n        // prev exists - change station\n        // otherwise nop\n        if (current_add != null && current_del != null) {\n            if (prev == null) {\n                return null;\n            } else {\n                return current_add;\n            }\n        }\n\n        if (current_add != null) {\n            return current_add;\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/BboxHelper.java",
    "content": "package ee.ivxv.common.service.bbox;\n\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.service.console.Progress;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\n\npublic interface BboxHelper {\n\n    Loader<?> getLoader(Path path, Progress.Factory pf, int nThreads);\n\n    interface Loader<U> {\n        BboxLoader<U> getBboxLoader(Path path, Reporter<Ref.BbRef> r) throws InvalidBboxException;\n\n        BboxLoader<U> getBboxLoader(Path path, Reporter<Ref.BbRef> r, long maxSignedBallotSizeInBytes) throws InvalidBboxException;\n\n        RegDataLoader<U> getRegDataLoader(Path path, Reporter<Ref.RegRef> r)\n                throws InvalidBboxException;\n    }\n\n    byte[] getChecksum(Path path) throws Exception;\n\n    boolean compareChecksum(byte[] sum1, byte[] sum2);\n\n    @FunctionalInterface\n    interface VoterProvider {\n        Voter find(String voterId, String version);\n    }\n\n    @FunctionalInterface\n    interface Reporter<T extends Ref> {\n        void report(T ref, Result res, Object... args);\n    }\n\n    interface BboxLoader<U> extends InitialStage {\n        /**\n         * Checks the integrity of the ballot box, i.e. all required data for each record must be\n         * present.\n         *\n         * @return\n         */\n        IntegrityChecked<U> checkIntegrity();\n    }\n\n    /**\n     * All records in ballot box are complete, i.e. all required data for each record is present.\n     */\n    interface IntegrityChecked<U> extends Stage {\n        /**\n         * Checks the consistency of the ballot box, e.g. the digital signature of all ballots.\n         *\n         * @param vp Function to find active voters.\n         * @param tsKey The key that was used to sign the timestamp requests.\n         * @param elStart The election start time.\n         * @return\n         */\n        BallotsChecked<U> checkBallots(VoterProvider vp, PublicKeyHolder tsKey, Instant elStart);\n\n        /**\n         * Exports the ballots of the current ballot box to the appointed destination.\n         *\n         * @param voterId The voter ID to export, may be missing.\n         * @param exporter An exporter callback.\n         */\n        void export(Optional<String> voterId, BiConsumer<Ref.BbRef, byte[]> exporter);\n\n        /**\n         * Lists the voters in the current ballot box. A voter is reported per ballot, so if they\n         * have voted multiple times, then they will be listed the same number of times (but not\n         * necessarily sequentially).\n         * <p>\n         * <tt>start</tt> and <tt>end</tt> can be used to limit the output to voters after a start\n         * time and/or before an end time (both inclusive).\n         *\n         * @param start The period start time, may be <tt>null</tt>.\n         * @param end The period end time, may be <tt>null</tt>.\n         * @param vp Function to find active voters.\n         * @param consumer A callback that consumes the list.\n         */\n        void listVoters(Instant start, Instant end, VoterProvider vp, Consumer<Voter> consumer);\n    }\n\n    /**\n     * All records in ballot box are consistent, e.g. digital signature is correct.\n     */\n    interface BallotsChecked<U> extends Stage {\n        /**\n         * Compares the ballot box to the registration data and retains only the intersection of\n         * records that are consistent.\n         *\n         * @param regData\n         * @return\n         */\n        BboxLoaderResult checkRegData(RegDataLoaderResult<U> regData);\n    }\n\n    /**\n     * Ballot box is fully loaded and checked. All records are correct and consistent.\n     */\n    interface BboxLoaderResult extends Stage {\n        /**\n         * Constructs ballot box instance with the loaded information.\n         *\n         * @param electionId\n         * @return\n         */\n        BallotBox getBallotBox(String electionId);\n    }\n\n    interface RegDataLoader<U> extends InitialStage {\n        /**\n         * Checks the integrity of the registration data container, i.e. all required data for each\n         * record must be present.\n         *\n         * @return\n         */\n        RegDataIntegrityChecked<U> checkIntegrity();\n    }\n\n    /**\n     * All records in the registration data container are complete, i.e all required data for each\n     * record is present.\n     */\n    interface RegDataIntegrityChecked<U> extends Stage {\n        /**\n         * Constructs and returns the registration data loader result object.\n         *\n         * @return\n         */\n        RegDataLoaderResult<U> getRegData();\n    }\n\n    /**\n     * Registration data is fully loaded and checked. All records are correct and consistent.\n     */\n    interface RegDataLoaderResult<U> extends Stage, Reporter<Ref.RegRef> {\n        /**\n         * @return Returns unmodifiable map with results. The map key is abstract correlation key\n         *         between ballot box and registration data that depends on the {@code Loader}\n         *         implementation.\n         */\n        Map<Object, RegDataRef<U>> getRegData();\n    }\n\n    class RegDataRef<U> {\n        public final Ref.RegRef ref;\n        public final U data;\n\n        public RegDataRef(Ref.RegRef ref, U data) {\n            this.ref = ref;\n            this.data = data;\n        }\n    }\n\n    interface InitialStage extends Stage {\n        @Override\n        default int getNumberOfInvalidBallots() {\n            return 0;\n        }\n    }\n\n    /**\n     * Stage represents generic stage of the process of loading ballot box or registration data.\n     */\n    interface Stage {\n        int getNumberOfValidBallots();\n\n        /**\n         * @return Returns the number of invalid ballots compared to the previous stage.\n         */\n        int getNumberOfInvalidBallots();\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/InvalidBboxException.java",
    "content": "package ee.ivxv.common.service.bbox;\n\nimport java.nio.file.Path;\n\npublic class InvalidBboxException extends RuntimeException {\n\n    private static final long serialVersionUID = -1888245283165329958L;\n\n    public final Path path;\n\n    public InvalidBboxException(Path path, Throwable e) {\n        super(String.format(\"Invalid container: %s\", path), e);\n        this.path = path;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/Ref.java",
    "content": "package ee.ivxv.common.service.bbox;\n\n/**\n * Ref represents logical reference to an entry in a either ballot box or registration data. Used in\n * error reporting.\n */\npublic interface Ref {\n\n    class BbRef implements Ref {\n        public final String voter;\n        public final String ballot;\n\n        public BbRef(String voter, String ballot) {\n            this.voter = voter;\n            this.ballot = ballot;\n        }\n\n        @Override\n        public int hashCode() {\n            return (voter == null ? 0 : voter.hashCode()) * 31 //\n                    + (ballot == null ? 0 : ballot.hashCode());\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj) {\n                return true;\n            }\n            if (obj == null || getClass() != obj.getClass()) {\n                return false;\n            }\n            BbRef o = (BbRef) obj;\n            if (ballot == null && o.ballot != null || ballot != null && !ballot.equals(o.ballot)) {\n                return false;\n            }\n            if (voter == null && o.voter != null || voter != null && !voter.equals(o.voter)) {\n                return false;\n            }\n            return true;\n        }\n    }\n\n    class RegRef implements Ref {\n        public final String ref;\n\n        public RegRef(String ref) {\n            this.ref = ref;\n        }\n\n        @Override\n        public int hashCode() {\n            return ref == null ? 0 : ref.hashCode();\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj) {\n                return true;\n            }\n            if (obj == null || getClass() != obj.getClass()) {\n                return false;\n            }\n            RegRef o = (RegRef) obj;\n            if (ref == null && o.ref != null || ref != null && !ref.equals(o.ref)) {\n                return false;\n            }\n            return true;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/Result.java",
    "content": "package ee.ivxv.common.service.bbox;\n\n/**\n * Represents the result of various operations related to ballot box processing.\n *\n * The ballot box processing includes importing and analyzing both ballots and\n * registration requests.\n *\n * Required files for a ballot are: version, ocsp, bdoc, tspreg\n * Required files for a registration request are: request\n *\n */\npublic enum Result {\n    /**\n     * Operation completed successfully.\n     */\n    OK,\n\n    /**\n     * The file name provided does not correspond to the expected pattern.\n     */\n    INVALID_FILE_NAME,\n\n    /**\n     * A required file for an entity is missing.\n     */\n    MISSING_FILE,\n\n    /**\n     * A required file for an entity has been found multiple times.\n     */\n    REPEATED_FILE,\n\n    /**\n     * The file type is unknown or unsupported.\n     */\n    UNKNOWN_FILE_TYPE,\n\n    /**\n     * The file size for a signed ballot does not meet the required criteria.\n     */\n    INVALID_FILE_SIZE,\n\n    /**\n     * The ballot signature is invalid.\n     */\n    INVALID_BALLOT_SIGNATURE,\n\n    /**\n     * The ballot does not contain the voter's signature\n     */\n    MISSING_VOTER_SIGNATURE,\n\n    /**\n     * The voter was not eligible according to the voterlists at the time of voting\n     */\n    VOTER_NOT_FOUND,\n\n    /**\n     * The voter list corresponding to the version cannot be found.\n     */\n    VOTERLIST_NOT_FOUND,\n\n    /**\n     * The vote has been cast before the beginning of the voting period.\n     */\n    TIME_BEFORE_START,\n\n    /**\n     * The registration response is invalid.\n     */\n    REG_RESP_INVALID,\n\n    /**\n     * The registration request is invalid.\n     */\n    REG_REQ_INVALID,\n\n    /**\n     * The registration response is not unique.\n     *\n     * It may happen, when registration request is sent for the same ballot\n     * more than once. Only the earliest response is used, the repeated\n     * responses are reported.\n     */\n    REG_RESP_NOT_UNIQUE,\n\n    /**\n     * The registration request is not unique.\n     *\n     * It may happen, when registration request is sent for the same ballot\n     * more than once. Only the earliest response is used, the repeated\n     * responses are reported.\n     *\n     */\n    REG_REQ_NOT_UNIQUE,\n\n    /**\n     * No nonce is provided in the registration request.\n     */\n    REG_NO_NONCE,\n\n    /**\n     * The provided nonce is not signed according to IVXV protocol.\n     */\n    REG_NONCE_NOT_SIG,\n\n    /**\n     * The algorithm used for the nonce signature does not match the expected one.\n     */\n    REG_NONCE_ALG_MISMATCH,\n\n    /**\n     * The nonce signature is invalid.\n     */\n    REG_NONCE_SIG_INVALID,\n\n    /**\n     * An unknown file is present in the vote container.\n     */\n    UNKNOWN_FILE_IN_VOTE_CONTAINER,\n\n    /**\n     * A technical error occurred during processing.\n     */\n    TECHNICAL_ERROR,\n\n    /**\n     * The data in registration response does not match the data in the\n     * registration request.\n     *\n     * This situation must be investigated, it must not occur under normal\n     * circumstances.\n     */\n    REG_RESP_REQ_UNMATCH,\n\n    /**\n     * A registration request has been provided, but the ballot is missing.\n     *\n     * This situation must be investigated, since it seems that collection\n     * service has handed over incomplete ballot box. It must not occur under\n     * normal circumstances.\n     */\n    REG_REQ_WITHOUT_BALLOT,\n\n    /**\n     * A ballot is submitted without a corresponding registration request.\n     */\n    BALLOT_WITHOUT_REG_REQ,\n\n    /**\n     *  There are two votes from the same voter that could be considered the latest.\n     *\n     *  Their timestamps identify the same time. The valid vote is selected randomly.\n     */\n    SAME_TIME_AS_LATEST,\n\n    /**\n     * The signature profile of the vote is invalid.\n     */\n    INVALID_SIGNATURE_PROFILE,\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/AbstractStage.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.BboxHelper.Stage;\n\nabstract class AbstractStage implements Stage {\n\n    private final int numberOfValidBallots;\n    private final int numberOfInvalidBallots;\n\n    AbstractStage(int numberOfValidBallots) {\n        this(numberOfValidBallots, numberOfValidBallots);\n    }\n\n    AbstractStage(int numberOfValidBallots, int oldValidBallots) {\n        this.numberOfValidBallots = numberOfValidBallots;\n        this.numberOfInvalidBallots = oldValidBallots - numberOfValidBallots;\n    }\n\n    @Override\n    public int getNumberOfValidBallots() {\n        return numberOfValidBallots;\n    }\n\n    @Override\n    public int getNumberOfInvalidBallots() {\n        return numberOfInvalidBallots;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/BboxHelperImpl.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.crypto.hash.HashType;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.impl.TspProfile.TsProfile;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.util.Util;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HexFormat;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class BboxHelperImpl implements BboxHelper {\n\n    private static final Logger log = LoggerFactory.getLogger(BboxHelperImpl.class);\n\n    private final ContainerReader container;\n\n    public BboxHelperImpl(Conf conf, ContainerReader container) {\n        this.container = container;\n    }\n\n    @Override\n    public Loader<?> getLoader(Path path, Progress.Factory pf, int nThreads) {\n        Profile<?, ?, ?, ?> profile = new TsProfile(container);\n        return new LoaderImpl<>(profile, pf, nThreads);\n    }\n\n    @Override\n    public byte[] getChecksum(Path path) throws Exception {\n        byte[] bytes = HashType.SHA256.getFunction().digest(Files.newInputStream(path));\n        String checksum = HexFormat.of().formatHex(bytes);\n        return Util.toBytes(checksum);\n    }\n\n    @Override\n    public boolean compareChecksum(byte[] sum1, byte[] sum2) {\n        String str1 = Util.toString(sum1).trim();\n        String str2 = Util.toString(sum2).trim();\n        boolean result = str1.equalsIgnoreCase(str2);\n        log.debug(\"Comparing checksum1 {}\", str1);\n        log.debug(\"Comparing checksum2 {}\", str2);\n        log.debug(\"Result: {}\", result);\n        return result;\n    }\n\n    static class LoaderImpl<T extends Record<?>, U extends Record<?>, RT extends Keyable, RU extends Keyable>\n            implements Loader<RU> {\n\n        private final Profile<T, U, RT, RU> profile;\n        private final Progress.Factory pf;\n        private final int nThreads;\n\n        LoaderImpl(Profile<T, U, RT, RU> profile, Progress.Factory pf, int nThreads) {\n            this.profile = profile;\n            this.pf = pf;\n            this.nThreads = nThreads;\n        }\n\n        @Override\n        public BboxLoader<RU> getBboxLoader(Path path, Reporter<Ref.BbRef> r)\n                throws InvalidBboxException {\n            return new IvxvBboxLoader<>(profile, new ZipSource(path), pf, r, nThreads);\n        }\n\n        @Override\n        public BboxLoader<RU> getBboxLoader(Path path, Reporter<Ref.BbRef> r, long maxSignedBallotSizeInBytes)\n                throws InvalidBboxException {\n            return new IvxvBboxLoader<>(profile, new ZipSource(path), pf, r, nThreads, maxSignedBallotSizeInBytes);\n        }\n\n        @Override\n        public RegDataLoader<RU> getRegDataLoader(Path path, Reporter<Ref.RegRef> r)\n                throws InvalidBboxException {\n            return new IvxvRegDataLoader<>(profile, new ZipSource(path), pf, r);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/FileName.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.Ref;\n\nclass FileName<T extends Ref> {\n\n    private static final char EXT_SEP = '.';\n    private static final String EXPECTED_FMT = \"%s\" + EXT_SEP + \"<ext>\";\n\n    final String path;\n    final T ref;\n    final String baseName;\n    final String type;\n\n    private FileName(T ref, String baseName, String type) {\n        this.path = new StringBuilder().append(baseName).append(EXT_SEP).append(type).toString();\n        this.ref = ref;\n        this.baseName = baseName;\n        this.type = type;\n    }\n\n    /**\n     * @param path The file path.\n     * @throws InvalidNameException If the path does not conform to expected file path pattern.\n     */\n    FileName(String path, RefProvider<T> provider) throws InvalidNameException {\n        int i = path.lastIndexOf(EXT_SEP);\n        if (i < 0) {\n            throw new InvalidNameException(path, String.format(EXPECTED_FMT, \"<name>\"));\n        }\n        this.path = path;\n        this.baseName = path.substring(0, i);\n        try {\n            this.ref = provider.get(baseName);\n        } catch (InvalidNameException e) {\n            throw new InvalidNameException(path, String.format(EXPECTED_FMT, e.expected));\n        }\n        type = path.substring(i + 1);\n    }\n\n    FileName<T> forType(Enum<?> typeEnum) {\n        if (typeEnum.name().equals(type)) {\n            return this;\n        }\n        return new FileName<>(ref, baseName, typeEnum.name());\n    }\n\n    interface RefProvider<T extends Ref> {\n        T get(String name);\n    }\n\n    static class InvalidNameException extends RuntimeException {\n        private static final long serialVersionUID = 6878488000246512903L;\n\n        final String path;\n        final String expected;\n\n        InvalidNameException(String path, String expected) {\n            super(\"Invalid path '\" + path + \"'. Expected: \" + expected);\n            this.path = path;\n            this.expected = expected;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/FileSource.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\n\n/**\n * Simple file system abstraction.\n */\npublic interface FileSource {\n\n    /**\n     * Iterates over all files and calls the processor on them.\n     *\n     * @param processor\n     */\n    void processFiles(BiConsumer<String, InputStream> processor);\n\n    /**\n     * Lists all file names and calls the processor on them.\n     *\n     * @param processor\n     */\n    default void list(Consumer<String> processor) {\n        processFiles((name, in) -> {\n            processor.accept(name);\n        });\n    }\n\n    /**\n     * Lists all file names and their sizes and then calls the processor on them.\n     * @param processor\n     */\n    void listFileNamesAndSizes(BiConsumer<String, Long> processor);\n\n    /**\n     * Count all found files.\n     *\n     * @return amount of found files.\n     */\n    default int countFiles() {\n        return 0;\n    }\n\n    /**\n     * Count files matching some specific suffix(es).\n     *\n     * @param suffixes the suffixes to match for\n     * @return the amount of found files\n     */\n    default int countFilesWithSuffix(List<String> suffixes) { return 0; }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/IvxvBboxLoader.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport static java.util.stream.Collectors.groupingBy;\nimport static java.util.stream.Collectors.mapping;\nimport static java.util.stream.Collectors.toCollection;\n\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.model.VoterBallots;\nimport ee.ivxv.common.service.bbox.BboxHelper.BallotsChecked;\nimport ee.ivxv.common.service.bbox.BboxHelper.BboxLoader;\nimport ee.ivxv.common.service.bbox.BboxHelper.BboxLoaderResult;\nimport ee.ivxv.common.service.bbox.BboxHelper.IntegrityChecked;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataLoaderResult;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataRef;\nimport ee.ivxv.common.service.bbox.BboxHelper.Reporter;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref.BbRef;\nimport ee.ivxv.common.service.bbox.Ref.RegRef;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.FileName.RefProvider;\nimport ee.ivxv.common.service.bbox.impl.verify.TsVerifier;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.container.InvalidContainerException;\nimport ee.ivxv.common.util.Util;\nimport eu.europa.esig.dss.model.DSSException;\nimport java.time.Instant;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeFormatterBuilder;\nimport java.time.format.ResolverStyle;\nimport java.time.temporal.ChronoField;\nimport java.util.Collections;\nimport java.util.ConcurrentModificationException;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Optional;\nimport java.util.TreeSet;\nimport java.util.Vector;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiConsumer;\nimport java.util.function.BiFunction;\nimport java.util.function.Consumer;\nimport java.util.function.Predicate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nclass IvxvBboxLoader<T extends Record<?>, U extends Record<?>, RT extends Keyable, RU extends Keyable>\n        extends AbstractStage implements BboxLoader<RU> {\n\n    static final Logger log = LoggerFactory.getLogger(IvxvBboxLoader.class);\n\n    static final int MAX_NUMBER_OF_RETRIES = 5;\n    static final DateTimeFormatter BALLOT_TIMESTAMP_FMT =\n            // Work around JDK-8031085 which prevents the use of SSS:\n            // https://bugs.openjdk.java.net/browse/JDK-8031085\n            new DateTimeFormatterBuilder().appendPattern(\"uuuuMMddHHmmss\")\n                    .appendValue(ChronoField.MILLI_OF_SECOND, 3).appendOffset(\"+HHMM\", \"+0000\")\n                    .toFormatter().withResolverStyle(ResolverStyle.STRICT);\n\n    final Profile<T, U, RT, RU> profile;\n    final LoaderHelper<BbRef> helper;\n    private final int nThreads;\n    private static final String BALLOT_CONTAINER_EXT = \"bdoc\";\n\n    public IvxvBboxLoader(Profile<T, U, RT, RU> profile, FileSource source, Progress.Factory pf,\n            Reporter<BbRef> reporter, int nThreads) throws InvalidBboxException {\n        this(profile, new LoaderHelper<>(source, new BbRefProvider(), pf, reporter), nThreads);\n    }\n\n    public IvxvBboxLoader(Profile<T, U, RT, RU> profile, FileSource source, Progress.Factory pf,\n                          Reporter<BbRef> reporter, int nThreads, long maxSignedBallotSizeInBytes) throws InvalidBboxException {\n        this(profile, new LoaderHelper<>(source, new BbRefProvider(), pf, reporter), nThreads, maxSignedBallotSizeInBytes);\n    }\n\n    IvxvBboxLoader(Profile<T, U, RT, RU> profile, LoaderHelper<BbRef> helper, int nThreads)\n            throws InvalidBboxException {\n        super(helper.getAllRefs().size());\n        this.profile = profile;\n        this.helper = helper;\n        this.nThreads = nThreads;\n        log.info(\"ZipBboxLoader instantiated with thread count {}\", nThreads);\n    }\n\n    IvxvBboxLoader(Profile<T, U, RT, RU> profile, LoaderHelper<BbRef> helper, int nThreads, long maxSignedBallotSizeInBytes)\n            throws InvalidBboxException {\n        super(helper.getAllFileNames(signedBallotSizeCheckFunction(maxSignedBallotSizeInBytes)).size());\n        this.profile = profile;\n        this.helper = helper;\n        this.nThreads = nThreads;\n        log.info(\"ZipBboxLoader instantiated with thread count {} and max signed ballot size of {} bytes\", nThreads,\n                maxSignedBallotSizeInBytes);\n    }\n\n    /**\n     * signedBallotSizeCheckFunction returns a function that checks whether a ballot is <= signedBallotMaxSizeInBytes.\n     * @param signedBallotMaxSizeInBytes\n     * @return\n     */\n    private static BiFunction<String, Long, Boolean> signedBallotSizeCheckFunction(long signedBallotMaxSizeInBytes) {\n        return (fileType, fileSize) -> {\n\n            // File is not a signed ballot, omit check\n            if (!fileType.equals(BALLOT_CONTAINER_EXT)) {\n                return true;\n            }\n\n            return signedBallotMaxSizeInBytes >= fileSize;\n        };\n    }\n\n    @Override\n    public IntegrityChecked<RU> checkIntegrity() {\n        int n = getNumberOfValidBallots();\n        return new IntegrityCheckedImpl(helper.checkIntegrity(profile::createBbRecord, n), n);\n    }\n\n    ExecutorService createExecutorService() {\n        if (nThreads <= 0) {\n            return Executors.newCachedThreadPool();\n        }\n        return Executors.newFixedThreadPool(nThreads);\n    }\n\n    static class BbRefProvider implements RefProvider<BbRef> {\n\n        private static final char DIR_SEP = '/';\n        private static final String EXPECTED = \"<voter-id>\" + DIR_SEP + \"<ballot-id>\";\n\n        @Override\n        public BbRef get(String s) {\n            int i = s.lastIndexOf(DIR_SEP);\n            if (i < 0) {\n                throw new FileName.InvalidNameException(s, EXPECTED);\n            }\n            int j = s.lastIndexOf(DIR_SEP, i - 1);\n            String voter = s.substring(j + 1, i);\n            String ballot = s.substring(i + 1);\n\n            return new BbRef(voter, ballot);\n        }\n    }\n\n    class IntegrityCheckedImpl extends AbstractStage implements IntegrityChecked<RU> {\n\n        private final Map<BbRef, T> records;\n\n        IntegrityCheckedImpl(Map<BbRef, T> records, int oldValid) {\n            super(records.size(), oldValid);\n            this.records = records;\n        }\n\n        @Override\n        public BallotsChecked<RU> checkBallots(VoterProvider vp, PublicKeyHolder tsKey,\n                Instant elStart) {\n            Map<BbRef, BallotResponse> ballots = Collections.synchronizedMap(new LinkedHashMap<>());\n            ExecutorService executor = createExecutorService();\n            TsVerifier tsv = new TsVerifier(tsKey);\n            Progress pb = helper.getProgress(getNumberOfValidBallots());\n            Predicate<FileName<BbRef>> filter = name -> records.containsKey(name.ref);\n\n            helper.processRecords(filter, profile::createBbRecord, (name, record) -> {\n                // Ensure stable order of votes\n                ballots.putIfAbsent(name.ref, null);\n\n                executor.submit(() -> {\n                    try {\n                        BallotResponse br = createBallotResponse(name, record, vp, tsv, elStart);\n                        if (br != null) {\n                            ballots.put(name.ref, br);\n                        }\n                    } finally {\n                        pb.increase(1);\n                    }\n                });\n            });\n\n            executor.shutdown();\n\n            try {\n                executor.awaitTermination(1, TimeUnit.DAYS);\n            } catch (InterruptedException e) {\n                throw new RuntimeException(e);\n            }\n\n            // Remove nulls added before\n            ballots.values().removeIf(v -> v == null);\n\n            removeRecurrentResponses(ballots);\n\n            pb.finish();\n\n            return new BallotsCheckedImpl(ballots, getNumberOfValidBallots());\n        }\n\n        @Override\n        public void export(Optional<String> voterId, BiConsumer<BbRef, byte[]> exporter) {\n            Optional<Progress> pb = voterId.isPresent() ? Optional.empty()\n                    : Optional.of(helper.getProgress(getNumberOfValidBallots()));\n            Predicate<FileName<BbRef>> filter = name -> records.containsKey(name.ref)\n                    && voterId.map(vid -> name.ref.voter.equals(vid)).orElse(true);\n\n            helper.processRecords(filter, profile::createBbRecord, (name, record) -> {\n                try {\n                    exporter.accept(name.ref, profile.combineBallotContainer(record));\n                } catch (Exception e) {\n                    helper.handleTechnicalError(name, e);\n                } finally {\n                    pb.ifPresent(p -> p.increase(1));\n                }\n            });\n\n            pb.ifPresent(p -> p.finish());\n        }\n\n        @Override\n        public void listVoters(Instant start, Instant end, VoterProvider vp,\n                Consumer<Voter> consumer) {\n            Progress pb = helper.getProgress(getNumberOfValidBallots());\n            byte[] buffer = new byte[1024];\n\n            Predicate<FileName<BbRef>> timeFilter = name -> {\n                if (start == null && end == null) {\n                    return true; // Fast path.\n                }\n                Instant timestamp = Instant.from(BALLOT_TIMESTAMP_FMT.parse(name.ref.ballot));\n                return (start == null || !timestamp.isBefore(start))\n                        && (end == null || !timestamp.isAfter(end));\n            };\n\n            // Use processFiles instead of processRecords to avoid reading file content that will\n            // never be used. We only need the BbRef and contents of version file.\n            helper.processFiles((name, in) -> {\n                Optional.of(name) //\n                        .filter(n -> n.type.equals(\"version\")) // Only version files.\n                        .filter(n -> records.containsKey(n.ref)) // Only complete records.\n                        .ifPresent(filename -> {\n                            try {\n                                Optional.of(filename).filter(timeFilter).ifPresent(n -> {\n                                    Voter v = vp.find(n.ref.voter, Util.toString(in, buffer));\n                                    if (v == null) {\n                                        throw new ResultException(Result.VOTER_NOT_FOUND);\n                                    }\n                                    consumer.accept(v);\n                                });\n                            } catch (ResultException e) {\n                                log.error(\"ResultException was thrown processing file {}: \",\n                                        filename.path, e);\n                                helper.report(filename.ref, e.result, e.args);\n                            } catch (Exception e) {\n                                helper.handleTechnicalError(name, e);\n                            } finally {\n                                // Increase progress on complete record only, not each file.\n                                pb.increase(1);\n                            }\n                        });\n            });\n\n            pb.finish();\n        }\n\n        private BallotResponse createBallotResponse(FileName<BbRef> name, T record,\n                VoterProvider vp, TsVerifier tsv, Instant elStart) {\n            int i = 0;\n            retry: try {\n                RT response = profile.getResponse(record);\n                log.info(\"BALLOT-KEY ballot: {}/{} key: {}\", name.ref.voter, name.ref.ballot,\n                        response.getKey());\n\n                Ballot b = profile.createBallot(name, record, vp, tsv);\n                if (b.getTime().isBefore(elStart)) {\n                    throw new ResultException(Result.TIME_BEFORE_START, b.getTime().toString(),\n                            elStart.toString());\n                }\n\n                return new BallotResponse(b, response);\n            } catch (ResultException e) {\n                log.error(\"ResultException was thrown processing file {}: \", name.path, e);\n                helper.report(name.ref, e.result, e.args);\n            } catch (InvalidContainerException e) {\n                log.error(\"Invalid container '{}': {}\", e.path, e.getMessage(), e);\n                helper.report(name.ref, Result.INVALID_BALLOT_SIGNATURE, e);\n            } catch (DSSException e) {\n                /*\n                 * In very rare cases a concurrent modification exception is re-thrown from a\n                 * DSS-(?) library. Just try again for MAX times.\n                 */\n                log.error(\"DSSException occurred while processing {}\", name.path, e);\n                if (e.getCause() instanceof ConcurrentModificationException) {\n                    log.error(\"!!! Caused by ConcurrentModificationException !!!\");\n                    if (++i < MAX_NUMBER_OF_RETRIES) {\n                        break retry;\n                    }\n                }\n                // Not concurrent modification exception - process as normally\n                helper.handleTechnicalError(name, e);\n            } catch (Exception e) {\n                helper.handleTechnicalError(name, e);\n            }\n            return null;\n        }\n\n        /**\n         * Ensure the uniqueness of all responses (response signatures/keys). In case of recurrence\n         * only use the earliest one, remove others and report.\n         *\n         * @param ballots\n         */\n        private void removeRecurrentResponses(Map<BbRef, BallotResponse> ballots) {\n            ballots.entrySet().stream() //\n                    .collect(groupingBy(e -> e.getValue().response.getKey(), // Group by resp key\n                            mapping(e -> e, toCollection(() -> new TreeSet<>(this::compare)))))\n                    .values().stream().filter(e -> e.size() > 1) // Having only colliding entries\n                    .flatMap(es -> es.stream().skip(1)) // Skip the first in sorted set as earliest\n                    .forEach(e -> { // Process the others as the recurring ones\n                        helper.report(e.getKey(), Result.REG_RESP_NOT_UNIQUE);\n                        ballots.remove(e.getKey());\n                    });\n        }\n\n        private int compare(Entry<BbRef, BallotResponse> e1, Entry<BbRef, BallotResponse> e2) {\n            int cmp1 = e1.getValue().ballot.getTime().compareTo(e2.getValue().ballot.getTime());\n            // Ensure equality is consistent with {@code equals} as required by {@code TreeSet}\n            if (cmp1 != 0) {\n                return cmp1;\n            }\n            return e1.getValue().ballot.getId().compareTo(e2.getValue().ballot.getId());\n        }\n\n    } // class IntegrityCheckedImpl\n\n    class BallotResponse {\n        final Ballot ballot;\n        final RT response;\n\n        public BallotResponse(Ballot ballot, RT response) {\n            this.ballot = ballot;\n            this.response = response;\n        }\n    }\n\n    class BallotsCheckedImpl extends AbstractStage implements BallotsChecked<RU> {\n\n        private final Map<BbRef, BallotResponse> ballots;\n\n        BallotsCheckedImpl(Map<BbRef, BallotResponse> ballots, int oldValid) {\n            super(ballots.size(), oldValid);\n            this.ballots = ballots;\n        }\n\n        @Override\n        public BboxLoaderResult checkRegData(RegDataLoaderResult<RU> regData) {\n            Map<String, List<Ballot>> voters = Collections.synchronizedMap(new LinkedHashMap<>());\n            Map<Object, RegRef> regFiles = Collections.synchronizedMap(new LinkedHashMap<>());\n            ExecutorService executor = createExecutorService();\n            Progress pb = helper.getProgress(getNumberOfValidBallots());\n\n            regData.getRegData().forEach((key, rr) -> regFiles.put(key, rr.ref));\n\n            ballots.forEach((ref, br) -> {\n                RegDataRef<RU> rr = regData.getRegData().get(br.response.getKey());\n                regFiles.remove(br.response.getKey());\n\n                voters.computeIfAbsent(ref.voter, s -> new Vector<>());\n\n                executor.submit(() -> {\n                    try {\n                        if (rr == null) {\n                            // Request not found - report and continue\n                            helper.report(ref, Result.BALLOT_WITHOUT_REG_REQ);\n                        } else {\n                            // Check registration request and response\n                            Result regResult = profile.checkRegistration(br.response, rr.data);\n                            if (regResult != Result.OK) {\n                                // Report and break\n                                helper.report(ref, regResult, rr.ref);\n                                return;\n                            }\n                        }\n\n                        voters.get(ref.voter).add(br.ballot);\n                    } catch (Exception e) {\n                        helper.handleTechnicalError(ref, e);\n                    } finally {\n                        pb.increase(1);\n                    }\n                });\n            });\n\n            pb.finish();\n\n            executor.shutdown();\n\n            try {\n                executor.awaitTermination(1, TimeUnit.DAYS);\n            } catch (InterruptedException e) {\n                throw new RuntimeException(e);\n            }\n\n            // Check all records not among the ballots just processed and remove them from regFiles\n            Progress pb2 = helper.getProgress(getNumberOfValidBallots());\n            helper.processRecords(name -> true, true, profile::createBbRecord, (name, record) -> {\n                pb2.increase(1);\n                if (ballots.containsKey(name.ref)) {\n                    return;\n                }\n                profile.getResponseKey(record).ifPresent(key -> {\n                    log.info(\"BALLOT-KEY (invalid) ballot: {}/{} key: {}\", name.ref.voter,\n                            name.ref.ballot, key);\n                    if (regFiles.remove(key) == null) {\n                        helper.report(name.ref, Result.BALLOT_WITHOUT_REG_REQ);\n                    }\n                });\n            });\n\n            // Report registration data without corresponding ballots using regData's reporting\n            regFiles.forEach((key, ref) -> regData.report(ref, Result.REG_REQ_WITHOUT_BALLOT));\n\n            // Remove entries for voters that never got a valid vote\n            voters.values().removeIf(ballotList -> ballotList.isEmpty());\n\n            pb2.finish();\n\n            return new BboxLoaderResultImpl(voters, getNumberOfValidBallots());\n        }\n\n    } // class BallotsCheckedImpl\n\n    class BboxLoaderResultImpl extends AbstractStage implements BboxLoaderResult {\n\n        private final Map<String, List<Ballot>> voters;\n\n        BboxLoaderResultImpl(Map<String, List<Ballot>> voters, int oldValid) {\n            super(voters.values().stream().mapToInt(m -> m.size()).sum(), oldValid);\n            this.voters = voters;\n        }\n\n        @Override\n        public BallotBox getBallotBox(String electionId) {\n            Map<String, VoterBallots> ballots = new LinkedHashMap<>();\n\n            voters.forEach((k, v) -> ballots.put(k, new VoterBallots(k, v)));\n\n            // Detect ballots with the same timestamp as the latest ballot - report only\n            ballots.values().forEach(vb -> Optional.ofNullable(vb.getLatest()).ifPresent(l -> {\n                vb.getBallots().stream().filter(b -> b != l && b.getTime().equals(l.getTime()))\n                        .forEach(b -> helper.report(new BbRef(vb.getVoterId(), b.getId()),\n                                Result.SAME_TIME_AS_LATEST, l.getTime().toString(), l.getId()));\n            }));\n\n            return new BallotBox(electionId, ballots);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/IvxvRegDataLoader.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataIntegrityChecked;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataLoader;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataLoaderResult;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataRef;\nimport ee.ivxv.common.service.bbox.BboxHelper.Reporter;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Ref.RegRef;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.console.Progress;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class IvxvRegDataLoader<T extends Record<?>, U extends Record<?>, RT extends Keyable, RU extends Keyable>\n        extends AbstractStage implements RegDataLoader<RU> {\n\n    static final Logger log = LoggerFactory.getLogger(IvxvRegDataLoader.class);\n\n    final Profile<T, U, RT, RU> profile;\n    final LoaderHelper<RegRef> helper;\n\n    IvxvRegDataLoader(Profile<T, U, RT, RU> profile, FileSource source, Progress.Factory pf,\n            Reporter<RegRef> reporter) throws InvalidBboxException {\n        this(profile, new LoaderHelper<>(source, RegRef::new, pf, reporter));\n    }\n\n    private IvxvRegDataLoader(Profile<T, U, RT, RU> profile, LoaderHelper<RegRef> helper)\n            throws InvalidBboxException {\n        super(helper.getAllRefs().size());\n        this.profile = profile;\n        this.helper = helper;\n    }\n\n    @Override\n    public RegDataIntegrityChecked<RU> checkIntegrity() {\n        int n = getNumberOfValidBallots();\n        return new RegDataIntegrityCheckedImpl(helper.checkIntegrity(profile::createRegRecord, n),\n                n);\n    }\n\n    class RegDataIntegrityCheckedImpl extends AbstractStage implements RegDataIntegrityChecked<RU> {\n\n        private final Map<Ref.RegRef, U> voters;\n\n        RegDataIntegrityCheckedImpl(Map<Ref.RegRef, U> voters, int oldValid) {\n            super(voters.size(), oldValid);\n            this.voters = voters;\n        }\n\n        @Override\n        public RegDataLoaderResult<RU> getRegData() {\n            Map<Object, RegDataRef<RU>> regData = new LinkedHashMap<>();\n            Predicate<FileName<RegRef>> filter = name -> voters.containsKey(name.ref);\n\n            helper.processRecords(filter, profile::createRegRecord, (name, record) -> {\n                try {\n                    RU request = profile.getRequest(record);\n                    log.info(\"REG-DATA-KEY reg-data: {} key: {}\", name.ref.ref, request.getKey());\n\n                    // Check request uniqueness\n                    if (regData.put(request.getKey(),\n                            new RegDataRef<>(name.ref, request)) != null) {\n                        helper.report(name.ref, Result.REG_REQ_NOT_UNIQUE);\n                        return;\n                    }\n                } catch (Exception e) {\n                    helper.handleTechnicalError(name, e);\n                }\n            });\n\n            return new RegDataLoaderResultImpl(regData, getNumberOfValidBallots());\n        }\n\n    } // class RegDataIntegrityCheckedImpl\n\n    class RegDataLoaderResultImpl extends AbstractStage implements RegDataLoaderResult<RU> {\n\n        private final Map<Object, RegDataRef<RU>> regData;\n\n        RegDataLoaderResultImpl(Map<Object, RegDataRef<RU>> regData, int oldValid) {\n            super(regData.size(), oldValid);\n            this.regData = Collections.unmodifiableMap(regData);\n        }\n\n        @Override\n        public Map<Object, RegDataRef<RU>> getRegData() {\n            return regData;\n        }\n\n        @Override\n        public void report(RegRef ref, Result res, Object... args) {\n            helper.report(ref, res, args);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/LoaderHelper.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.BboxHelper.Reporter;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.FileName.RefProvider;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.util.Util;\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.*;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Helper class for common logic of BboxLoader and RegDataLoader.\n *\n * @param <T> The reference type\n */\npublic class LoaderHelper<T extends Ref> {\n\n    private static final Logger log = LoggerFactory.getLogger(LoaderHelper.class);\n\n    private final FileSource source;\n    private final RefProvider<T> refProvider;\n    private final Progress.Factory pf;\n    private final Reporter<T> reporter;\n\n    LoaderHelper(FileSource source, RefProvider<T> refProvider, Progress.Factory pf,\n            Reporter<T> reporter) {\n        this.source = source;\n        this.refProvider = refProvider;\n        this.pf = pf;\n        this.reporter = reporter;\n    }\n\n    Set<T> getAllRefs() {\n        Map<T, Boolean> initialData = new LinkedHashMap<>();\n\n        source.list(path -> {\n            try {\n                FileName<T> name = new FileName<>(path, refProvider);\n                initialData.put(name.ref, Boolean.TRUE);\n            } catch (FileName.InvalidNameException e) {\n                log.warn(\"Invalid file name: {}\", path, e);\n                reporter.report(null, Result.INVALID_FILE_NAME, e.path, e.expected);\n            }\n        });\n\n        return initialData.keySet();\n    }\n\n    Set<T> getAllFileNames(BiFunction<String, Long, Boolean> fileSizeChecker) {\n        Map<T, Boolean> initialData = new LinkedHashMap<>();\n\n        source.listFileNamesAndSizes((path, fileSize) -> {\n            T fileName = null;\n            try {\n                FileName<T> name = new FileName<>(path, refProvider);\n                boolean ok = fileSizeChecker.apply(name.type, fileSize);\n                if (!ok) {\n                    fileName = name.ref;\n                    throw new InvalidFileSizeException(name.path, fileSize);\n                }\n                initialData.put(name.ref, Boolean.TRUE);\n            } catch (FileName.InvalidNameException e) {\n                log.warn(\"Invalid file name: {}\", path, e);\n                reporter.report(null, Result.INVALID_FILE_NAME, e.path, e.expected);\n            } catch (InvalidFileSizeException e) {\n                log.warn(\"Invalid file size: {}\", path, e);\n                reporter.report(fileName, Result.INVALID_FILE_SIZE, e.path, e.gotSize);\n            }\n        });\n\n        return initialData.keySet();\n    }\n\n    /**\n     * Performs the integrity-check for the data being loaded.\n     *\n     * @param supplier Record supplier.\n     * @param total Total number of records for progress reporting.\n     * @return Returns integrity-checked map from reference to record.\n     */\n    <U extends Record<?>> Map<T, U> checkIntegrity(Supplier<U> supplier, int total) {\n        Map<T, U> result = new LinkedHashMap<>();\n        Progress pb = getProgress(total);\n\n        list(name -> {\n            U record = result //\n                    .computeIfAbsent(name.ref, s -> {\n                        pb.increase(1);\n                        return supplier.get();\n                    });\n\n            Result res = record.set(name.type);\n            if (res != Result.OK) {\n                report(name.ref, res, name.path);\n            }\n        });\n\n        // Report missing files\n        result.forEach((ref, r) -> r.forMissingFiles(t -> report(ref, Result.MISSING_FILE, t)));\n\n        // Remove incomplete records\n        result.values().removeIf(record -> !record.isComplete());\n\n        pb.finish();\n\n        return result;\n    }\n\n    void list(Consumer<FileName<T>> processor) {\n        source.list(path -> {\n            try {\n                FileName<T> name = new FileName<>(path, refProvider);\n                try {\n                    processor.accept(name);\n                } catch (Exception e) {\n                    handleTechnicalError(name, e);\n                }\n            } catch (FileName.InvalidNameException e) {\n                // Skip invalid file names. Error is reported by 'loadInitialData()'.\n            }\n        });\n    }\n\n    void processFiles(BiConsumer<FileName<T>, InputStream> processor) {\n        source.processFiles((path, in) -> {\n            try {\n                FileName<T> name = new FileName<>(path, refProvider);\n                try {\n                    processor.accept(name, in);\n                } catch (Exception e) {\n                    handleTechnicalError(name, e);\n                }\n            } catch (FileName.InvalidNameException e) {\n                // Skip invalid file names. Error is reported by 'loadInitialData()'.\n            }\n        });\n    }\n\n    <U extends Record<?>> void processRecords(Predicate<FileName<T>> filter, Supplier<U> supplier,\n            BiConsumer<FileName<T>, U> processor) {\n        processRecords(filter, false, supplier, processor);\n    }\n\n    <U extends Record<?>> void processRecords(Predicate<FileName<T>> filter,\n            boolean processIncomplete, Supplier<U> supplier, BiConsumer<FileName<T>, U> processor) {\n        Map<T, NameRecord<T, U>> work = new HashMap<>();\n        byte[] buffer = new byte[1024];\n\n        processFiles((name, in) -> {\n            if (!filter.test(name)) {\n                return;\n            }\n            U record = work.computeIfAbsent(name.ref,\n                    x -> new NameRecord<>(name, supplier.get())).record;\n            record.set(name.type, Util.toBytes(in, buffer));\n            if (record.isComplete()) {\n                // Release memory\n                work.remove(name.ref);\n                processor.accept(name, record);\n            }\n        });\n\n        // Report incomplete records\n        if (processIncomplete) {\n            work.forEach((ref, nr) -> processor.accept(nr.name, nr.record));\n        }\n    }\n\n    void handleTechnicalError(FileName<T> name, Exception e) {\n        log.error(\"Tehcnical error occurred while processing file {}: \", name.path, e);\n        report(name.ref, Result.TECHNICAL_ERROR, e);\n    }\n\n    void handleTechnicalError(T ref, Exception e) {\n        log.error(\"Tehcnical error occurred while processing record {}: \", ref, e);\n        report(ref, Result.TECHNICAL_ERROR, e);\n    }\n\n    void report(T ref, Result res, Object... args) {\n        reporter.report(ref, res, args);\n    }\n\n    Progress getProgress(int total) {\n        return pf.apply(total);\n    }\n\n    private static class NameRecord<T extends Ref, U extends Record<?>> {\n        final FileName<T> name;\n        final U record;\n\n        public NameRecord(FileName<T> name, U record) {\n            this.name = name;\n            this.record = record;\n        }\n    }\n\n    static class InvalidFileSizeException extends RuntimeException {\n        private static final long serialVersionUID = 6878488000246512904L;\n\n        final String path;\n        final Long gotSize;\n\n        InvalidFileSizeException(String path, Long gotSize) {\n            super(\"Invalid file '\" + path + \"' size - \" + gotSize);\n            this.path = path;\n            this.gotSize = gotSize;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/Profile.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.verify.TsVerifier;\nimport ee.ivxv.common.service.container.InvalidContainerException;\nimport java.util.Optional;\n\n/**\n * Profile abstracts the contents of ballot box and registration data.\n * \n * <p>\n * The generic concept is:\n * <ul>\n * <li>There are 2 main data types - ballot box and registration data.\n * <li>{@code Profile} class provides method to create records for each.\n * <li>Ballot box records are the primary source for creating instances of {@code Ballot}.\n * <li>Registration data records are needed only to check correlation between the two collections.\n * <li>For correlation a <i>registration response</i> must match a <i>registration request</i>.\n * <li>Registration requests are stored in the registration data and can be created from a\n * registration record.\n * <li>Registration responses are stored in the ballot box and can be created from a ballot box\n * record.\n * <li>Both request and response must have a <u>unique</u> hash key for quick lookup.\n * </ul>\n * \n * @param <T> Ballot box record type\n * @param <U> Registration data record type\n * @param <RT> Registration response type - bound to ballot box\n * @param <RU> Registration request type - bound to registration data\n */\npublic interface Profile<T extends Record<?>, U extends Record<?>, RT extends Keyable, RU extends Keyable> {\n\n    T createBbRecord();\n\n    U createRegRecord();\n\n    byte[] combineBallotContainer(T record);\n\n    Ballot createBallot(FileName<Ref.BbRef> name, T record, VoterProvider vp, TsVerifier tsv)\n            throws Exception, InvalidContainerException, ResultException;\n\n    RT getResponse(T record);\n\n    RU getRequest(U record);\n\n    /**\n     * Tries to create response key from potentially partial or invalid record in order to match it\n     * later against the request key.\n     * \n     * @param record\n     * @return Optional response key, possibly empty optional.\n     */\n    Optional<Object> getResponseKey(T record);\n\n    Result checkRegistration(RT response, RU request);\n\n}\n\n\ninterface Keyable {\n    Object getKey();\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/Record.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.Result;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Consumer;\n\nclass Record<T extends Enum<T>> {\n\n    private static final byte[] EMPTY = new byte[0];\n\n    private final Class<T> clazz;\n    final Map<T, byte[]> files = new HashMap<>();\n    private int missingFiles;\n\n    Record(Class<T> clazz) {\n        this.clazz = clazz;\n        missingFiles = clazz.getEnumConstants().length;\n    }\n\n    Result set(String type) {\n        return set(type, EMPTY);\n    }\n\n    Result set(String type, byte[] content) {\n        T t;\n\n        try {\n            t = Enum.valueOf(clazz, type);\n        } catch (Exception e) {\n            return Result.UNKNOWN_FILE_TYPE;\n        }\n\n        byte[] oldValue = files.put(t, content);\n        if (oldValue != null) {\n            missingFiles = -1;\n            return Result.REPEATED_FILE;\n        }\n\n        missingFiles--;\n\n        return Result.OK;\n    }\n\n    byte[] get(T type) {\n        return files.get(type);\n    }\n\n    boolean isComplete() {\n        return missingFiles == 0;\n    }\n\n    void forMissingFiles(Consumer<T> consumer) {\n        for (T t : clazz.getEnumConstants()) {\n            if (!files.containsKey(t)) {\n                consumer.accept(t);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/ResultException.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.service.bbox.Result;\n\npublic class ResultException extends RuntimeException {\n\n    private static final long serialVersionUID = -1689828260220357741L;\n\n    final Result result;\n    final Object[] args;\n\n    public ResultException(Result result, Object... args) {\n        super(result.name());\n        this.result = result;\n        this.args = args;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/TspProfile.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport ee.ivxv.common.crypto.hash.HashType;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.verify.TsVerifier;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.InvalidContainerException;\nimport ee.ivxv.common.service.container.Signature;\nimport ee.ivxv.common.service.container.Subject;\nimport ee.ivxv.common.util.ByteArrayWrapper;\nimport ee.ivxv.common.util.Util;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.apache.xml.security.c14n.Canonicalizer;\nimport org.bouncycastle.asn1.cms.ContentInfo;\nimport org.bouncycastle.tsp.TimeStampRequest;\nimport org.bouncycastle.tsp.TimeStampToken;\n\n/**\n * TspProfile is an abstract implementation of {@code Profile} that uses timestamp provider as the\n * ballot registration service.\n *\n * @param <T>\n * @param <U>\n */\nabstract class TspProfile<T extends TspRespRecord<?>, U extends TspReqRecord<?>>\n        implements Profile<T, U, KeyableValue<?>, KeyableValue<?>> {\n\n    private static final String EXT_BALLOT = \".ballot\";\n    // The canonicalization algorithm: http://www.w3.org/2006/12/xml-c14n11\n    private static final String TS_C14N_ALG = Canonicalizer.ALGO_ID_C14N11_OMIT_COMMENTS;\n    private static final HashType TS_HASH = HashType.SHA256;\n    private static final String SN_PNOEE_PREFIX = \"PNOEE-\"; // Make country code configurable.\n\n    final ContainerReader container;\n\n    TspProfile(ContainerReader container) {\n        this.container = container;\n    }\n\n    Voter findVoter(VoterProvider vp, String voterId, String version) throws ResultException {\n        Voter voter = vp.find(voterId, version);\n        if (voter == null) {\n            throw new ResultException(Result.VOTER_NOT_FOUND);\n        }\n        return voter;\n    }\n\n    Ballot createBallot(FileName<Ref.BbRef> name, Container c, String version, Voter voter)\n            throws ResultException {\n        // Find the voter's signature.\n        Signature signature = c.getSignatures().stream() //\n                .filter(s -> s.getSigner() != null\n                        && name.ref.voter.equals(getVoterIdentifier(s.getSigner())))\n                .findFirst().orElseThrow(() -> new ResultException(Result.MISSING_VOTER_SIGNATURE));\n\n        Map<String, byte[]> votes = new LinkedHashMap<>();\n\n        c.getFiles().stream().forEach(f -> {\n            if (!f.getName().endsWith(EXT_BALLOT)) {\n                throw new ResultException(Result.UNKNOWN_FILE_IN_VOTE_CONTAINER, name.path,\n                        f.getName());\n            }\n            // NB! This is not pure <question id>, but <election id>.<question id>\n            String voteId = f.getName().substring(0, f.getName().length() - EXT_BALLOT.length());\n            votes.put(voteId, Util.toBytes(f.getStream()));\n        });\n\n        return new Ballot(name.ref.ballot, signature.getSigningTime(), version, voter, votes);\n    }\n\n    private String getVoterIdentifier(Subject s) {\n        String serial = s.getSerialNumber();\n        if (serial.startsWith(SN_PNOEE_PREFIX)) {\n            return serial.substring(SN_PNOEE_PREFIX.length());\n        }\n        return serial;\n    }\n\n    void checkSignatureProfiles(Container c, Signature.Profile profile) {\n        c.getSignatures().stream().filter(s -> s.getProfile() != profile).findAny()\n                .ifPresent(invalid -> {\n                    throw new ResultException(Result.INVALID_SIGNATURE_PROFILE,\n                            invalid.getProfile());\n                });\n    }\n\n    @Override\n    public KeyableValue<?> getResponse(T record) {\n        return new KeyableValue<>(getResponseKey(record).get());\n    }\n\n    @Override\n    public KeyableValue<?> getRequest(U record) {\n        TimeStampRequest request = record.getRegRequest();\n        byte[] bytes = request.getMessageImprintDigest();\n        return new KeyableValue<>(createRegKey(bytes));\n    }\n\n    @Override\n    public Optional<Object> getResponseKey(T record) {\n        if (record.hasRegResponse()) {\n            TimeStampToken response = record.getRegResponse();\n            byte[] bytes = response.getTimeStampInfo().getMessageImprintDigest();\n            return Optional.of(createRegKey(bytes));\n        }\n        byte[] c = getContainer(record);\n        if (c == null) {\n            return Optional.empty();\n        }\n        byte[] bytes = TS_HASH.getFunction().digest(container.getTimestampData(c, TS_C14N_ALG));\n        return Optional.of(createRegKey(bytes));\n    }\n\n    abstract byte[] getContainer(T record);\n\n    private Object createRegKey(byte[] bytes) {\n        return new ByteArrayWrapper(bytes);\n    }\n\n    @Override\n    public Result checkRegistration(KeyableValue<?> response, KeyableValue<?> request) {\n        return response.value.equals(request.value) ? Result.OK : Result.REG_RESP_REQ_UNMATCH;\n    }\n\n    /**\n     * Timestamp profile. Consists of the following files:\n     * <ul>\n     * <li>bdoc - the BES type vote container</li>\n     * <li>ocsp - the OCSP response</li>\n     * <li>tspreg - the registration response, also the timestamp - the source of voting time</li>\n     * <li>version - the voterlist version</li>\n     * </ul>\n     */\n    static class TsProfile extends TspProfile<TspRespRecord<TsType>, TspReqRecord<RegType>> {\n\n        TsProfile(ContainerReader container) {\n            super(container);\n        }\n\n        @Override\n        byte[] getContainer(TspRespRecord<TsType> record) {\n            return record.get(TsType.bdoc);\n        }\n\n        @Override\n        public TspRespRecord<TsType> createBbRecord() {\n            return new TspRespRecord<>(TsType.class, TsType.tspreg);\n        }\n\n        @Override\n        public TspReqRecord<RegType> createRegRecord() {\n            return new TspReqRecord<>(RegType.class, RegType.request);\n        }\n\n        @Override\n        public byte[] combineBallotContainer(TspRespRecord<TsType> record) {\n            byte[] besBdoc = record.get(TsType.bdoc);\n            byte[] ocsp = record.get(TsType.ocsp);\n            byte[] ts = record.get(TsType.tspreg);\n            return container.combine(besBdoc, ocsp, ts, TS_C14N_ALG);\n        }\n\n        @Override\n        public Ballot createBallot(FileName<Ref.BbRef> name, TspRespRecord<TsType> record,\n                VoterProvider vp, TsVerifier tsv)\n                throws Exception, InvalidContainerException, ResultException {\n            String version = Util.toString(record.get(TsType.version));\n            Voter voter = findVoter(vp, name.ref.voter, version);\n\n            FileName<Ref.BbRef> bdocName = name.forType(TsType.bdoc);\n            Container c = container.open(combineBallotContainer(record), bdocName.path);\n\n            checkSignatureProfiles(c, Signature.Profile.BDOC_TS);\n\n            tsv.verify(record.getRegResponse());\n\n            return createBallot(bdocName, c, version, voter);\n        }\n    }\n\n    enum TsType {\n        version, bdoc, ocsp, tspreg;\n    }\n\n    enum RegType {\n        request;\n    }\n}\n\n\nclass TspRespRecord<T extends Enum<T>> extends Record<T> {\n\n    private final T respKey;\n\n    TspRespRecord(Class<T> clazz, T respKey) {\n        super(clazz);\n        this.respKey = respKey;\n    }\n\n    boolean hasRegResponse() {\n        return get(respKey) != null;\n    }\n\n    TimeStampToken getRegResponse() {\n        try {\n            return new TimeStampToken(ContentInfo.getInstance(get(respKey)));\n        } catch (Exception e) {\n            throw new ResultException(Result.REG_RESP_INVALID, e);\n        }\n    }\n\n}\n\n\nclass TspReqRecord<T extends Enum<T>> extends Record<T> {\n\n    private final T reqKey;\n\n    TspReqRecord(Class<T> clazz, T reqKey) {\n        super(clazz);\n        this.reqKey = reqKey;\n    }\n\n    TimeStampRequest getRegRequest() {\n        try {\n            return new TimeStampRequest(get(reqKey));\n        } catch (Exception e) {\n            throw new ResultException(Result.REG_REQ_INVALID, e);\n        }\n    }\n\n}\n\n\nclass KeyableValue<T> implements Keyable {\n    final Object key;\n    final T value;\n\n    /**\n     * Constructs instance with the given {@code value} as both key and value.\n     *\n     * @param value\n     */\n    KeyableValue(T value) {\n        this(value, value);\n    }\n\n    private KeyableValue(Object key, T value) {\n        this.key = key;\n        this.value = value;\n    }\n\n    @Override\n    public Object getKey() {\n        return key;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/ZipSource.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport static ee.ivxv.common.util.Util.CHARSET;\n\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\n\npublic class ZipSource implements FileSource {\n\n    private final Path path;\n\n    public ZipSource(Path path) {\n        this.path = path;\n    }\n\n    private static void processZippedStream(Path path,\n            BiConsumer<ZipEntry, InputStream> processor) {\n        try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(path), CHARSET)) {\n            for (ZipEntry ze; (ze = zis.getNextEntry()) != null;) {\n                if (ze.isDirectory()) {\n                    continue;\n                }\n                processor.accept(ze, zis);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(new InvalidBboxException(path, e));\n        }\n    }\n\n    private static void processZipFile(Path path, Consumer<ZipEntry> processor) {\n        try (ZipFile zip = openZipFile(path)) {\n            zip.stream().forEach(ze -> {\n                if (ze.isDirectory()) {\n                    return;\n                }\n                processor.accept(ze);\n            });\n        } catch (Exception e) {\n            throw new InvalidBboxException(path, e);\n        }\n    }\n\n    private static ZipFile openZipFile(Path path) {\n        try {\n            return new ZipFile(path.toFile());\n        } catch (Exception e) {\n            throw new InvalidBboxException(path, e);\n        }\n    }\n\n    @Override\n    public void processFiles(BiConsumer<String, InputStream> processor) {\n        processZippedStream(path, (ze, in) -> {\n            processor.accept(ze.getName(), in);\n        });\n    }\n\n    @Override\n    public void list(Consumer<String> processor) {\n        processZipFile(path, ze -> {\n            processor.accept(ze.getName());\n        });\n    }\n\n    @Override\n    public void listFileNamesAndSizes(BiConsumer<String, Long> processor) {\n        processZipFile(path, ze -> processor.accept(ze.getName(), ze.getSize()));\n    }\n\n    @Override\n    public int countFiles() {\n        try (ZipFile zipFile = new ZipFile(this.path.toFile())){\n            return zipFile.size();\n        } catch (Exception e) {\n            throw new InvalidBboxException(this.path, e);\n        }\n    }\n\n    @Override\n    public int countFilesWithSuffix(List<String> suffixes) {\n        try (ZipFile zipFile = new ZipFile(this.path.toFile())){\n            List<String> fileContent = zipFile.stream().filter(ze -> {\n                String fileName = ze.getName();\n                for (String suffix: suffixes) if (fileName.endsWith(suffix)) return true;\n                return false;\n            }).map(ZipEntry::getName).toList();\n            return fileContent.size();\n        } catch (Exception e) {\n            throw new InvalidBboxException(this.path, e);\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/ZipSourceRaw.java",
    "content": "package ee.ivxv.common.service.bbox.impl;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.util.function.BiConsumer;\nimport java.util.function.Consumer;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\n\nimport static ee.ivxv.common.util.Util.CHARSET;\n\npublic class ZipSourceRaw implements FileSource {\n\n    private final byte[] zipFileAsBytes;\n\n    public ZipSourceRaw(byte[] zipFileAsBytes) {\n        this.zipFileAsBytes = zipFileAsBytes;\n    }\n\n    private void processZipStream(BiConsumer<ZipEntry, InputStream> processor) {\n        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.zipFileAsBytes);\n             ZipInputStream zis = new ZipInputStream(byteArrayInputStream, CHARSET)) {\n            for (ZipEntry ze; (ze = zis.getNextEntry()) != null; ) {\n                if (ze.isDirectory()) {\n                    continue;\n                }\n                processor.accept(ze, zis);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private void processZipFilesFromZipStream(Consumer<ZipEntry> processor) {\n        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.zipFileAsBytes);\n             ZipInputStream zis = new ZipInputStream(byteArrayInputStream, CHARSET)) {\n\n            for (ZipEntry ze; (ze = zis.getNextEntry()) != null; ) {\n                if (ze.isDirectory()) {\n                    continue;\n                }\n                processor.accept(ze);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void processFiles(BiConsumer<String, InputStream> processor) {\n        processZipStream((ze, in) -> processor.accept(ze.getName(), in));\n    }\n\n    @Override\n    public void list(Consumer<String> processor) {\n        processZipFilesFromZipStream(ze -> processor.accept(ze.getName()));\n    }\n\n    @Override\n    public void listFileNamesAndSizes(BiConsumer<String, Long> processor) {\n        // TODO: unimplemented\n    }\n}\n\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/verify/TsSignature.java",
    "content": "package ee.ivxv.common.service.bbox.impl.verify;\n\nimport java.io.IOException;\nimport org.bouncycastle.asn1.ASN1Encodable;\nimport org.bouncycastle.asn1.ASN1OctetString;\nimport org.bouncycastle.asn1.ASN1Sequence;\nimport org.bouncycastle.asn1.DEROctetString;\nimport org.bouncycastle.asn1.DERSequence;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\n\npublic class TsSignature {\n\n    private final AlgorithmIdentifier alg;\n    private final byte[] signature;\n\n    public TsSignature(AlgorithmIdentifier alg, byte[] signature) {\n        this.alg = alg;\n        this.signature = signature;\n    }\n\n    public static TsSignature fromBytes(byte[] in) {\n        ASN1Sequence seq = ASN1Sequence.getInstance(in);\n        AlgorithmIdentifier alg = AlgorithmIdentifier.getInstance(seq.getObjectAt(0));\n        byte[] signature = ASN1OctetString.getInstance(seq.getObjectAt(1)).getOctets();\n\n        return new TsSignature(alg, signature);\n    }\n\n    public AlgorithmIdentifier getAlg() {\n        return alg;\n    }\n\n    public byte[] getSignature() {\n        return signature;\n    }\n\n    /**\n     * <pre>\n     *    Signature ::= SEQUENCE  {\n     *       signingAlgorithm   AlgorithmIdentifier,\n     *       signature          ANY DEFINED BY signingAlgorithm\n     * }\n     * </pre>\n     */\n    public byte[] toBytes() throws IOException {\n        return new DERSequence(new ASN1Encodable[] {alg, new DEROctetString(signature)})\n                .getEncoded();\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/bbox/impl/verify/TsVerifier.java",
    "content": "package ee.ivxv.common.service.bbox.impl.verify;\n\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.bbox.impl.ResultException;\nimport java.math.BigInteger;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.DERNull;\nimport org.bouncycastle.asn1.tsp.MessageImprint;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\nimport org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;\nimport org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;\nimport org.bouncycastle.tsp.TimeStampToken;\nimport org.bouncycastle.tsp.TimeStampTokenInfo;\n\npublic class TsVerifier {\n\n    private static final String NONE_WITH_RSA = \"NONEWithRsa\";\n    private static final DigestAlgorithmIdentifierFinder digestAlgFinder =\n            new DefaultDigestAlgorithmIdentifierFinder();\n\n    private final PublicKeyHolder key;\n\n    public TsVerifier(PublicKeyHolder key) {\n        this.key = key;\n    }\n\n    public TimeStampToken verify(TimeStampToken token) throws ResultException {\n        try {\n            return verifyInternal(token);\n        } catch (ResultException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new ResultException(Result.REG_RESP_INVALID, e);\n        }\n    }\n\n    private TimeStampToken verifyInternal(TimeStampToken token) throws Exception {\n        TimeStampTokenInfo info = token.getTimeStampInfo();\n\n        TsSignature s = parseNonce(info.getNonce());\n        ASN1ObjectIdentifier sOid = digestAlgFinder.find(s.getAlg()).getAlgorithm();\n\n        /*\n         * AlgorithmIdentifier ::= SEQUENCE {\n         *   algorithm OBJECT IDENTIFIER,\n         *   parameters ANY DEFINED BY algorithm OPTIONAL\n         * }\n         *\n         * If \"parameters\" is null, then NULL value should be added to the ASN1\n         *\n         * digestAlg ::= SEQUENCE {\n         *  sOid\n         *  NULL\n         * }\n         *\n        */\n        AlgorithmIdentifier digestAlg = new AlgorithmIdentifier(sOid, DERNull.INSTANCE);\n\n        if (digestAlg.getAlgorithm() == null || !info.getMessageImprintAlgOID().equals(digestAlg.getAlgorithm())) {\n            throw new ResultException(Result.REG_NONCE_ALG_MISMATCH,\n                    digestAlg.getAlgorithm() == null ? \"null\" : digestAlg.getAlgorithm(),\n                    info.getMessageImprintAlgOID());\n        }\n\n        byte[] data = new MessageImprint(digestAlg, info.getMessageImprintDigest()).getEncoded();\n        if (!key.verify(data, s.getSignature(), NONE_WITH_RSA)) {\n            throw new ResultException(Result.REG_NONCE_SIG_INVALID);\n        }\n\n        return token;\n    }\n\n    private TsSignature parseNonce(BigInteger nonce) {\n        if (nonce == null) {\n            throw new ResultException(Result.REG_NO_NONCE);\n        }\n        try {\n            return TsSignature.fromBytes(nonce.toByteArray());\n        } catch (Exception e) {\n            throw new ResultException(Result.REG_NONCE_NOT_SIG);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/BlockingQueueConsole.java",
    "content": "package ee.ivxv.common.service.console;\n\nimport ee.ivxv.common.util.Util;\nimport java.io.PrintStream;\nimport java.util.Arrays;\nimport java.util.Optional;\nimport java.util.Scanner;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Supplier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * ConsoleImpl implements Console interface and prints messages simply to <tt>System.out</tt>.\n */\npublic class BlockingQueueConsole implements Console {\n\n    private static final Logger log = LoggerFactory.getLogger(BlockingQueueConsole.class);\n\n    private static final String KEY_VALUE = \"{VALUE}\";\n    private static final String KEY_TOTAL = \"{TOTAL}\";\n    private static final String KEY_PERCENT = \"{PERCENT}\";\n    private static final String KEY_BAR = \"{BAR}\";\n    private static final int BAR_WIDTH = 50;\n    private static final char BAR_CHAR = '.';\n    private static final long QUEUE_CHECK_FREQ_MS = 50;\n    private static final String PROGRESS_FINISHED_MSG = \"shurely-unique-\" + Math.random();\n\n    private final Scanner in = new Scanner(System.in);\n    private final BlockingQueue<String> queue = new LinkedBlockingQueue<>();\n    private final ExecutorService executor;\n    private final TextFormatter formatter;\n    // Reference to 'progress supplier', i.e 'ps'.\n    private final AtomicReference<Supplier<String>> psRef = new AtomicReference<>();\n\n    public BlockingQueueConsole() {\n        executor = Executors.newSingleThreadExecutor();\n        formatter = new TextFormatter();\n        executor.submit(() -> printQueue(System.out));\n    }\n\n    @Override\n    public void println() {\n        println(\"\");\n    }\n\n    @Override\n    public void println(String format, Object... args) {\n        queue.add(String.format(format, args).replaceAll(\"[0-9]{7}\", \"*******\"));\n    }\n\n    @Override\n    public void printlnraw(String format, Object... args) {\n        queue.add(String.format(format, args));\n    }\n\n    @Override\n    public String readln() {\n        return in.nextLine();\n    }\n\n    @Override\n    public String readPw() {\n        java.io.Console console = System.console();\n        // Should warn user when console is null and password input will be visible?\n        // Should be possible ONLY when running from IDE\n        if (console == null) {\n            return readln();\n        }\n        char[] chars = console.readPassword();\n        String str = new String(chars);\n        // Minimize the lifetime of sensitive data in memory\n        Arrays.fill(chars, ' ');\n        return str;\n    }\n\n    @Override\n    public Progress startProgress(String format, long total) {\n        ProgressImpl progress = new ProgressImpl(total, () -> queue.add(PROGRESS_FINISHED_MSG));\n        psRef.set(new PbFormatter(progress, format));\n        return progress;\n    }\n\n    @Override\n    public Progress startInfiniteProgress(String format, long total) {\n        ProgressImpl progress =\n                new InfiniteProgressImpl(total, () -> queue.add(PROGRESS_FINISHED_MSG));\n        psRef.set(new PbFormatter(progress, format));\n        return progress;\n    }\n\n    @Override\n    public void shutdown() {\n        queue.add(Util.EOT);\n        executor.shutdown();\n        try {\n            executor.awaitTermination(1, TimeUnit.DAYS);\n        } catch (InterruptedException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /*\n     * This can be called only through PbFormatter.get().\n     */\n    void progressFinished(Supplier<String> psToFinish) {\n        // Only unset the reference if it has not been changed by a 'startProgress' call\n        psRef.compareAndSet(psToFinish, null);\n    }\n\n    private Void printQueue(PrintStream out) {\n        AtomicBoolean onProgressRow = new AtomicBoolean();\n        String line = queue.poll();\n\n        while (true) {\n            try {\n                for (; line != null; line = queue.poll()) {\n                    if (line.equals(Util.EOT)) {\n                        out.flush();\n                        return null;\n                    }\n                    if (line.equals(PROGRESS_FINISHED_MSG)) {\n                        break;\n                    }\n                    if (onProgressRow.getAndSet(false)) {\n                        out.println();\n                    }\n                    String formatted = formatter.formatLine(line);\n                    log.info(\"[CONSOLE]: \" + formatted);\n                    out.println(formatted);\n                }\n\n                Optional.ofNullable(psRef.get()).ifPresent(p -> {\n                    out.print(p.get());\n                    onProgressRow.set(true);\n                });\n\n                line = queue.poll(QUEUE_CHECK_FREQ_MS, TimeUnit.MILLISECONDS);\n            } catch (Exception e) {\n                log.error(\"Exception occurred in console printer thread\", e);\n            }\n        }\n    }\n\n    /*\n     * Not thread safe.\n     */\n    private class PbFormatter implements Supplier<String> {\n\n        private final ProgressImpl progress;\n        private final String format;\n\n        private int lastPercent = -1;\n        private String lastPercentStr = \"\";\n        private String lastBarStr = \"\";\n\n        PbFormatter(ProgressImpl progress, String format) {\n            this.progress = progress;\n            this.format = format;\n        }\n\n        @Override\n        public String get() {\n            return formatProgress();\n        }\n\n        private String formatProgress() {\n            // For infinite progress overwrite the end of the line by spaces and move to the start\n            String result = \"\\b\\b\\b\\b    \\r\" + format;\n            int percent = progress.getTotal() == 0 ? 100\n                    : (int) ((progress.getValue() * 100) / progress.getTotal());\n\n            result = result.replace(KEY_VALUE, String.valueOf(progress.getValue()));\n            result = result.replace(KEY_TOTAL, String.valueOf(progress.getTotal()));\n            result = result.replace(KEY_PERCENT, formatPercent(percent));\n            result = result.replace(KEY_BAR, formatBar(percent));\n\n            if (progress.isFinished()) {\n                progressFinished(this);\n            }\n\n            lastPercent = percent;\n\n            return result;\n        }\n\n        private String formatPercent(int percent) {\n            if (percent == lastPercent) {\n                return lastPercentStr;\n            }\n            lastPercentStr = String.format(\"%3s\", percent);\n            return lastPercentStr;\n        }\n\n        private String formatBar(int percent) {\n            if (percent == lastPercent) {\n                return lastBarStr;\n            }\n            StringBuffer result = new StringBuffer();\n            double done = (percent * BAR_WIDTH) / 100d;\n\n            for (int i = 0; i < BAR_WIDTH; i++) {\n                result.append(i < done ? BAR_CHAR : ' ');\n            }\n\n            lastBarStr = result.toString();\n\n            return lastBarStr;\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/Console.java",
    "content": "package ee.ivxv.common.service.console;\n\n/**\n * Channel to communicate with the user.\n */\npublic interface Console {\n\n    void println();\n\n    void println(String format, Object... args);\n\n    void printlnraw(String format, Object... args);\n\n    String readln();\n\n    String readPw();\n\n    Progress startProgress(String format, long total);\n\n    Progress startInfiniteProgress(String format, long total);\n\n    void shutdown();\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/InfiniteProgressImpl.java",
    "content": "package ee.ivxv.common.service.console;\n\npublic class InfiniteProgressImpl extends ProgressImpl {\n    InfiniteProgressImpl(long total, Runnable finishedNotifier) {\n        super(total, finishedNotifier);\n    }\n\n    @Override\n    public void increase(int amount) {\n        if (super.getValue() >= super.getTotal()) {\n            super.reset();\n        }\n        super.increase(amount);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/Progress.java",
    "content": "package ee.ivxv.common.service.console;\n\nimport java.util.function.Function;\n\n/**\n * Progress is a thread-safe class for tasks to report back progress.\n */\npublic interface Progress {\n\n    void increase(int amount);\n\n    void finish();\n\n    /**\n     * {@code Factory} is a named function for providing {@code Progress} instance for the 'total'.\n     */\n    @FunctionalInterface\n    public interface Factory extends Function<Integer, Progress> {\n        // Inherit only. Just for providing nicer name.\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/ProgressImpl.java",
    "content": "package ee.ivxv.common.service.console;\n\nimport java.util.concurrent.atomic.LongAdder;\n\n/**\n * Progress is a thread-safe class for tasks to report back progress.\n */\nclass ProgressImpl implements Progress {\n\n    private final long total;\n    private final Runnable finishedNotifier;\n    private LongAdder value = new LongAdder();\n    private boolean isFinished = false;\n\n    ProgressImpl(long total, Runnable finishedNotifier) {\n        this.total = Math.max(total, 0);\n        this.finishedNotifier = finishedNotifier;\n    }\n\n    @Override\n    public void increase(int amount) {\n        if (amount <= 0) {\n            return;\n        }\n        if (isFinished) {\n            return;\n        }\n        value.add(amount);\n        // Call update() callback here if necessary\n    }\n\n    @Override\n    public void finish() {\n        if (isFinished) {\n            return;\n        }\n        isFinished = true;\n        finishedNotifier.run();\n    }\n\n    long getValue() {\n        return isFinished ? total : value.sum();\n    }\n\n    long getTotal() {\n        return total;\n    }\n\n    boolean isFinished() {\n        return isFinished;\n    }\n\n    void reset() {\n        value.reset();\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/console/TextFormatter.java",
    "content": "package ee.ivxv.common.service.console;\n\nimport java.util.Collections;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class TextFormatter {\n\n    private static final Logger log = LoggerFactory.getLogger(TextFormatter.class);\n\n    private static final String TAG_START = \"[[\";\n    private static final String TAG_END = \"]]\";\n    /** Absolute tabulation: col:2 + col:4 = column 4. */\n    private static final String TAG_COL = \"col\";\n    /** Cumulative tabulation: tab:2 + tab:2 = column 4. */\n    private static final String TAG_TAB = \"tab\";\n\n    public String formatLine(String line) {\n        if (line == null || line.isEmpty()) {\n            return line;\n        }\n        try {\n            StringBuffer result = new StringBuffer();\n            int i = 0;\n            int lastTab = 0;\n\n            while (true) {\n                int j = line.indexOf(TAG_START, i);\n                if (j == -1) {\n                    break;\n                }\n                int k = line.indexOf(TAG_END, j);\n                if (k == -1) {\n                    throw new RuntimeException(\"Invalid format: tag end not found\");\n                }\n\n                result.append(line.substring(i, j));\n                int resLen = result.length();\n\n                int tabWidth;\n                int paddingWidth;\n                String tagContent = line.substring(j + TAG_START.length(), k);\n                if (tagContent.startsWith(TAG_COL)) {\n                    tabWidth = Integer.parseInt(tagContent.substring(TAG_COL.length()).trim());\n                    paddingWidth = Math.max(0, tabWidth - resLen);\n                } else if (tagContent.startsWith(TAG_TAB)) {\n                    tabWidth = Integer.parseInt(tagContent.substring(TAG_TAB.length()).trim());\n                    paddingWidth = Math.max(0, lastTab + tabWidth - resLen);\n                } else {\n                    throw new RuntimeException(\"Invalid format: unknown tag: \" + tagContent);\n                }\n\n                String padding = String.join(\"\", Collections.nCopies(paddingWidth, \" \"));\n                result.append(padding);\n\n                i = k + TAG_END.length();\n                lastTab += tabWidth;\n            }\n\n            result.append(line.substring(i));\n\n            return result.toString();\n        } catch (Exception e) {\n            log.warn(\"Exception occurred while processing tags\", e);\n            return line;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/Container.java",
    "content": "package ee.ivxv.common.service.container;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Container is a generic signed file container.\n */\npublic class Container {\n\n    private final List<DataFile> files;\n    private final List<Signature> signatures;\n\n    public Container(List<DataFile> files, List<Signature> signatures) {\n        this.files = Collections.unmodifiableList(files);\n        this.signatures = Collections.unmodifiableList(signatures);\n    }\n\n    /**\n     * @return Returns an unmodifiable not-null list of data files.\n     */\n    public List<DataFile> getFiles() {\n        return files;\n    }\n\n    /**\n     * @return Returns an unmodifiable not-null list of signatures.\n     */\n    public List<Signature> getSignatures() {\n        return signatures;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/ContainerReader.java",
    "content": "package ee.ivxv.common.service.container;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Path;\n\npublic interface ContainerReader {\n\n    /**\n     * Performs primary essential checks whether the path might be a valid container. Does not throw\n     * an exception.\n     * \n     * @param path\n     * @return\n     */\n    boolean isContainer(Path path);\n\n    /**\n     * If {@code isContainer(path)} returns {@code false}, throws a {@code MessageException}.\n     * \n     * @param path\n     * @throws MessageException If the given path is not a container.\n     */\n    default void requireContainer(Path path) throws MessageException {\n        if (!isContainer(path)) {\n            throw new MessageException(M.e_file_not_container, path);\n        }\n    }\n\n    /**\n     * Reads a signed container from the specified path and returns it if the container is valid.\n     * \n     * @param path\n     * @return\n     * @throws InvalidContainerException if the container is not valid.\n     */\n    Container read(String path) throws InvalidContainerException;\n\n    /**\n     * Reads a signed container from the specified path and returns it if the container is valid.\n     * Container size is limited by limitSize.\n     *\n     * @param path\n     * @param limitSize\n     * @return\n     * @throws InvalidContainerException if the container is not valid.\n     */\n    Container read(String path, int limitSize) throws InvalidContainerException;\n\n    /**\n     * Reads a signed container from the specified stream and returns it if the container is valid.\n     * NB! BDOC appended bytes presence is not validated when container is passed as InputStream, use read(String path)\n     * for it.\n     * \n     * @param input\n     * @param ref The reference name of the container\n     * @return\n     * @throws InvalidContainerException if the container is not valid.\n     */\n    Container read(InputStream input, String ref) throws InvalidContainerException;\n\n    /**\n     * Combine a signed container that consists of the provided components.\n     * \n     * @param data The initial container data without OCSP and TS data.\n     * @param ocsp The OCSP response\n     * @param ts The time stamp token data\n     * @param tsC14nAlg The canonicalization algorithm to be used. May be {@code null}.\n     * @return\n     */\n    byte[] combine(byte[] data, byte[] ocsp, byte[] ts, String tsC14nAlg);\n\n    /**\n     * Opens a signed container with the specified bytes.\n     * \n     * @param bytes The bytes of the container.\n     * @param ref The reference name of the container\n     * @return\n     * @throws InvalidContainerException\n     */\n    default Container open(byte[] bytes, String ref) throws InvalidContainerException {\n        try (InputStream input = new ByteArrayInputStream(bytes)) {\n            return read(input, ref);\n        } catch (IOException e) {\n            /*\n             * Does not happen - the JavaDoc of {@code ByteArrayInputStream} says:\n             * \"Closing a <tt>ByteArrayInputStream</tt> has no effect\", but must be handled anyway.\n             */\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Extracts and returns the bytes from which the Message Digest structure is created which is\n     * sent to the timestamp server.\n     * \n     * @param data The initial container data without OCSP and TS data.\n     * @param c14nAlg The canonicalization algorithm to be used.\n     * @return\n     */\n    byte[] getTimestampData(byte[] data, String c14nAlg);\n\n    /**\n     * @return Returns the file extension for the container type.\n     */\n    String getFileExtension();\n\n    /**\n     * Should be called before program exits, to gracefully shut down all the executor service and\n     * threads used by the implementation. The default implementation does nothing.\n     */\n    default void shutdown() {\n        // Nothing\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/DataFile.java",
    "content": "package ee.ivxv.common.service.container;\n\nimport java.io.InputStream;\n\n/**\n * DataFile is interface for data files.\n */\npublic interface DataFile {\n\n    String getName();\n\n    InputStream getStream();\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/InvalidContainerException.java",
    "content": "package ee.ivxv.common.service.container;\n\npublic class InvalidContainerException extends RuntimeException {\n\n    private static final long serialVersionUID = 4698298758772827643L;\n\n    public final String path;\n\n    public InvalidContainerException(String path) {\n        super(String.format(\"Invalid signed container: %s\", path));\n        this.path = path;\n    }\n\n    public InvalidContainerException(String path, Throwable e) {\n        super(String.format(\"Invalid signed container: %s\", path), e);\n        this.path = path;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/Signature.java",
    "content": "package ee.ivxv.common.service.container;\n\nimport java.time.Instant;\n\npublic class Signature {\n\n    private final Subject signer;\n    private final Instant signingTime;\n    private final Profile profile;\n    private final byte[] value;\n\n    public Signature(Subject signer, Instant signingTime, Profile profile, byte[] value) {\n        this.signer = signer;\n        this.signingTime = signingTime;\n        this.profile = profile;\n        this.value = value;\n    }\n\n    public Subject getSigner() {\n        return signer;\n    }\n\n    public Instant getSigningTime() {\n        return signingTime;\n    }\n\n    public Profile getProfile() {\n        return profile;\n    }\n\n    public byte[] getValue() {\n        return value;\n    }\n\n    public enum Profile {\n        UNKNOWN, BDOC_TS;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/Subject.java",
    "content": "package ee.ivxv.common.service.container;\n\npublic class Subject {\n\n    private final String serialNumber;\n    private final String name;\n\n    public Subject(String serialNumber, String name) {\n        this.serialNumber = serialNumber;\n        this.name = name;\n    }\n\n    public String getSerialNumber() {\n        return serialNumber;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/bdoc/BdocContainerReader.java",
    "content": "package ee.ivxv.common.service.container.bdoc;\n\nimport static ee.ivxv.common.util.Util.CHARSET;\n\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.InvalidContainerException;\nimport ee.ivxv.common.util.Util;\n\nimport java.io.*;\nimport java.nio.file.Path;\nimport java.security.cert.X509Certificate;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.function.Function;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\nimport java.util.zip.ZipOutputStream;\nimport javax.xml.crypto.dsig.XMLSignature;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\nimport ee.ivxv.common.zip.Zip;\nimport ee.ivxv.common.zip.ZipFileCommentException;\nimport org.apache.xml.security.Init;\nimport org.apache.xml.security.c14n.Canonicalizer;\nimport org.digidoc4j.*;\nimport org.digidoc4j.impl.asic.tsl.TSLCertificateSourceImpl;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.NodeList;\n\npublic class BdocContainerReader implements ContainerReader {\n\n    private static final Logger log = LoggerFactory.getLogger(BdocContainerReader.class);\n\n    public static final String FILE_EXTENSION = \"bdoc\";\n    private static final String SIG_FILE_PREFIX = \"META-INF/signatures\";\n    private static final String SIG_VALUE_EL = \"SignatureValue\";\n    private static final int MAX_BDOC_SIZE = 10 * 1024 * 1024; // 10 MiB\n    // Although a valid vote has only [mimetype, META-INF/signatures0.xml, META-INF/manifest.xml, *.ballot] files,\n    // we allow overall zip size to be < 500 MiB\n    private static final int MAX_FILES_COUNT = 50;\n\n    private final Configuration conf;\n\n    static {\n        Init.init();\n    }\n\n    public BdocContainerReader(Conf conf, int nThreads) {\n        this(createConfiguration(conf, nThreads));\n        log.info(\"BdocContainerReader instantiated with thread count {}\", nThreads);\n    }\n\n    private BdocContainerReader(Configuration conf) {\n        this.conf = conf;\n    }\n\n    private static Configuration createConfiguration(Conf conf, int nThreads) {\n        return ConfigurationBuilder\n                .aConfiguration()\n                .withNThreads(nThreads)\n                .withX509Certs(conf.getCaCerts())\n                .withX509Certs(conf.getOcspCerts())\n                .withX509Certs(conf.getTsaCerts())\n                .build();\n    }\n\n    @Override\n    public boolean isContainer(Path path) {\n        try (ZipFile zip = new ZipFile(path.toFile())) {\n            return true;\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    @Override\n    public final Container read(String path) throws InvalidContainerException {\n        log.debug(\"readContainer({}) called\", path);\n        ContainerBuilder cb = ContainerBuilder\n                .aContainer()\n                .fromExistingFile(path)\n                .withConfiguration(conf);\n        return read(cb, path, true, MAX_BDOC_SIZE);\n    }\n\n    @Override\n    public final Container read(String path, int limitSize) throws InvalidContainerException {\n        log.debug(\"readContainer({}) called with size limit {}\", path, limitSize);\n        ContainerBuilder cb = ContainerBuilder\n                .aContainer()\n                .fromExistingFile(path)\n                .withConfiguration(conf);\n        return read(cb, path, true, limitSize);\n    }\n\n    @Override\n    public final Container read(InputStream input, String ref) throws InvalidContainerException {\n        log.debug(\"readContainer(<InputStream>, {}) called\", ref);\n        ContainerBuilder cb = ContainerBuilder\n                .aContainer()\n                .fromStream(input)\n                .withConfiguration(conf);\n        return read(cb, ref, false, 0);\n    }\n\n    private Container read(ContainerBuilder containerBuilder, String ref, boolean isFullRefPath, int limitSize)\n            throws InvalidContainerException {\n        try {\n            org.digidoc4j.Container c = containerBuilder.build();\n\n            if (log.isDebugEnabled()) {\n                String files = c\n                        .getDataFiles()\n                        .stream()\n                        .map(DataFile::getName)\n                        .collect(Collectors.joining(\", \"));\n                Function<Signature, String> subjNameOrNull =\n                        (s) -> Optional\n                                .ofNullable(s.getSigningCertificate())\n                                .map(X509Cert::getSubjectName)\n                                .orElse(\"null\");\n                String signers = c\n                        .getSignatures()\n                        .stream()\n                        .map(subjNameOrNull)\n                        .collect(Collectors.joining(\", \"));\n                log.debug(\"container {}, files: {}\", ref, files);\n                log.debug(\"container {}, signers: {}\", ref, signers);\n            }\n\n            validate(c, ref);\n\n            // NB! c.saveAsStream() sets general purpose flag's bit 3, however ZipInputStream cannot determine the\n            // uncompressed/compressed sizes ahead from [data descriptor n], although bytes are read into memory by\n            // that time\n            if (isFullRefPath) {\n                // digidoc4j doesn't validate appended bytes at the end of BDOC(zip).\n                try (InputStream is = new BufferedInputStream(new FileInputStream(ref))) {\n                    Zip.endOfCentralDirectoryRecord(is, MAX_FILES_COUNT, limitSize, limitSize);\n\n                    // If at least 1 byte can be read, then there are appended bytes at the end of BDOC(zip)\n                    if (is.read(new byte[1]) != -1) {\n                        throw new ZipFileCommentException();\n                    }\n                }\n            }\n\n            return DataConverter.convert(c);\n        } catch (ZipFileCommentException | IOException e) { // zip errors\n            throw new InvalidContainerException(e.getMessage());\n        } catch (InvalidContainerException e) {\n            throw e;\n        } catch (Throwable e) {\n            throw new InvalidContainerException(ref, e);\n        }\n    }\n\n    /**\n     * Validates the specified container, i.e. throws a runtime exception if the container is not\n     * valid. Subclasses can override it to bypass validation, which is generally not recommended.\n     *\n     * @param c digidoc container\n     * @param ref The reference name of the container\n     * @throws InvalidContainerException if the specified BDOC container is invalid.\n     */\n    protected void validate(org.digidoc4j.Container c, String ref)\n            throws InvalidContainerException {\n        ContainerValidationResult vr = c.validate();\n\n        if (!vr.isValid()) {\n            log.warn(\"BDOC container does not validate! Validation report: {}\", vr.getReport());\n            throw new InvalidContainerException(ref);\n        }\n    }\n\n    @Override\n    public byte[] combine(byte[] bdoc, byte[] ocsp, byte[] ts, String tsC14nAlg) {\n        Objects.requireNonNull(ts);\n        ByteArrayOutputStream out = new ByteArrayOutputStream(5000);\n        byte[] buffer = new byte[1024];\n\n        log.debug(\"combine(): combining container data\");\n\n        try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bdoc), CHARSET);\n             ZipOutputStream zos = new ZipOutputStream(out, CHARSET)) {\n            for (ZipEntry ze; (ze = zis.getNextEntry()) != null;) {\n                zos.putNextEntry(new ZipEntry(ze.getName()));\n\n                // Modify signature file, copy others\n                if (ze.getName().startsWith(SIG_FILE_PREFIX)) {\n                    String xml = Util.toString(Util.toBytes(zis, buffer));\n\n                    zos.write(addDataToSignature(xml, ocsp, ts, tsC14nAlg));\n                } else {\n                    for (int len; (len = zis.read(buffer)) > 0;) {\n                        zos.write(buffer, 0, len);\n                    }\n                }\n            }\n            zos.closeEntry();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return out.toByteArray();\n    }\n\n    private byte[] addDataToSignature(String sigXml, byte[] ocsp, byte[] ts, String tsC14nAlg) {\n        String unsignedProps = UNSIGNED_PROPS_EL;\n\n        // Insert OCSP response\n        String ocspRespStr = Base64.getEncoder().encodeToString(ocsp);\n        unsignedProps = unsignedProps.replace(OCSP_RESP_KEY, ocspRespStr);\n\n        // Insert Timestamp\n        String c14nEl = tsC14nAlg == null ? \"\" : TS_C14N_EL.replace(TS_C14N_ALG_KEY, tsC14nAlg);\n        String tsRespStr = Base64.getEncoder().encodeToString(ts);\n        String tsStr = TS_EL;\n        tsStr = tsStr.replace(TS_C14N_EL_KEY, c14nEl);\n        tsStr = tsStr.replaceAll(TS_RESP_KEY, tsRespStr);\n\n        unsignedProps = unsignedProps.replace(TS_EL_KEY, tsStr);\n\n        // First remove any traces of possible existing 'UnsignedProperties' element\n        String cleanSigXml = REMOVE_USP\n                .matcher(sigXml)\n                .replaceAll(\"\");\n        // Then add new 'UnsignedProperties' element\n        String result = ADD_USP\n                .matcher(cleanSigXml)\n                .replaceFirst(\"$1\\n\" + unsignedProps);\n\n        return Util.toBytes(result);\n    }\n\n    @Override\n    public byte[] getTimestampData(byte[] bdoc, String c14nAlg) {\n        try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bdoc), CHARSET)) {\n            for (ZipEntry ze; (ze = zis.getNextEntry()) != null;) {\n                if (ze.getName().startsWith(SIG_FILE_PREFIX)) {\n                    return calculateTimestampData(Util.toBytes(zis), c14nAlg);\n                }\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        throw new RuntimeException(\"Signature file with prefix\" + SIG_FILE_PREFIX + \" not found\");\n    }\n\n    private byte[] calculateTimestampData(byte[] sigXml, String c14nAlg) throws Exception {\n        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();\n        dbf.setNamespaceAware(true);\n        Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(sigXml));\n        NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, SIG_VALUE_EL);\n\n        Canonicalizer c11r = Canonicalizer.getInstance(c14nAlg);\n\n        ByteArrayOutputStream bos = new ByteArrayOutputStream();\n        c11r.canonicalizeSubtree(nl.item(0), bos);\n\n        return bos.toByteArray();\n    }\n\n    @Override\n    public String getFileExtension() {\n        return FILE_EXTENSION;\n    }\n\n    @Override\n    public void shutdown() {\n        if (conf.getThreadExecutor() != null) {\n            conf.getThreadExecutor().shutdown();\n        }\n    }\n\n    private static final Pattern REMOVE_USP = Pattern.compile(\n            \"<\\\\s*\\\\w+:UnsignedProperties[^>]*>.*</\\\\s*\\\\w+:UnsignedProperties[^>]*>\",\n            Pattern.DOTALL);\n    private static final Pattern ADD_USP = Pattern.compile(\"(</\\\\s*\\\\w+:SignedProperties[^>]*>)\");\n\n    private static final String OCSP_RESP_KEY = \"OCSP_RESPONSE\";\n    private static final String TS_EL_KEY = \"TS_ELEMENT\";\n    private static final String TS_RESP_KEY = \"TS_RESPONSE\";\n    private static final String TS_C14N_EL_KEY = \"TS_C14N_EL\";\n    private static final String TS_C14N_ALG_KEY = \"TS_C14N_ALG\";\n\n    // (Re-)defining namespaces for local use to be sure\n    private static final String UNSIGNED_PROPS_EL = \"\" //\n            + \"        <xades:UnsignedProperties\\n\" //\n            + \"            xmlns:xades=\\\"http://uri.etsi.org/01903/v1.3.2#\\\"\\n\"\n            + \"            xmlns:ds=\\\"http://www.w3.org/2000/09/xmldsig#\\\">\\n\"\n            + \"          <xades:UnsignedSignatureProperties>\\n\" //\n            + TS_EL_KEY //\n            + \"            <xades:RevocationValues>\\n\" //\n            + \"              <xades:OCSPValues>\\n\" //\n            + \"                <xades:EncapsulatedOCSPValue Id=\\\"N0\\\">\\n\" //\n            + \"                  \" + OCSP_RESP_KEY + \"\\n\" //\n            + \"                </xades:EncapsulatedOCSPValue>\\n\" //\n            + \"              </xades:OCSPValues>\\n\" //\n            + \"            </xades:RevocationValues>\\n\"\n            + \"          </xades:UnsignedSignatureProperties>\\n\"\n            + \"        </xades:UnsignedProperties>\";\n\n    private static final String TS_EL = \"\" //\n            + \"            <xades:SignatureTimeStamp Id=\\\"S0-T0\\\">\\n\" //\n            + TS_C14N_EL_KEY //\n            + \"              <xades:EncapsulatedTimeStamp>\\n\" //\n            + \"                \" + TS_RESP_KEY + \"\\n\" //\n            + \"              </xades:EncapsulatedTimeStamp>\\n\" //\n            + \"            </xades:SignatureTimeStamp>\\n\";\n\n    private static final String TS_C14N_EL = \"\" //\n            + \"              <ds:CanonicalizationMethod Algorithm=\\\"\" + TS_C14N_ALG_KEY + \"\\\" />\\n\";\n\n    /**\n     * ConfigurationBuilder is helper class for composing configuration.\n     */\n    static class ConfigurationBuilder {\n\n        private final TSLCertificateSource certSource = new TSLCertificateSourceImpl();\n        private int nThreads;\n\n        static ConfigurationBuilder aConfiguration() {\n            return new ConfigurationBuilder();\n        }\n\n        ConfigurationBuilder withNThreads(int n) {\n            nThreads = n;\n            return this;\n        }\n\n        /**\n         * Add x509 certificate to the runtime Trusted Service List (TSL).\n         * TSL will be used by digidocj to ensure that {@code cert}\n         * is authorized to act as an intermediate or root CA to validate:\n         * <ul>\n         *     <li>user's certificate</li>\n         *     <li>OCSP response</li>\n         *     <li>TSA timestamp</li>\n         * </ul>\n         * NB! Use with caution, if user's certificate issuer is {@code cert}\n         * then digidocj will authorize that {@code cert} and no\n         * full certificate chain needed\n         *\n         * @param cert intermediate/root CA (to validate user cert, OCSP response or TSA response)\n         */\n        void withX509Cert(X509Certificate cert) {\n            certSource.addTSLCertificate(cert);\n        }\n\n        ConfigurationBuilder withX509Certs(List<X509Certificate> certs) {\n            certs.forEach(this::withX509Cert);\n            return this;\n        }\n\n        Configuration build() {\n            Configuration conf = new Configuration();\n            conf.setTSL(certSource);\n            conf.setThreadExecutor(createExecutorService());\n            conf.setAllowASN1UnsafeInteger(true);\n            return conf;\n        }\n\n        private ExecutorService createExecutorService() {\n            if (nThreads <= 0) {\n                return Executors.newCachedThreadPool();\n            }\n            return Executors.newFixedThreadPool(nThreads);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/container/bdoc/DataConverter.java",
    "content": "package ee.ivxv.common.service.container.bdoc;\n\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.container.Signature;\nimport ee.ivxv.common.service.container.Subject;\nimport java.io.InputStream;\nimport java.time.Instant;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.digidoc4j.X509Cert;\nimport org.digidoc4j.X509Cert.SubjectName;\nimport org.digidoc4j.impl.asic.AsicSignature;\n\nclass DataConverter {\n\n    static Container convert(org.digidoc4j.Container c) {\n        return new Container(convertFiles(c.getDataFiles()), convertSignatures(c.getSignatures()));\n    }\n\n    static List<DataFile> convertFiles(List<org.digidoc4j.DataFile> files) {\n        return files.stream().map(DataConverter::convertFile).collect(Collectors.toList());\n    }\n\n    static DataFile convertFile(org.digidoc4j.DataFile df) {\n        return new DataFileImpl(df);\n    }\n\n    static List<Signature> convertSignatures(List<org.digidoc4j.Signature> signatures) {\n        return signatures.stream().map(DataConverter::convertSignature)\n                .collect(Collectors.toList());\n    }\n\n    static Signature convertSignature(org.digidoc4j.Signature s) {\n        Subject signer = getSubject(s.getSigningCertificate());\n        Instant time = Stream.of(s.getTrustedSigningTime(), s.getClaimedSigningTime())\n                .filter(d -> d != null).findFirst().map(Date::toInstant).orElse(null);\n        byte[] value = null;\n        if (s instanceof AsicSignature) {\n            // The base64 representation of the value is the contents of <SignatureValue> tag.\n            value = ((AsicSignature) s).getOrigin().getSignatureValue();\n        }\n        Signature.Profile profile = null;\n        switch (s.getProfile()) {\n            case LT:\n                profile = Signature.Profile.BDOC_TS;\n                break;\n            default:\n                profile = Signature.Profile.UNKNOWN;\n        }\n\n        return new Signature(signer, time, profile, value);\n    }\n\n    private static Subject getSubject(X509Cert cert) {\n        if (cert == null) {\n            return null;\n        }\n        String serial = cert.getSubjectName(SubjectName.SERIALNUMBER);\n        String first = cert.getSubjectName(SubjectName.GIVENNAME);\n        String last = cert.getSubjectName(SubjectName.SURNAME);\n\n        if (serial == null || first == null || last == null) {\n            // Common name is \"SURNAME,GIVENNAME,SERIALNUMBER\", possibly in quotes\n            String cn = Optional.ofNullable(cert.getSubjectName(SubjectName.CN))\n                    .map(n -> n.replaceAll(\"^\\\"|\\\"$\", \"\")).orElse(\"\");\n            String[] splits = cn.split(\",\", 3);\n\n            if (splits.length > 2) {\n                if (serial == null) {\n                    serial = splits[2];\n                }\n                if (first == null) {\n                    first = splits[1];\n                }\n                if (last == null) {\n                    last = splits[0];\n                }\n            }\n        }\n\n        String name = Stream.of(first, last) //\n                .filter(s -> s != null) //\n                .map(String::trim) //\n                .filter(s -> !s.isEmpty()) //\n                .collect(Collectors.joining(\" \"));\n\n        return new Subject(serial, name);\n    }\n\n    /**\n     * DataFileImpl is implementation of DataFile specific to digidoc4j library.\n     */\n    static class DataFileImpl implements DataFile {\n\n        private final org.digidoc4j.DataFile df;\n\n        DataFileImpl(org.digidoc4j.DataFile df) {\n            this.df = df;\n        }\n\n        @Override\n        public String getName() {\n            return df.getName();\n        }\n\n        @Override\n        public InputStream getStream() {\n            return df.getStream();\n        }\n\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/Cal10nI18nImpl.java",
    "content": "package ee.ivxv.common.service.i18n;\n\nimport ch.qos.cal10n.IMessageConveyor;\nimport ch.qos.cal10n.MessageConveyor;\nimport ee.ivxv.common.conf.LocaleConf;\nimport java.util.Arrays;\nimport java.util.Locale;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class Cal10nI18nImpl extends DefaultI18n implements I18n {\n\n    private static final Logger log = LoggerFactory.getLogger(Cal10nI18nImpl.class);\n\n    private final LocaleConf locale;\n    private IMessageConveyor mc;\n\n    public Cal10nI18nImpl(LocaleConf locale) {\n        this.locale = locale;\n\n        setTranslatorForLocale(locale.getLocale());\n\n        // Register to locale change events\n        locale.addLocaleChangeListener(this, this::setTranslatorForLocale);\n    }\n\n    private void setTranslatorForLocale(Locale l) {\n        /*\n         * If translation resource bundles are to be loaded at run-time along with other\n         * configuration, the usage of MessageConveyor class needs to be replaced with a\n         * modification of it, where CAL10NResourceBundleFinder is invoked with a URLClassLoader\n         * that loads resources from configuration bundle (directory, bdoc, zip, jar), rather than\n         * the main class path.\n         * \n         * Note: Similar modification has to be done with MessageKeyVerifier in I18nTest.\n         */\n        mc = new MessageConveyor(l);\n    }\n\n    @Override\n    public String getInternal(Enum<?> key, Object... args) {\n        try {\n            return mc.getMessage(key, args);\n        } catch (RuntimeException e) {\n            String s = String.format(\"[Missing translation for language '%s': %s %s]\",\n                    locale.getLocale(), key, Arrays.asList(args));\n            log.error(s, e);\n            return s;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/DefaultI18n.java",
    "content": "package ee.ivxv.common.service.i18n;\n\nimport ee.ivxv.common.M;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Default implementation of i18n service that does not depend on any actual translation provider.\n */\npublic abstract class DefaultI18n implements I18n {\n\n    /** The parsed and re-used date-time formatters. */\n    private final Map<String, DateTimeFormatter> formats = new ConcurrentHashMap<>();\n\n    /**\n     * Returns {@code getInternal} with the specified key and resolved arguments.\n     */\n    @Override\n    public final String get(Enum<?> key, Object... args) {\n        return getInternal(key, resolveArgs(args));\n    }\n\n    /**\n     * The implementation of translation.\n     * \n     * @param key\n     * @param args Resolved arguments.\n     * @return\n     */\n    protected abstract String getInternal(Enum<?> key, Object... args);\n\n    @Override\n    public String format(Instant i) {\n        String p = get(M.m_datetime_pattern);\n        DateTimeFormatter fmt = formats.computeIfAbsent(p, key -> DateTimeFormatter.ofPattern(p));\n        return fmt.format(i.atZone(ZoneId.systemDefault()));\n    }\n\n    /**\n     * Returns a new array of arguments applying {@code resolveArg(arg)} to each element.\n     * \n     * @param args\n     * @return\n     */\n    public Object[] resolveArgs(Object[] args) {\n        if (args == null) {\n            return new Object[0];\n        }\n        return Stream.of(args).map(a -> resolveArg(a)).toArray();\n    }\n\n    /**\n     * Performs the following replacements for {@code arg}:\n     * <ul>\n     * <li>{@code Object[]} or {@code Collection<?>} {@literal -> } {@code resolveArg} applied to\n     * elements, result is converted to space-separated string.\n     * <li>{@code Translatable} {@literal -> } {@code get((Translatable) arg)},\n     * <li>{@code Throwable} {@literal -> } {@code ((Throwable) arg).getMessage()},\n     * <li>{@code Instant} {@literal -> } {@code format((Instant) arg)},\n     * </ul>\n     * \n     * @param arg\n     * @return\n     */\n    public Object resolveArg(Object arg) {\n        if (arg instanceof Object[]) {\n            return Stream.of((Object[]) arg).map(a -> String.valueOf(resolveArg(a)))\n                    .filter(s -> !s.isEmpty()).collect(Collectors.joining(\" \"));\n        }\n        if (arg instanceof Collection<?>) {\n            return ((Collection<?>) arg).stream().map(a -> String.valueOf(resolveArg(a)))\n                    .filter(s -> !s.isEmpty()).collect(Collectors.joining(\" \"));\n        }\n        if (arg instanceof Translatable) {\n            return get((Translatable) arg);\n        }\n        if (arg instanceof Throwable) {\n            String message = ((Throwable) arg).getMessage();\n            return message != null ? message : arg;\n        }\n        if (arg instanceof Instant) {\n            return format((Instant) arg);\n        }\n        return arg;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/I18n.java",
    "content": "package ee.ivxv.common.service.i18n;\n\nimport java.time.Instant;\n\n/**\n * I18n service provides translated messages for the locale the service was initialized with.\n */\npublic interface I18n {\n\n    /**\n     * Returns the translated message for the locale of the I18n service.\n     * \n     * The following modifications are applied to the arguments before composing the translation:\n     * <ul>\n     * <li>{@code Translatable} {@literal -> } {@code get((Translatable) arg)},\n     * <li>{@code Throwable} {@literal -> } {@code ((Throwable) arg).getMessage()},\n     * <li>{@code Instant} {@literal -> } {@code format((Instant) arg)},\n     * </ul>\n     * \n     * @param key The message key.\n     * @param args The message arguments.\n     * @return The internationalized message.\n     */\n    String get(Enum<?> key, Object... args);\n\n    /**\n     * @param msg The message description.\n     * @return The result of <code>get(msg.key, msg.args)</code> by default.\n     * @throws NullPointerException If the argument is <code>null</code>.\n     */\n    default String get(Translatable msg) throws NullPointerException {\n        return get(msg.getKey(), msg.getArgs());\n    }\n\n    /**\n     * @param i An instant of time.\n     * @return Returns formatted instant.\n     */\n    String format(Instant i);\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/Message.java",
    "content": "package ee.ivxv.common.service.i18n;\n\n/**\n * Container class to transport translatable message with parameters from lower levels to\n * application level, where it can be translated.\n */\npublic class Message implements Translatable {\n\n    public final Enum<?> key;\n    public final Object[] args;\n\n    public Message(Enum<?> key, Object... args) {\n        this.key = key;\n        this.args = args;\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return key;\n    }\n\n    @Override\n    public Object[] getArgs() {\n        return args;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/MessageException.java",
    "content": "package ee.ivxv.common.service.i18n;\n\npublic class MessageException extends RuntimeException implements Translatable {\n\n    private static final long serialVersionUID = 2925407779875606910L;\n\n    private final Message msg;\n\n    public MessageException(Enum<?> key, Object... args) {\n        this(new Message(key, args));\n    }\n\n    public MessageException(Throwable cause, Enum<?> key, Object... args) {\n        this(new Message(key, args), cause);\n    }\n\n    public MessageException(Message msg) {\n        super(msg.key.name());\n        this.msg = msg;\n    }\n\n    public MessageException(Message msg, Throwable cause) {\n        super(msg.key.name(), cause);\n        this.msg = msg;\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return msg.key;\n    }\n\n    @Override\n    public Object[] getArgs() {\n        return msg.args;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/i18n/Translatable.java",
    "content": "package ee.ivxv.common.service.i18n;\n\n/**\n * Translatable is an interface the implementations of which are automatically translated if they\n * are present among the <tt>I18n</tt> arguments.\n */\npublic interface Translatable {\n\n    /**\n     * @return Returns the translation key.\n     */\n    Enum<?> getKey();\n\n    /**\n     * @return Returns the translation arguments, {@code null} by default.\n     */\n    default Object[] getArgs() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/report/CsvReporterImpl.java",
    "content": "package ee.ivxv.common.service.report;\n\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.util.Util;\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * CsvReporterImpl implements methods regarding the output format being used, which is {@code CSV}.\n */\npublic class CsvReporterImpl extends DefaultReporter implements Reporter {\n\n    static final Logger log = LoggerFactory.getLogger(CsvReporterImpl.class);\n\n    private static final String TAB = \"\\t\";\n    private static final String LF = \"\\n\";\n    private static final String VERSION_NUMBER = \"1\";\n\n    public CsvReporterImpl(I18n i18n) {\n        super(i18n);\n    }\n\n    /**\n     * Writes report in {@code CSV} format into the specified file. The report version number is 1.\n     *\n     * <p>\n     * The format is:\n     * </p>\n     *\n     * <pre>\n     * {@literal\n     * report = version-number LF election-id LF *record\n     * version-number = 1*2DIGIT\n     * election-id = 1*28CHAR\n     * record = <tab-separated-content> LF\n     * }\n     * </pre>\n     *\n     * @param out     The output file\n     * @param eid     The election ID\n     * @param records The list of log records to be written in\n     * @param headers The list of additional headers\n     * @throws UncheckedIOException\n     */\n    @Override\n    public <T extends Record> void write(Path out, String eid, List<T> records, AnonymousFormatter formatter, String... headers)\n            throws UncheckedIOException {\n        try {\n            Util.createFile(out);\n        } catch (FileAlreadyExistsException e) {\n            // do nothing, since file is allowed to exist already\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n\n        try (BufferedWriter writer = Files.newBufferedWriter(out, Util.CHARSET)) {\n            writer.write(VERSION_NUMBER);\n            writer.write(LF);\n            writer.write(eid);\n            writer.write(LF);\n\n            if (headers != null) {\n                for (String header : headers) {\n                    writer.write(header);\n                    writer.write(LF);\n                }\n            }\n\n            for (Record r : records) {\n                writer.write(format(r, formatter));\n                writer.write(LF);\n            }\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n    }\n\n    /**\n     * Formats a single report record as a {@code CSV} record, using tabs as separators.\n     * In case of anonymous output - hides requested data according to the AnonymousFormatter type.\n     *\n     * @param r\n     * @return\n     */\n    @Override\n    public String format(Record r, AnonymousFormatter formatter) {\n        if (formatter.equals(AnonymousFormatter.REVOCATION_REPORT_CSV)) {\n            // Remove indexes i=1 and i=2 from the List<String>\n            // At index i=1 are NAME and SURNAME\n            // At index i=2 is IDENTITY CODE (isikukood (Estonian))\n            return IntStream\n                    .range(0, r.fields.size())\n                    .filter(i -> i != 1 && i != 2)\n                    .mapToObj(r.fields::get)\n                    .collect(Collectors.joining(TAB));\n\n        } else {\n            return r.fields.stream().collect(Collectors.joining(TAB));\n        }\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/report/DefaultReporter.java",
    "content": "package ee.ivxv.common.service.report;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.crypto.hash.HashType;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.LName;\nimport ee.ivxv.common.model.Region;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.common.util.PdfDoc;\nimport ee.ivxv.common.util.PdfDoc.Alignment;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.UncheckedIOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.time.LocalDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.SortedMap;\nimport java.util.SortedSet;\nimport java.util.TreeMap;\nimport java.util.TreeSet;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport ee.ivxv.common.util.Util;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * DefaultReporter implements methods that do not depend on output format, but only the\n * specification.\n */\npublic abstract class DefaultReporter implements Reporter {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultReporter.class);\n\n    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern(\"yyyyMMddHHmmss\");\n\n    private final I18n i18n;\n\n    public DefaultReporter(I18n i18n) {\n        this.i18n = i18n;\n    }\n\n    @Override\n    public String getCurrentTime() {\n        return TIME_FMT.format(LocalDateTime.now());\n    }\n\n    @Override\n    public LogNRecord newLog123Record(String voterId, Ballot b) {\n        Map<String, Record> records = new LinkedHashMap<>();\n        b.getVotes().forEach((qid, bytes) -> records.put(qid, newLog123Record(voterId, b, qid)));\n        return new LogNRecord(records);\n    }\n\n    @Override\n    public Record newLog123Record(String voterId, Ballot b, String qid) {\n        String time = getCurrentTime();\n        LName d = b.getDistrict();\n        String s = b.getParish();\n        return new Record(time, getVoteHash(b.getVotes().get(qid)), d.getRegionCode(),\n                d.getNumber(), voterId);\n    }\n\n    private String getVoteHash(byte[] bytes) {\n        return Base64.getEncoder().encodeToString(HashType.SHA256.getFunction().digest(bytes));\n    }\n\n    @Override\n    public Record newRevocationRecordForRecurrentVote(String voterId, Ballot b) {\n        return newRevocationRecord(RevokeAction.RECURRENT, voterId, b, \"\");\n    }\n\n    @Override\n    public Record newRevocationRecordForInvalidVote(String voterId, Ballot b) {\n        return newRevocationRecord(RevokeAction.INVALID, voterId, b, \"\");\n    }\n\n    @Override\n    public Record newRevocationRecord(RevokeAction action, String voterId, Ballot b,\n            String operator) {\n        String time = getCurrentTime();\n        return new Record(action.value, voterId, b.getName(), format(b.getTime()), time, operator);\n    }\n\n    @Override\n    public Map<String, Path> writeLogN(Path dir, String eid, String disc, LogType type,\n            Stream<LogNRecord> records) throws UncheckedIOException {\n        Map<String, List<Record>> rmap = new LinkedHashMap<>();\n\n        records.forEach(r -> r.records.forEach(\n                (qid, record) -> rmap.computeIfAbsent(qid, x -> new ArrayList<>()).add(record)));\n\n        return writeRecords(dir, eid, disc, type, rmap);\n    }\n\n    @Override\n    public Path writeEmptyLog(Path outDir, String electionID, String logDiscriminator, LogType type, String questionID) throws UncheckedIOException {\n\n        Path outFile = logNName(outDir, type, logDiscriminator, questionID);\n        try {\n            Util.createFile(outFile);\n        } catch (IOException e) {\n            throw new UncheckedIOException(e);\n        }\n\n        return outFile;\n    }\n\n    @Override\n    public Map<String, Path> writeRecords(Path dir, String eid, String disc, LogType type,\n            Map<String, List<Record>> rmap) {\n        Map<String, Path> paths = new TreeMap<>();\n        rmap.forEach((qid, rs) -> write(paths.computeIfAbsent(qid, x -> logNName(dir, type, disc, qid)),\n                eid, rs, AnonymousFormatter.NOT_ANONYMOUS, type.value));\n        return paths;\n    }\n\n    private Path logNName(Path dir, LogType type, String disc, String qid) {\n        String name = String.format(\"%s.%s.%s\", qid, disc, type.name().toLowerCase())\n                .replaceAll(\"[^a-zA-Z0-9.-]\", \"_\");\n        return dir.resolve(name);\n    }\n\n    @Override\n    public void writeIVoterList(Path jsonOut, Path pdfOut, BallotBox bb, DistrictList dl)\n            throws Exception {\n        SortedIvlData data = new SortedIvlData(bb);\n\n        IVoterList jsonIvl = createIVoterList(data, bb.getElection());\n        Json.write(jsonIvl, jsonOut);\n\n        if (pdfOut != null) {\n            Set<ParishBallots> pdfIvl = createIvlDataForPdf(dl, data);\n            writeIVoterListPdf(pdfIvl, bb.getElection(), pdfOut);\n        }\n    }\n\n    private String format(Instant i) {\n        return TIME_FMT.format(i.atZone(ZoneId.systemDefault()));\n    }\n\n    private IVoterList createIVoterList(SortedIvlData data, String election) {\n        Map<String, Map<String, List<String>>> onlinevoters = new LinkedHashMap<>();\n\n        data.districts.forEach((did, s) -> s.forEach((sid, ballots) -> ballots.forEach(vb -> {\n            onlinevoters.computeIfAbsent(did, x -> new LinkedHashMap<>())\n                    .computeIfAbsent(sid, x -> new ArrayList<String>()).add(vb.voterId);\n        })));\n\n        return new IVoterList(election, onlinevoters);\n    }\n\n    private Set<ParishBallots> createIvlDataForPdf(DistrictList dl, SortedIvlData data) {\n        Set<ParishBallots> result = new TreeSet<>();\n        Set<String> parish = data.districts.values().stream().flatMap(d -> d.keySet().stream())\n                .collect(Collectors.toSet());\n\n        dl.getDistricts().forEach((did, district) -> district.getParish().forEach(pid -> {\n            String regionName = \"\";\n            try {\n                regionName = getRegionName(dl.getRegions().get(pid));\n            }catch (Exception e){\n                // pid == \"FOREIGN\"\n            }\n            String parishName = i18n.get(M.r_ivl_parish_name, pid, regionName);\n            String districtName = i18n.get(M.r_ivl_district_name, district.getName());\n            SortedSet<VoterBallot> ballots = Optional.ofNullable(data.districts.get(did))\n                    .map(sb -> sb.get(pid)).orElse(new TreeSet<>());\n            parish.remove(pid);\n            result.add(new ParishBallots(did + \"|\" + pid, parishName, districtName, ballots));\n        }));\n        if (!parish.isEmpty()) {\n            throw new MessageException(M.e_dist_bb_parish_missing, parish);\n        }\n\n        return result;\n    }\n\n    private String getRegionName(Region r) {\n        return Stream.of(r.getCounty(), r.getParish()) // Name parts to use\n                .filter(x -> x != null) // Discard nulls\n                .collect(Collectors.joining(\", \")); // Join by ','\n    }\n\n    private void writeIVoterListPdf(Set<ParishBallots> parish, String election, Path out) throws Exception {\n        try (OutputStream os = Files.newOutputStream(out); PdfDoc doc = new PdfDoc(os)) {\n            AtomicBoolean guard = new AtomicBoolean();\n            parish.forEach(sb -> {\n                try {\n                    if (guard.getAndSet(true)) {\n                        doc.newPage();\n                    }\n\n                    doc.resetPageNumber();\n                    doc.addText(i18n.get(M.r_ivl_description, election), -1, Alignment.LEFT);\n                    doc.newLine();\n                    doc.newLine();\n                    doc.addTitle(sb.districtName, -1, Alignment.LEFT);\n                    doc.newLine();\n                    doc.addTitle(sb.parishName, -1, Alignment.LEFT);\n                    doc.newLine();\n\n                    sb.ballots.forEach(vb -> {\n                        try {\n                            doc.newLine();\n                            doc.addText(vb.ballot.getName(), 240, Alignment.LEFT);\n                            doc.tab(250);\n                            doc.addText(vb.voterId);\n                        } catch (Exception e) {\n                            log.error(\"Exception while writing voter {} ({}) data to PDF\",\n                                    vb.voterId, vb.ballot.getName(), e);\n                            throw new RuntimeException(e);\n                        }\n                    });\n                } catch (Exception e) {\n                    log.error(\"Exception while writing station '{}' to PDF\", sb.parishName, e);\n                    throw new RuntimeException(e);\n                }\n            });\n        }\n    }\n\n    private static class SortedIvlData {\n        /**\n         * Map from district id to map from station id to set of voter ballots. All sorted\n         * naturally.\n         */\n        final SortedMap<String, SortedMap<String, SortedSet<VoterBallot>>> districts =\n                new TreeMap<>();\n\n        SortedIvlData(BallotBox bb) {\n            bb.getBallots().forEach((voterId, ballots) -> ballots.getBallots().forEach(b -> {\n                districts.computeIfAbsent(b.getDistrictId(), x -> new TreeMap<>())\n                        .computeIfAbsent(b.getParish(), x -> new TreeSet<>())\n                        .add(new VoterBallot(voterId, b));\n            }));\n        }\n    }\n\n    private static class VoterBallot implements Comparable<VoterBallot> {\n        final String voterId;\n        final Ballot ballot;\n\n        VoterBallot(String voterId, Ballot ballot) {\n            this.voterId = voterId;\n            this.ballot = ballot;\n        }\n\n        @Override\n        public int compareTo(VoterBallot o) {\n            return voterId.compareTo(o.voterId);\n        }\n    }\n\n    private static class ParishBallots implements Comparable<ParishBallots> {\n        final String districtStationId;\n        final String parishName;\n        final String districtName;\n        final Set<VoterBallot> ballots;\n\n        ParishBallots(String districtStationId, String parishName, String districtName, Set<VoterBallot> ballots) {\n            this.districtStationId = districtStationId;\n            this.parishName = parishName;\n            this.districtName = districtName;\n            this.ballots = ballots;\n        }\n\n        @Override\n        public int compareTo(ParishBallots o) {\n            return districtStationId.compareTo(o.districtStationId);\n        }\n    }\n\n    /**\n     * <pre>\n     * {\n     *   \"election\": \"TESTKOV2017\",\n     *   \"onlinevoters\": {\n     *     \"164.1\": {\n     *       \"164\": [\n     *         \"23074322661\",\n     *         \"31633238606\"\n     *       ]\n     *     },\n     *     \"296.1\": {\n     *       \"296\": [\n     *         \"43421413240\",\n     *         \"17368648225\"\n     *         ]\n     *       }\n     *     }\n     *   }\n     * }\n     * </pre>\n     */\n    static class IVoterList {\n\n        private final String election;\n        private final Map<String, Map<String, List<String>>> onlinevoters =\n                new LinkedHashMap<>();\n\n        IVoterList(String election, Map<String, Map<String, List<String>>> onlinevoters) {\n            this.election = election;\n            this.onlinevoters.putAll(onlinevoters);\n        }\n\n        public String getElection() {\n            return election;\n        }\n\n        public Map<String, Map<String, List<String>>> getOnlinevoters() {\n            return onlinevoters;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/report/Reporter.java",
    "content": "package ee.ivxv.common.service.report;\n\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport java.io.UncheckedIOException;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\npublic interface Reporter {\n\n    /**\n     * @return Returns the properly formatted current time for various types of log records.\n     */\n    String getCurrentTime();\n\n    /**\n     * Creates and returns new record for {@code LOG1..LOG3} log.\n     *\n     * @param voterId\n     * @param b\n     * @return\n     */\n    LogNRecord newLog123Record(String voterId, Ballot b);\n\n    /**\n     * Creates and returns new record for {@code LOG1..LOG3} log.\n     *\n     * @param voterId\n     * @param b\n     * @param qid The question id\n     * @return\n     */\n    Record newLog123Record(String voterId, Ballot b, String qid);\n\n    /**\n     * Creates and returns new record of revocation report for recurrent vote.\n     *\n     * @param voterId\n     * @param b\n     * @return\n     */\n    Record newRevocationRecordForRecurrentVote(String voterId, Ballot b);\n\n    /**\n     * Creates and returns new record of revocation report for invalid vote.\n     *\n     * @param voterId\n     * @param b\n     * @return\n     */\n    Record newRevocationRecordForInvalidVote(String voterId, Ballot b);\n\n    /**\n     * Creates and returns new record for revocation report.\n     *\n     * @param action\n     * @param voterId\n     * @param b\n     * @param operator\n     * @return\n     */\n    Record newRevocationRecord(RevokeAction action, String voterId, Ballot b, String operator);\n\n    /**\n     * Writes the log records of the specified type into files in the specified directory and\n     * returns the map from question id to the corresponding log file path.\n     *\n     * @param dir\n     * @param eid\n     * @param disc\n     * @param type\n     * @param records\n     * @return The map from question id to the corresponding log file path used.\n     * @throws UncheckedIOException\n     */\n    Map<String, Path> writeLogN(Path dir, String eid, String disc, LogType type, Stream<LogNRecord> records)\n            throws UncheckedIOException;\n\n    /**\n     * Writes the log records grouped by question id into files in the specified directory and\n     * returns the map from question id to the corresponding log file path.\n     *\n     * @param dir\n     * @param eid\n     * @param disc\n     * @param type\n     * @param rmap\n     * @return\n     */\n    Map<String, Path> writeRecords(Path dir, String eid, String disc, LogType type,\n            Map<String, List<Record>> rmap);\n\n    /**\n     * Writes empty log records of the specified type into a file in the specified directory and\n     * returns the corresponding log file path.\n     *\n     * @param outDir\n     * @param electionID\n     * @param logDiscriminator\n     * @param type\n     * @param questionID\n     * @return The corresponding log file path used.\n     * @throws UncheckedIOException\n     */\n    Path writeEmptyLog(Path outDir, String electionID, String logDiscriminator, LogType type, String questionID)\n            throws UncheckedIOException;\n\n    /**\n     * Writes the i-voter list of the specified ballot box into the specified file.\n     *\n     * @param jsonOut\n     * @param pdfOut\n     * @param bb\n     * @throws Exception\n     */\n    void writeIVoterList(Path jsonOut, Path pdfOut, BallotBox bb, DistrictList dl) throws Exception;\n\n    /**\n     * Writes the report on the specified path.\n     *\n     * @param out\n     * @param eid\n     * @param records\n     * @param headers Additional headers\n     * @throws UncheckedIOException\n     */\n    <T extends Record> void write(Path out, String eid, List<T> records, AnonymousFormatter formatter, String... headers)\n            throws UncheckedIOException;\n\n    /**\n     * Formats single report record according to the current implementation rules.\n     *\n     * @param r\n     * @return\n     */\n    String format(Record r,AnonymousFormatter formatter);\n\n    /**\n     * Gives information about anonymization type of the output files\n     */\n    enum AnonymousFormatter {\n        NOT_ANONYMOUS,\n        REVOCATION_REPORT_CSV,\n    }\n    /**\n     * Generic report record - just a list of strings.\n     */\n    class Record {\n\n        final List<String> fields;\n\n        public Record(String... fields) {\n            this.fields = Arrays.asList(fields);\n        }\n    }\n\n    /**\n     * Set of log 1..5 records for single ballot.\n     */\n    class LogNRecord {\n\n        /** Record from question id to log record */\n        final Map<String, Record> records;\n\n        public LogNRecord(Map<String, Record> records) {\n            this.records = records;\n        }\n    }\n\n    enum RevokeAction {\n        RECURRENT(\"korduv e-hääl\"), RESTORED(\"ennistatud\"), REVOKED(\"tühistatud\"), INVALID(\"kehtetu sedel\");\n\n        final String value;\n\n        private RevokeAction(String value) {\n            this.value = value;\n        }\n    }\n\n    enum LogType {\n        LOG1(\"1\"), LOG2(\"2\"), LOG3(\"3\");\n\n        public final String value;\n\n        private LogType(String value) {\n            this.value = value;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/Apdu.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.util.Arrays;\nimport java.util.HexFormat;\nimport javax.smartcardio.CardChannel;\nimport javax.smartcardio.CardException;\nimport javax.smartcardio.CommandAPDU;\nimport javax.smartcardio.ResponseAPDU;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Helper methods for sending APDUs to a smart card reader.\n * <p>\n * The methods handle also authentication. If authentication is needed, then PIN is prompted from\n * the console or from pinpad.\n */\npublic class Apdu {\n    private static final Logger log = LoggerFactory.getLogger(Apdu.class);\n    private static final int APDU_RESPONSE_CODE_SUCCESS = 36864; // 0x9000\n    private static final int APDU_RESPONSE_CODE_PIN_REQUIRED = 27010; // 0x6982\n\n    private static final byte FEATURE_VERIFY_PIN_DIRECT = 0x06;\n    private static final int IOCTL_GET_FEATURE_REQUEST = scardCtlCode(3400);\n\n    private final CardChannel channel;\n    private final I18nConsole console;\n    private String cardId;\n\n    private enum Instruction {\n        CREATE_FILE(0xE0, \"CREATE FILE\"), READ_BINARY(0xB0, \"READ BINARY\"), SELECT_FILE(0xA4,\n                \"SELECT FILE\"), UPDATE_BINARY(0xD6, \"UPDATE BINARY\"), VERIFY(0x20, \"VERIFY\");\n\n        private final int code;\n        private final String str;\n\n        Instruction(int code, String str) {\n            this.code = code;\n            this.str = str;\n        }\n\n        public int getCode() {\n            return code;\n        }\n\n        @Override\n        public String toString() {\n            return str;\n        }\n    }\n\n    /**\n     * Initialize using values.\n     * \n     * @param channel Channel to send the APDUS\n     * @param console Console for logging\n     */\n    public Apdu(CardChannel channel, I18nConsole console) {\n        this.channel = channel;\n        this.console = console;\n    }\n\n    public void setId(String id) {\n        this.cardId = id;\n    }\n\n    private static int scardCtlCode(int code) {\n        int ioctl;\n        String os_name = System.getProperty(\"os.name\").toLowerCase();\n        if (os_name.contains(\"windows\")) {\n            ioctl = (0x31 << 16 | (code) << 2);\n        } else {\n            ioctl = 0x42000000 + (code);\n        }\n        return ioctl;\n    }\n\n    /**\n     * Create new file on the card\n     *\n     * @param data Apdu data field value\n     */\n    public ResponseAPDU createFile(byte[] data) throws CardException {\n        return transmit(Instruction.CREATE_FILE, 0, 0, data, 0);\n    }\n\n    /**\n     * Read the selected file\n     *\n     * @param len number of bytes to read\n     * @return Read bytes\n     * @throws CardException\n     */\n    public byte[] readBinary(int len) throws CardException {\n        int offset = 0;\n        int toRead = len < 0xFF ? len : 0xFF;\n        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(len);\n        byte[] data;\n        do {\n            ResponseAPDU r = readBinaryChunk(offset, toRead);\n            data = r.getData();\n            try {\n                outputStream.write(data);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n            offset += data.length;\n            toRead = len - offset < 0xFF ? len - offset : 0xFF;\n        } while (toRead != 0);\n        return outputStream.toByteArray();\n    }\n\n    private ResponseAPDU readBinaryChunk(int offset, int len) throws CardException {\n        return transmit(Instruction.READ_BINARY, offset >> 8, offset & 0xFF, null, len);\n    }\n\n    /**\n     * Select file by path relative from MF\n     *\n     * @param path path without the identifier of the MF\n     * @return Response of the select command\n     * @throws CardException\n     */\n    public ResponseAPDU selectFile(byte[] path) throws CardException {\n        if (path[0] == 0x3f && path[1] == 0x00) {\n            return transmit(Instruction.SELECT_FILE, 8, 0, Arrays.copyOfRange(path, 2, path.length),\n                    0);\n        } else {\n            return transmit(Instruction.SELECT_FILE, 8, 0, path, 0);\n        }\n    }\n\n    /**\n     * Update the file at offset.\n     * \n     * @param data Data to write to file\n     * @param offset Offset at the card to write at\n     * @throws CardException\n     */\n    public void updateBinary(byte[] data, int offset) throws CardException {\n        int len = data.length;\n        int dataOffset = 0;\n        int toWrite = len < 0xFF ? len : 0xFF;\n        ByteBuffer buf = ByteBuffer.wrap(data);\n        do {\n            byte[] chunk = new byte[toWrite];\n            buf.get(chunk);\n\n            updateBinaryChunk(offset, chunk);\n\n            dataOffset += toWrite;\n            offset += toWrite;\n            toWrite = len - dataOffset < 0xFF ? len - dataOffset : 0xFF;\n        } while (toWrite != 0);\n    }\n\n    private ResponseAPDU updateBinaryChunk(int offset, byte[] chunk) throws CardException {\n        return transmit(Instruction.UPDATE_BINARY, offset >> 8, offset & 0xFF, chunk, 0);\n    }\n\n    private ResponseAPDU transmit(Instruction ins, int p1, int p2, byte[] data, int le)\n            throws CardException {\n        return transmit(ins, p1, p2, data, le, 2);\n    }\n\n    private ResponseAPDU transmit(Instruction ins, int p1, int p2, byte[] data, int le,\n            int retryCount) throws CardException {\n        CommandAPDU apdu = new CommandAPDU(0, ins.getCode(), p1, p2, data, le);\n        log.debug(\"APDU(INS={}, P1={}, P2={}, DATA=[HIDDEN], LE={})\", ins, p1, p2, le);\n        ResponseAPDU r = channel.transmit(apdu);\n        try {\n            if (r.getSW() == APDU_RESPONSE_CODE_PIN_REQUIRED) {\n                verify();\n                transmit(ins, p1, p2, data, le, retryCount);\n            } else if (r.getSW() != APDU_RESPONSE_CODE_SUCCESS && retryCount > 0) {\n                log.debug(\"unsuccessful transmit, retying({}): {}\", retryCount,\n                        Integer.toHexString(r.getSW()));\n                transmit(ins, p1, p2, data, le, retryCount - 1);\n            } else {\n                checkResponseStatus(ins, r);\n            }\n        } catch (SmartCardException e) {\n            throw new CardException(e);\n        }\n        return r;\n    }\n\n    private void verify() throws CardException, SmartCardException {\n        javax.smartcardio.Card card = channel.getCard();\n        byte[] resp = null;\n        boolean usePinPad = false;\n        int i = 0;\n        try {\n            resp = card.transmitControlCommand(IOCTL_GET_FEATURE_REQUEST, new byte[0]);\n            for (; i < resp.length; i += 6) {\n                if (resp[i] == FEATURE_VERIFY_PIN_DIRECT) {\n                    usePinPad = true;\n                    break;\n                }\n            }\n        } catch (CardException e) {\n            // console.log(e);\n        }\n\n        byte[] apdu = new byte[] {0x00, // CLA\n                0x20, // Verify INS\n                0x00, // P1\n                0x01, // Reference number of the PIN to verify against\n                0x08, // PIN length\n                (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, // will be replaced with real pw\n                (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,};\n        if (usePinPad) {\n            int verify_ioctl = (0xff & resp[i + 2]) << 24 | (0xff & resp[i + 3]) << 16\n                    | (0xff & resp[i + 4]) << 8 | 0xff & resp[i + 5];\n            verifyPinPad(verify_ioctl, apdu);\n        } else {\n            verifyKeyboard(apdu);\n        }\n    }\n\n    private void verifyPinPad(int ioctl, byte[] apdu) throws CardException, SmartCardException {\n        byte[] cmd = Util.concatAll(constructPINVerifyStructure(), apdu);\n        boolean success;\n        do {\n            console.println(Msg.enter_pin_pinpad, cardId);\n            byte[] resp;\n            int retryCount = 3;\n            while (true) {\n                try {\n                    resp = channel.getCard().transmitControlCommand(ioctl, cmd);\n                    break;\n                } catch (CardException e) {\n                    if (retryCount-- == 0) {\n                        throw e;\n                    }\n                }\n            }\n            success = processVerifyResponse(HexFormat.of().formatHex(resp).toUpperCase());\n        } while (!success);\n    }\n\n    private void verifyKeyboard(byte[] apdu) throws CardException, SmartCardException {\n        boolean success;\n        do {\n            console.println(Msg.enter_pin_keyboard, cardId);\n            byte[] pw = console.console.readPw().getBytes();\n            System.arraycopy(pw, 0, apdu, 5, pw.length);\n            ResponseAPDU r = channel.transmit(new CommandAPDU(apdu));\n            success = processVerifyResponse(Integer.toHexString(r.getSW()));\n        } while (!success);\n    }\n\n    private boolean processVerifyResponse(String respHex) throws SmartCardException {\n        respHex = respHex.toUpperCase();\n        if (respHex.equals(\"9000\")) {\n            return true;\n        } else if (respHex.substring(0, 3).equals(\"63C\")) {\n            console.println(Msg.verify_fail_retry, respHex.substring(3));\n            return false;\n        } else if (respHex.equals(\"6983\")) {\n            throw new SmartCardException(\n                    \"Verification failed and no number of retries left - PIN blocked\");\n        } else if (respHex.equals(\"6985\")) {\n            throw new SmartCardException(\"PIN locked - CONDITIONS NOT SATISFIED\");\n        } else {\n            throw new SmartCardException(\"Unexpected response from VERIFY PIN: \" + respHex);\n        }\n    }\n\n    private byte[] constructPINVerifyStructure() {\n        return new byte[] {0x1E, // bTimeOut (30 sec)\n                0x00, // bTimeOut2\n                (byte) 0x82, // bmFormatString\n                0x04, // bmPINBlockString\n                0x00, // bmPINLengthFormat\n                0x08, // max PIN length\n                0x04, // min PIN length\n                0x02, // bEntryValidationCondition\n                0x01, // bNumberMessage\n                0x09, 0x04, // wLangId (English)\n                0x00, // bMsgIndex\n                0x00, 0x00, 0x00, // bTeoPrologue\n                0x0D, 0x00, 0x00, 0x00 // ulDataLength\n        };\n    }\n\n    private void checkResponseStatus(Instruction command, ResponseAPDU r)\n            throws SmartCardException {\n        if (r.getSW() != APDU_RESPONSE_CODE_SUCCESS) {\n            log.debug(\"BADRESPONSE: {}\", HexFormat.of().formatHex(r.getBytes()).toUpperCase());\n            String code = Integer.toHexString(r.getSW());\n            throw new SmartCardException(\n                    String.format(\"Non-successful response code to %s: %s\", command, code));\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/Card.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\n/**\n * Interface for abstract card token communication\n */\npublic interface Card {\n    /**\n     * Get the index of the terminal which is used.\n     * <p>\n     * \n     * @return Terminal identifier or -1 when terminal is not set.\n     */\n    int getTerminal();\n\n    /**\n     * Set the terminal for the card\n     * \n     * @param terminalNo\n     */\n    void setTerminal(int terminalNo);\n\n    /**\n     * Initialize the connections to the card.\n     * \n     * @throws SmartCardException\n     */\n    void initialize() throws SmartCardException;\n\n    /**\n     * Return if the card is initialized\n     * \n     * @return\n     */\n    boolean isInitialized();\n\n    /**\n     * Close the connection to the card.\n     * \n     * @throws SmartCardException\n     */\n    void close() throws SmartCardException;\n\n    /**\n     * Get the card identifier.\n     * \n     * @return\n     */\n    String getId();\n\n    /**\n     * Get an indexed file from the card.\n     * \n     * @param aid Authentication identifier if needed\n     * @param identifier File identifier\n     * @return\n     * @throws SmartCardException\n     */\n    IndexedBlob getIndexedBlob(byte[] aid, byte[] identifier) throws SmartCardException;\n\n    /**\n     * Store an indexed blob with authentication identifier\n     * \n     * @param aid Authentication identifier if needed\n     * @param identifier File identifier to store at\n     * @param blob File to store\n     * @param index File index\n     * @throws SmartCardException\n     */\n    void storeIndexedBlob(byte[] aid, byte[] identifier, byte[] blob, int index)\n            throws SmartCardException;\n\n    /**\n     * Get card information\n     * \n     * @return\n     * @throws SmartCardException\n     */\n    CardInfo getCardInfo() throws SmartCardException;\n\n    /**\n     * Store card information.\n     * \n     * @param data\n     * @throws SmartCardException\n     */\n    void storeCardInfo(CardInfo data) throws SmartCardException;\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/CardInfo.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.asn1.Sequence;\n\n/**\n * CardInfo is a class for storing the information about the card.\n */\npublic class CardInfo {\n    private final String id;\n    public final static byte[] IDENTIFIER = \"CARDINFO\".getBytes();\n\n    /**\n     * Initialize using card identifier.\n     * @param id\n     */\n    public CardInfo(String id) {\n        this.id = id;\n    }\n\n    /**\n     * Get the card identifier.\n     * @return\n     */\n    public String getId() {\n        return id;\n    }\n\n    /**\n     * Parse CardInfo from serialized data.\n     * \n     * @param file\n     * @return\n     * @throws SmartCardException\n     */\n    public static CardInfo create(byte[] file) throws SmartCardException {\n        Sequence s = new Sequence();\n        try {\n            s.readFromBytes(file);\n        } catch (ASN1DecodingException e) {\n            throw new SmartCardException(\"Error reading card info blob: \" + e.toString());\n        }\n        byte[][] fields;\n        try {\n            fields = s.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new SmartCardException(\"Error decoding blob: \" + e.toString());\n        }\n        if (fields.length != 1) {\n            throw new SmartCardException(\"Invalid format for card info blob\");\n        }\n        Field idField = new Field();\n        try {\n            idField.readFromBytes(fields[0]);\n        } catch (ASN1DecodingException e) {\n            throw new SmartCardException(\"Invalid field: \" + e.toString());\n        }\n        String id;\n        try {\n            id = idField.getString();\n        } catch (ASN1DecodingException e) {\n            throw new SmartCardException(\"Error while decoding index field: \" + e.toString());\n        }\n        return new CardInfo(id);\n    }\n\n    /**\n     * Serialize the instance.\n     * <p>\n     * Returns\n     * {@code\n     * SEQUENCE ( id GENERALSTRING )\n     * }\n     * @return\n     */\n    public byte[] encode() {\n        return new Sequence(new Field(id).encode()).encode();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/CardService.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\n/**\n * CardService is an interface for creating smartcards and card sets.\n */\npublic interface CardService {\n\n    /**\n     * Creates new instance of uninitialized card with no terminal attached.\n     * \n     * @param id\n     * @return\n     */\n    Card createCard(String id);\n\n    /**\n     * Creates an empty instance of {@code Cards}.\n     * \n     * @return\n     */\n    Cards createCards();\n\n    /**\n     * Denotes if it is card service which handles physical cards which can be inserted and removed.\n     * \n     * @return\n     */\n    boolean isPluggableService();\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/Cards.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\nimport ee.ivxv.common.util.I18nConsole;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\nimport javax.smartcardio.CardException;\nimport javax.smartcardio.CardTerminal;\n\n/**\n * Cards is a collection of cards. Class is abstract only to emphasize that instances should be\n * created using card service.\n */\npublic abstract class Cards {\n    private static final int RETRY_COUNT = 3;\n\n    private final CardService cardService;\n    private final I18nConsole console;\n    protected final List<Card> cards = new ArrayList<>();\n    private final Set<String> processedCardIds = new HashSet<>();\n\n    /**\n     * Initialize using CardService and console.\n     * \n     * @param cardService\n     * @param console\n     */\n    public Cards(CardService cardService, I18nConsole console) {\n        this.cardService = cardService;\n        this.console = console;\n    }\n\n    /**\n     * Enable fast mode if possible.\n     * <p>\n     * In fast mode, the user is not asked to insert the card terminal identifiers and they are\n     * assigned automatically.\n     * \n     * @param requiredCardCount Number of required cards\n     * @return Boolean indicating if fast mode was enabled.\n     * @throws CardException\n     */\n    public boolean enableFastMode(int requiredCardCount) throws CardException {\n        if (!cardService.isPluggableService()) {\n            return true;\n        }\n        if (cards.size() != requiredCardCount) {\n            return false;\n        }\n        for (Card card : cards) {\n            if (card.getTerminal() != -1) {\n                return false;\n            }\n        }\n        if (TerminalUtil.getCardCount() != requiredCardCount) {\n            return false;\n        }\n        Iterator<Card> cardIterator = cards.iterator();\n        List<CardTerminal> terminalList = TerminalUtil.getTerminals();\n        for (int i = 0; i < terminalList.size(); i++) {\n            if (terminalList.get(i).isCardPresent()) {\n                cardIterator.next().setTerminal(i);\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Add card.\n     * \n     * @param id\n     */\n    public void addCard(String id) {\n        cards.add(cardService.createCard(id));\n    }\n\n    /**\n     * Get list of all cards\n     * \n     * @return\n     */\n    public List<Card> getCardList() {\n        return cards;\n    }\n\n    public void initUnprocessedCard(Card card) throws SmartCardException, CardException {\n        List<CardTerminal> terminalList = TerminalUtil.getTerminals();\n        while (true) {\n            for (int i = 0; i < terminalList.size(); i++) {\n                CardTerminal terminal = terminalList.get(i);\n                if (!terminal.isCardPresent()) {\n                    continue;\n                }\n                card.setTerminal(i);\n                card.initialize();\n                String id = card.getCardInfo().getId();\n                if (!processedCardIds.contains(id)) {\n                    processedCardIds.add(id);\n                    console.println(Msg.inserted_card_id, id);\n                    return;\n                } else {\n                    card.close();\n                }\n            }\n            console.println(Msg.insert_unprocessed_card);\n            console.console.readln();\n        }\n    }\n\n    /**\n     * Get card with specified index\n     * \n     * @param index\n     * @return\n     * @throws SmartCardException\n     */\n    public Card getCard(int index) throws SmartCardException {\n        Card card = cards.get(index);\n        int termNo = card.getTerminal();\n        if (termNo == -1) {\n            termNo = askTermNo(index);\n            card.setTerminal(termNo);\n        }\n        CardTerminal terminal = null;\n        for (int i = RETRY_COUNT; i >= 0; i--) {\n            try {\n                terminal = TerminalUtil.getTerminal(termNo);\n                break;\n            } catch (CardException e) {\n                if (i == 0) {\n                    throw new SmartCardException(\"Failed to get terminal\", e);\n                }\n            } catch (SmartCardException e) {\n                if (i == 0) {\n                    throw e;\n                }\n            }\n        }\n        for (int i = RETRY_COUNT; i >= 0; i--) {\n            try {\n                waitForSpecificCard(terminal, card, termNo, 0);\n                break;\n            } catch (CardException e) {\n                if (i == 0) {\n                    throw new SmartCardException(\"Failed to wait for smart card\", e);\n                }\n            } catch (SmartCardException e) {\n                if (i == 0) {\n                    throw e;\n                }\n            }\n        }\n        return card;\n    }\n\n    private void waitForSpecificCard(CardTerminal terminal, Card card, int termNo, int tries)\n            throws CardException, SmartCardException {\n        tries = tries > 0 ? tries : Integer.MAX_VALUE;\n        for (int i = 0; i < tries; i++) {\n            waitUntilCardPresent(terminal, card, termNo);\n            String currentCard = checkCardIndex(terminal, card, termNo);\n            if (!currentCard.equals(card.getId())) {\n                waitUntilCardRemoved(terminal, card, currentCard, termNo);\n            } else {\n                return;\n            }\n        }\n    }\n\n    private void waitUntilCardPresent(CardTerminal terminal, Card card, int termNo)\n            throws CardException, SmartCardException {\n        if (!terminal.isCardPresent()) {\n            console.println(Msg.insert_card_indexed, card.getId(), termNo);\n        }\n        terminal.waitForCardPresent(0);\n        if (!card.isInitialized()) {\n            card.initialize();\n        }\n    }\n\n    private void waitUntilCardRemoved(CardTerminal terminal, Card card, String currentCard,\n            int termNo) throws CardException, SmartCardException {\n        if (terminal.isCardPresent()) {\n            console.println(Msg.remove_card_indexed, currentCard, termNo);\n        }\n        card.close();\n        terminal.waitForCardAbsent(0);\n    }\n\n    private String checkCardIndex(CardTerminal terminal, Card card, int termNo)\n            throws SmartCardException {\n        CardInfo ci = card.getCardInfo();\n        if (ci == null) {\n            card.storeCardInfo(new CardInfo(card.getId()));\n            return card.getId();\n        } else {\n            return ci.getId();\n        }\n\n    }\n\n    private int askTermNo(int cardNo) {\n        while (true) {\n            console.println(Msg.enter_terminal_id, cardNo);\n            try {\n                // may return -1 which indicates in Card interface that the terminal is not set\n                return Integer.parseInt(console.console.readln());\n            } catch (NumberFormatException e) {\n                // Ignored\n            }\n        }\n    }\n\n    @Override\n    public String toString() {\n        if (cards.size() == 0) {\n            return \"[]\";\n        }\n        StringBuilder str = new StringBuilder(\"[\" + cards.get(0).getId());\n\n        for (int i = 1; i < cards.size(); i++) {\n            str.append(\", \").append(cards.get(i).getId());\n        }\n        return str.append(\"]\").toString();\n    }\n\n    public int count() {\n        return cards.size();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/IndexedBlob.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\n/**\n * IndexedBlob is a blob with an index\n */\npublic class IndexedBlob {\n    public final byte[] blob;\n    public final int index;\n\n    /**\n     * Initialize using index and data\n     * \n     * @param index\n     * @param blob\n     */\n    public IndexedBlob(int index, byte[] blob) {\n        this.blob = blob;\n        this.index = index;\n    }\n\n    /**\n     * Get blob index.\n     * \n     * @return\n     */\n    public int getIndex() {\n        return index;\n    }\n\n    /**\n     * Get blob.\n     * \n     * @return\n     */\n    public byte[] getBlob() {\n        return blob;\n    }\n\n    @Override\n    public String toString() {\n        return String.valueOf(index);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/Msg.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\n\n@BaseName(\"i18n.common-smartcard-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg {\n    insert_card_indexed, remove_card_indexed, enter_terminal_id, //\n    insert_unprocessed_card, inserted_card_id,\n\n    // APDU\n    enter_pin_keyboard, enter_pin_pinpad, verify_fail_retry\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/SmartCardException.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\n@SuppressWarnings(\"serial\")\npublic class SmartCardException extends Exception {\n    public SmartCardException(String message) {\n        super(message);\n    }\n\n    public SmartCardException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/TerminalUtil.java",
    "content": "package ee.ivxv.common.service.smartcard;\n\nimport java.util.List;\nimport javax.smartcardio.CardException;\nimport javax.smartcardio.CardTerminal;\nimport javax.smartcardio.TerminalFactory;\n\n/**\n * Helper methods for managing card terminals.\n */\npublic class TerminalUtil {\n    private static final TerminalFactory factory = TerminalFactory.getDefault();\n\n    /**\n     * Get all available terminals.\n     * \n     * @return\n     * @throws CardException\n     */\n    public static List<CardTerminal> getTerminals() throws CardException {\n        return factory.terminals().list();\n    }\n\n    /**\n     * Get specific terminal.\n     * \n     * @param termNo\n     * @return\n     * @throws CardException\n     * @throws SmartCardException\n     */\n    public static CardTerminal getTerminal(int termNo) throws CardException, SmartCardException {\n        List<CardTerminal> terminals = TerminalUtil.getTerminals();\n        try {\n            return terminals.get(termNo);\n        } catch (IndexOutOfBoundsException e) {\n            throw new SmartCardException(String.format(\"Terminal with index %d not found\", termNo),\n                    e);\n        }\n    }\n\n    /**\n     * Get the number of available terminals.\n     * \n     * @return\n     * @throws CardException\n     */\n    public static int getTerminalCount() throws CardException {\n        return getTerminals().size();\n    }\n\n    /**\n     * Get the number of cards attached to the terminals.\n     * \n     * @return\n     * @throws CardException\n     */\n    public static int getCardCount() throws CardException {\n        int cardCount = 0;\n        for (CardTerminal terminal : getTerminals()) {\n            if (terminal.isCardPresent()) {\n                cardCount++;\n            }\n        }\n        return cardCount;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/pkcs15/PKCS15Card.java",
    "content": "package ee.ivxv.common.service.smartcard.pkcs15;\n\nimport ee.ivxv.common.service.smartcard.Apdu;\nimport ee.ivxv.common.service.smartcard.CardInfo;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.common.service.smartcard.SmartCardException;\nimport ee.ivxv.common.service.smartcard.TerminalUtil;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HexFormat;\nimport java.util.List;\nimport javax.smartcardio.Card;\nimport javax.smartcardio.CardChannel;\nimport javax.smartcardio.CardException;\nimport javax.smartcardio.CardTerminal;\nimport javax.smartcardio.ResponseAPDU;\n\nimport org.bouncycastle.asn1.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * PKCS15Card implements a card with PKCS15 file system.\n */\npublic class PKCS15Card implements ee.ivxv.common.service.smartcard.Card {\n    private static final Logger log = LoggerFactory.getLogger(PKCS15Card.class);\n    private static final byte[] MF_PATH = new byte[] {0x3f, 0x00};\n    private static final byte[] PKCS15_PATH = new byte[] {0x50, 0x15};\n    private static final byte[] ODF_PATH = Util.concatAll(PKCS15_PATH, new byte[] {0x50, 0x31});\n    private static final byte[] DEF_DF_PATH = new byte[] {0x45, 0x01};\n    private static final int RETRY_DELAY_SEC = 5;\n    private static final int RETRY_COUNT = 3;\n    private int retryCount;\n\n    protected final I18nConsole console;\n    private String id;\n    private Card card;\n    private CardChannel cardChannel;\n    private int termNo = -1;\n    private Apdu apdu;\n\n    /**\n     * Initialize using a card identifier and a console.\n     *\n     * @param id\n     * @param console\n     */\n    public PKCS15Card(String id, I18nConsole console) {\n        this(id, -1, console);\n    }\n\n    /**\n     * Initialize using a card identifier, terminal number and a console.\n     *\n     * @param id\n     * @param termNo\n     * @param console\n     */\n    private PKCS15Card(String id, int termNo, I18nConsole console) {\n        this.id = id;\n        // store the terminal number for later use. do not open the channel\n        // just yet\n        this.termNo = termNo;\n        this.console = console;\n    }\n\n    /**\n     * Get all available PKCS15Cards.\n     *\n     * @param console\n     * @return\n     * @throws CardException\n     */\n    public static PKCS15Card[] getCards(I18nConsole console) throws CardException {\n        List<CardTerminal> terminals = TerminalUtil.getTerminals();\n        List<PKCS15Card> cardList = new ArrayList<>();\n        for (int i = 0; i < terminals.size(); i++) {\n            CardTerminal ct = terminals.get(i);\n            if (ct.isCardPresent()) {\n                cardList.add(new PKCS15Card(String.valueOf(i), i, console));\n            }\n        }\n        PKCS15Card[] ret = new PKCS15Card[cardList.size()];\n        return cardList.toArray(ret);\n    }\n\n    private static boolean comparePathRoot(byte[] first, byte[] second) {\n        int l = first.length > second.length ? first.length : second.length;\n        for (int i = 0; i < l - 1; i++) {\n            if (first[i] != second[i]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private void retryThrow(CardException t, int retryThreshold) throws PKCS15Exception {\n        if (t.getCause() != null && t.getCause().getMessage().equals(\"SCARD_W_RESET_CARD\")) {\n            initialize();\n        }\n        retryThrow(new PKCS15Exception(\"Communication with card failed\", t), retryThreshold);\n    }\n\n    private void retryThrow(PKCS15Exception t, int retryThreshold) throws PKCS15Exception {\n        if (retryCount >= retryThreshold) {\n            log.debug(\"Retry threshold reached: {}/{}\", retryCount, retryThreshold);\n            retryCount = 0;\n            throw t;\n        }\n        log.debug(\"Caught exception. Waiting and retrying. Try {}/{}\", retryCount, retryThreshold);\n        retryCount++;\n        try {\n            Thread.sleep(1000 * RETRY_DELAY_SEC);\n        } catch (InterruptedException e) {\n            // single-threaded application\n            log.error(\"Retry interrupted\", e);\n        }\n    }\n\n    private static ASN1Primitive[] readObjects(byte[] bytes) throws IOException {\n        ByteArrayInputStream bs = new ByteArrayInputStream(bytes);\n        ArrayList<ASN1Primitive> res = new ArrayList<>();\n        try (ASN1InputStream input = new ASN1InputStream(bs, false)) {\n            int b;\n            while (true) {\n                bs.mark(0); // ahead does not affect methods\n                b = bs.read();\n                if (b == 0) {\n                    // we have padding\n                    break;\n                } else if (b == -1) {\n                    // stream end\n                    break;\n                }\n                bs.reset();\n                ASN1Primitive obj = input.readObject();\n                if (obj == null) {\n                    // end of meaningful data in the stream\n                    break;\n                }\n                res.add(obj);\n            }\n        }\n        return res.toArray(new ASN1Primitive[] {});\n    }\n\n    @Override\n    public boolean isInitialized() {\n        if (cardChannel == null) {\n            return false;\n        }\n        try {\n            cardChannel.getChannelNumber();\n        } catch (IllegalStateException e) {\n            return false;\n        }\n        return true;\n    }\n\n    @Override\n    public void initialize() throws PKCS15Exception {\n        List<CardTerminal> list;\n        try {\n            list = TerminalUtil.getTerminals();\n        } catch (Exception e) {\n            throw new PKCS15Exception(\"Could not get card teminal list\", e);\n        }\n        try {\n            card = list.get(termNo).connect(\"*\");\n        } catch (Exception e) {\n            throw new PKCS15Exception(\"Could not get connect to card\", e);\n        }\n        cardChannel = card.getBasicChannel();\n        apdu = new Apdu(cardChannel, console);\n        try {\n            CardInfo info = getCardInfo();\n            id = info == null ? id : info.getId();\n        } catch (SmartCardException e) {\n            throw new PKCS15Exception(\"Could not read from the card\", e);\n        }\n        apdu.setId(getId());\n\n    }\n\n    @Override\n    public void close() throws PKCS15Exception {\n        if (!isInitialized()) {\n            return;\n        }\n        try {\n            // Card works seemingly the same regardless how/whether we\n            // close the connection.\n            card.disconnect(true);\n        } catch (CardException e) {\n            throw new PKCS15Exception(\"Could not close connection to card\", e);\n        }\n        cardChannel = null;\n        apdu = null;\n    }\n\n    @Override\n    public String getId() {\n        return id;\n    }\n\n    /*\n     * interface-specific methods start here\n     */\n\n    /**\n     * Erase the file system.\n     * <p>\n     * Currently erasing the file system is not implemented and this method is a no-op.\n     *\n     * @throws PKCS15Exception\n     */\n    public void eraseFilesystem() throws PKCS15Exception {\n        // not supported\n    }\n\n    /**\n     * Create a PKCS15 file system.\n     * <p>\n     * Currently erasing the file system is not implemented and this method is a no-op.\n     *\n     * @throws PKCS15Exception\n     */\n    public void createFilesystem() throws PKCS15Exception {\n        // not supported\n    }\n\n    @Override\n    public void storeIndexedBlob(byte[] aid, byte[] identifier, byte[] blob, int index)\n            throws SmartCardException {\n        storeBlobRetry(aid, identifier, new PKCS15IndexedBlob(index, blob).encode(), RETRY_COUNT);\n    }\n\n    @Override\n    public void storeCardInfo(CardInfo data) throws SmartCardException {\n        storeBlobRetry(null, CardInfo.IDENTIFIER, data.encode(), RETRY_COUNT);\n    }\n\n    public static DLSequence[] decodeDODF(byte[] dodfBytes) throws IOException, PKCS15Exception {\n        ASN1Primitive[] vals = readObjects(dodfBytes);\n        DLSequence[] res = new DLSequence[vals.length];\n        for (int i = 0; i < vals.length; i++) {\n            DLSequence v = (DLSequence) vals[i];\n            if (v.size() != 3) {\n                throw new PKCS15Exception(\"DODF entry corrupted\");\n            }\n            if (((DLSequence) v.getObjectAt(1)).size() != 1) {\n                throw new PKCS15Exception(\"DODF entry corrupted\");\n            }\n            res[i] = (DLSequence) vals[i];\n        }\n        return res;\n    }\n\n    private DLSequence[] getDODF() throws CardException, IOException, PKCS15Exception {\n        short len = selectDODF();\n        byte[] dodfBytes = apdu.readBinary(len);\n        // Get the metainfo of objects written to the card\n        return decodeDODF(dodfBytes);\n    }\n\n    static public byte[] getAvailablePath(DLSequence[] blobmetas) throws PKCS15Exception {\n        byte[] path = Util.concatAll(PKCS15_PATH, DEF_DF_PATH);\n        if (blobmetas.length == 0) {\n            return path;\n        }\n        byte[] objpath;\n        for (DLSequence obj : blobmetas) {\n            objpath = getPath((ASN1TaggedObject) obj.getObjectAt(2));\n            objpath = getRelativePath(objpath);\n            // first case is when there are no free paths left\n            if (objpath[objpath.length - 1] == (byte) 0xFF) {\n                throw new PKCS15Exception(\"No free path on card\");\n            }\n            // we are only interested in paths having specific root\n            if (!comparePathRoot(path, objpath)) {\n                continue;\n            }\n            // otherwise we take such path which is at least one higher than any\n            // existing path\n            if (objpath[objpath.length - 1] >= path[path.length - 1]) {\n                path[path.length - 1] = (byte) (objpath[objpath.length - 1] + 1);\n            }\n        }\n        log.debug(\"Available path: {}\", HexFormat.of().formatHex(path).toUpperCase());\n        return path;\n    }\n\n    private static byte[] getRelativePath(byte[] objpath) {\n        if (objpath[0] == MF_PATH[0] && objpath[1] == MF_PATH[1]) {\n            return Arrays.copyOfRange(objpath, 2, objpath.length);\n        }\n        return objpath;\n    }\n\n    private void createNewFile(byte[] path, byte[] blob, boolean readAuth) throws CardException {\n        // Select blob's parent DF\n        apdu.selectFile(Arrays.copyOfRange(path, 0, path.length - 2));\n        // Create new file to store blob in\n        short len = (short) blob.length;\n        byte[] relatPath = Arrays.copyOfRange(path, path.length - 2, path.length);\n        byte[] createFileData = constructCreateFileApduData(relatPath, len, readAuth);\n        apdu.createFile(createFileData);\n    }\n\n    private void writeFile(byte[] path, byte[] blob) throws CardException {\n        // Select created file\n        apdu.selectFile(path);\n        // Update file with blob data\n        apdu.updateBinary(blob, 0);\n    }\n\n    private void updateDODF(byte[] path, byte[] aid, byte[] identifier, DLSequence[] existing)\n            throws CardException, PKCS15Exception, IOException {\n        // Create new dodf entry\n        byte[] dodfEntry = createDodfEntry(Util.concatAll(MF_PATH, path), aid, identifier);\n        if (existing == null) {\n            existing = getDODF();\n        } else {\n            selectDODF();\n        }\n        int offset = 0;\n        for (DLSequence v : existing) {\n            try {\n                offset += v.getEncoded(\"DER\").length;\n            } catch (IOException e) {\n                // does not happen. the input is controlled\n            }\n        }\n        apdu.updateBinary(dodfEntry, offset);\n    }\n\n    /**\n     * Store a blob on the card.\n     *\n     * @param aid Authentication identifier to associate with the file.\n     * @param identifier File identifier.\n     * @param blob File data.\n     * @throws PKCS15Exception\n     */\n    public void storeBlob(byte[] aid, byte[] identifier, byte[] blob) throws PKCS15Exception {\n        storeBlobRetry(aid, identifier, blob, 0);\n    }\n\n    // store a data object 'blob' at a location 'identifier', protected by\n    // authentication ID 'aid'\n    private void storeBlobRetry(byte[] aid, byte[] identifier, byte[] blob, int retryThreshold)\n            throws PKCS15Exception {\n        if (!isInitialized()) {\n            throw new PKCS15Exception(\"Card not initialized\");\n        }\n        boolean readAuth = aid != null;\n        DLSequence[] bm;\n        for (retryCount = 0; true;) {\n            try {\n                bm = getDODF();\n                break;\n            } catch (IOException e) {\n                retryThrow(new PKCS15Exception(\"Metainfo corrupted\"), retryThreshold);\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n            }\n        }\n        if (findDF(bm, identifier) != null) {\n            throw new PKCS15Exception(\"Blob with identifier already exists\");\n        }\n        byte[] path;\n        for (retryCount = 0; true;) {\n            try {\n                path = getAvailablePath(bm);\n                break;\n            } catch (PKCS15Exception e) {\n                retryThrow(e, retryThreshold);\n            }\n        }\n        log.debug(\"Blob path: {}\", HexFormat.of().formatHex(path).toUpperCase());\n        for (retryCount = 0; true;) {\n            try {\n                createNewFile(path, blob, readAuth);\n                break;\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n            }\n        }\n        for (retryCount = 0; true;) {\n            try {\n                writeFile(path, blob);\n                break;\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n\n            }\n        }\n        for (retryCount = 0; true;) {\n            try {\n                updateDODF(path, aid, identifier, bm);\n                break;\n            } catch (PKCS15Exception e) {\n                retryThrow(e, retryThreshold);\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n            } catch (IOException e) {\n                // does not happen, we provide bm and then the path where\n                // IOException happens is not taken\n            }\n        }\n    }\n\n    /**\n     * Remove the data object at a location protected with an authentication identifier.\n     * <p>\n     * The implementation does not support removing files. It returns false on every call.\n     *\n     * @param aid Authentication identifier\n     * @param identifier File identifier\n     * @return Success of removing the blob.\n     * @throws PKCS15Exception\n     */\n    public boolean removeBlob(byte[] aid, byte[] identifier) throws PKCS15Exception {\n        return false;\n    }\n\n    @Override\n    public IndexedBlob getIndexedBlob(byte[] aid, byte[] identifier) throws SmartCardException {\n        return PKCS15IndexedBlob.create(getBlob(aid, identifier));\n    }\n\n    @Override\n    public CardInfo getCardInfo() throws PKCS15Exception, SmartCardException {\n        try {\n            return CardInfo.create(getBlob(null, CardInfo.IDENTIFIER));\n        } catch (PKCS15Exception e) {\n            if (e.getMessage().equals(\"File does not exist\")) {\n                return null;\n            }\n            throw e;\n        }\n    }\n\n    /**\n     * Get the blob with authentication identifier.\n     *\n     * @param aid Authentication identifier.\n     * @param identifier File identifier.\n     * @return File content.\n     * @throws PKCS15Exception\n     */\n    public byte[] getBlob(byte[] aid, byte[] identifier) throws PKCS15Exception {\n        return getBlobRetry(aid, identifier, RETRY_COUNT);\n    }\n\n    private byte[] getBlobRetry(byte[] aid, byte[] identifier, int retryThreshold)\n            throws PKCS15Exception {\n        if (!isInitialized()) {\n            throw new PKCS15Exception(\"Card not initialized\");\n        }\n        DLSequence[] bm;\n        ResponseAPDU r;\n        DLSequence seq;\n        int len;\n        byte[] blob, path;\n        for (retryCount = 0; true;) {\n            try {\n                bm = getDODF();\n                break;\n            } catch (IOException e) {\n                retryThrow(new PKCS15Exception(\"Metainfo corrupted\"), retryThreshold);\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n            }\n        }\n        seq = findDF(bm, identifier);\n        if (seq == null) {\n            throw new PKCS15Exception(\"File does not exist\");\n        }\n        // verify aid\n        if (aid != null) {\n            verifyAid((DLSequence) seq.getObjectAt(0), aid);\n        }\n        // get path of blob data\n        path = getPath((ASN1TaggedObject) seq.getObjectAt(2));\n        // read the blob\n        for (retryCount = 0; true;) {\n            try {\n                r = apdu.selectFile(path);\n                len = getFileLen(r.getData());\n                blob = apdu.readBinary(len);\n                break;\n            } catch (CardException e) {\n                retryThrow(e, retryThreshold);\n            } catch (IOException e) {\n                retryThrow(new PKCS15Exception(\"Card response corrupted\"), retryThreshold);\n            }\n        }\n        return blob;\n    }\n\n    @Override\n    public int getTerminal() {\n        return termNo;\n    }\n\n    @Override\n    public void setTerminal(int terminalNo) {\n        this.termNo = terminalNo;\n    }\n\n    private DLSequence findDF(DLSequence[] DFs, byte[] identifier) {\n        DLSequence idSeq;\n        byte[] eid;\n        for (DLSequence d : DFs) {\n            idSeq = (DLSequence) d.getObjectAt(1);\n            eid = ((DERUTF8String) idSeq.getObjectAt(0)).getString().getBytes();\n            if (Arrays.equals(eid, identifier)) {\n                return d;\n            }\n        }\n        return null;\n    }\n\n    public static byte[] getPath(ASN1TaggedObject obj) throws PKCS15Exception {\n        ASN1Sequence seq = getSingleSequence(obj);\n\n        ASN1Encodable el = getSingleElement(seq);\n\n        if (el instanceof ASN1Sequence) {\n            ASN1Encodable nestedEl = getSingleElement((ASN1Sequence) el);\n\n            if (nestedEl instanceof DEROctetString) {\n                return ((DEROctetString) nestedEl).getOctets();\n            }\n\n            // Handle the newer Aventra myEID driver structure\n            if (nestedEl instanceof ASN1Sequence) {\n                ASN1Encodable innerEl = getSingleElement((ASN1Sequence) nestedEl);\n\n                if (innerEl instanceof DEROctetString) {\n                    return ((DEROctetString) innerEl).getOctets();\n                }\n            }\n        }\n\n        throw new PKCS15Exception(\"Invalid ASN1 structure\");\n    }\n\n    private static ASN1Sequence getSingleSequence(ASN1TaggedObject obj) throws PKCS15Exception {\n        ASN1Sequence seq = (ASN1Sequence) obj.getBaseUniversal(false, BERTags.SEQUENCE);\n\n        if (seq.size() != 1) {\n            throw new PKCS15Exception(\"Invalid ASN1 structure\");\n        }\n\n        return seq;\n    }\n\n    private static ASN1Encodable getSingleElement(ASN1Sequence seq) throws PKCS15Exception {\n        if (seq.size() != 1) {\n            throw new PKCS15Exception(\"Invalid ASN1 structure\");\n        }\n\n        return seq.getObjectAt(0);\n    }\n\n    public static ASN1TaggedObject getObjByTagNo(byte[] bytes, int tagNo) throws IOException {\n        ASN1Primitive[] vals = readObjects(bytes);\n        ASN1TaggedObject vv;\n        for (ASN1Primitive v : vals) {\n            vv = (ASN1TaggedObject) v;\n            if (vv.getTagNo() == tagNo) {\n                return vv;\n            }\n        }\n        return null;\n    }\n\n    private byte[] constructCreateFileApduData(byte[] identifier, short len, boolean readAuth) {\n        byte authByte = readAuth ? (byte) 0x11 : (byte) 0x01;\n        return new byte[] {0x62, // FCP tag\n                0x17, // length\n                (byte) 0x80, // File Size tag - transparent EF\n                0x02, // length\n                (byte) (len >> 8), (byte) len, // File Size value\n                (byte) 0x82, // File Description tag\n                0x01, // length\n                0x01, // File Descriptor - transparent EF\n                (byte) 0x83, // File Identifier tag\n                0x02, // length\n                identifier[0], identifier[1], // File Identifier value\n                (byte) 0x86, // Security Attributes tag\n                0x03, // length\n                authByte, // Pin reference for Read and Update\n                0x1F, // Pin reference for Delete\n                (byte) 0xFF, // RFU\n                (byte) 0x85, // Proprietary Information tag\n                0x02, // length\n                0x00, // RFU in case of EF\n                0x00, // RFU\n                (byte) 0x8A, // Life Cycle Status tag\n                0x01, // length\n                0x00, // RFU\n        };\n    }\n\n    private byte[] createDodfEntry(byte[] path, byte[] aid, byte[] identifier) {\n        ASN1EncodableVector root = new ASN1EncodableVector();\n\n        ASN1EncodableVector v1 = new ASN1EncodableVector();\n        if (aid != null) {\n            v1.add(new DERBitString(new byte[] {(byte) 0x80}, 6));\n            v1.add(new DEROctetString(aid));\n        } else {\n            v1.add(new DERBitString(new byte[] {(byte) 0x80}, 6));\n        }\n\n        ASN1EncodableVector v2 = new ASN1EncodableVector();\n        v2.add(new DERUTF8String(new String(identifier)));\n\n        ASN1EncodableVector v3 = new ASN1EncodableVector();\n        v3.add(new DEROctetString(path));\n\n        root.add(new DERSequence(v1));\n        root.add(new DERSequence(v2));\n        root.add(new DERTaggedObject(1, new DERSequence(v3)));\n\n        byte[] res;\n        try {\n            res = new DERSequence(root).getEncoded(\"DER\");\n        } catch (IOException e) {\n            // if exception is thrown here then this is a programming error.\n            // these error must be fixed during development phase and thus they\n            // are not thrown in production.\n            res = null;\n        }\n        return res;\n    }\n\n    public static short getFileLen(byte[] bytes) throws IOException {\n        log.debug(\"DEBUGFILEAPDU {}\", HexFormat.of().formatHex(bytes).toUpperCase());\n        ASN1InputStream stream = new ASN1InputStream(new ByteArrayInputStream(bytes));\n        ASN1ApplicationSpecific parent = (ASN1ApplicationSpecific) stream.readObject();\n        stream = new ASN1InputStream(new ByteArrayInputStream(parent.getContents()));\n        ASN1TaggedObject obj;\n        do {\n            obj = (ASN1TaggedObject) stream.readObject();\n        } while ((obj.getTagNo() != 0) || (obj.getTagClass() != BERTags.CONTEXT_SPECIFIC));\n        DEROctetString octetString = (DEROctetString) obj.getBaseUniversal(false, BERTags.OCTET_STRING);\n        return new BigInteger(1, octetString.getOctets()).shortValueExact();\n    }\n\n    private short selectDODF() throws CardException, PKCS15Exception {\n        // read Object Directory File (ODF)\n        byte[] bytes;\n        ResponseAPDU r;\n        short len;\n        r = apdu.selectFile(ODF_PATH);\n        try {\n            len = getFileLen(r.getData());\n        } catch (IOException e) {\n            throw new PKCS15Exception(\"Card ODF file corrupted\");\n        }\n        bytes = apdu.readBinary(len);\n        // find object with Data Objects tag (7)\n        ASN1TaggedObject obj;\n        try {\n            obj = getObjByTagNo(bytes, 7);\n        } catch (IOException e) {\n            throw new PKCS15Exception(\"Card ODF entry corrupted\");\n        }\n        if (obj == null) {\n            throw new PKCS15Exception(\"Card does not have DODF object\");\n        }\n        // get Data Object Directory File (DODF) path\n        byte[] path = getPath(obj);\n        // read DODF\n        r = apdu.selectFile(path);\n        try {\n            len = getFileLen(r.getData());\n        } catch (IOException e) {\n            throw new PKCS15Exception(\"Card DODF file corrupted\");\n        }\n        return len;\n    }\n\n    private void verifyAid(DLSequence parent, byte[] aid) throws PKCS15Exception {\n        ASN1OctetString octetString = (ASN1OctetString) parent.getObjectAt(1);\n        byte[] foundAid = octetString.getOctets();\n        if (!Arrays.equals(foundAid, aid)) {\n            throw new PKCS15Exception(String.format(\"Aid mismatch! required: %s, found: %s\",\n                    HexFormat.of().formatHex(aid).toUpperCase(),\n                    HexFormat.of().formatHex(foundAid).toUpperCase()));\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/pkcs15/PKCS15CardService.java",
    "content": "package ee.ivxv.common.service.smartcard.pkcs15;\n\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.smartcard.Card;\nimport ee.ivxv.common.service.smartcard.CardService;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.util.I18nConsole;\n\n/**\n * PKCS15CardService is a card service which handles cards with PKCS15 file system.\n */\npublic class PKCS15CardService implements CardService {\n\n    private final I18nConsole console;\n\n    public PKCS15CardService(Console console, I18n i18n) {\n        this.console = new I18nConsole(console, i18n);\n    }\n\n    @Override\n    public Card createCard(String id) {\n        return new PKCS15Card(id, console);\n    }\n\n    @Override\n    public Cards createCards() {\n        return new Cards(this, console) {\n            // Empty block\n        };\n    }\n\n    @Override\n    public boolean isPluggableService() {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/pkcs15/PKCS15Exception.java",
    "content": "package ee.ivxv.common.service.smartcard.pkcs15;\n\nimport ee.ivxv.common.service.smartcard.SmartCardException;\n\n@SuppressWarnings(\"serial\")\npublic class PKCS15Exception extends SmartCardException {\n\n    public PKCS15Exception(String message) {\n        super(message);\n    }\n\n    public PKCS15Exception(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/service/smartcard/pkcs15/PKCS15IndexedBlob.java",
    "content": "package ee.ivxv.common.service.smartcard.pkcs15;\n\nimport ee.ivxv.common.asn1.ASN1DecodingException;\nimport ee.ivxv.common.asn1.Field;\nimport ee.ivxv.common.asn1.Sequence;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.common.util.Util;\n\n/**\n * PKCS15IndexedBlob is a blob with an index.\n */\npublic class PKCS15IndexedBlob extends IndexedBlob {\n    /**\n     * Initialize using a file data and the index.\n     * \n     * @param index\n     * @param blob\n     */\n    public PKCS15IndexedBlob(int index, byte[] blob) {\n        super(index, blob);\n    }\n\n    /**\n     * Parse a serialized blob.\n     * \n     * @param file\n     * @return\n     * @throws PKCS15Exception\n     */\n    public static PKCS15IndexedBlob create(byte[] file) throws PKCS15Exception {\n        Sequence s = new Sequence();\n        try {\n            s.readFromBytes(file);\n        } catch (ASN1DecodingException e) {\n            throw new PKCS15Exception(\"Error reading indexed blob: \" + e.toString());\n        }\n        byte[][] fields;\n        try {\n            fields = s.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new PKCS15Exception(\"Error decoding blob: \" + e.toString());\n        }\n        if (fields.length != 2) {\n            throw new PKCS15Exception(\"Invalid format for indexed blob\");\n        }\n        Field indexField = new Field();\n        Field blobField = new Field();\n        try {\n            indexField.readFromBytes(fields[0]);\n            blobField.readFromBytes(fields[1]);\n        } catch (ASN1DecodingException e) {\n            throw new PKCS15Exception(\"Invalid field: \" + e.toString());\n        }\n        int index;\n        try {\n            index = Util.toInt(indexField.getBytes());\n        } catch (ASN1DecodingException e) {\n            throw new PKCS15Exception(\"Error while decoding index field: \" + e.toString());\n        }\n        byte[] blob;\n        try {\n            blob = blobField.getBytes();\n        } catch (ASN1DecodingException e) {\n            throw new PKCS15Exception(\"Error while decoding blob field: \" + e.toString());\n        }\n        return new PKCS15IndexedBlob(index, blob);\n    }\n\n    /**\n     * Serialize and indexed blob.\n     * <p>\n     * The index is encoded as a 4-byte big-endian value\n     * {@code\n     * SEQUENCE (\n     *    index OCTETSTRING\n     *    blob  OCTETSTRING\n     *    )\n     * }\n     * \n     * @return\n     */\n    public byte[] encode() {\n        return new Sequence(new Field(Util.toBytes(index)).encode(), new Field(blob).encode())\n                .encode();\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/ByteArrayWrapper.java",
    "content": "package ee.ivxv.common.util;\n\nimport java.util.Arrays;\nimport java.util.HexFormat;\n\n/**\n * Class to wrap byte array to make it suitable for hash key.\n */\npublic class ByteArrayWrapper {\n\n    private final byte[] data;\n\n    public ByteArrayWrapper(byte[] data) {\n        this.data = data;\n    }\n\n    @Override\n    public boolean equals(Object other) {\n        if (!(other instanceof ByteArrayWrapper)) {\n            return false;\n        }\n        return Arrays.equals(data, ((ByteArrayWrapper) other).data);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(data);\n    }\n\n    @Override\n    public String toString() {\n        return HexFormat.of().formatHex(data);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/CandidatesUtil.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.io.InputStream;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class CandidatesUtil {\n\n    private static final Logger log = LoggerFactory.getLogger(CandidatesUtil.class);\n\n    public static CandidateList readCandidates(InputStream in, DistrictList dl) {\n        try {\n            CandidateList cl = Json.read(in, CandidateList.class);\n\n            validateCandidates(cl, dl);\n\n            return cl;\n        } catch (Exception e) {\n            throw new MessageException(e, M.e_cand_read_error, e);\n        }\n    }\n\n    static void validateCandidates(CandidateList cl, DistrictList dl) {\n        Set<String> districtIds = dl.getDistricts().keySet();\n        cl.getCandidates().keySet().forEach(id -> {\n            if (!districtIds.contains(id)) {\n                log.error(\"Candidate district {} is not in district list\", id);\n                throw new MessageException(M.e_cand_invalid_dist, id);\n            }\n        });\n\n        Set<String> candidateIds = new HashSet<>();\n        cl.getCandidates().values().forEach(d -> d.values().forEach(p -> p.keySet().forEach(c -> {\n            if (!candidateIds.add(c)) {\n                log.error(\"Duplicate candidate id {} in candidate list\", c);\n                throw new MessageException(M.e_cand_duplicate_id, c);\n            }\n        })));\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/ContainerHelper.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.util.stream.Collectors;\n\n/**\n * ContainerHelper is a dedicated class containing helper methods for handling document containers.\n */\npublic class ContainerHelper {\n\n    private final I18nConsole console;\n    private final Container c;\n\n    public ContainerHelper(I18nConsole console, Container c) {\n        this.console = console;\n        this.c = c;\n    }\n\n    public String getSignerNames() {\n        return c.getSignatures().stream().map(s -> s.getSigner().getName())\n                .collect(Collectors.joining(\", \"));\n    }\n\n    /**\n     * Checks that the container has signatures and contains a single file. Also reports standard\n     * messages about checking signature, signer name, signing time and the result on the console.\n     * \n     * @param nameParam The first parameter for {@code M.m_cont_*} messages describing the container\n     * @return\n     * @throws MessageException\n     */\n    public DataFile getSingleFileAndReport(Object nameParam) throws MessageException {\n        if (c.getFiles().size() != 1) {\n            throw new MessageException(M.e_cont_single_file_expected, c.getFiles().size());\n        }\n\n        reportSignatures(nameParam);\n\n        return c.getFiles().get(0);\n    }\n\n    public void reportSignatures(Object nameParam) throws MessageException {\n        if (c.getSignatures().isEmpty()) {\n            throw new MessageException(M.e_cont_signature_expected);\n        }\n\n        console.println(M.m_cont_checking_signature, nameParam);\n        c.getSignatures().forEach(s -> {\n            console.println(M.m_cont_signer, nameParam, s.getSigner().getName());\n            console.println(M.m_cont_signature_time, nameParam, s.getSigningTime());\n        });\n        console.println(M.m_cont_signature_is_valid, nameParam);\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/DistrictsUtil.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.io.InputStream;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class DistrictsUtil {\n\n    private static final Logger log = LoggerFactory.getLogger(DistrictsUtil.class);\n\n    public static DistrictList readDistricts(InputStream in) {\n        try {\n            DistrictList dl = Json.read(in, DistrictList.class);\n\n            validateDistricts(dl);\n\n            return dl;\n        } catch (Exception e) {\n            throw new MessageException(M.e_dist_read_error, e);\n        }\n    }\n\n    static void validateDistricts(DistrictList dl) {\n        Set<String> parishIds = new HashSet<>();\n        dl.getDistricts().forEach((d, sl) -> sl.getParish().forEach(pid -> {\n            if (pid.equals(\"FOREIGN\")){\n                pid = \"0000\";\n            }\n            if (!parishIds.add(d + \"|\" + pid)) {\n                log.error(\"Voting station id '{}' not unique\", pid);\n                throw new MessageException(M.e_dist_parish_id_not_unique, pid);\n            }\n            if (pid == null || pid.isEmpty()) {\n                log.error(\"Voting parish {} is invalid\", pid);\n                throw new MessageException(M.e_dist_parish_id_invalid, pid);\n            }\n            if (!dl.getRegions().containsKey(pid)) {\n                log.error(\"Voting parish '{}' does not conform to any region\", pid);\n                throw new MessageException(M.e_dist_parish_region_unknown, pid);\n            }\n        }));\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/I18nConsole.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.service.console.Console;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.i18n.I18n;\nimport ee.ivxv.common.service.i18n.Translatable;\n\n/**\n * Convenience class to write internationalized messages to the console.\n */\npublic class I18nConsole {\n\n    public final Console console;\n    public final I18n i18n;\n\n    public I18nConsole(Console console, I18n i18n) {\n        this.console = console;\n        this.i18n = i18n;\n    }\n\n    public void println() {\n        console.println();\n    }\n\n    public void println(Translatable msg) {\n        console.println(i18n.get(msg));\n    }\n\n    public void println(Enum<?> key, Object... args) {\n        console.println(i18n.get(key, args));\n    }\n\n    public void printlnraw(Enum<?> key, Object... args) {\n        console.printlnraw(i18n.get(key, args));\n    }\n\n    public Progress startProgress(long total) {\n        return this.startProgress(total, false);\n    }\n\n    public Progress startProgress(long total, boolean relative_only) {\n        M bar = relative_only ? M.m_relative_progress_bar : M.m_progress_bar;\n        return console.startProgress(i18n.get(bar), total);\n    }\n\n    public Progress startInfiniteProgress(long total) {\n        return this.startInfiniteProgress(total, false);\n    }\n\n    public Progress startInfiniteProgress(long total, boolean relative_only) {\n        M bar = relative_only ? M.m_relative_progress_bar : M.m_progress_bar;\n        return console.startInfiniteProgress(i18n.get(bar), total);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/Json.java",
    "content": "package ee.ivxv.common.util;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\n\n/**\n * Json is a class for reading and writing JSON format.\n */\npublic class Json {\n\n    /**\n     * Reads the specified path as JSON file and parses the result into new instance of the\n     * specified type.\n     *\n     * @param path Location of a JSON file.\n     * @param type The target class to parse the result into.\n     * @return Instance of the target type.\n     * @throws Exception if an i/o or parsing error occurs.\n     */\n    public static <T> T read(Path path, Class<T> type) throws Exception {\n        return read(Files.newInputStream(path), type);\n    }\n\n    public static <T> T read(InputStream in, Class<T> type) throws Exception {\n        ObjectMapper mapper = getMapper();\n\n        return mapper.readValue(in, type);\n    }\n\n    /**\n     * Writes the specified object in JSON format at the specified path. All parent folders of the\n     * specified path will be created.\n     *\n     * @param o The object to write\n     * @param path Location of the JSON file\n     * @throws Exception if an i/o or parsing error occurs.\n     */\n    public static void write(Object o, Path path) throws Exception {\n        if (path.getParent() != null) {\n            Files.createDirectories(path.getParent());\n        }\n        write(o, Files.newOutputStream(path));\n    }\n\n    public static void write(Object o, OutputStream out) throws Exception {\n        ObjectMapper mapper = getMapper();\n\n        // Using pretty print for debugging\n        mapper.writerWithDefaultPrettyPrinter().writeValue(out, o);\n    }\n\n    /**\n     * @return Creates and returns a mapper that is properly set up for serialization and\n     *         deserialization as required in this project.\n     */\n    private static ObjectMapper getMapper() {\n        ObjectMapper mapper = new ObjectMapper();\n        SimpleModule module = new SimpleModule();\n\n        module.addSerializer(Instant.class, new InstantSerializer());\n        module.addDeserializer(Instant.class, new InstantDeserializer());\n\n        mapper.registerModule(module);\n\n        return mapper;\n    }\n\n    private static class InstantSerializer extends StdSerializer<Instant> {\n        private static final long serialVersionUID = 4737351183174945801L;\n\n        protected InstantSerializer() {\n            super(Instant.class);\n        }\n\n        @Override\n        public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider)\n                throws IOException {\n            gen.writeString(value.toString());\n        }\n    }\n\n    private static class InstantDeserializer extends StdDeserializer<Instant> {\n        private static final long serialVersionUID = 4737351183174945801L;\n\n        protected InstantDeserializer() {\n            super(Instant.class);\n        }\n\n        @Override\n        public Instant deserialize(JsonParser p, DeserializationContext ctxt)\n                throws IOException, JsonProcessingException {\n            return Instant.parse(p.getText());\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/NameHolder.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.service.i18n.Translatable;\n\n/**\n * NameHolder creates a connection between a logical name and translatable name. This is useful e.g.\n * for command line arguments - they have static part (argument name) and translatable description.\n * It is possible that different tools of an application may want to use the same argument name, but\n * different or customized translation.\n * \n * <p>\n * Note that <tt>NameHolder</tt> instances are <i>Translatable</i>, i.e they are automatically\n * translated when used as a message parameter.\n * </p>\n * \n * <p>\n * Implementation hint: implement by Enum class, implement <tt>getName()</tt> with\n * <tt>extractName(name())</tt> to support different translations for the same argument name, e.g\n * constants <tt>tool1_arg</tt> and <tt>tool2_arg</tt> would have the same logical name \"arg\", but\n * have different translations.\n */\npublic interface NameHolder extends Translatable {\n\n    /**\n     * @return Returns the static short name.\n     */\n    String getShortName();\n\n    /**\n     * @return Returns the static name.\n     */\n    String getName();\n\n    /**\n     * @param name\n     * @return Excludes everything before and including the first '_'.\n     */\n    default String extractName(String name) {\n        return name.substring(name.indexOf('_') + 1);\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/PdfDoc.java",
    "content": "package ee.ivxv.common.util;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.apache.pdfbox.pdmodel.PDDocument;\nimport org.apache.pdfbox.pdmodel.PDPage;\nimport org.apache.pdfbox.pdmodel.PDPageContentStream;\nimport org.apache.pdfbox.pdmodel.common.PDRectangle;\nimport org.apache.pdfbox.pdmodel.font.PDFont;\nimport org.apache.pdfbox.pdmodel.font.PDType0Font;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class PdfDoc implements Closeable {\n\n    private static final Logger log = LoggerFactory.getLogger(PdfDoc.class);\n\n    private static final String FONT_PATH_REGULAR = \"fonts/Roboto-Regular.ttf\";\n    private static final String FONT_PATH_BOLD = \"fonts/Roboto-Bold.ttf\";\n\n    private static final float FONT_SIZE_NORMAL = 11;\n    private static final float FONT_SIZE_TITLE = 16;\n    private static final float LINE_SPACING_COEF = 1.2f;\n    private static final float MARGIN_L = 50;\n    private static final float MARGIN_R = 50;\n    private static final float MARGIN_T = 40;\n    private static final float MARGIN_B = 40;\n\n    private final OutputStream out;\n    private final PDDocument document;\n    private final PDFont fontRegular;\n    private final PDFont fontBold;\n\n    private PDPage page;\n    private PDPageContentStream cs;\n    private PDFont font;\n    private float fontSize;\n    private float leading;\n    private float offsetY;\n    private float tab;\n    private int pageNumber = 0;\n\n    public PdfDoc(OutputStream out) throws IOException {\n        this.out = out;\n        document = new PDDocument();\n        fontRegular = PDType0Font.load(document, Util.getResource(FONT_PATH_REGULAR));\n        fontBold = PDType0Font.load(document, Util.getResource(FONT_PATH_BOLD));\n        font = fontRegular;\n        fontSize = FONT_SIZE_NORMAL;\n        startPage();\n    }\n\n    private static List<String> safeString(Object o, PDFont font) throws IOException {\n        List<String> lines = new ArrayList<>();\n\n        String s = o == null ? \"\" : String.valueOf(o);\n        for (String line : s.split(\"\\n\")) {\n            lines.add(safeString(line, font));\n        }\n\n        return lines;\n    }\n\n    private static String safeString(String s, PDFont font) throws IOException {\n        try {\n            // Check if the provided font supports the provided string.\n            font.encode(s);\n            return s;\n        } catch (RuntimeException e) {\n            // May be \"U+FFFD is not available in this font's encoding\". Fall through.\n            log.error(\"Exception occurred while encoding string {}\", s, e);\n        }\n\n        // Font does not support all characters in the string. Try 1-by-1 and replace unsupported.\n\n        StringBuilder sb = new StringBuilder();\n        for (int offset = 0; offset < s.length();) {\n            int codePoint = s.codePointAt(offset);\n            char[] chars = Character.toChars(codePoint);\n            String u = new String(chars);\n\n            try {\n                font.encode(u);\n            } catch (RuntimeException e) {\n                // May be \"U+FFFD is not available in this font's encoding\". Replace character.\n                log.error(\"Exception occurred while encoding string {}\", s, e);\n                u = \"?\";\n            }\n            sb.append(u);\n\n            offset += Character.charCount(codePoint);\n        }\n\n        return sb.toString();\n    }\n\n    public void addText(Object o) throws IOException {\n        setFont(fontRegular, FONT_SIZE_NORMAL);\n        addTextFlow(o);\n    }\n\n    public void addText(Object o, float w, Alignment a) throws IOException {\n        setFont(fontRegular, FONT_SIZE_NORMAL);\n        addTextBox(o, normalizeTextWidth(w), a);\n    }\n\n    public void addTitle(Object o) throws IOException {\n        setFont(fontBold, FONT_SIZE_TITLE);\n        addTextFlow(o);\n    }\n\n    public void addTitle(Object o, float w, Alignment a) throws IOException {\n        setFont(fontBold, FONT_SIZE_TITLE);\n        addTextBox(o, normalizeTextWidth(w), a);\n    }\n\n    private float normalizeTextWidth(float w) {\n        float maxWidth = page.getMediaBox().getWidth() - MARGIN_L - tab - MARGIN_R;\n        return w <= 0 ? maxWidth : Math.min(w, maxWidth);\n    }\n\n    private void addTextBox(Object o, float w, Alignment a) throws IOException {\n        List<String> lines = new ArrayList<>();\n\n        for (String s : safeString(o, font)) {\n            int lineStart = 0;\n\n            for (int space = -1, lastSpace = -1; space < s.length(); lastSpace = space) {\n                space = s.indexOf(' ', lastSpace + 1);\n                space = space < 0 ? s.length() : space;\n                // If line length exceeds the allowed width, split\n                String current = s.substring(lineStart, space);\n                float currentWidth = getWidth(current);\n                if (currentWidth > w && lastSpace >= 0) {\n                    lines.add(s.substring(lineStart, lastSpace));\n                    lineStart = lastSpace + 1;\n                }\n            }\n\n            lines.add(s.substring(lineStart));\n        }\n\n        float origTab = tab;\n        boolean isFirst = true;\n        for (String l : lines) {\n            if (!isFirst) {\n                // Use internal newLine, remember the number of new lines and consider it in\n                // public newLine method to avoid overwriting. Keep in mind page-breaks!\n                newLine();\n                tab(origTab);\n            }\n            isFirst = false;\n\n            float currentTab = 0;\n            float currentWidth = getWidth(l);\n            if (a == Alignment.RIGHT) {\n                currentTab = w - currentWidth;\n            } else if (a == Alignment.CENTER) {\n                currentTab = (w - currentWidth) / 2;\n            }\n\n            tab(currentTab);\n            addTextFlow(l);\n            tab(-currentTab);\n        }\n    }\n\n    private float getWidth(String s) throws IOException {\n        return fontSize * font.getStringWidth(s) / 1000;\n    }\n\n    private void addTextFlow(Object o) throws IOException {\n        boolean isFirst = true;\n        for (String s : safeString(o, font)) {\n            if (!isFirst) {\n                newLine();\n            }\n            isFirst = false;\n            cs.showText(s);\n        }\n    }\n\n    public void newLine() throws IOException {\n        if (offsetY - leading < MARGIN_B) {\n            endPage();\n            startPage();\n        } else {\n            cs.newLine();\n            cs.newLineAtOffset(-1 * tab, 0);\n            offsetY -= leading;\n            tab = 0;\n        }\n    }\n\n    public void newPage() throws IOException {\n        endPage();\n        startPage();\n    }\n\n    public void tab(float x) throws IOException {\n        tab += x;\n        cs.newLineAtOffset(x, 0);\n    }\n\n    public void resetPageNumber() {\n        pageNumber = 1;\n    }\n\n    @Override\n    public void close() throws IOException {\n        endPage();\n        document.save(out);\n        document.close();\n    }\n\n    private void startPage() throws IOException {\n        page = new PDPage(PDRectangle.A4);\n        cs = new PDPageContentStream(document, page);\n\n        cs.beginText();\n        setFont(font, fontSize);\n\n        offsetY = page.getMediaBox().getHeight() - MARGIN_T;\n        tab = 0;\n        pageNumber++;\n\n        cs.newLineAtOffset(MARGIN_L, offsetY);\n    }\n\n    private void endPage() throws IOException {\n        addPageNumber();\n        cs.endText();\n        cs.close();\n        document.addPage(page);\n    }\n\n    private void setFont(PDFont font, float fontSize) throws IOException {\n        this.font = font;\n        this.fontSize = fontSize;\n        this.leading = LINE_SPACING_COEF * fontSize;\n        cs.setFont(font, fontSize);\n        cs.setLeading(leading);\n    }\n\n    private void addPageNumber() throws IOException {\n        // Place the page number not in the middle of the bottom margin but a bit higher\n        cs.newLineAtOffset(-1 * tab, MARGIN_B * 3f / 5f - offsetY);\n        tab = 0;\n        addText(pageNumber, -1, Alignment.CENTER);\n    }\n\n    public enum Alignment {\n        LEFT, CENTER, RIGHT;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/SkipCommandUtil.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.model.SkipCommand;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.io.InputStream;\nimport java.util.Map;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.yaml.snakeyaml.Yaml;\n\npublic class SkipCommandUtil {\n\n    private static final Logger log = LoggerFactory.getLogger(SkipCommandUtil.class);\n\n    public static SkipCommand readSkipCommand(InputStream in) {\n\n        Yaml yaml = new Yaml();\n        Map<String, Object> obj = yaml.load(in);\n        return new SkipCommand(\n                String.valueOf(obj.get(\"changeset\")),\n                String.valueOf(obj.get(\"election\")),\n                String.valueOf(obj.get(\"skip_voter_list\")));\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/ToolHelper.java",
    "content": "package ee.ivxv.common.util;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.SkipCommand;\nimport ee.ivxv.common.model.IBallotBox;\nimport ee.ivxv.common.model.Proof;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.ContainerReader;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\npublic class ToolHelper {\n\n    private static final String CHECKSUM_SUFFIX = \".sha256sum\";\n\n    private final I18nConsole console;\n    private final ContainerReader container;\n    private final BboxHelper bbox;\n\n    public ToolHelper(I18nConsole console, ContainerReader container, BboxHelper bbox) {\n        this.console = console;\n        this.container = container;\n        this.bbox = bbox;\n    }\n\n    public void checkBbChecksum(Path bb, Path checksum) throws Exception {\n        checkChecksum(bb, checksum, M.m_bb_arg_for_checksum);\n    }\n\n    public void checkRegChecksum(Path bb, Path checksum) throws Exception {\n        checkChecksum(bb, checksum, M.m_reg_arg_for_checksum);\n    }\n\n    public void checkChecksum(Path bb, Path checksum, Enum<?> name) throws Exception {\n        console.println();\n        console.println(M.m_checksum_loading, name, checksum);\n        container.requireContainer(checksum);\n        Container c = container.read(checksum.toString());\n        console.println(M.m_checksum_loaded, name);\n\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(new Message(M.m_checksum_arg_for_cont, name));\n        byte[] sum1 = Util.toBytes(file.getStream());\n\n        console.println(M.m_checksum_calculate, name, bb);\n        byte[] sum2 = bbox.getChecksum(bb);\n\n        if (!bbox.compareChecksum(sum1, sum2)) {\n            throw new MessageException(M.e_checksum_mismatch, name, bb, checksum);\n        }\n        console.println(M.m_checksum_ok, name);\n    }\n\n    public BallotBox readJsonBb(Path path, BallotBox.Type requiredType) throws Exception {\n        return readJsonBb(path, BallotBox.class, requiredType);\n    }\n\n    public AnonymousBallotBox readJsonAbb(Path path, BallotBox.Type requiredType) throws Exception {\n        return readJsonBb(path, AnonymousBallotBox.class, requiredType);\n    }\n\n    private <T extends IBallotBox> T readJsonBb(Path path, Class<T> clazz,\n            BallotBox.Type requiredType) throws Exception {\n        console.println();\n        console.println(M.m_bb_loading, path);\n        T bb = Json.read(path, clazz);\n        console.println(M.m_bb_loaded);\n\n        console.println(M.m_bb_checking_type);\n        bb.requireType(requiredType);\n        console.println(M.m_bb_type, bb.getType());\n\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n\n        return bb;\n    }\n\n    public void writeJsonBb(IBallotBox bb, Path out) throws Exception {\n        console.println();\n        console.println(M.m_bb_saving, bb.getType(), out);\n        Json.write(bb, out);\n        console.println(M.m_bb_saved, bb.getType());\n\n        Path checksumOut = Paths.get(out.toString() + CHECKSUM_SUFFIX);\n        console.println(M.m_bb_checksum_saving, checksumOut);\n        byte[] checksum = bbox.getChecksum(out);\n        Files.write(checksumOut, checksum);\n        console.println(M.m_bb_checksum_saved);\n    }\n\n    public CandidateList readJsonCandidates(Path path, DistrictList dl) throws Exception {\n        console.println();\n        console.println(M.m_cand_loading, path);\n        container.requireContainer(path);\n        Container c = container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(M.m_cand_arg_for_cont);\n        CandidateList candidates = CandidatesUtil.readCandidates(file.getStream(), dl);\n\n        console.println(M.m_cand_loaded);\n        console.println(M.m_cand_count, candidates.getCount());\n        console.println(M.m_election_id, candidates.getElection());\n\n        return candidates;\n    }\n\n    public DistrictList readJsonDistricts(Path path) throws Exception {\n        console.println();\n        console.println(M.m_dist_loading, path);\n        container.requireContainer(path);\n        Container c = container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(M.m_dist_arg_for_cont);\n        DistrictList districts = DistrictsUtil.readDistricts(file.getStream());\n\n        console.println(M.m_dist_loaded);\n        console.println(M.m_dist_count, districts.getCount());\n        console.println(M.m_election_id, districts.getElection());\n\n        return districts;\n    }\n\n    public SkipCommand readSkipCommand(Path path) throws Exception {\n        console.println();\n        console.println(M.m_skip_cmd_loading, path);\n        container.requireContainer(path);\n        Container c = container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(M.m_skip_cmd_arg_for_cont);\n        SkipCommand skip = SkipCommandUtil.readSkipCommand(file.getStream());\n        console.println(M.m_skip_cmd_loaded);\n        return skip;\n    }\n\n    public Proof readJsonProofs(Path path) throws Exception {\n        console.println();\n        console.println(M.m_proof_loading, path);\n        Proof proofs = Json.read(path, Proof.class);\n        console.println(M.m_proof_loaded);\n\n        console.println(M.m_proof_count, proofs.getCount());\n\n        return proofs;\n    }\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/Util.java",
    "content": "package ee.ivxv.common.util;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\nimport java.util.Arrays;\nimport java.util.Base64;\n\npublic class Util {\n\n    public static final Charset CHARSET = StandardCharsets.UTF_8;\n    public static final String X509 = \"X.509\";\n    public static final String BEGIN_CERTIFICATE = \"-----BEGIN CERTIFICATE-----\";\n    public static final String END_CERTIFICATE = \"-----END CERTIFICATE-----\";\n    public static final String BEGIN_PUB_KEY = \"-----BEGIN PUBLIC KEY-----\";\n    public static final String END_PUB_KEY = \"-----END PUBLIC KEY-----\";\n    public static final String BEGIN_PRIVATE_KEY = \"-----BEGIN PRIVATE KEY-----\";\n    public static final String END_PRIVATE_KEY = \"-----END PRIVATE KEY-----\";\n    public static final String EOT = Character.toString((char) 0x04); // EOT character\n    public static final String UNIT_SEPARATOR = Character.toString((char) 0x1F); // Unit separator\n                                                                                 // character\n\n    /**\n     * Return byte array with UTF-8 encoded string.\n     * \n     * @param string\n     * @return Returns UTF-8 encoded byte array.\n     */\n    public static byte[] toBytes(String string) {\n        return string.getBytes(CHARSET);\n    }\n\n    /**\n     * Decode byte array as UTF-8 encoded string.\n     * \n     * @param bytes\n     * @return Returns a string decoded from the bytes using UTF-8 encoding.\n     */\n    public static String toString(byte[] bytes) {\n        return new String(bytes, CHARSET);\n    }\n\n    /**\n     * Return the InputStream as a byte array.\n     * \n     * @param stream\n     * @return\n     */\n    public static byte[] toBytes(InputStream stream) {\n        return toBytes(stream, new byte[1024]);\n    }\n\n    /**\n     * Return the InputStream as a byte array using the buffer for storing temporary values.\n     * \n     * @param stream\n     * @param buffer\n     * @return\n     */\n    public static byte[] toBytes(InputStream stream, byte[] buffer) {\n        ByteArrayOutputStream bytes = new ByteArrayOutputStream();\n\n        try {\n            for (int len; (len = stream.read(buffer)) != -1;) {\n                bytes.write(buffer, 0, len);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return bytes.toByteArray();\n    }\n\n    /**\n     * Return the InputStream as an UTF-8 encoded string using the buffer for storing temporary\n     * values.\n     * \n     * @param stream\n     * @param buffer\n     * @return\n     */\n    public static String toString(InputStream stream, byte[] buffer) {\n        return new String(toBytes(stream, buffer), CHARSET);\n    }\n\n    /**\n     * Return the integer encoded as a big-endian byte array.\n     * \n     * @param i\n     * @return\n     */\n    public static byte[] toBytes(int i) {\n        byte[] ret = new byte[4];\n        ret[0] = (byte) ((i >>> 24) & 0xff);\n        ret[1] = (byte) ((i >>> 16) & 0xff);\n        ret[2] = (byte) ((i >>> 8) & 0xff);\n        ret[3] = (byte) ((i >>> 0) & 0xff);\n        return ret;\n    }\n\n    /**\n     * Return the integer encoded as 4-byte big-endian byte array.\n     * \n     * @param b\n     * @return\n     */\n    public static int toInt(byte[] b) {\n        // we assume 4 bytes. if b is other length, then the result may be\n        // undefined\n        int k = 0;\n        for (int i = 0; i < b.length && i < 4; i++) {\n            k |= (b[i] << (24 - (i * 8)));\n        }\n        return k;\n    }\n\n    /**\n     * Create a file at a given path.\n     * \n     * @param path\n     * @throws IOException\n     */\n    public static void createFile(Path path) throws IOException {\n        if (path.getParent() != null) {\n            Files.createDirectories(path.getParent());\n        }\n        Files.createFile(path);\n    }\n\n    /**\n     * Concatenate the byte arrays into a single byte array.\n     * \n     * @param first\n     * @param rest\n     * @return\n     */\n    public static byte[] concatAll(byte[] first, byte[]... rest) {\n        int totalLength = first.length;\n        for (byte[] array : rest) {\n            totalLength += array.length;\n        }\n        byte[] result = Arrays.copyOf(first, totalLength);\n        int offset = first.length;\n        for (byte[] array : rest) {\n            System.arraycopy(array, 0, result, offset, array.length);\n            offset += array.length;\n        }\n        return result;\n    }\n\n    /**\n     * MIME-encode the byte array as a certificate.\n     * \n     * @param bytes\n     * @return\n     */\n    public static String encodeCertificate(byte[] bytes) {\n        return encodeKey(bytes, BEGIN_CERTIFICATE, END_CERTIFICATE);\n    }\n\n    /**\n     * MIME-encode the byte array as a public key.\n     * \n     * @param bytes\n     * @return\n     */\n    public static String encodePublicKey(byte[] bytes) {\n        return encodeKey(bytes, BEGIN_PUB_KEY, END_PUB_KEY);\n    }\n\n    /**\n     * MIME-encode the byte array as a private key.\n     * \n     * @param bytes\n     * @return\n     */\n    public static String encodePrivateKey(byte[] bytes) {\n        return encodeKey(bytes, BEGIN_PRIVATE_KEY, END_PRIVATE_KEY);\n    }\n\n    private static String encodeKey(byte[] bytes, String prefix, String suffix) {\n        String base64Data = Base64.getMimeEncoder().encodeToString(bytes);\n        String out = prefix + \"\\n\" + base64Data + \"\\n\" + suffix;\n        return out;\n    }\n\n    /**\n     * Decode certificate from MIME-encoded string.\n     * \n     * @param certString\n     * @return\n     * @throws IllegalArgumentException\n     */\n    public static byte[] decodeCertificate(String certString) throws IllegalArgumentException {\n        return decodeKey(certString, BEGIN_CERTIFICATE, END_CERTIFICATE);\n    }\n\n    /**\n     * Decode certificate from MIME-encoded file.\n     * \n     * @param keyString\n     * @return\n     * @throws IllegalArgumentException\n     */\n    public static byte[] decodeCertificate(Path path) throws IllegalArgumentException, IOException {\n        String certString = new String(Files.readAllBytes(path), Util.CHARSET);\n        return decodeCertificate(certString);\n    }\n\n    /**\n     * Decode public key from MIME-encoded string.\n     * \n     * @param keyString\n     * @return\n     * @throws IllegalArgumentException\n     */\n    public static byte[] decodePublicKey(String keyString) throws IllegalArgumentException {\n        return decodeKey(keyString, BEGIN_PUB_KEY, END_PUB_KEY);\n    }\n\n    /**\n     * Decode public key from MIME-encoded file.\n     * \n     * @param path\n     * @return Public key bytes\n     * @throws IllegalArgumentException\n     * @throws IOException\n     */\n    public static byte[] decodePublicKey(Path path) throws IllegalArgumentException, IOException {\n        String keyString = new String(Files.readAllBytes(path), Util.CHARSET);\n        return decodePublicKey(keyString);\n    }\n\n    /**\n     * Decode private key from MIME-encoded string.\n     * \n     * @param keyString\n     * @return\n     * @throws IllegalArgumentException\n     */\n    public static byte[] decodePrivateKey(String keyString) throws IllegalArgumentException {\n        return decodeKey(keyString, BEGIN_PRIVATE_KEY, END_PRIVATE_KEY);\n    }\n\n    /**\n     * Decode private key from MIME-encoded file.\n     * \n     * @param path\n     * @return Private key bytes\n     * @throws IllegalArgumentException\n     * @throws IOException\n     */\n    public static byte[] decodePrivateKey(Path path) throws IllegalArgumentException, IOException {\n        String keyString = new String(Files.readAllBytes(path), Util.CHARSET);\n        return decodePrivateKey(keyString);\n    }\n\n    private static byte[] decodeKey(String keyString, String prefix, String suffix)\n            throws IllegalArgumentException {\n        keyString = keyString.trim();\n        if (!keyString.startsWith(prefix) || !keyString.endsWith(suffix)) {\n            throw new IllegalArgumentException(\"The key does not have expected format\");\n        }\n        keyString = keyString.substring(keyString.indexOf(prefix) + prefix.length(),\n                keyString.indexOf(suffix));\n        return Base64.getMimeDecoder().decode(toBytes(keyString));\n    }\n\n    /**\n     * @return An input stream for reading the resource, or <tt>null</tt> if the resource could not\n     *         be found\n     */\n    public static InputStream getResource(String name) {\n        return Util.class.getClassLoader().getResourceAsStream(name);\n    }\n\n    /**\n     * @return A <tt>URL</tt> object for reading the resource, or <tt>null</tt> if the resource\n     *         could not be found or the invoker doesn't have adequate privileges to get the\n     *         resource.\n     */\n    public static URL getResourceUrl(String name) {\n        return Util.class.getClassLoader().getResource(name);\n    }\n\n    /**\n     * Read the input stream and return it as an X509Certificate.\n     * \n     * @param pem\n     * @return\n     * @throws CertificateException\n     */\n    public static X509Certificate readCertAsPem(InputStream pem) throws CertificateException {\n        CertificateFactory certificateFactory = CertificateFactory.getInstance(X509);\n        return (X509Certificate) certificateFactory.generateCertificate(pem);\n    }\n\n    /**\n     * Join sanitized identifier and name, return as Path.\n     * \n     * Everything except lower and upper case letters, digits and symbols '-', '_' are replaced with\n     * underscores in the identifier. More formally, the regular expression\n     * <code>[^a-zA-Z0-9-_]</code> is applied and all matches are replaced with underscores.\n     * \n     * The identifier and name are joined with hyphen '-'.\n     * \n     * @param identifier\n     * @param name\n     * @return\n     */\n    public static Path prefixedPath(String identifier, String name) {\n        return Paths.get(sanitize(identifier) + \"-\" + name);\n    }\n\n    /**\n     * Sanitize the identifier.\n     * \n     * Every match for the regular expression <code>[^a-zA-Z0-9-_]</code> are replaced with\n     * underscores '_'. The regular expression correspond to all characters which are NOT lower or\n     * upper case ASCII letters, digits or symbols '-', '_'.\n     * \n     * @param identifier\n     * @return\n     */\n    public static String sanitize(String identifier) {\n        // this pattern matches anything which is not:\n        // * lower case letters\n        // * upper case letters\n        // * digits\n        // * symbols '-' and '_'\n        String exclude = \"[^a-zA-Z0-9-_]\";\n        return identifier.replaceAll(exclude, \"_\");\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/log/LogJsonFormatter.java",
    "content": "package ee.ivxv.common.util.log;\n\nimport ch.qos.logback.contrib.jackson.JacksonJsonFormatter;\nimport java.io.IOException;\nimport java.util.Map;\n\npublic class LogJsonFormatter extends JacksonJsonFormatter {\n\n    private static final String NEWLINE = System.getProperty(\"line.separator\", \"\\n\");\n\n    @Override\n    @SuppressWarnings(\"rawtypes\")\n    public String toJsonString(Map arg0) throws IOException {\n        return super.toJsonString(arg0) + NEWLINE;\n    }\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/util/log/PerformanceLog.java",
    "content": "package ee.ivxv.common.util.log;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * The published logger of this class is configured to log performance messages in dedicated log,\n * but reflect everything in the main log. The dedicated log file is not rolled, since it is\n * supposed to gather information over longer periods of time and the log should be rather small.\n */\npublic class PerformanceLog {\n\n    public static final Logger log = LoggerFactory.getLogger(PerformanceLog.class);\n\n}\n"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/zip/Zip.java",
    "content": "package ee.ivxv.common.zip;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\n\n/**\n * Zip implements .ZIP File Format Specification, version 6.3.10 as defined in\n * https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT.\n */\npublic final class Zip {\n\n    // Size in bytes of a buffer to read a zip file content.\n    private static final int zipFileBuffer = 8192; // 8 KB\n\n    // 6.0 Traditional PKWARE Encryption.\n    private static final int traditionalPKWAREEncryption = 12;\n\n    // 4.4.4 general purpose bit flag: (2 bytes). Bit 0.\n    private static final byte encryptedFlag = 0b00000001;\n\n    // 4.4.4 general purpose bit flag: (2 bytes). Bit 3.\n    private static final byte dataDescriptorFlag = 0b00001000;\n\n    // APPENDIX E - AE-x encryption marker: https://www.winzip.com/en/support/aes-encryption/. Little-endian value\n    // of 0x9901.\n    private static final byte[] aexEncryption = {1, -103};\n\n    // 4.4.5 compression method: (2 bytes). The file is stored (no compression).\n    private static final byte[] noCompression = {0, 0};\n\n    // 4.3.7 Local file header. Little-endian of 0x04034b50.\n    private static final byte[] localFileHeaderSignature = {80, 75, 3, 4};\n\n    // 8.5.5 The signature value 0x08074b50 is also used by some ZIP implementations as a marker for the\n    // Data Descriptor record. Little-endian of 0x08074b50.\n    private static final byte[] dataDescriptorSignature = {80, 75, 7, 8};\n\n    // 4.3.11 Archive extra data record. Little-endian of 0x08064b50.\n    private static final byte[] archiveExtraDataSignature = {80, 75, 6, 8};\n\n    // 4.3.12 Central directory structure. Little-endian value of 0x02014b50.\n    private static final byte[] centralFileHeaderSignature = {80, 75, 1, 2};\n\n    // 4.3.13 Digital signature. Little-endian value of 0x05054b50.\n    private static final byte[] headerSignature = {80, 75, 5, 5};\n\n    // 4.3.14 Zip64 end of central directory record. Little-endian value of 0x06064b50.\n    private static final byte[] zip64EndOfCentralDirSignature = {80, 75, 6, 6};\n\n    // 4.3.15 Zip64 end of central directory locator. Little-endian value of 0x07064b50.\n    private static final byte[] zip64EndOfCentralDirLocatorSignature = {80, 75, 6, 7};\n\n    // 4.3.16 End of central directory record. Little-endian value of 0x06054b50.\n    private static final byte[] endOfCentralDirSignature = {80, 75, 5, 6};\n\n    /**\n     * Segment is a zip segment.\n     */\n    public static final class Segment {\n        // Offset of the segment from the beginning of a zip stream.\n        private int off;\n\n        // Signature of the segment.\n        private byte[] signature;\n\n        // Is true when next segment must be taken for parsing.\n        private boolean hasNext;\n\n        // This parameter is only relevant when the segment is [file data n], and is true\n        // when this segment, having hasNext=true, is not yet counted as a zip file.\n        private boolean isNotCountedFile;\n\n        private Segment() {\n        }\n    }\n\n    /**\n     * File is a [file data n] segment of a zip.\n     *\n     * @param name\n     * @param compressedSize\n     * @param uncompressedSize\n     */\n    private record File(String name, long compressedSize, long uncompressedSize) {\n    }\n\n    /**\n     * endOfCentralDirectoryRecord locates the end of [end of central directory record] segment in a zip stream.\n     *\n     * @param zipStream\n     * @return\n     * @throws IOException occurs when reading from a zip stream fails\n     */\n    public static int endOfCentralDirectoryRecord(InputStream zipStream, int filesCountLimit, int fileSizeLimit, int zipSizeLimit) throws IOException {\n        // We have to buffer a stream, so after reading files from a stream we could reset the position pointer back\n        // to the start of a stream\n        zipStream.mark(zipSizeLimit);\n\n        // Validate zip stream according to zip specification as much as java.util.zip.ZipInputStream allows\n        ZipInputStream zis = new ZipInputStream(zipStream);\n\n        // We have to locate all zip files beforehand, using java.util.zip.ZipInputStream, in order to obtain\n        // compressed/uncompressed sizes of each file. The edge case that we are delegating to\n        // java.util.zip.ZipInputStream is the situation where general purpose flag bit 3 is set, and therefore\n        // compressed/uncompressed sizes may not appear in [local file header n], but rather in [data descriptor n],\n        // which, unfortunately, appears after [local file header n] and therefore makes it hard to parse these values\n        // in a sequential stream\n        List<File> files = new ArrayList<>();\n\n        // Read uncompressed zip file content step-by-step by a buffer size\n        byte[] buf = new byte[zipFileBuffer];\n\n        int filesCount = 0;\n\n        for (ZipEntry entry; (entry = zis.getNextEntry()) != null; ) {\n            filesCount++;\n\n            if (filesCount > filesCountLimit) {\n                throw new IOException(\"there are more files in a zip than expected\");\n            }\n\n            // Standard, but unreliable approach to catch a file that exceeds a size limit\n            if (entry.getSize() > fileSizeLimit) {\n                throw new IOException(\"zip file uncompressed size exceeds a limit\");\n            }\n\n            int fileSize = 0;\n\n            // https://wiki.sei.cmu.edu/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream\n            while (true) { // safe endless loop, since we don't let to read more that limit bytes\n                int n = zis.read(buf);\n                if (n == -1) {\n                    // End of file\n                    break;\n                }\n\n                fileSize += n;\n\n                // Reliable way to catch a file that exceeds a size limit\n                if (fileSize > fileSizeLimit) {\n                    throw new IOException(\"zip file content size exceeds a limit\");\n                }\n            }\n\n            String name = entry.getName();\n            long compressedSize = entry.getCompressedSize();\n            long uncompressedSize = entry.getSize();\n\n            files.add(new File(name, compressedSize, uncompressedSize));\n        }\n\n        // Reset stream pointer back to the beginning of a stream, so we could read it again\n        zipStream.reset();\n\n        // Mutable zip stream segment. Each method that parses a segment will mutate this instance\n        Segment segment = new Segment();\n\n        int countFiles = 0;\n\n        // Infinite loop is dangerous, but we have validated zip with ZipInputStream, so it is safe here\n        for (; ; countFiles++) {\n            readFile(zipStream, files, segment);\n\n            if (segment.hasNext) { // no [local file header n] ... [data descriptor n] segments left\n                if (segment.isNotCountedFile) { // if the file is not the same as before, then count it\n                    countFiles++;\n                }\n\n                break;\n            }\n\n            // More [local file header n] ... [data descriptor n] segments left\n        }\n\n        // [archive decryption header] \n        readArchiveDecryptionHeader(zipStream, segment);\n\n        // [archive extra data record]\n        readArchiveExtraDataRecord(zipStream, segment);\n\n        // [central directory header 1] ... [central directory header n]\n        readCentralDirectoryHeader(zipStream, countFiles, segment);\n\n        // [zip64 end of central directory record]\n        readZip64EndOfCentralDirectoryRecord(zipStream, segment);\n\n        // [zip64 end of central directory locator]\n        readZip64EndOfCentralDirectoryLocator(zipStream, segment);\n\n        // [end of central directory record]\n        readEndOfCentralDirectoryRecord(zipStream, segment);\n\n        return segment.off;\n    }\n\n    /**\n     * readFile returns the offset of [local file header n] ... [data descriptor n]. Files are [file data n]\n     * segments of a zip stream, which must be parsed beforehand.\n     *\n     * @param zipStream\n     * @return\n     * @throws IOException\n     */\n    private static void readFile(InputStream zipStream, List<File> files, Segment s) throws IOException {\n        byte[] signature = new byte[4];\n\n        zipStream.read(signature); // offset local file header signature\n        s.off += 4;\n\n        if (MessageDigest.isEqual(signature, localFileHeaderSignature)) {\n            // There is a file in a zip\n        } else if (MessageDigest.isEqual(signature, centralFileHeaderSignature)) {\n            // All zip files are read already\n            s.hasNext = true;\n            s.signature = centralFileHeaderSignature;\n\n            return;\n        } else if (MessageDigest.isEqual(signature, endOfCentralDirSignature)) {\n            // Empty zip\n            s.hasNext = true;\n            s.signature = endOfCentralDirSignature;\n\n            return;\n        } else {\n            throw new IOException(\"expected [local file header n]\");\n        }\n\n        zipStream.skip(2); // offset version needed to extract\n        s.off += 2;\n\n        byte[] field = new byte[2];\n\n        zipStream.read(field); // offset general purpose bit flag\n        s.off += 2;\n\n        // 4.4.4 general purpose bit flag: (2 bytes). Bit 0: If set, indicates that the file is encrypted.\n        boolean isEncrypted = (field[0] & encryptedFlag) == encryptedFlag;\n\n        // 4.3.9.1 This descriptor MUST exist if bit 3 of the general purpose bit flag is set.\n        boolean withDataDescriptor = (field[0] & dataDescriptorFlag) == dataDescriptorFlag;\n\n        zipStream.read(field); // offset compression method\n        s.off += 2;\n\n        boolean isCompressed = !MessageDigest.isEqual(field, noCompression);\n\n        if (isCompressed) {\n            zipStream.skip(8); // offset last mod file time ... crc-32\n            s.off += 8;\n        } else {\n            zipStream.skip(12); // offset last mod file time ... compressed size\n            s.off += 12;\n        }\n\n        zipStream.read(signature); // offset either compressed or uncompressed size\n        s.off += 4;\n\n        ByteBuffer buf = ByteBuffer.wrap(signature);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        int size = buf.getInt(); // either compressed or uncompressed size\n\n        if (isCompressed) {\n            zipStream.skip(4); // offset uncompressed size\n            s.off += 4;\n        }\n\n        zipStream.read(field); // offset file name length\n        s.off += 2;\n\n        buf = ByteBuffer.wrap(field);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        short name = buf.getShort();\n\n        zipStream.read(field); // offset extra field length\n        s.off += 2;\n\n        buf = ByteBuffer.wrap(field);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        short extra = buf.getShort();\n\n        byte[] nameBytes = new byte[name];\n        zipStream.read(nameBytes); // offset file name (variable size)\n        s.off += name;\n        String fileName = new String(nameBytes, StandardCharsets.UTF_8);\n\n        // Only use provided files if size doesn't present in [local file header n], which actually means that\n        // size is stored in [data descriptor n], which we cannot be parsed yet (sequential stream)\n        if (size <= 0) {\n            Optional<File> entry = files\n                    .stream()\n                    .filter(x -> fileName.equals(x.name))\n                    .findFirst();\n            if (entry.isEmpty()) {\n                throw new IOException(\"no matching file (\" + fileName + \") found in a zip stream\");\n            }\n\n            size = (int) entry.get().uncompressedSize;\n            if (isCompressed) {\n                size = (int) entry.get().compressedSize;\n            }\n        }\n\n        byte[] extraField = new byte[extra];\n        byte[] headerID = new byte[2]; // Header ID - 2 bytes\n\n        zipStream.read(extraField); // offset extra field (variable size)\n        s.off += extra;\n\n        if (extraField.length > 1) {\n            System.arraycopy(extraField, 0, headerID, 0, 2);\n        }\n\n        boolean isPKWAREEncryption = false; // traditional PKWARE Encryption\n\n        // Gather encryption info from extra field\n        if (isEncrypted) {\n            if (MessageDigest.isEqual(headerID, aexEncryption)) {\n                // AE-x (e.g. AES) encryption marker, encryption info is stored within extra field, not within\n                // [encryption header n]\n            } else {\n                // Encryption info is stored withing [encryption header n]\n                isPKWAREEncryption = true;\n            }\n        }\n\n        // If the file is encrypted, the encryption header for the file SHOULD be placed after the local header and\n        // before the file data (relevant for traditional PKWARE Encryption only)\n        if (isPKWAREEncryption) {\n            zipStream.skip(traditionalPKWAREEncryption);\n            s.off += traditionalPKWAREEncryption; // offset [encryption header n]\n        }\n\n        // After possibly [encryption header n] there must be [file data n]\n        zipStream.skip(size); // offset file data\n        s.off += size;\n\n        if (withDataDescriptor) {\n            zipStream.read(signature); // read possibly data descriptor signature, or crc-32\n            s.off += 4;\n\n            if (!MessageDigest.isEqual(signature, dataDescriptorSignature)) {\n                zipStream.skip(8); // offset data descriptor without signature (4+4), crc-32 already read\n                s.off += 8;\n            } else {\n                zipStream.skip(12); // offset data descriptor with signature (4+4+4), signature already read\n                s.off += 12;\n            }\n\n            zipStream.read(signature); // offset next 4 bytes\n            s.off += 4;\n\n            if (MessageDigest.isEqual(signature, localFileHeaderSignature)) {\n                // [data descriptor 1] [local file header 2], next file to be parsed\n                s.hasNext = false;\n                s.signature = localFileHeaderSignature;\n\n                return;\n            } else if (MessageDigest.isEqual(signature, centralFileHeaderSignature)) {\n                // [data descriptor n] [central directory header 1], all files are parsed, start parsing central dir\n                s.hasNext = true;\n                s.signature = centralFileHeaderSignature;\n                s.isNotCountedFile = true;\n\n                return;\n            } else {\n                // [data descriptor n] is either zip64, or we have encountered [archive decryption header] segment.\n                //\n                // Assume it is zip64, so possibly offset zip64 data descriptor\n                zipStream.skip(4);\n                s.off += 4;\n            }\n\n            zipStream.read(signature); // offset next 4 bytes\n            s.off += 4;\n\n            if (MessageDigest.isEqual(signature, localFileHeaderSignature)) {\n                // zip64 [data descriptor 1] [local file header 2], so next file to be parsed\n                s.hasNext = false;\n                s.signature = localFileHeaderSignature;\n\n                return;\n            } else if (MessageDigest.isEqual(signature, centralFileHeaderSignature)) {\n                // zip64 [data descriptor n] [central directory header 1], all files are parsed, start parsing central\n                // dir\n                s.hasNext = true;\n                s.signature = centralFileHeaderSignature;\n                s.isNotCountedFile = true;\n\n                return;\n            } else {\n                // Either zip64 [data descriptor n] [archive decryption header] or we have already read 8 bytes of\n                // [archive decryption header].\n                //\n                // Assume we have read 8 bytes of [archive decryption header], so read 4 more to reach\n                // [archive extra data record]\n            }\n\n            zipStream.read(signature); // offset 4 bytes to possibly reach [archive extra data record]\n            s.off += 4;\n\n            if (MessageDigest.isEqual(signature, archiveExtraDataSignature)) {\n                // Reached [archive extra data record]. There are no files left in a stream\n                s.hasNext = false;\n                s.signature = archiveExtraDataSignature;\n\n                return;\n            } else {\n                // We have read 4 bytes of [archive decryption header], now read 8 more to reach\n                // [archive extra data record]\n                zipStream.skip(traditionalPKWAREEncryption - 4);\n                s.off += traditionalPKWAREEncryption - 4;\n            }\n        }\n\n        // Read next zip file\n        s.hasNext = false;\n        s.signature = null;\n    }\n\n    /**\n     * readArchiveDecryptionHeader possibly reads [archive decryption header] segment, where off must point at the\n     * beginning of the segment in a zip stream. If the segment doesn't present in a stream at the location off, the\n     * returned value is the same as off. Only supports traditional PKWARE encryption header.\n     *\n     * @param zipStream\n     * @param s\n     * @throws IOException\n     */\n    private static void readArchiveDecryptionHeader(InputStream zipStream, Segment s) throws IOException {\n        if (MessageDigest.isEqual(s.signature, centralFileHeaderSignature)) {\n            // Non-empty zip file without [archive decryption header]\n            return;\n        } else if (MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            // Empty zip file without [archive decryption header]\n            return;\n        }\n\n        zipStream.skip(traditionalPKWAREEncryption); // 6.1 Traditional PKWARE Encryption\n        s.off += traditionalPKWAREEncryption;\n    }\n\n    /**\n     * readArchiveExtraDataRecord possibly reads [archive extra data record] segment, where off must point at the\n     * beginning of the segment in a zip stream. If the segment doesn't present in a stream at the location off, the\n     * returned value is the same as off.\n     *\n     * @param zipStream\n     * @param s\n     * @throws IOException\n     */\n    private static void readArchiveExtraDataRecord(InputStream zipStream, Segment s) throws IOException {\n        byte[] signature = new byte[4];\n\n        if (MessageDigest.isEqual(s.signature, archiveExtraDataSignature)) {\n            // [archive decryption header] is present\n            zipStream.skip(4); // offset archive extra data signature\n            s.off += 4;\n        } else if (MessageDigest.isEqual(s.signature, centralFileHeaderSignature)) {\n            // Non-empty zip file without [archive decryption header]\n            return;\n        } else if (MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            // Empty zip file without [archive decryption header]\n            return;\n        } else {\n            throw new IOException(\"expected [archive extra data record]\");\n        }\n\n        zipStream.read(signature); // offset extra field length\n        s.off += 4;\n\n        ByteBuffer buf = ByteBuffer.wrap(signature);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        int extra = buf.getInt();\n\n        zipStream.skip(extra); // offset extra field data (variable size)\n        s.off += extra;\n\n        s.signature = centralFileHeaderSignature; // next segment must be [central directory header 1]\n    }\n\n    /**\n     * readCentralDirectoryHeader possibly reads [central directory header 1] ... [central directory header n]\n     * segment, where off must point at the beginning of the segment in a zip stream. If the segment doesn't present\n     * in a stream at the location off, the returned value is the same as off.\n     *\n     * @param zipStream\n     * @param filesCount\n     * @param s\n     * @throws IOException\n     */\n    private static void readCentralDirectoryHeader(InputStream zipStream, int filesCount, Segment s) throws IOException {\n        byte[] signature = new byte[4];\n\n        if (MessageDigest.isEqual(s.signature, centralFileHeaderSignature)) {\n            // Do nothing as we offset central file header signature in a 'for loop' anyway\n        } else if (MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            // Empty zip\n            return;\n        } else {\n            throw new IOException(\"expected [central directory header n]\");\n        }\n\n        byte[] field = new byte[2];\n\n        // All files within [local file header 1] ... [local file header n] must be present in\n        // [central directory header 1] ... [central directory header n]\n        for (int i = 0; i < filesCount; i++) {\n            // Offset central file header signature ... uncompressed size\n            if (i > 0) {\n                zipStream.skip(4);\n                s.off += 4;\n            }\n            zipStream.skip(24);\n            s.off += 24;\n\n            zipStream.read(field); // offset file name length\n            s.off += 2;\n\n            ByteBuffer buf = ByteBuffer.wrap(field);\n            buf.order(ByteOrder.LITTLE_ENDIAN);\n            short name = buf.getShort();\n\n            zipStream.read(field); // offset extra field length\n            s.off += 2;\n\n            buf = ByteBuffer.wrap(field);\n            buf.order(ByteOrder.LITTLE_ENDIAN);\n            short extra = buf.getShort();\n\n            zipStream.read(field); // offset file comment length\n            s.off += 2;\n\n            buf = ByteBuffer.wrap(field);\n            buf.order(ByteOrder.LITTLE_ENDIAN);\n            short com = buf.getShort();\n\n            zipStream.skip(12); // offset disk number start ... relative offset of local header\n            s.off += 12;\n\n            zipStream.skip(name + extra + com); // offset file name (variable size) ... file comment (variable size)\n            s.off += name + extra + com;\n        }\n\n        // 4.3.13 Digital signature, optional\n        zipStream.read(signature);\n        s.off += 4; // offset possibly header signature\n\n        if (MessageDigest.isEqual(signature, headerSignature)) {\n            // Digital signature present\n        } else if (MessageDigest.isEqual(signature, zip64EndOfCentralDirSignature)) {\n            // Non-empty zip64 file without a digital signature\n            s.signature = zip64EndOfCentralDirSignature;\n\n            return;\n        } else if (MessageDigest.isEqual(signature, endOfCentralDirSignature)) {\n            // Empty zip file without a digital signature\n            s.signature = endOfCentralDirSignature;\n\n            return;\n        } else {\n            throw new IOException(\"expected central directory header digital signature\");\n        }\n\n        zipStream.read(field);  // offset size of data\n        s.off += 2;\n\n        ByteBuffer buf = ByteBuffer.wrap(field);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        short data = buf.getShort();\n\n        zipStream.skip(data); // offset signature data\n        s.off += data;\n\n        zipStream.read(signature); // offset extra 4 bytes to determine which segment comes next\n        s.off += 4;\n\n        if (MessageDigest.isEqual(signature, zip64EndOfCentralDirSignature)) {\n            // Non-empty zip64 file with a digital signature\n            s.signature = zip64EndOfCentralDirSignature;\n\n            return;\n        } else if (MessageDigest.isEqual(signature, endOfCentralDirSignature)) {\n            // Empty zip file with a digital signature\n            s.signature = endOfCentralDirSignature;\n\n            return;\n        }\n\n        throw new IOException(\"expected [zip64 end of central directory record] or [end of central directory record]\");\n\n    }\n\n    /**\n     * readZip64EndOfCentralDirectoryRecord possibly reads [zip64 end of central directory record] segment, where\n     * off must point at the beginning of the segment in a zip stream. If the segment doesn't present in a stream at\n     * the location off, the returned value is the same as off.\n     *\n     * @param zipStream\n     * @param s\n     * @throws IOException\n     */\n    private static void readZip64EndOfCentralDirectoryRecord(InputStream zipStream, Segment s) throws IOException {\n        byte[] size = new byte[8];\n\n        if (MessageDigest.isEqual(s.signature, zip64EndOfCentralDirSignature)) {\n            // [zip64 end of central directory record] is present\n        } else if (MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            // Non-empty/empty zip\n            return;\n        } else {\n            throw new IOException(\"expected [zip64 end of central directory record]\");\n        }\n\n        zipStream.read(size); // offset size of zip64 end of central directory record\n        s.off += 8;\n\n        // 4.3.14.1 The value stored into the \"size of zip64 end of central directory record\" SHOULD be the size of\n        // the remaining record and SHOULD NOT include the leading 12 bytes.\n        ByteBuffer buf = ByteBuffer.wrap(size);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        int extra = buf.getInt();\n\n        zipStream.skip(extra); // offset extra field data (variable size)\n        s.off += extra;\n\n        // Next segment must be [zip64 end of central directory locator]\n        s.signature = zip64EndOfCentralDirLocatorSignature;\n    }\n\n    /**\n     * readZip64EndOfCentralDirectoryLocator possibly reads [zip64 end of central directory locator] segment, where\n     * off must point at the beginning of the segment in a zip stream. If the segment doesn't present in a stream at\n     * the location off, the returned value is the same as off.\n     *\n     * @param zipStream\n     * @param s\n     * @throws IOException\n     */\n    private static void readZip64EndOfCentralDirectoryLocator(InputStream zipStream, Segment s) throws IOException {\n        byte[] signature = new byte[4];\n\n        if (MessageDigest.isEqual(s.signature, zip64EndOfCentralDirLocatorSignature)) {\n            // [zip64 end of central directory record] [zip64 end of central directory locator] are present\n            zipStream.skip(4); // offset zip64 end of central dir locator signature\n            s.off += 4;\n        } else if (MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            // Non-empty/empty zip\n            return;\n        } else {\n            throw new IOException(\"expected [zip64 end of central directory locator]\");\n        }\n\n        zipStream.skip(16); // offset [zip64 end of central directory locator]\n        s.off += 16;\n\n        zipStream.read(signature); // offset next 4 bytes, which must be end of central dir signature\n        s.off += 4;\n\n        if (!MessageDigest.isEqual(signature, endOfCentralDirSignature)) {\n            throw new IOException(\"expected end of central dir signature\");\n        }\n\n        s.signature = endOfCentralDirSignature;\n    }\n\n    /**\n     * readEndOfCentralDirectoryRecord possibly reads [end of central directory record] segment, where off must point\n     * at the beginning of the segment in a zip stream. If the segment doesn't present in a stream at the location off,\n     * the returned value is the same as off.\n     *\n     * @param zipStream\n     * @param s\n     * @throws IOException\n     */\n    private static void readEndOfCentralDirectoryRecord(InputStream zipStream, Segment s) throws IOException {\n        if (!MessageDigest.isEqual(s.signature, endOfCentralDirSignature)) {\n            throw new IOException(\"expected [end of central directory record]\");\n        }\n\n        zipStream.skip(16); // offset number of this disk ... starting disk number\n        s.off += 16;\n\n        byte[] field = new byte[2];\n\n        zipStream.read(field); // offset .ZIP file comment length\n        s.off += 2;\n\n        ByteBuffer buf = ByteBuffer.wrap(field);\n        buf.order(ByteOrder.LITTLE_ENDIAN);\n        short com = buf.getShort();\n\n        zipStream.skip(com); // offset .ZIP file comment (variable size)\n        s.off += com;\n    }\n}"
  },
  {
    "path": "common/java/src/main/java/ee/ivxv/common/zip/ZipFileCommentException.java",
    "content": "package ee.ivxv.common.zip;\n\npublic class ZipFileCommentException extends Exception {\n    public ZipFileCommentException() {\n        super(\"invalid .ZIP file comment length\");\n    }\n}"
  },
  {
    "path": "common/java/src/main/resources/bootstrap-i18n/common-conf-localeconfloader-msg_et.properties",
    "content": "# Lokaadi konfiguratsiooni laadimise tekstid\ne_lang_conf_not_found = Keelte konfiguratsioonifaili ''{0}'' ei leitud\ne_langs_property_missing = Keelte konfiguratsioonifailis ''{0}'' puudub võti ''{1}''\ne_langs_empty = Keelte konfiguratsioonifailis ''{0}'' on lubatud keeled ''{1}'' tühi\n\n"
  },
  {
    "path": "common/java/src/main/resources/log4j.properties",
    "content": "log4j.rootLogger=OFF\n"
  },
  {
    "path": "common/java/src/main/resources/logback.xml",
    "content": "<configuration>\n\n  <!-- Stop output INFO at start -->\n  <statusListener class=\"ch.qos.logback.core.status.NopStatusListener\" />\n\n  <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n    <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n      <fileNamePattern>log/log-%d.log</fileNamePattern>\n    </rollingPolicy>\n    <!--\n    <encoder class=\"ch.qos.logback.core.encoder.LayoutWrappingEncoder\">\n      <layout class=\"ch.qos.logback.contrib.json.classic.JsonLayout\">\n        <jsonFormatter class=\"ee.ivxv.common.util.log.LogJsonFormatter\">\n          <prettyPrint>true</prettyPrint>\n        </jsonFormatter>\n        <timestampFormat>yyyy-MM-dd HH:mm:ss.SSS</timestampFormat>\n      </layout>\n    </encoder>\n    -->\n    <encoder>\n      <pattern>%d [%thread] %-5level %mdc %logger{36} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <appender name=\"PERFORMANCE\" class=\"ch.qos.logback.core.FileAppender\">\n    <file>log/performance.log</file>\n    <encoder>\n      <pattern>%d [%thread] %-5level %mdc %logger{36} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <logger name=\"ee.ivxv.common.util.log.PerformanceLog\" level=\"DEBUG\">\n    <appender-ref ref=\"PERFORMANCE\" />\n  </logger>\n\n  <logger name=\"ee.ivxv\" level=\"DEBUG\"/>\n\n  <root level=\"WARN\">\n    <appender-ref ref=\"FILE\" />\n  </root>\n</configuration>\n"
  },
  {
    "path": "common/java/translations/i18n/audit-msg_et.properties",
    "content": "# Rakendus\napp_audit = Auditirakendus\n\n# Tööriistad\ntool_decrypt = Dekrüpteerimistõestuste verifitseerimine\ntool_mixer = Miksimistõendi kontrollimine\ntool_convert = Mixneti teisenduse kontrollimine\ntool_integrity = Töötlemisrakenduse logide kontrollimine\n\n# Tööriistade argumendid\narg_hash = Räsifunktsioon\narg_proofs = Kehtivate sedelite lugemistõendi asukoht\narg_invalidity_proofs = Kehtetute sedelite lugemistõendi asukoht\narg_discarded = Kehtetute sedelite asukoht\narg_plain_bb = Dekrüptitud sedelite asukoht\narg_anon_bb = Anonüümitud e-valimiskast\narg_links = Kontroll-lülide faili asukoht\narg_out = Väljundkaust\narg_pbb = PBB väljundfaili asukoht\narg_pub = Avaliku võtme faili asukoht\narg_revoke = Tühistamisnimekirja faili asukoht\narg_seed = Räsiahela seeme\narg_storage = Kogumisteenuse väljundfaili asukoht\narg_signaturepub = Avalik võti signatuuri kontrollimiseks\narg_threads = Lõimede arv\narg_input_bb = Sisend e-valimiskast\narg_output_bb = Väljund e-valimiskast\narg_protinfo = Miksimistõendi protokollifaili asukoht\narg_proofdir = Miksimistõendi asukoht\narg_threaded = Kasuta mitmelõimelist implementatsiooni\narg_ballotbox = E-valimiskast\narg_ballotbox_checksum = E-valimiskasti SHA-256 kontrollsumma allkirjastatud konteiner\narg_log_accepted = E-valimiskasti verifitseerimise logi\narg_log_squashed = Korduvhäälte tühistamise logi\narg_log_revoked = Topelthäälte tühistamise logi\narg_log_anonymised = E-häälte anonüümimise logi\narg_bb_errors = E-valimiskasti töötlemisvigade raport\narg_abort_early = Peatu esimese ebakõla korral\narg_candidates = Kandidaatide nimekiri\narg_districts = Ringkondade nimekiri\narg_questioncount = Küsimuste arv anonüümistatud e-valimiskastis\narg_tally = Tulemuste fail\n\n# arg_log_accepted = Vastuvõetud hääled\n# arg_log_squashed = Tühistatud korduvhääled\n# arg_log_revoked = Tühistatud topelthääled\n# arg_log_anonymised = Anonüümitud ja lugemisele läinud hääled\n\n# Veateated\ne_abb_invalid_question_count = Oodatav küsimuste arv on {0}, e-valimiskast sisaldab {1}!\ne_proof_verif_false = Tõestus ei verifitseerunud: {0}\ne_proof_verif_exception = Tõestuse verifitseerimisel tekkis erind: {0}\ne_file_missing = Ei leidnud faili ''{0}''\n\n# Teated\nm_anon_loading = Anonüümitud e-valimiskasti laadimine failist ''{0}''\nm_anon_loaded = Anonüümitud e-hääled laaditud\nm_plain_loading = Sedelite laadimine failist ''{0}''\nm_plain_loaded = Sedelid laaditud\nm_plain_count = Failis on {0} sedelit\nm_tally_loading = Hääletamistulemuste laadimine failist ''{0}''\nm_tally_loaded = Hääletamistulemused laaditud\nm_tally_match = Hääletamistulemused klapivad dekrüptitud sedelitega: {0}\nm_bb_loading = E-valimiskasti laadimine failist ''{0}''\nm_bb_loaded = E-valimiskast laaditud\nm_pub_loading = Avaliku võtme lugemine failist ''{0}''\nm_pub_loaded = Avalik võti loetud\nm_failurecount = Vigaste tõestuste arv: {0}\nm_verify_start = Alustan tõestuste verifitseerimist\nm_verify_finish = Verifitseerimine on lõppenud\nm_discarded_loading = Kehtetute sedelite krüptogrammide laadimine failist ''{0}''\nm_discarded_loaded = Kehtetute sedelite krüptogrammid laaditud\nm_discarded_count = Failis on {0} kehtetute sedelite krüptogrammi\n\nm_decrypt_bb_has_proofs = Kõik tõestuste krüptogrammid esinevad anonüümitud e-valimiskastis: {0}\nm_decrypt_bb_has_invalids = Kõik kehtetute sedelite krüptogrammid esinevad anonüümitud e-valimiskastis: {0}\nm_decrypt_dec_consistent = Kõik anonüümitud e-valimiskasti krüptogrammid on dekrüptitud: {0}\nm_decrypt_one_per_file = Krüptogrammid ei esine konfliktsetes failides: {0}\nm_decrypt_consistent_valids_begin = Alustan kehtivate sedelite võrdlemist lugemistõendiga\nm_decrypt_consistent_valids = Kehtivad sedelid klapivad lugemistõendiga: {0}\nm_decrypt_consistent_invalids = Kehtetud sedelid klapivad lugemistõendiga: {0}\nm_decrypt_verified_valid = Kehtivad sedelid on õigesti kehtivaks tunnistatud: {0}\nm_decrypt_verified_invalid = Kehtetud sedelid on õigesti kehtetuks tunnistatud: {0}\nm_decrypt_success = Kõik kontrollid edukalt läbitud\nm_decrypt_failure = Osad kontrollid kukkusid läbi. Palun kontrollige sisendfaile ja rakenduse logi\n\nm_shuffle_proof_loading = Miksimistõendi protokollifaili lugemine failist ''{0}'' ja tõendi lugemine failist ''{1}''\nm_shuffle_proof_failed_reason = Miksimistõendi kontrolli viga: {0}\nm_shuffle_proof_failed = Miksimistõendi kontroll ebaõnnestus\nm_shuffle_proof_succeeded = Miksimistõendi kontroll õnnestus\nm_convert_publickey_failed = Avaliku võtme konverteerimise kontroll ebaõnnestus\nm_convert_publickey_succ = Avaliku võtme konverteerimise kontroll õnnestus\nm_convert_bb_to_bt_failed = Anonümiseeritud e-valimiskasti Verificatumi krüptogrammideks konverteerimise kontroll ebaõnnestus\nm_convert_bb_to_bt_succ = Anonümiseeritud e-valimiskasti Verificatumi krüptogrammideks konverteerimise kontroll õnnestus\nm_convert_bt_to_bb_failed = Verificatumi krüptogrammide anonümiseeritud e-valimiskastiks konverteerimise kontroll ebaõnnestus\nm_convert_bt_to_bb_succ = Verificatumi krüptogrammide anonümiseeritud e-valimiskastiks konverteerimise kontroll õnnestus\n\nm_shuffle_step = [[tab {0}]]{1}{2}\nm_shuffle_read = Loen sisendit\nm_shuffle_read_prot_info = protokollifail\nm_shuffle_read_pubkey = avalik võti\nm_shuffle_read_pc = protokolli kehtestus\nm_shuffle_read_posc = protokolli väljakutse\nm_shuffle_read_posr = protokolli vastus\nm_shuffle_read_ciphs = krüptogrammid\nm_shuffle_read_shuffled = miksitud krüptogrammid\nm_shuffle_verify = Kontrollin miksimistõendit\nm_shuffle_verify_params = tõendi parameetrite arvutamine\nm_shuffle_verify_ni = mitte-interaktiivse verifitseerija initsialiseerimine\nm_shuffle_verify_permutation = ümberjärjestamise kontrollimine\nm_shuffle_verify_rerandomisation = rerandomiseerimise kontrollimine\n\nm_log_accepted = E-valimiskasti verifitseerimise logifail: {0}\nm_log_squashed = Korduvhäälte tühistamise logifail: {0}\nm_log_revoked = Topelthäälte tühistamise logifail: {0}\nm_log_anonymised = E-häälte anonüümimise logifail: {0}\nm_bb_errors = E-valimiskasti töötlemisvigade raport: {0}\nm_integrity_match_anon = Anonüümitud e-valimiskast klapib e-valimiskastiga: {0}\nm_integrity_match_anon_logs = Anonüümimise logi klapib anonüümitud e-valimiskastiga: {0}\nm_integrity_match_valid = Vastuvõetud hääled klapivad e-valimiskastiga: {0}\nm_integrity_match_logs = Tühistuslogid klapivad e-valimiskastidega: {0}\nm_integrity_bb_consistent = E-valimiskasti verifitseerimise logid on terviklikud: {0}\nm_integrity_bb_inconsistent = E-valimiskastis on hääli, millele pole logides vastet\nm_integrity_recurring_ct = Vastuvõetud häälte seas on korduvaid krüptogramme\n\nm_tally_start = Alustan e-häälte kokkulugemist\nm_tally_done = E-hääled kokku loetud\nm_out_tally = Väljastan tulemused kausta ''{0}''\nm_tally_read = Hääletamistulemuste fail: {0}\n\nm_yes = jah\nm_no = ei\n"
  },
  {
    "path": "common/java/translations/i18n/common-cli-msg_et.properties",
    "content": "# Üldised rakenduse käivitamise tekstid\ne_app_error = Rakenduse ''{0}'' käivitamisel tekkis viga: {1}\ne_common_args_invalid = Rakenduse käsurea argumendid on vigased\ne_multiple_tools = Korraga saab käivitada ainult ühte tööriista, sisestati: {0}\ne_tool_args_invalid = Tööriista parameetrid on vigased\ne_tool_error = Tööriista ''{0}'' käivitamisel tekkis viga: {1}\ne_tool_missing = Palun valige tööriist\ne_unknown_arg = Tundmatu argument: ''{0}''\ne_unknown_args_present = Rakenduse käivitamisel sisestati tundmatuid argumente\ne_unknown_tool = Tundmatu tööriist: ''{0}''\n\nw_test_mode = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* Rakendus on käivitatud arendusrežiimis ning rakenduse käitumine võib      *\\n\\\n* erineda tavarežiimist.                                                    *\\n\\\n* Rakenduse käivitamiseks tavarežiimis tuleb rakendus ümber kompileerida.   *\\n\\\n*****************************************************************************\\n\n\nw_java_data_model = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* 64-bitise Java andmemudeli tuvastamine ebaõnnestus. Rakendus on vähem-    *\\n\\\n* efektiivsem. Rakenduse jõudluse suurendamiseks tuleb kasutada 64-bitise   *\\n\\\n* andmemudeliga Java keskkonda.                                             *\\n\\\n*****************************************************************************\\n\n\nw_unknown_java_runtime = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* Java käivituskeskkona tuvastamine ebaõnnestus. Rakenduse võib olla eba-   *\\n\\\n* stabiilne. Rakenduse stabiilsuse garanteerimiseks tuleb kasutada          *\\n\\\n* OpenJDK-d või Oracle Javat.                                               *\\n\\\n*****************************************************************************\\n\n\n# Rakenduse käivitusjuhised\napp = Rakendus ''{0}''[[col 23]] - {1}\ntool = Tööriist ''{0}''[[col 23]] - {1}\ntool_placeholder = <tööriist>\nvalue_placeholder = <väärtus>\n# 'row_indent' rakendatakse nii usage, 'tool_row', 'arg_row' kui 'param_row' ridadele.\nrow_indent = [[tab 2]]{0}\nusage = Kasutamine:\nusage_arg_w_value = {0} <{1}>\nusage_arg_names = {0} | {1}\nusage_arg_optional = [{0}]\ntools = Tööriistad:\ntool_row = {0}[[col 15]] - {1}\nargs = Käsurea argumendid:\narg_row = {0}[[col 23]] - {1}\narg_name_required = {0} (*)\nparams = Tööriista parameetrite 'yaml' struktuur:\nparam_row = {0}[[col 31]] - {1}\nparam_name_required = {0} (*)\n\napp_result_success = {0} lõpetas töö ilma vigadeta\napp_result_failure = {0} lõpetas töö vigadega\n\n# Käsurea argumentide töötlemise veateated, 'e_invalid_*' teated mähitakse 'e_arg_parse_error' sisse.\ne_arg_parse_error = Viga argumendi ''{0}'' väärtuse ''{1}'' parsimisel: {2}\ne_invalid_boolean = Vigane tõeväärtus: ''{0}''\ne_invalid_choice = Vigane väärtus: ''{0}''. Lubatud väärtused on: {1}\ne_invalid_int = Vigane 32-bitine täisarv: ''{0}''\ne_invalid_long = Vigane 64-bitine täisarv: ''{0}''\ne_invalid_number = Vigane täisarv: ''{0}''\ne_invalid_instant = Vigane kuupäev-kellaaeg: ''{0}''. Lubatud vorming on ISO-8601 (yyyy-mm-ddThh:mm:ss[.nnnnnn](Z|±hh:mm)).\ne_invalid_local_date = Vigane kuupäev: ''{0}''. Lubatud vorming on ISO-8601 (yyyy-mm-dd).\ne_invalid_path_exists = Faili ei tohi eksisteerida: ''{0}''\ne_invalid_path_not_exists = Faili ei eksisteeri: ''{0}''\ne_invalid_path_not_dir = Väärtus peab viitama kaustale: ''{0}''\ne_invalid_path_not_file = Väärtus peab viitama failile: ''{0}''\ne_invalid_public_key = Vigane avalik võti: {0}. Võtme algoritm peab olema ''{1}''. Tehniline veateade: {2}\n\ne_multiple_assignments = Argumendile ''{0}'' on juba väärtus omistatud\ne_requires_single_value = Argumendil ''{0}'' on vaja ühte väärtust, aga sisestati: ''{1}''\ne_value_not_allowed = Argumendile ''{0}'' ei saa väärtust omistada, aga sisestati: ''{1}''\ne_branch_expected = Argumendi ''{0}'' väärtus peab olema struktuurse elemendi nimi, aga sisestati: ''{1}''\n\ne_yaml_invalid_file = Vigane YAML fail: ''{0}''\ne_yaml_invalid_key = Elemendi ''{0}'' tundmatu alamelement: ''{1}''\ne_yaml_list_expected = Elemendi ''{0}'' väärtus peab olema list, aga oli ''{1}''\ne_yaml_map_expected = Elemendi ''{0}'' väärtus peab olema struktuurne element, aga oli ''{1}''\ne_yaml_scalar_expected = Elemendi ''{0}'' väärtus peab olema skalaar, aga oli ''{1}''\n\ne_arg_required = Argument ''{0}'' on kohustuslik\n\n# Üldised tööriistad\ntool_verify = Abifunktsioon: allkirjastatud konteineri verifitseerimine\n\n# Üldised argumendid\narg_help = Abi\narg_conf = Konfiguratsioon\narg_params = Tööriista parameetrid\narg_force = Ära küsi kasutajalt kinnitust\narg_quiet = Vaikne käivitusrežiim\narg_lang = Keel\narg_container_threads = Allkirjastatud konteinerite teegi poolt kasutatav lõimede arv (<= 0 korral dünaamiline)\narg_threads = Rakenduse poolt paralleeltöötluse korral kasutatav lõimede arv (<= 0 korral dünaamiline)\n\n# Verifitseerimistööriista argumendid\narg_file = Fail\n"
  },
  {
    "path": "common/java/translations/i18n/common-conf-msg_et.properties",
    "content": "# Konfiguratsiooni laadimisel kasutatavad tekstid\ne_conf_file_not_found = Konfiguratsioonifaili ''{0}'' ei leitud\ne_conf_file_open_error = Konfiguratsioonifaili ''{0}'' lugemisel tekkis viga: {1}\ne_unsupported_conf_type = Tundmatu konfiguratsiooni tüüp: {0}. Toetatud on fail laiendiga 'bdoc' ning avatud kaust.\ne_unused_file = Konfiguratsiooni pakis eksisteeriv kuid kasutamata fail: {0}\ne_ca_not_ca_cert = Konfiguratsioonis olev CA sertifikaat {0} ei vasta CA sertifikaadile kehtivatele nõuetele\ne_ocsp_not_ocsp_cert = Konfiguratsioonis olev OCSP sertifikaat {0} ei vasta OCSP sertifikaadile kehtivatele nõuetele\ne_tsp_not_tsp_cert = Konfiguratsioonis olev TSP sertifikaat {0} ei vasta TSP sertifikaadile kehtivatele nõuetele\n\nw_unknown_property = Tundmatu konfiguratsioonifaili võti: {0}\n\ne_unsupported_locale = Lokaat ''{0}'' pole toetatud. Lubatud lokaadid on: {1}\n\nm_loading_conf = Konfiguratsiooni laadimine failist {0}\nm_conf_arg_for_cont = Konfiguratsiooni\n"
  },
  {
    "path": "common/java/translations/i18n/common-model-bb-type_et.properties",
    "content": "UNORGANIZED = Korrastamata\nBACKUP = E-valimiskasti varukoopia\nINTEGRITY_CONTROLLED = Tervikluskontrolliga korrastatud\nRECURRENT_VOTES_REMOVED = Korduvhäältest puhastatud\nINVALID_CIPHERTEXTS_REMOVED = Korduvhäältest puhastatud korrektsete sedelitega\nDOUBLE_VOTERS_REMOVED = Topelthääletajate häältest puhastatud\nANONYMIZED = Anonüümistatud\n\n"
  },
  {
    "path": "common/java/translations/i18n/common-msg_et.properties",
    "content": "# Üldised veateated\ne_bb_invalid_type = Vigane e-valimiskasti tüüp. Tüüp peaks olema ''{0}'', aga on ''{1}''\ne_cert_not_found = Sertifikaati ei leitud: {0}\ne_cert_read_error = Sertifikaadi ''{0}'' lugemisel tekkis viga: {1}\ne_checksum_mismatch = {0} ''{1}'' kontrollsumma ei klapi konteineris ''{2}'' olevaga!\ne_cont_signature_expected = Konteineril polnud ühtegi digiallkirja\ne_cont_single_file_expected = Konteineris oleks pidanud olema 1 fail, aga oli {0}\ne_decryption_error = Hääle {0} dekrüpteerimisel tekkis viga\ne_file_not_container = Tegemist pole allkirjastatud konteineriga: {0}\ne_file_not_found = Faili ei leitud: {0}\ne_invalid_container = Allkirjastatud konteiner ''{0}'' ei valideeru\n\ne_dist_read_error = Viga ringkondade nimekirja lugemisel: {0}\ne_dist_parish_id_not_unique = Valimise omavalitsuse kood pole unikaalne: {0}\ne_dist_parish_id_invalid = Vigane omavalitsuse kood: {0}\ne_dist_parish_region_unknown = Omavalitsuse koodis ''{0}'' olevale regiooninumbrile ''{1}'' vastavat regiooni ei leitud\ne_dist_bb_parish_missing = E-valimiskastis leidub omavalitsusi, mis puuduvad ringkondade nimekirjast: {0}\n\ne_cand_read_error = Viga kandidaatide nimekirja lugemisel: {0}\ne_cand_invalid_dist = Kandidaatide nimekirjas on ringkondade nimekirjas puuduv ringkond: {0}\ne_cand_duplicate_id = Kandidaatide nimekirjas on korduv kandidaadinumber: {0}\n\nm_election_id = Valimiste identifikaator on {0}\n# Kuupäeva-kellaaja muster, mida kasutatakse DateTimeFormatter isendi loomiseks\n# (https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html),\n# mida omakorda kasutatakse ajahetkede vormindamiseks rakenduste väljundis.\n# Muster peab olema olema korrektne. Seda kontrollitakse ühiktestiga.\nm_datetime_pattern = dd.MM.uuuu HH:mm\n# Asendatakse järgmiseid kohatäiteid (ühekordselt):\n#   VALUE - täidetud sammude arv\n#   TOTAL - sammude eeldatav koguarv\n#   PERCENT - täidetud protsent ümardatuna täisarvuks, kuvatakse 3 sümboli laiuselt\n#   BAR - visuaalne edenemisriba laiusega 50 sümbolit, täidetakse protsentuaalselt sümbolitega '.'\nm_progress_bar = {PERCENT}% [{BAR}] {VALUE} / {TOTAL}\nm_relative_progress_bar = {PERCENT}% [{BAR}]\n\nm_loading_params = Tööriista parameetrite laadimine failist {0}\nm_params_arg_for_cont = Tööriista parameetrite\n\nm_reading_container = Loen allkirjastatud konteinerit: {0}\nm_files = Failid:\nm_file_row = \\u0020\\u0020{0}\nm_signatures = Allkirjad:\nm_signature_row = \\u0020\\u0020Isikukood:\\t{0},\\n\\u0020\\u0020Nimi:\\t\\t{1},\\n\\u0020\\u0020Aeg:\\t\\t{2}\n\nm_cont_checking_signature = {0} allkirja kontrollimine\nm_cont_signer = {0} allkirja on andnud {1}\nm_cont_signature_time = {0} allkirja andmise aeg on {1}\nm_cont_signature_is_valid = {0} allkiri on korrektne ja kehtiv\n\nm_checksum_loading = {0} kontrollsumma laadimine failist ''{1}''\nm_checksum_loaded = {0} kontrollsumma on laaditud\nm_checksum_arg_for_cont = {0} kontrollsumma\nm_checksum_calculate = {0} kontrollsumma arvutamine failist ''{1}''\nm_checksum_ok = {0} arvutatud kontrollsumma klapib allkirjastatud kontrollsummaga\n\nm_bb_arg_for_checksum = E-valimiskasti\nm_bb_loading = E-valimiskasti laadimine failist ''{0}''\nm_bb_loaded = E-valimiskast on laaditud\nm_bb_checking_type = E-valimiskasti liigi kontrollimine\nm_bb_type = E-valimiskasti liik on \"{0} e-valimiskast\"\nm_bb_checking_integrity = E-valimiskasti andmetervikluse kontrollimine\nm_bb_data_is_integrous = E-valimiskastis sisalduvad andmed on terviklikud\nm_bb_numof_collector_ballots = Kogumisteenus andis e-valimiskasti koosseisus üle {0} häält\nm_bb_numof_ballots = E-valimiskastis on {0} kvalifitseerimiseks sobivat häält\nm_bb_checking_ballot_sig = E-valimiskastis sisalduvate häälte digiallkirja vormingule vastavuse kontrollimine\nm_bb_total_ballots = E-valimiskastis sisalduvate häälte koguarv: {0}\nm_bb_numof_ballots_sig_valid = E-valimiskastis sisalduvate korrektse allkirjaga häälte arv: {0}\nm_bb_numof_ballots_sig_invalid = E-valimiskastis sisalduvate vigase allkirjaga häälte arv: {0}\nm_bb_all_ballots_sig_valid = Kõik e-valimiskastis sisalduvad hääled vastavad digiallkirja vormingule\nm_bb_compare_with_reg = E-valimiskastis sisalduvate häälte vastavuse kontrollimine registreerimisandmetega\nm_bb_ballot_missing_reg = E-valimiskastis sisalduvate häälte arv, millele vastavat ajamärgendit registreerimisandmetest ei leitud: {0}\nm_bb_reg_missing_ballot = Registreerimisandmete ajamärgendite arv, millele vastavat häält e-valimiskastist ei leitud: {0}\nm_bb_in_compliance_with_reg = E-valimiskastis sisalduvad hääled on registreerimisandmetega vastavuses\nm_reg_in_compliance_with_bb = Registreerimisandmed on vastavuses e-valimiskastis sisalduvate häältega\nm_bb_total_checked_ballots = E-valimiskastis sisalduvate kontrollitud häälte koguarv: {0}\nm_bb_exporting = Valimissedelite eksportimine kausta {0}\nm_bb_exporting_voter = Valija {0} valimissedelite eksportimine kausta {1}\nm_bb_exported = Valimissedelid on eksporditud\n\nm_reg_arg_for_checksum = Registreerimisandmete\nm_reg_loading = Registreerimisandmete laadimine failist ''{0}''\nm_reg_loaded = Registreerimisandmed on laaditud\nm_reg_checking_integrity = Registreerimisandmete andmetervikluse kontrollimine\nm_reg_data_is_integrous = Registreerimisandmed on terviklikud\nm_reg_numof_records = Registreerimisandmetes on {0} ajamärgendit\n\nm_bb_saving = {0} e-valimiskasti salvestamine faili {1}\nm_bb_saved = {0} e-valimiskast on salvestatud\nm_bb_checksum_saving = E-valimiskasti kontrollsumma salvestamine faili {0}\nm_bb_checksum_saved = E-valimiskasti kontrollsumma on salvestatud\n\nm_cand_loading = Kandidaatide nimekirja laadimine failist ''{0}''\nm_cand_arg_for_cont = Kandidaatide nimekirja\nm_cand_loaded = Kandidaatide nimekiri laaditud\nm_cand_count = Kandidaatide nimekirjas on {0} kandidaati\n\nm_dist_loading = Ringkondade nimekirja laadimine failist ''{0}''\nm_dist_arg_for_cont = Ringkondade nimekirja\nm_dist_loaded = Ringkondade nimekiri on laaditud\nm_dist_count = Ringkondade nimekirjas on {0} ringkonda\n\nm_skip_cmd_loading = Nimekirja vahelejätmise korralduse laadimine failist ''{0}''\nm_skip_cmd_arg_for_cont = Nimekirja vahelejätmise korralduse\nm_skip_cmd_loaded = Nimekirja vahelejätmise korraldus on laaditud\n\nm_proof_loading = Tõestuste laadimine failist ''{0}''\nm_proof_loaded = Tõestused laaditud\nm_proof_count = Tõestuste failis on {0} tõestust\n\nm_out_start = Väljundfailide väjastamine kausta ''{0}''\nm_out_done = Väljundfailide väljastamine edukas\n\nr_ivl_description = VALIMISED {0}, E-HÄÄLETANUTE NIMEKIRI\n# '00A0' on 'non-breaking space' Unicode kood\nr_ivl_parish_name = Omavalitsus\\u00A0nr\\u00A0{0}. {1}\nr_ivl_district_name = {0}\n\n"
  },
  {
    "path": "common/java/translations/i18n/common-smartcard-msg_et.properties",
    "content": "insert_card_indexed = Sisesta kaart ''{0}'' kaarditerminali number {1}\nremove_card_indexed = Eemalda kaart ''{0}'' kaarditerminalist number {1}\nenter_terminal_id = Sisestage kaardile {0} vastav terminalinumber\ninsert_unprocessed_card = Sisestage kaart, mida ei ole veel kasutatud ning vajutage ENTER\ninserted_card_id = Sisestati kaart ''{0}''\nenter_pin_keyboard = Sisestage kaardi ''{0}'' PIN kood:\nenter_pin_pinpad = Sisestage kaardi ''{0}'' PIN kood kaarditerminali\nverify_fail_retry = Vale PIN. Jäänud katsete arv: {0}\n"
  },
  {
    "path": "common/java/translations/i18n/key-msg_et.properties",
    "content": "# Rakendus\napp_key = Võtmerakendus\n\n# Tööriistad\ntool_decrypt = Elektrooniliste häälte dekrüpteerimine\ntool_groupgen = Võtmeparameetrite genereerimine\ntool_init = Võtme genereerimine\ntool_util = Kiipkaardi tööriistad\ntool_testkey = Võtme testimine\n\n# Ühised tööriistade argumendid\narg_identifier = Valimiste identifikaator\narg_parties = Osapoolte koguarv\narg_threshold = Osapoolte lävend\narg_out = Väljundkausta asukoht\narg_mod = Krüpto üle täisarvukorpuse\narg_ec = Krüpto üle elliptkõrverate\narg_paramtype = Võtmetüüp\narg_random_source = Juhuslikkuse allikad\narg_random_source_type = Juhuslikkuse allika tüüp\narg_random_source_path = Juhuslikkuse allika asukoht\narg_fastmode = Kaartidele automaatne terminalide määramine\n\n# Tööriista 'decrypt' argumendid\nd_anonballotbox = Anonüümistatud e-valimiskast\nd_anonballotbox_checksum = Anonüümistatud e-valimiskasti SHA-256 kontrollsumma allkirjastatud konteiner\nd_questioncount = Küsimuste arv anonüümistatud e-valimiskastis\nd_candidates = Kandidaatide nimekiri\nd_districts = Ringkondade nimekiri\nd_recover = Protokolli tüüp\nd_protocol = Kasutatav protokoll\nd_provable = Väljasta dekrüpteerimise kohta tõestus\nd_check_decodable = Kontrolli dekodeeritavust\nd_prove_invalid = Tõesta kehtetute sedelite korrektne dekrüpteerimine\n\n# Tööriista 'groupgen' argumendid\ng_length = Soovitud võtmepikkus\ng_init_template = Rühma parameetrite väljundfail\n\n# Tööriista 'init' argumendid\ni_p = Rühma moodul\ni_g = Rühma generaator\ni_name = Elliptkõvera nimi\ni_signaturekeylen = Võtme pikkus bittides\ni_signcn = Signeerimissertifikaadi subjekti CN\ni_signsn = Signeerimissertifikaadi seerianumber\ni_enccn = Krüpteerimissertifikaadi subjekti CN\ni_encsn = Krüpteerimissertifikaadi seerianumber\ni_desmedt = Protokolli tüüp\ni_genprotocol = Kasutatav protokoll\ni_skiptest = Ära testi genereeritud võtmeid\ni_required_randomness = Kohustuslik loetava juhuslikkuse kogus baitides\n\n# Tööriista 'util' argumendid\nu_listreaders = Loetle olemasolevad kaardilugejad\nu_testkey = Testi genereeritud võtmeid\n\n# Veateated\ne_testencryption_fail = Genereeritud võtme testimine ebaõnnestus\ne_quorum_test_fail = Genereeritud võtme testimine ebaõnnestus kvoorumiga {0}\ne_no_cardterminals_found = Ei leidnud ühtegi kaardilugejat\ne_abb_invalid_question_count = Oodatav küsimuste arv on {0}, e-valimiskast sisaldab {1}!\ne_illegal_vote_district = E-valimiskast sisaldab ringkonda ''{0}'', mis ei esine ringkondade nimekirjas!\ne_illegal_vote_parish = E-valimiskast sisaldab omavalitsust ''{0}'', mis ei esine ringkondade nimekirjas!\n\n# Teated\nm_id = ID\nm_name = NIMI\nm_with_card = KAARDIGA\nm_yes = Jah\nm_no = Ei\nm_quorum_test_ok = Genereeritud võtme testimine õnnestus kvoorumiga {0}\nm_gen_group_params = Genereerin täisarvukorpuse parameetreid pikkusega {0}\nm_certificates_generated = Allkirjastamise ja salastamise sertifikaadid salvestatud väljundkausta failidena ''{0}'' ja ''{1}''\nm_generate_decryption_key = Genereerin krüpteerimise võtmepaari\nm_test_decryption_key = Testin krüpteerimise võtmepaari\nm_generate_signature_key = Genereerin signeerimise võtmepaari\nm_test_signature_key = Testin signeerimise võtmepaari\nm_votecount = Hääli kokku {0}\nm_abb_dist_verifying = Kontrollin e-valimiskasti vastavust ringkondade nimekirjale\nm_abb_dist_ok = E-valimiskast vastab ringkondade nimekirjale\nm_protocol_init = Dekrüpteerimis- ja signeerimisprotokollide initsialiseerimine\nm_protocol_init_ok = Dekrüpteerimis- ja signeerimisprotokollide initsialiseerimine edukas\nm_dec_start = Häälte dekrüpteerimine\nm_dec_done = Häälte dekrüpteerimine edukas\nm_out_tally = Väljastan hääletamistulemused\nm_out_plainbb = Väljastan dekrüpteeritud e-valimiskasti\nm_out_proof = Väljastan dekrüpteerimistõestused\nm_out_proof_invalid = Väljastan dekrüpteerimistõestused rikutud sedelite kohta\nm_out_invalid = Väljastan rikutud sedelid\nm_out_logs = Väljastan LOG4 ja LOG5\nm_keys_saved = Salastamise võti salvestatud väljundkausta DER- ja PEM-formaadis failidena ''{0}'' ja ''{1}''\nm_collecting_required_randomness = Kogun {0} baiti kohustuslikku juhuslikkust\nm_with_proof = Dekrüpteerimine koos tõestustega\nm_without_proof = Dekrüpteerimine ilma tõestusteta\nm_card_id = kaardi ID\nm_fastmode_disabled = Kaartidele ei määrata automaatselt terminali\nm_fastmode_enabled = Kaartidele määratakse automaatselt terminal\nm_storing_shares = Salvestan võtmeosakuid kaartidele\nm_generating_certificate = Genereerin dekrüpteerimise ja allkirjastamise sertifikaate\n"
  },
  {
    "path": "common/java/translations/i18n/processor-msg_et.properties",
    "content": "# Rakendus\napp_processor = Töötlemisrakendus\n\n# Tööriistad\ntool_check = I etapp: e-valimiskasti tervikluse kontroll ja e-hääletanute nimekirja väljastamine\ntool_squash = II etapp: korduvate e-häälte tühistamine\ntool_revoke = III etapp: topelthääletanute häälte tühistamine (tühistus- ja ennistusnimekirjade rakendamine) ja lugemisele minevate e-hääletanute nimekirja väljastamine\ntool_anonymize = IV etapp: e-häälte anonüümistamine\ntool_export = Valimissedelite allkirjastatud konteinerite eksportimine\ntool_stats = E-valimiskasti statistika genereerimine\ntool_statsdiff = E-valimiskasti statistika võrdlemine\ntool_checkAndSquash = I ja II etapp: kombineeri esimene ja teine etapp\ntool_revokeAndAnonymize = III ja IV etapp: kombineeri kolmas ja neljas etapp\n\n# Argumendid\narg_ballotbox = E-valimiskast\narg_ballotbox_checksum = E-valimiskasti SHA-256 kontrollsumma allkirjastatud konteiner\narg_signed_ballot_max_size_bytes = Allkirjastatud hääle maksimaalne suurus baitides\narg_registrationlist = Registreerimisandmed\narg_registrationlist_checksum = Registreerimisandmete SHA-256 kontrollsumma allkirjastatud konteiner\narg_districts = Ringkondade nimekiri\narg_revocationlists = Tühistus-/ennistusnimekirjad\narg_tskey = Registreerimispäringute verifitseerimiseks kasutatav koguja avalik võti\narg_vlkey = Valijate nimekirjade verifitseerimiseks kasutatav avalik võti\narg_voterlists = Valijate nimekirjad\narg_path = Fail\narg_signature = Digitaalse allkirja fail\narg_skip_cmd = Korraldus valijate nimekirja vahelejätmiseks\narg_districts_mapping = Valijate nimekirjas oleva ringkonna ja omavalitsuse teisendusfail\narg_voter_id = Valija identifikaator\narg_election_start = Valimiste algusaeg ISO-8601 vormingus. Sellele ajahetkele eelnevad hääled loetakse proovihäälteks ning need lugemisele ei lähe.\narg_enckey = Krüpteerimise avaliku võtme faili asukoht\narg_election_day = Valimispäev, mille põhjal arvutatakse e-hääletanu vanus\narg_period_start = Statistikaperioodi algus\narg_period_end = Statistikaperioodi lõpp\narg_compare = Statistikafailide võrdluse alus\narg_to = Võrreldav statistikafail\narg_diff = Statistikafailide vahe väljundfail\narg_voterforeignehak = Välismaa hääletaja ehak kood\narg_out = Väljundkaust\n\n# Veateated\ne_tehcnical_error = Tehniline viga: {0}\n\ne_vl_voterlists_missing = Ringkondade kaupa statistika väljastamiseks on parameeter 'voterlists' kohustuslik\ne_vl_vlkey_missing = Valijate nimekirjade kontrollimiseks on parameeter 'vlkey' kohustuslik\ne_vl_first_not_initial = Esimene valijate nimekiri ''{0}'' peab olema tüübiga ''algne''\ne_vl_initial_not_first = Valijate nimekiri ''{0}'' tüübiga ''algne'' peab olema esimene\ne_vl_read_error = Valijate nimekirja ''{0}'' lugemisel tekkis viga: {1}\ne_vl_signature_error = Signatuur ''{0}'' ei verifitseeru\ne_vl_election_id = Valijate nimekirjas ''{0}'' olev valimiste identifikaator {1} ei lange kokku ringkondade nimekirjas olevaga {2}\ne_vl_invalid_header = Vigane valijate nimekirja päis.\\n\\\nOodatud vorming on: valijate-nimekiri = versiooninumber LF valimise-identifikaator LF tüüp LF *valija\ne_vl_invalid_voter_row = Vigane valijate nimekirja valija rida: {0}\\n\\\nOodatud vorming on: valija = isikukood TAB nimi TAB tegevus TAB jaoskond TAB rea-number-voi-tyhi TAB pohjus-voi-tyhi LF\ne_vl_invalid_row_number = Vigane rea numbri väärtus ''{0}'' real ''{1}''\ne_vl_invalid_changeset = Vigane valijate nimekirja järjekorranumber\ne_vl_invalid_version = Vigane valijate nimekirja versioon ''{0}''\ne_vl_invalid_time = Vigane ajatempel\ne_vl_invalid_district = Valijate nimekirja {0} valija {1} ({2}) valimisringkond {3} on tundmatu\ne_vl_invalid_parish = Valijate nimekirja {0} valija {1} ({2}) omavalitsus {3} on tundmatu\ne_vl_voter_already_removed = Viga valijate nimekirja {0} valija {1} ({2}) eemaldamisel: valija on antud nimekirjaga juba eemaldatud\ne_vl_removed_voter_missing = Viga valijate nimekirja {0} valija {1} ({2}) eemaldamisel: valija puudub eelmisest valimisnimekirjast\ne_vl_voter_already_added = Viga valijate nimekirja {0} valija {1} ({2}) lisamisel: valija on antud nimekirjaga juba lisatud\ne_vl_added_voter_exists = Viga valijate nimekirja {0} valija {1} ({2}) lisamisel: valija eksisteerib eelmises valimisnimekirjas\ne_vl_fictive_single_district_and_parish_required = Valijate nimekirju ei antud! Sellisel juhul peab ringkondade \\\n  nimekirjas sisalduma täpselt üks ringkond ning üks omavalitsus!\ne_vl_error_report = Valijate nimekirjade töötlemisel esines vigu. Vigade raport asub failis {0}.\n\ne_dist_mapping_invalid_row = Vigane ringkonna ja omavalitsuse teisendusfaili rida: {0}\\n\\\nOodatud vorming on: ''omavalitsus TAB omavalitsus'', kus esimene on asendatav ning teine on asendus\n\ne_skip_cmd_loading = Nimekirja vahelejätmise korralduse laadimine / verifitseerimine ebaõnnestus\n\ne_reg_checksum_missing = Kui registreerimisandmed on antud, siis peab parameeter 'registrationlist_checksum' olema antud\ne_bb_read_error = Viga valimiskasti ''{0}'' lugemisel: {1}\ne_bb_ballot_processing = Viga valija {0} hääle {1} töötlemisel: {2}\ne_reg_record_processing = Viga registreerimisandmete kirje {0} töötlemisel: {1}\ne_bb_invalid_file_name = Vigane faili nimi ''{0}''. Lubatud kuju on: {1}\ne_bb_invalid_file_size = Failil ''{0}'' on vigane suurus - {1} baiti\ne_bb_missing_file = Fail laiendiga ''{0}'' on puudu\ne_bb_repeated_file = Fail ''{0}'' esineb mitmekordselt\ne_bb_unknown_file_type = Tundmatu faili laiend: {0}\ne_ballot_signature_invalid = Häälel on vigane allkiri: {0}\ne_ballot_missing_voter_signature = Häälel puudub valija signatuur\ne_active_voter_not_found = Valijate nimekirjade põhjal puudus valijal hääle andmise ajal valimisõigus\ne_active_voterlist_not_found = Valijate nimekirja muudatuse versiooniga {0} ei leitud\ne_time_before_start = Hääletamise aeg ''{0}'' on varasem valimiste algusajast ''{1}''\ne_reg_resp_invalid = Registreerimispäringu vastus on vigane. Tehniline veateade: {0}\ne_reg_req_invalid = Registreerimispäring on vigane. Tehniline veateade: {0}\ne_reg_resp_not_unique = Registreerimispäringu vastus pole unikaalne\ne_reg_req_not_unique = Registreerimispäring pole unikaalne\ne_reg_resp_no_nonce = Registreerimispäringu vastusel puudub nonss\ne_reg_resp_nonce_not_sig = Registreerimispäringu vastuses olev nonss pole ettenähtud struktuuriga signatuur\ne_reg_resp_nonce_alg_mismatch = Registreerimispäringu vastuses oleva nonssi signeerimisalgoritmile vastav räsialgoritm ''{0}'' ning registreerimispäringus oleva sõnumi räsialgoritm ''{1}'' on erinevad\ne_reg_resp_nonce_sig_invalid = Registreerimispäringu vastuse nonss pole koguja võtmega allkirjastatud registreerimispäringu sisu\ne_unknown_file_in_vote_container = Hääle allkirjastatud konteineris ''{0}'' sisaldub tundmatu fail: {1}\ne_reg_resp_req_unmatch = Registreerimisandmetes olev registreerimispäring {0} ning e-valimiskastis olev registreerimispäringu vastus pole vastavuses\ne_reg_req_without_ballot = Ei leia registreerimisteenuse päringule vastavat häält\ne_ballot_without_reg_req = Ei leia häälele vastavat registreerimisteenuse päringut\ne_same_time_as_latest = Hiliseima kellaajaga ({0}) hääli on mitu! Arvesse läheb hiliseimatest juhuslikult valitud hääl ''{1}''.\ne_invalid_signature_profile = Hääle signatuuril on vale profiil: {0}\ne_bb_error_report = E-valimiskasti töötlemisel esines vigu. Vigade raport asub failis {0}.\ne_invalid_error_report = E-valimiskasti töötlemisel esines kehtetuid sedeleid. Kehtetute sedelite raport asub failis {0}.\n\ne_rl_read_error = Tühistus-/ennistusnimekirja ''{0}'' lugemisel tekkis viga: {1}\ne_rl_election_id = Tühistus-/ennistusnimekirjas olev valimiste identifikaator {0} ei lange kokku e-valimiskastis olevaga {1}\ne_rl_processing_error = \\u0020\\u0020Tühistus-/ennistusnimekirja töötlemisel leiti viga: {0}\ne_rl_voter_not_found_in_bb = Valija ''{0}'' puudub e-valimiskastist\ne_rl_ballot_already_revoked = Valija ''{0}'' hääl on juba tühistatud\ne_rl_ballot_already_restored = Valija ''{0}'' hääl on juba ennistatud\n\ne_bb_ciphertext_checking = Valija {0} hääle {1} küsimuse {2} krüptogrammi kontrollimisel leiti viga: {3}\ne_ciphertext_invalid = Vigane krüptogramm\ne_ciphertext_invalid_bytes = Krüptogrammi ei ole võimalik dekodeerida\ne_ciphertext_invalid_group = Krüptogrammi väärtus ei esita nõutud rühma elementi\ne_ciphertext_invalid_point = Krüptogrammi väärtus ei asu nõutud elliptkõveral\ne_ciphertext_invalid_qr = Krüptogrammi väärtus ei ole ruutjääk\ne_ciphertext_invalid_range = Krüptogrammi väärtus on lubatud vahemikust väljas\n\ne_writing_ivoter_list = E-hääletanute nimekirja väljastamisel tekkis viga: {0}\ne_writing_revocation_report = Tühistamiste ja ennistamiste aruande ''{0}'' väljastamisel tekkis viga: {1}\ne_writing_log_n = Log{0} failide väljastamisel kausta ''{1}'' tekkis viga: {2}\ne_writing_error_report = Vigade raporti ''{0}'' väljastamisel tekkis viga: {1}\n\ne_stats_code_not_estonian = Valija identifikaator ''{0}'' ei ole Eesti isikukood\n\n# Teated\nm_output_file = Väljundfail: {0}\nm_read = Loetud:\n\nm_bb_unsigned_skipping_output = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* Rakenduse sisendandmetest puudub e-valimiskasti allkirjastatud            *\\n\\\n* kontrollsumma. E-valimiskast on tundmatut päritolu! Rakendus kontrollib   *\\n\\\n* e-valimiskasti terviklust, kuid ei väljasta andmeid töötlemisprotsessi    *\\n\\\n* järgmisteks etappideks.                                                   *\\n\\\n*****************************************************************************\nm_reg_skipping_compare = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* Rakenduse sisendandmetest puuduvad registreerimisandmed.                  *\\n\\\n* Ei saa kontrollida kõikide registreeritud häälte olemasolu! Häälele       *\\n\\\n* vastava registreerimispäringu puudumist ignoreeritakse.                   *\\n\\\n*****************************************************************************\nm_bb_grouping_votes_by_voter = Grupeerin e-hääli valijate järgi\n\nm_vl_reading = Loen valijate nimekirju\nm_vl = Valijate nimekiri: {0}\nm_vl_type = \\u0020\\u0020Tüüp: {0}\nm_vl_skipped = \\u0020\\u0020Korralduse alusel vahele jäetud\nm_vl_total_added = \\u0020\\u0020Lisatud: {0}\nm_vl_total_removed = \\u0020\\u0020Eemaldatud: {0}\nm_vl_fictive_warning = *****************************************************************************\\n\\\n*                           !!! HOIATUS !!!                                 *\\n\\\n*                                                                           *\\n\\\n* Rakenduse sisendandmetes puuduvad valijate nimekirjad.                    *\\n\\\n* Ei saa kontrollida valijate hääleõigust valijate nimekirja alusel!        *\\n\\\n* Kasutatakse fiktiivset valija nime ''{0}''!                               *\\n\\\n*****************************************************************************\\n\nm_vl_fictive_voter_name = <Valija Nimi>\n\nm_rl_loading = E-häälte tühistusnimekirja laadimine failist ''{0}''\nm_rl_loaded = E-häälte tühistusnimekiri on laaditud\nm_rl_arg_for_cont = E-häälte tühistusnimekirja\nm_rl_checking_integrity = E-häälte tühistusnimekirja andmetervikluse kontrollimine\nm_rl_data_is_integrous = E-häälte tühistusnimekiri on terviklik\nm_rl_revoke_total = E-häälte tühistusnimekiri sisaldab andmeid {0} isiku hääle tühistamiseks\nm_rl_restore_total = E-häälte tühistusnimekiri sisaldab andmeid {0} isiku hääle ennistamiseks\nm_rl_revoke_start = Tühistusnimekirjaga määratud isikute e-häälte tühistamine\nm_rl_revoke_ballots_before = E-häälte arv enne tühistamist: {0}\nm_rl_revoke_count = Tühistatud e-häälte arv: {0}\nm_rl_revoke_ballots_after = E-häälte arv tühistamise järel: {0}\nm_rl_revoke_done = Tühistusnimekirjaga määratud isikute e-hääled on tühistatud\nm_rl_restore_start = Tühistusnimekirjaga määratud isikute e-häälte ennistamine\nm_rl_restore_ballots_before = E-häälte arv enne ennistamist: {0}\nm_rl_restore_count = Ennistatud e-häälte arv: {0}\nm_rl_restore_ballots_after = E-häälte arv ennistamise järel: {0}\nm_rl_restore_done = Tühistusnimekirjaga määratud isikute e-hääled on ennistatud\n\nm_dist_mapping_loading = Ringkonna ja omavalitsus teisenduste laadimine failist ''{0}''\nm_dist_mapping_arg_for_cont = Ringkonna ja omavalitsus teisenduste\nm_dist_mapping_loaded = Ringkonna ja omavalitsus teisendused on laaditud\n\nm_removing_recurrent_votes = Eemaldan korduvad hääled\nm_removing_invalid_ciphertexts = Kontrollin krüptogrammide korrektsust\nm_applying_revocation_lists = Rakendan tühistus-/ennistusnimekirju\nm_anonymizing_ballot_box = Anonümiseerin e-valimiskasti\n\nm_writing_ivoter_list = Väljastan e-hääletanute nimekirja\nm_writing_revocation_report = Väljastan tühistamiste ja ennistamiste aruande\nm_writing_log_n = Väljastan Log{0} failid\n\nm_stats_generating = Koostan e-valimiskasti statistikat\nm_stats_generated = E-valimiskasti statistika koostatud\nm_stats_ballot_errors = E-häälte vigu statistikaperioodis: {0}\nm_stats_valid_ballots = Arvesse läinud e-häälte arv statistikaperioodis: {0}\nm_stats_json_saved = E-valimiskasti statistika JSON-kuju salvestatud faili ''{0}''\nm_stats_csv_saved = E-valimiskasti statistika CSV-kuju salvestatud faili ''{0}''\nm_stats_diff_saved = Statistikafailide vahe salvestatud faili ''{0}''\n"
  },
  {
    "path": "common/java/translations/lang.properties",
    "content": "# Kõik keeled, mida rakendus toetab. Esimene keel nimistus on rakenduse vaikimisi keel.\n# NB! Kõigi sisestatud keelte kohta kontrollitakse kompileerimise ajal tõlgete olemasolu.\nlanguages = et\n"
  },
  {
    "path": "common/tools/go/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "common/tools/go/Makefile",
    "content": "include ../../../common/go/govar.mk\n\n.PHONY: all\nall: bin/gen\n\nbin/gen:\n\t$(GO) generate ./...\n\tenv GOBIN=$(or $(IVXV_GOBIN),$(abspath bin)) \\\n\t\t$(GO) install $(if $(DEVELOPMENT),-tags development )./...\n\n.PHONY: clean\nclean:\n\trm -rf cmd/gen/gentmpl.go\n"
  },
  {
    "path": "common/tools/go/cmd/gen/.gitignore",
    "content": "gentmpl.go\n"
  },
  {
    "path": "common/tools/go/cmd/gen/description.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nconst (\n\tLOG_DESCRIPTION_FILENAME = \"log_desc.go\"\n\tLOG_DESCRIPTION_REGEX    = `%s\\s*=\\s*\"(.*?)\"`\n)\n\ntype LogDescriptionParser interface {\n\tParse(desc ast.Expr) (string, error)\n}\n\ntype LogDescriptionString struct{}\n\nfunc (lds *LogDescriptionString) Parse(expr ast.Expr) (string, error) {\n\t// Remove quotes from a string, to prevent comments like:\n\t// \"This is quoted comment\"\n\tdesc, ok := expr.(*ast.BasicLit)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"cannot cast expr to *ast.BasicLit\")\n\t}\n\n\treturn strings.Trim(desc.Value, \"\\\"\"), nil\n}\n\ntype LogDescriptionConstExpr struct {\n\t// LogDescAbsDirPath is a directory that contains log_desc.go file\n\tLogDescAbsDirPath string\n}\n\nfunc (ldce *LogDescriptionConstExpr) Parse(expr ast.Expr) (string, error) {\n\tdesc, ok := expr.(*ast.Ident)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"cannot cast expr to *ast.Ident\")\n\t}\n\n\tf := new(os.File)\n\tdefer f.Close()\n\tvar err error\n\n\tf, err = os.Open(fmt.Sprintf(\"%s/%s\", filepath.Dir(ldce.LogDescAbsDirPath), LOG_DESCRIPTION_FILENAME))\n\tdefer f.Close()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read a local %s file: %v\\n\", LOG_DESCRIPTION_FILENAME, err)\n\t}\n\n\tscanner := bufio.NewScanner(f)\n\n\t// Read log_desc.go file line by line until EOF or any non-EOF error encounter\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tpattern := fmt.Sprintf(LOG_DESCRIPTION_REGEX, desc.Name)\n\t\tmatch := regexp.MustCompile(pattern).FindStringSubmatch(strings.TrimSpace(line))\n\t\tif len(match) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn match[1], nil\n\t}\n\n\t// Any errors encountered during Scan(), except EOF, will be registered in Err()\n\tif err := scanner.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read a line from a local %s file: %v\\n\", LOG_DESCRIPTION_FILENAME, err)\n\t}\n\n\treturn \"\", fmt.Errorf(\"no log description found in a local %s file for a %s log record\\n\", LOG_DESCRIPTION_FILENAME, desc.Name)\n}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/gen.tmpl",
    "content": "{{/* vim: set ft=gotexttmpl: */}}\npackage {{.Package.Name}}\n\nimport \"fmt\"\n\n{{range .Literals}}\n// BEGIN {{.Name}}\n\n// auto-generated from {{base .Pos.String}}.\n// ivxv.ee/{{$.Package.ImportPath}}.{{.Name}}\n// {{.Comment}}\ntype {{.Name}} struct {\n\t{{range .Fields}}\n\t{{.}} {{if eq . \"Err\"}}error{{else if eq . \"Description\"}}string `+\"`json:\\\"-\\\"`\"+`{{else}}interface{}{{end}}\n\t{{- end}}\n}\n// END {{.Name}}\n\n// EntryID implements the ivxv.ee/log.Entry interface.\nfunc (x {{.Name}}) EntryID() string { return \"ivxv.ee/{{$.Package.ImportPath}}.{{.Name}}\" }\n\n{{$name := .Name}}\n{{range .Fields}}{{if eq . \"Err\"}}\n// Cause implements the ivxv.ee/errors.Causer interface.\nfunc (x {{$name}}) Cause() error { return x.Err }\n{{end}}{{end}}\n\nfunc (x {{.Name}}) Error() string {\n\treturn fmt.Sprint(x.EntryID(), \"{\",\n\t\t{{- range $i, $e := .Fields -}}\n\t\t{{- if gt $i 0}}\", \",{{end}}\n\t\t{{if ne . \"Description\"}}\"{{.}}:\", x.{{.}},{{end}}\n\t\t{{- end -}}\n\t\t\"}\")\n}\n{{end}}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/gen_test.go",
    "content": "package main_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nconst bin = \"./testgen\"\n\nvar gobin = \"go\"\n\nfunc TestMain(m *testing.M) {\n\t// Prefer go binary from environment.\n\tif goenv, ok := os.LookupEnv(\"GO\"); ok {\n\t\tgobin = goenv\n\t}\n\n\t// Generate the template source file, if not already done.\n\tout, err := exec.Command(gobin, \"generate\").CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, string(out))\n\t\tfmt.Fprintln(os.Stderr, \"go generate failed:\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Build the testing binary.\n\tout, err = exec.Command(gobin, \"build\", \"-o\", bin).CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, string(out))\n\t\tfmt.Fprintf(os.Stderr, \"building %s failed: %v\\n\", bin, err)\n\t\tos.Exit(1)\n\t}\n\n\t// Run tests.\n\tos.Unsetenv(\"GOPATH\") // Clear for testing.\n\tcode := m.Run()\n\n\t// Remove the test binary.\n\tos.Remove(bin)\n\tos.Exit(code)\n}\n\n// res is the result of running the test binary.\ntype res struct {\n\tt   *testing.T\n\tout []byte\n}\n\n// testrun runs the test binary and assserts that it succeeds.\nfunc testrun(t *testing.T, args ...string) *res {\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tt.Logf(string(out))\n\t\tt.Fatal(bin, args, \"failed unexpectedly:\", err)\n\t}\n\treturn &res{t, out}\n}\n\n// testfail runs the test binary and assserts that it fails.\nfunc testfail(t *testing.T, args ...string) *res {\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err == nil {\n\t\tt.Logf(string(out))\n\t\tt.Fatal(bin, args, \"succeeded unexpectedly\")\n\t}\n\treturn &res{t, out}\n}\n\n// expect checks that the result contains the pattern, reporting an error\n// otherwise.\nfunc (r *res) expect(pattern string) {\n\tif !grep(pattern, r.out) {\n\t\tr.t.Error(\"output does not contain\", pattern)\n\t}\n}\n\n// expectnot checks that the result does NOT contain the pattern, reporting an\n// error otherwise.\nfunc (r *res) expectnot(pattern string) {\n\tif grep(pattern, r.out) {\n\t\tr.t.Error(\"output contains\", pattern)\n\t}\n}\n\n// grep checks if b contains the pattern. Matching is performed per-line.\nfunc grep(pattern string, b []byte) bool {\n\tre := regexp.MustCompile(pattern)\n\tfor _, line := range bytes.Split(b, []byte{'\\n'}) {\n\t\tif re.Match(line) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc TestEmpty(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/empty\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^generating testdata/empty/gen_types(_test)\\.go$`)\n}\n\nfunc TestSingle(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/single\")\n\tdefer os.Remove(\"testdata/single/gen_types.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Single{Field Field2 Description} at`)\n\tr.expect(`^generating testdata/single/gen_types\\.go$`)\n\tr.expectnot(`^generating testdata/single/gen_types_test\\.go$`)\n\n\t// Ensure the generated code compiles.\n\tout, err := exec.Command(gobin, \"build\", \"./testdata/single\").CombinedOutput()\n\tif err != nil {\n\t\tt.Logf(string(out))\n\t\tt.Error(\"building ./testdata/single failed:\", err)\n\t}\n}\n\nfunc TestWithTests(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/withtests\")\n\tdefer os.Remove(\"testdata/withtests/gen_types.go\")\n\tdefer os.Remove(\"testdata/withtests/gen_types_test.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Main{Field Description} at`)\n\tr.expect(`^found Test{Field} at`)\n\tr.expect(`^generating testdata/withtests/gen_types\\.go$`)\n\tr.expect(`^generating testdata/withtests/gen_types_test\\.go$`)\n}\n\nfunc TestDeclared(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/declared\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^found Declared{Field Description} at`)\n\tr.expectnot(`^generating testdata/declared/gen_types(_test)\\.go$`)\n}\n\nfunc TestLocal(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/local\")\n\tdefer os.Remove(\"testdata/local/gen_types.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^found InnerLocal{Field} at`)\n\tr.expect(`^found OuterLocal{Field Description} at`)\n\tr.expect(`^generating testdata/local/gen_types\\.go$`)\n\tr.expectnot(`^generating testdata/local/gen_types_test\\.go$`)\n}\n\nfunc TestDuplicate(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/duplicate\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Duplicate{Field Description} at`)\n\tr.expect(`^error: .* duplicate Duplicate`)\n\tr.expectnot(`^generating testdata/duplicate/gen_types(_test)\\.go$`)\n}\n\nfunc TestNoKeys(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/nokeys\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^error: .* NoKeys must have key:value fields$`)\n\tr.expectnot(`^generating testdata/nokeys/gen_types(_test)\\.go$`)\n}\n\nfunc TestUnexportedKeys(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/unexportedkeys\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^error: .* UnexportedKeys must have exported identifier keys$`)\n\tr.expectnot(`^generating testdata/unexportedkeys/gen_types(_test)\\.go$`)\n}\n\nfunc TestNoDescription(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/nodescription\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(\"^error: .* undeclared struct literal NoDescription log record should have a 'Description:' field\")\n}\n\nfunc TestNoDescriptionEmpty(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/nodescriptionempty\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(\"^error: .* undeclared struct literal NoDescriptionEmpty log record should have a 'Description:' field\")\n}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/generate.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/build\"\n\t\"go/format\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n// header is added to the gen.tmpl template to identify files created by gen.\nconst header = \"// Generated by ivxv.ee/common/collector/log/cmd/bin/gen. DO NOT EDIT!\\n\\n\"\n\n// funcMap contains the helper functions used in gen.tmpl.\nvar funcMap = template.FuncMap{\n\t\"base\": filepath.Base,\n}\n\n// Generate a Go source file which contains the template in gen.tmpl and parses\n// it into the variable gentmpl. The variable name is given as an argument, so\n// that it can easily be changed here, where it will also be used.\n//go:generate ./gentmpl.sh gentmpl\n\n// generate generates type declarations for the struct literals in pkg.\nfunc generate(pkg *build.Package, literals []literal) (ok bool) {\n\tok = true\n\tmain, test := split(literals)\n\tif len(main) > 0 {\n\t\tok = ok && genfile(pkg, *namep+\".go\", main)\n\t}\n\tif len(test) > 0 {\n\t\tok = ok && genfile(pkg, *namep+\"_test.go\", test)\n\t}\n\treturn\n}\n\nfunc genfile(pkg *build.Package, name string, literals []literal) bool {\n\tpath := rel(filepath.Join(pkg.Dir, name))\n\tfp, err := open(path)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", err)\n\t\treturn false\n\t}\n\tdefer fp.Close()\n\n\tif *vp {\n\t\tfmt.Println(\"generating\", path)\n\t}\n\n\tdata := struct {\n\t\tPackage  *build.Package\n\t\tLiterals []literal\n\t}{\n\t\tpkg,\n\t\tliterals,\n\t}\n\n\t// Write to both a memory buffer and the result file: this way have the\n\t// result in memory for formatting, but also any Go formatting errors\n\t// will refer to an actually existing file, greatly reducing debugging\n\t// complexity.\n\tvar buf bytes.Buffer\n\ttee := io.MultiWriter(&buf, fp)\n\tif err = gentmpl.Execute(tee, data); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: template execution:\", err)\n\t\treturn false\n\t}\n\n\tformatted, err := format.Source(buf.Bytes())\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: format generated code: %s:%v\\n\", path, err)\n\t\treturn false\n\t}\n\n\t// Rewind, now writing formatted code.\n\tif _, err = fp.Seek(0, io.SeekStart); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: rewind\", path, \"for formatting:\", err)\n\t\treturn false\n\t}\n\n\tif _, err = fp.Write(formatted); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: write generated code %s: %v\\n\", path, err)\n\t\treturn false\n\t}\n\n\t// Truncate fp in case it already contained a larger file when opened\n\t// or if the formatted code is shorter that unformatted.\n\tif err = fp.Truncate(int64(len(formatted))); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"truncate %s to %d: %v\\n\", path, len(formatted), err)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// split partitions the literals into two sets: ones from regular Go source\n// files and ones from Go test files.\nfunc split(literals []literal) (main []literal, test []literal) {\n\tfor _, l := range literals {\n\t\tif strings.HasSuffix(l.Pos.Filename, \"_test.go\") {\n\t\t\ttest = append(test, l)\n\t\t} else {\n\t\t\tmain = append(main, l)\n\t\t}\n\t}\n\treturn\n}\n\n// open attempts to create the path. If a file already exists there, then it\n// will check if it is safe to overwrite.\nfunc open(path string) (fp *os.File, err error) {\n\t// Attempt to open the path for reading and writing.\n\tvar created bool\nloop:\n\tfp, err = os.OpenFile(path, os.O_RDWR, 0)\n\tif err != nil {\n\t\t// If it does not exist, attempt to create it, otherwise fail.\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn\n\t\t}\n\t\tfp, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)\n\t\tif err != nil {\n\t\t\t// If it now does exist, retry from beginning.\n\t\t\tif os.IsExist(err) {\n\t\t\t\tgoto loop\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tcreated = true\n\t}\n\n\t// If we did not create the file, ensure that it contains our header,\n\t// so it is safe to overwrite, and rewind.\n\tif !created {\n\t\thbuf := make([]byte, len(header))\n\t\t_, err := io.ReadFull(fp, hbuf)\n\t\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\t\treturn nil, fmt.Errorf(\"read %s header: %v\", path, err)\n\t\t}\n\n\t\tif bytes.Compare(hbuf, []byte(header)) != 0 {\n\t\t\treturn nil, fmt.Errorf(\"%s already exists and is not generated, \"+\n\t\t\t\t\"not overwriting\", path)\n\t\t}\n\t\tif _, err = fp.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"rewind %s: %v\", path, err)\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/gentmpl.sh",
    "content": "#!/bin/sh -eu\n\n# gentmpl.sh: Generate a Go source file with a template file inlined. This way\n# we can use template-specific syntax-highlighting when editing the template,\n# yet still embed it into the final binary.\n\ntmpl=\"gen.tmpl\"\ngo=\"gentmpl.go\" # NB! if \"gen.tmpl.go\", then go clean will delete gen.tmpl.\nvar=\"$1\"\n\n# Use a variable for the header of the vim modeline used in the generated file\n# so that vim will not pick it up and apply to this file.\nvimhdr=\"vim\"\n\ncat > \"$go\" <<HERE\n// Generated by gentmpl.sh. DO NOT EDIT!\n\npackage main\n\nimport \"text/template\"\n\nvar $var = template.Must(template.New(\"$var\").Funcs(funcMap).Parse(header + \\`\nHERE\n\ncat \"$tmpl\" >> \"$go\"\ncat >> \"$go\" <<HERE\n\\`))\n\n// Override the modeline in the template.\n// $vimhdr: set ft=go:\nHERE\n"
  },
  {
    "path": "common/tools/go/cmd/gen/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"go/build\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n)\n\nfunc usage() {\n\tfmt.Fprintln(os.Stderr, \"usage:\", os.Args[0], `[options] [packages...]\n\ngen searches Go packages for source files with struct literals that have\nundeclared exported types from the same package and attempts to generate the\nstruct declaration. Declarations are put in output files named gen_types.go and\ngen_types_test.go in the same package, configurable via --name.\n\nPackages are either absolute or relative (beginning with a . or .. element)\npaths to package directories, or names of packages that will be searched for in\nGOPATH. If no packages are provided, then the package in the current directory\nis processed. If a package contains one or more \"...\" wildcards, then it will\nemulate the behavior of the go application (see 'go help packages').\n\noptions:`)\n\tflag.PrintDefaults()\n}\n\nvar (\n\tnamep = flag.String(\"name\", \"gen_types\", \"base name of output files\")\n\tvp    = flag.Bool(\"v\", false, \"verbose output\")\n\tbase  = flag.String(\"base\", \"\", \"project root\")\n\n\tpwd string\n)\n\nfunc main() {\n\tflag.Usage = usage\n\tflag.Parse()\n\tpaths := flag.Args()\n\tif len(paths) == 0 {\n\t\t// Default to current directory.\n\t\tpaths = []string{\".\"}\n\t}\n\n\tvar err error\n\tpwd, err = os.Getwd()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: cannot get current working directory:\", err)\n\t\tos.Exit(1)\n\t}\n\tpkgs, ok := resolve(*base, paths)\n\tif !ok {\n\t\t// Errors have already been logged by resolve.\n\t\tos.Exit(1)\n\t}\n\tif len(pkgs) == 0 {\n\t\t// Wildcards matched no packages: this is not necessarily an\n\t\t// error so just return.\n\t\treturn\n\t}\n\n\t// Simply start a parse-walk-generate goroutine for each package: there\n\t// will not be so many source files that we need to set up a worker\n\t// pool with a pipeline.\n\tvar wg sync.WaitGroup\n\tvar lock sync.Mutex // Used to synchronize access to ok.\n\tfor _, pkg := range pkgs {\n\t\tif *vp {\n\t\t\tfmt.Println(\"checking\", pkg.ImportPath)\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(pkg *build.Package) {\n\t\t\tpok := pwg(pkg)\n\t\t\tlock.Lock()\n\t\t\tok = ok && pok\n\t\t\tlock.Unlock()\n\t\t\twg.Done()\n\t\t}(pkg)\n\t}\n\twg.Wait()\n\tif !ok {\n\t\t// Errors have already been logged by pwg.\n\t\tos.Exit(1)\n\t}\n}\n\n// pwg parses the package AST, walks it looking for struct literals that need\n// generation, and generates the missing type declarations.\nfunc pwg(pkg *build.Package) (ok bool) {\n\tfset, parsed, ok := parse(pkg)\n\tif !ok {\n\t\treturn\n\t}\n\tliterals, ok := walk(pkg.Directives, pkg.ImportPath, fset, parsed)\n\tif !ok {\n\t\treturn\n\t}\n\tif len(literals) > 0 {\n\t\tok = generate(pkg, literals)\n\t}\n\treturn\n}\n\n// rel attempts to convert the path to a relative path from the current working\n// directory.\nfunc rel(path string) string {\n\tif r, err := filepath.Rel(pwd, path); err == nil {\n\t\treturn r\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/resolve.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"go/build\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// resolve resolves package import paths into packages. It emulates the \"...\"\n// wilcard behavior of matchPackages in cmd/go/main.go.\nfunc resolve(base string, paths []string) (pkgs []*build.Package, ok bool) {\n\tok = true\n\tfor _, p := range paths {\n\t\t// Check if local import and if so, make absolute so that\n\t\t// build.Import can determine the package import path.\n\t\tlocal := filepath.IsAbs(p) || build.IsLocalImport(filepath.ToSlash(p))\n\t\tpath := p\n\t\tif local && !filepath.IsAbs(p) {\n\t\t\tpath = filepath.Join(pwd, p)\n\t\t}\n\t\t// Search for packages matching the wildcards.\n\t\tif strings.Contains(p, \"...\") {\n\t\t\t// List of source directories to search.\n\t\t\tsrcdirs := []string{\"\"}\n\t\t\tif !local {\n\t\t\t\tsrcdirs = build.Default.SrcDirs()\n\t\t\t\tpath = filepath.FromSlash(p)\n\t\t\t}\n\t\t\tbefore := len(pkgs)\n\t\t\tfor _, src := range srcdirs {\n\t\t\t\texpanded, expok := expand(path, src, base)\n\t\t\t\tpkgs = append(pkgs, expanded...)\n\t\t\t\tok = ok && expok\n\t\t\t}\n\t\t\tif len(pkgs) == before {\n\t\t\t\tfmt.Fprintln(os.Stderr, \"warning: no packages match\", p)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\t// Attempt to import non-wildcard package.\n\t\timp, src := p, \"\"\n\t\tif local {\n\t\t\timp, src = \".\", path // Import package \".\" from abs(p).\n\t\t}\n\t\tpkg, err := build.Import(imp, src, 0)\n\t\tpkg.ImportPath = strings.TrimPrefix(imp, base)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"error: package %s: %v\\n\", p, err)\n\t\t\tok = false\n\t\t\tcontinue\n\t\t}\n\t\tpkgs = append(pkgs, pkg)\n\t}\n\treturn\n}\n\n// expand expands \"...\" wildcards in the import path p into packages rooted at\n// the given directory.\nfunc expand(p, src, base string) (expanded []*build.Package, ok bool) {\n\tok = true\n\n\t// Clean src and p, and append the non-wildcard prefix of p to src for\n\t// the walking root directory.\n\tif len(src) > 0 {\n\t\tsrc = filepath.Clean(src) + string(filepath.Separator)\n\t}\n\tp = filepath.Clean(p)\n\tdir := src + p[:strings.Index(p, \"...\")]\n\n\t// Compile a regular expression from the slash-separated path with\n\t// \"...\" wildcards.\n\tre := regexp.QuoteMeta(filepath.ToSlash(p))\n\tre = strings.ReplaceAll(re, `\\.\\.\\.`, \".*\")\n\tif strings.HasSuffix(re, \"/.*\") {\n\t\t// Special case: \"foo\" matches \"foo/...\".\n\t\tre = re[:len(re)-len(\"/.*\")] + \"(/.*)?\"\n\t}\n\tmatch := regexp.MustCompile(\"^\" + re + \"$\").MatchString\n\n\t// Walk the directory looking for paths that match.\n\t//\n\t//nolint:errcheck // Only returns nil.\n\tfilepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {\n\t\t// Ignore faulty or non-directory files.\n\t\tif err != nil || !fi.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Ignore \"testdata\" and directories that start with a dot or underscore.\n\t\tname := fi.Name()\n\t\tif strings.HasPrefix(name, \".\") || strings.HasPrefix(name, \"_\") || name == \"testdata\" {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\t// Convert the path to the format expected by match: remove the\n\t\t// src prefix and convert to slash-separated path.\n\t\timp := filepath.ToSlash(path[len(src):])\n\n\t\t// Check if the path matches the pattern and attempt to import\n\t\t// the package, ignoring non-Go directories that match.\n\t\tif match(imp) {\n\t\t\tpkg, err := build.ImportDir(path, 0)\n\n\t\t\tpkg.ImportPath = strings.TrimPrefix(path, base)\n\t\t\tif err != nil {\n\t\t\t\tif _, nogo := err.(*build.NoGoError); nogo {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tfmt.Fprintf(os.Stderr, \"error: matched package %s: %v\\n\", rel(path), err)\n\t\t\t\tok = false\n\t\t\t}\n\t\t\texpanded = append(expanded, pkg)\n\t\t}\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/declared/declared.go",
    "content": "// Package declared tests that gen does not match already declared types.\npackage declared\n\ntype Declared struct {\n\tField int\n}\n\nvar x = Declared{Field: 0, Description: \"Declared\"}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/duplicate/duplicate.go",
    "content": "// Package duplicate tests that gen reports duplicate literals.\npackage duplicate\n\nvar (\n\tx = Duplicate{Field: 0, Description: \"Duplicate\"}\n\ty = Duplicate{Field: 0, Description: \"Duplicate\"}\n)\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/empty/empty.go",
    "content": "// Package empty tests that gen does not create the output file if no literals\n// were found.\npackage empty\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/local/local.go",
    "content": "// Package local tests that gen does not match locally declared types.\npackage local\n\nfunc x() {\n\ttype InnerLocal struct {\n\t\tField int\n\t}\n\n\ttype OuterLocal struct {\n\t\tField int\n\t}\n\n\t_ = InnerLocal{Field: 0}\n}\n\nvar _ = OuterLocal{Field: 0, Description: \"OuterLocal\"}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/nodescription/nodescription.go",
    "content": "// Package no_description tests that gen fails if literal doesn't contain 'Description' field.\npackage nodescription\n\nvar x = NoDescription{Field: 0}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/nodescriptionempty/nodescriptionempty.go",
    "content": "// Package nodescriptionempty tests that gen fails if empty literal doesn't contain 'Description' field.\npackage nodescriptionempty\n\nvar x = NoDescriptionEmpty{}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/nokeys/nokeys.go",
    "content": "// Package nokeys tests that gen reports literals with non-key:value fields.\npackage nokeys\n\nvar x = NoKeys{0, Description: \"NoKeys\"}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/single/single.go",
    "content": "// Package single is a simple smoke test that checks if gen works.\npackage single\n\n// This structure is created to achieve\n// the following purpose\nvar x = Single{Field: 0, Field2: 0, Description: \"Single\"}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/unexportedkeys/unexportedkeys.go",
    "content": "// Package unexportedkeys tests that gen reports literals with unexported field\n// identifiers.\npackage unexportedkeys\n\nvar x = UnexportedKeys{field: 0}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/withtests/withtests.go",
    "content": "// Package withtests tests that literals found in test files are written to a\n// test file.\npackage withtests\n\nvar x = Main{Field: 0, Description: \"Main\"}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/testdata/withtests/withtests_test.go",
    "content": "package withtests\n\nvar x = Test{Field: 0}\n"
  },
  {
    "path": "common/tools/go/cmd/gen/walk.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/build\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// parse parses the AST of the package.\nfunc parse(pkg *build.Package) (fset *token.FileSet, parsed *ast.Package, ok bool) {\n\tfset = token.NewFileSet()\n\tpkgmap, err := parser.ParseDir(fset, pkg.Dir, nil, parser.ParseComments)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: could not parse %s: %v\\n\", pkg.ImportPath, err)\n\t\treturn\n\t}\n\tparsed, ok = pkgmap[pkg.Name]\n\tif !ok {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", rel(pkg.Dir), \"does not contain package\", pkg.Name)\n\t}\n\treturn\n}\n\ntype literal struct {\n\tPos     token.Position\n\tName    string\n\tFields  []string\n\tComment string\n}\n\nfunc (l literal) String() string {\n\treturn fmt.Sprintf(\"%s{%s}\", l.Name, strings.Join(l.Fields, \" \"))\n}\n\n// walk walks the package AST looking for struct literals with undeclared\n// exported local package types and key:value fields.\nfunc walk(goTags []build.Directive, path string, fset *token.FileSet, pkg *ast.Package) (literals []literal, ok bool) {\n\tscope := pkg.Scope\n\tif scope == nil {\n\t\t// Collect all file scopes.\n\t\tscope = ast.NewScope(nil)\n\t\tfor name, file := range pkg.Files {\n\t\t\t// Skip the scope of files that are generated by us.\n\t\t\t// Otherwise when a new literal is added, the old ones\n\t\t\t// generated by us will no longer be included.\n\t\t\tbase := filepath.Base(name)\n\t\t\tif base == *namep+\".go\" || base == *namep+\"_test.go\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, obj := range file.Scope.Objects {\n\t\t\t\t// Do not care if the scope already contained\n\t\t\t\t// an object with this name: it still means\n\t\t\t\t// that the name is declared.\n\t\t\t\tscope.Insert(obj)\n\t\t\t}\n\t\t}\n\t}\n\n\tok = true\n\tseen := make(map[string]token.Position)\n\tvar blocklvl int\n\tvar inspector func(ast.Node) bool // Declare for recursion.\n\tinspector = func(node ast.Node) (recurse bool) {\n\t\tvar descriptionPresent bool\n\t\trecurse = true\n\t\tif node == nil {\n\t\t\treturn\n\t\t}\n\n\t\t// For block statements, create a new block scope, increase\n\t\t// block level, and inspect statements in the block ourselves.\n\t\tif block, match := node.(*ast.BlockStmt); match {\n\t\t\tscope = ast.NewScope(scope)\n\t\t\tblocklvl++\n\t\t\tfor _, stmt := range block.List {\n\t\t\t\tast.Inspect(stmt, inspector)\n\t\t\t}\n\t\t\tblocklvl--\n\t\t\tscope = scope.Outer\n\t\t\treturn false // We already recursed.\n\t\t}\n\n\t\t// If we are inside a block, add type declarations to the scope.\n\t\tif spec, match := node.(*ast.TypeSpec); match && blocklvl > 0 {\n\t\t\tscope.Insert(ast.NewObj(ast.Typ, spec.Name.Name))\n\t\t\treturn\n\t\t}\n\n\t\t// Return as soon as a condition is not met: avoid deep nesting\n\t\t// of control blocks.\n\t\tcomplit, match := node.(*ast.CompositeLit) // Composite literal.\n\t\tif !match {\n\t\t\treturn\n\t\t}\n\t\tident, match := complit.Type.(*ast.Ident) // With simple identifier type.\n\t\tif !match {\n\t\t\treturn\n\t\t}\n\t\tif !ident.IsExported() { // That is exported.\n\t\t\treturn\n\t\t}\n\t\tfor s := scope; s != nil; s = s.Outer { // And does not exist in the scope.\n\t\t\tif s.Lookup(ident.Name) != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// Check if we have already seen this name before.\n\t\tname := ident.Name\n\t\tpos := fset.Position(ident.NamePos)\n\t\tif prev, dup := seen[ident.Name]; dup {\n\t\t\tfmt.Fprintf(os.Stderr, \"error: %s: duplicate %s, \"+\n\t\t\t\t\"previously seen at %s\\n\", relpos(pos), name, prev)\n\t\t\tok = false\n\t\t\treturn\n\t\t}\n\t\tseen[name] = relpos(pos)\n\n\t\t// Check the fields of the literal and add it to the result.\n\t\tl := literal{Pos: pos, Name: name}\n\n\t\tfilename := pos.Filename\n\n\t\t// Simulate do while loop, since log record may have zero amount of fields,\n\t\t// which is only allowed in '//go:development' tagged and '_test.go' files\n\t\tfor i := -1; i < len(complit.Elts); i++ {\n\t\t\tskipping := false\n\n\t\t\t// Skip literal from test file\n\t\t\tif isGoTestFile(filename) {\n\t\t\t\tskipping = true\n\t\t\t}\n\n\t\t\t// Skip literal from development file\n\t\t\tif isGoDevelopmentFile(goTags, filename) {\n\t\t\t\tskipping = true\n\t\t\t}\n\n\t\t\tif i == -1 {\n\t\t\t\t// Empty literal, e.g. EmptyLiteral{}\n\t\t\t\tif len(complit.Elts) == 0 {\n\t\t\t\t\tif !skipping {\n\t\t\t\t\t\tlogRecordWithoutDescription(pos, name)\n\t\t\t\t\t\tok = false\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ti++\n\t\t\t}\n\n\t\t\tel := complit.Elts[i]\n\n\t\t\tfilename = relpos(fset.Position(el.Pos())).Filename\n\n\t\t\tkv, match := el.(*ast.KeyValueExpr)\n\t\t\tif !match {\n\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\"must have key:value fields\"})\n\t\t\t\tok = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkey, match := kv.Key.(*ast.Ident)\n\t\t\tif !match || !key.IsExported() {\n\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\"must have exported identifier keys\"})\n\t\t\t\tok = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tl.Fields = append(l.Fields, key.Name)\n\n\t\t\tif skipping {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Each log record should have a 'Description' field\n\t\t\tif key.Name == \"Description\" {\n\t\t\t\tvar logDescParser LogDescriptionParser\n\t\t\t\tdescriptionPresent = true\n\t\t\t\tswitch kv.Value.(type) {\n\t\t\t\tcase *ast.BasicLit:\n\t\t\t\t\tlogDescParser = new(LogDescriptionString)\n\t\t\t\tcase *ast.Ident:\n\t\t\t\t\tlogDescConstExpr := &LogDescriptionConstExpr{\n\t\t\t\t\t\tLogDescAbsDirPath: pos.Filename,\n\t\t\t\t\t}\n\t\t\t\t\tlogDescParser = logDescConstExpr\n\t\t\t\tdefault:\n\t\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\t\"must have 'Description:' field to be of either const expression or a string type\"})\n\t\t\t\t\tok = false\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcomment, err := logDescParser.Parse(kv.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\terr.Error()})\n\t\t\t\t\tok = false\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tl.Comment = comment\n\t\t\t}\n\n\t\t\t// There should be 'Description:' field in a log record\n\t\t\tif i == len(complit.Elts)-1 && !descriptionPresent {\n\t\t\t\tlogRecordWithoutDescription(relpos(fset.Position(el.Pos())), name)\n\t\t\t\tok = false\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tliterals = append(literals, l)\n\n\t\tif *vp {\n\t\t\tvar pre string\n\t\t\tif path != \".\" {\n\t\t\t\tpre = path + \".\"\n\t\t\t}\n\t\t\tfmt.Printf(\"found %s%s at %s\\n\", pre, l, relpos(l.Pos))\n\t\t}\n\t\treturn\n\t}\n\tast.Inspect(pkg, inspector)\n\treturn\n}\n\n// relpos sets the filename in p to rel(p).\nfunc relpos(p token.Position) token.Position {\n\tp.Filename = rel(p.Filename)\n\treturn p\n}\n\ntype fieldError struct {\n\tpos  token.Position\n\tname string\n\terr  string\n}\n\nfunc (e fieldError) Error() string {\n\treturn fmt.Sprintf(\"error: %s: undeclared struct literal %s %s\", e.pos, e.name, e.err)\n}\n\nfunc isGoDevelopmentFile(goTags []build.Directive, filename string) bool {\n\tfor _, goTag := range goTags {\n\t\tif strings.Contains(goTag.Text, \"development\") {\n\t\t\tif strings.Contains(goTag.Pos.String(), filename) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isGoTestFile(filename string) bool {\n\treturn strings.Contains(filename, \"_test.go\")\n}\n\nfunc logRecordWithoutDescription(pos token.Position, name string) {\n\tfmt.Fprintln(os.Stderr, fieldError{\n\t\tpos,\n\t\tname,\n\t\t\"log record should have a 'Description:' field\",\n\t})\n}\n"
  },
  {
    "path": "common/tools/go/go.mod",
    "content": "module ivxv.ee/common/tools/go\n\ngo 1.23\n"
  },
  {
    "path": "common/tools/pylintrc",
    "content": "# IVXV Internet voting framework\n# Pylint resource file\n\n[BASIC]\n# Good variable names which should always be accepted, separated by a comma\ngood-names=_,cn,db,i,fp,log\n"
  },
  {
    "path": "common/tools/update_project_version.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nUpdate project version in subcomponents.\n\nRelease string is defined in debian/changelog.\nVersion string is release string without suffix (e.g. ~dev).\n\"\"\"\n\nimport os\nimport re\n\nfrom debian import debfile\n\n\ndef update_version(filename, pattern, repl):\n    \"\"\"Update version data in file.\"\"\"\n    file_content = []\n    updated = None\n    pattern = re.compile(pattern)\n    with open(filename) as fp:\n        for line in fp:\n            if pattern.match(line):\n                oldline = line\n                line = pattern.sub(repl, line)\n                updated = line != oldline\n            file_content.append(line)\n    assert updated is not None\n\n    if updated:\n        with open(filename, 'w') as fp:\n            fp.write(''.join(file_content))\n\n\ndef main():\n    \"\"\"Main routine.\"\"\"\n    # detect debian version\n    with open('debian/changelog') as fp:\n        debfile_data = debfile.Changelog(fp.read())\n        release = str(debfile_data.version)\n\n    version = release.split('~')[0]\n    py_version = release\n    # fix version for Python package (PEP 440)\n    if '~' in py_version:\n        py_version = py_version.replace('~', '.')\n        try:\n            int(py_version[-1])\n        except ValueError:\n            py_version += '0'\n    debfile_year = int(debfile_data.date.split(' ')[3])\n    copyright = '2016-{}, Cybernetica AS'.format(debfile_year)\n\n    update_version(\"setup.py\",\n                   r\"( +version=').+(',.*)\", \"\\\\g<1>%s\\\\2\" % py_version)\n    update_version(\"collector-admin/ivxv_admin/__init__.py\",\n                   r\"(__version__ = ').+(')\", \"\\\\g<1>%s\\\\2\" % py_version)\n    update_version(\"collector-admin/ivxv_admin/__init__.py\",\n                   r\"(DEB_PKG_VERSION = ').+(')\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version(\"common/java/common-build.gradle\",\n                   \"^(version ').+('.*)\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version(\"tests/features/steps/__init__.py\",\n                   r\"(DEB_VERSION = ').+(')\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version(\"collector-admin/site/ivxv/about.html\",\n                   r'(.+id=\"version\">).+(<)', '\\\\g<1>%s\\\\2' % release)\n    update_version(\"Documentation/documents.py\",\n                   r\"^(release *= *').+('.*)\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version(\"Documentation/documents.py\",\n                   r\"^(copyright *= *').+('.*)\", \"\\\\g<1>%s\\\\2\" % copyright)\n    update_version(\"Documentation/et/seadistuste_koostejuhend/mixnet.rst\",\n                   r\"(.*ivxv-verificatum-).+(-runner.zip)\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version('tests/templates/test-report/conf.py',\n                   \"^(version *= *').+('.*)\", \"\\\\g<1>%s\\\\2\" % version)\n    update_version('tests/templates/test-report/conf.py',\n                   \"^(release *= *').+('.*)\", \"\\\\g<1>%s\\\\2\" % release)\n    update_version(\"Documentation/et/audiitor/audit.rst\",\n                   r\"^(.*(auditor|key|processor)-).+(.zip)\", \"\\\\g<1>%s\\\\3\" % release)\n\n\nif __name__ == '__main__':\n    exit(main())\n"
  },
  {
    "path": "common/tools/win32/entropy/Aggregator.cpp",
    "content": "#include \"Aggregator.h\"\n\n#define OUTBYTES 20\n#define OUTBITS (OUTBYTES * 8)\n#define MOUSE_ENTROPY_PER_SAMPLE 1.5\n#define KEY_ENTROPY_PER_SAMPLE 1\n\nHCRYPTPROV Aggregator::hProvider = 0;\n\nBOOL Aggregator::initialize()\n{\n    return CryptAcquireContext(&hProvider, 0, MS_DEF_PROV, PROV_RSA_FULL,\n                               CRYPT_VERIFYCONTEXT);\n}\n\nint Aggregator::outbytes()\n{\n    return OUTBYTES;\n}\n\nvoid Aggregator::finalize()\n{\n    if (hProvider) {\n        CryptReleaseContext(hProvider, 0);\n    }\n}\n\n\nAggregator::Aggregator()\n{\n    dEntropy = 0.0;\n    cbRequested = OUTBITS;\n    ptLastPos.x = 0;\n    ptLastPos.y = 0;\n    bLastScanCode = 0;\n    pbHashData = NULL;\n    dwLastTime = 0;\n    hHash = 0;\n}\n\nDWORD Aggregator::requested() const\n{\n    return cbRequested;\n}\n\ndouble Aggregator::entropy() const\n{\n    return dEntropy;\n}\n\nBOOL Aggregator::init()\n{\n    BOOL ret = false;\n    DWORD dwByteCount = sizeof(DWORD);\n    DWORD cbHashData = 0;\n    HCRYPTHASH tmpHash = 0;\n    BYTE* tmpHashData = 0;\n\n    if (!GetCursorPos(&ptLastPos)) {\n        goto error;\n    }\n\n    if (!CryptCreateHash(hProvider, CALG_SHA1, 0, 0, &tmpHash)) {\n        goto error;\n    }\n\n    if (!CryptGetHashParam(tmpHash, HP_HASHSIZE, (BYTE*)&cbHashData,\n                           &dwByteCount, 0)) {\n        goto error;\n    }\n\n    if (OUTBYTES > cbHashData) {\n        goto error;\n    }\n\n    tmpHashData = (BYTE*)LocalAlloc(LMEM_FIXED, cbHashData);\n\n    if (tmpHashData == NULL) {\n        goto error;\n    }\n\n    hHash = tmpHash;\n    tmpHash = 0;\n    pbHashData = tmpHashData;\n    tmpHashData = 0;\n    ret = true;\nerror:\n\n    if (tmpHashData) {\n        LocalFree(tmpHashData);\n    }\n\n    if (tmpHash) {\n        CryptDestroyHash(tmpHash);\n    }\n\n    return ret;\n}\n\n\nAggregator::~Aggregator()\n{\n    if (pbHashData) {\n        LocalFree(pbHashData);\n        pbHashData = NULL;\n    }\n\n    if (hHash) {\n        CryptDestroyHash(hHash);\n        hHash = 0;\n    }\n}\n\nBOOL Aggregator::enoughEntropy()\n{\n    return (dEntropy >= cbRequested);\n}\n\nBOOL Aggregator::prepareSlice()\n{\n    return CryptGetHashParam(hHash, HP_HASHVAL, pbHashData, &cbRequested, 0);\n}\n\nBYTE* Aggregator::getSlice()\n{\n    BYTE* ret = pbHashData;\n    pbHashData = NULL;\n    return ret;\n}\n\nBOOL Aggregator::handle(MousePosition* event)\n{\n    CryptHashData(hHash, (BYTE*)event, sizeof(*event), 0);\n\n    if ((event->ptMousePos.x != ptLastPos.x || event->ptMousePos.y !=\n            ptLastPos.y) && event->dwTickCount - dwLastTime > 100) {\n        ptLastPos = event->ptMousePos;\n        dwLastTime = event->dwTickCount;\n        dEntropy += MOUSE_ENTROPY_PER_SAMPLE;\n        return TRUE;\n    }\n\n    return FALSE;\n}\n\nBOOL Aggregator::handle(KeyPressed* event)\n{\n    CryptHashData(hHash, (BYTE*)event, sizeof(*event), 0);\n\n    if (event->isUp || (bLastScanCode != event->bScanCode && event->dwTickCount\n                        - dwLastTime > 100)) {\n        bLastScanCode = event->bScanCode;\n        dwLastTime = event->dwTickCount;\n        dEntropy += KEY_ENTROPY_PER_SAMPLE;\n        return TRUE;\n    }\n\n    return FALSE;\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Aggregator.h",
    "content": "#pragma once\n\n#include <windows.h>\n#include <wincrypt.h>\n\n\ntypedef struct {\n    BYTE bScanCode;\n    DWORD dwTickCount;\n    BOOL isUp;\n} KeyPressed;\n\n\ntypedef struct {\n    POINT ptMousePos;\n    DWORD dwTickCount;\n} MousePosition;\n\n\nclass Aggregator {\n\npublic:\n\n    Aggregator();\n    virtual ~Aggregator();\n\n    BOOL init();\n    BOOL enoughEntropy();\n    BOOL prepareSlice();\n    BYTE* getSlice();\n\n    double entropy() const;\n    DWORD requested() const;\n    BOOL handle(MousePosition* event);\n    BOOL handle(KeyPressed* event);\n\n    static BOOL initialize();\n    static int outbytes();\n    static void finalize();\n\nprotected:\n\n    double dEntropy;\n    DWORD cbRequested;\n    POINT ptLastPos;\n    BYTE bLastScanCode;\n    BYTE* pbHashData;\n    DWORD dwLastTime;\n    HCRYPTHASH hHash;\n\nprivate:\n\n    Aggregator(const Aggregator&);\n    Aggregator& operator = (const Aggregator&);\n\n    static HCRYPTPROV hProvider;\n};\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Counter.cpp",
    "content": "#include \"Counter.h\"\n\n\nCounter::Counter()\n{\n    lastRequest = 0;\n    lastSuccessRequest = 0;\n    maxRequest = 0;\n    maxSuccessRequest = 0;\n    cntRequest = 0;\n    cntSuccessRequest = 0;\n}\n\nCounter::Counter(const Counter& other)\n{\n    init(other);\n}\n\nCounter& Counter::operator = (const Counter& other)\n{\n    if (&other != this) {\n        init(other);\n    }\n\n    return *this;\n}\n\nvoid Counter::init(const Counter& other)\n{\n    lastRequest = other.lastRequest;\n    lastSuccessRequest = other.lastSuccessRequest;\n    maxRequest = other.maxRequest;\n    maxSuccessRequest = other.maxSuccessRequest;\n    cntRequest = other.cntRequest;\n    cntSuccessRequest = other.cntSuccessRequest;\n}\n\nCounter::~Counter()\n{\n}\n\nvoid Counter::registerRequest(int size, bool success)\n{\n    lastRequest = size;\n\n    if (lastRequest > maxRequest) {\n        maxRequest = lastRequest;\n    }\n\n    cntRequest += 1;\n\n    if (success) {\n        lastSuccessRequest = size;\n\n        if (lastSuccessRequest > maxSuccessRequest) {\n            maxSuccessRequest = lastSuccessRequest;\n        }\n\n        cntSuccessRequest += 1;\n    }\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Counter.h",
    "content": "#pragma once\n\nclass Counter {\n\npublic:\n\n    Counter();\n    Counter(const Counter&);\n    Counter& operator = (const Counter&);\n\n    virtual ~Counter();\n\n    void registerRequest(int size, bool success);\n\n    int lastRequest;\n    int lastSuccessRequest;\n    int maxRequest;\n    int maxSuccessRequest;\n    int cntRequest;\n    int cntSuccessRequest;\n\nprotected:\n\n    void init(const Counter&);\n\n};\n\n"
  },
  {
    "path": "common/tools/win32/entropy/CounterManager.cpp",
    "content": "#include \"CounterManager.h\"\n\n#include <windows.h>\n\nCRITICAL_SECTION CriticalSectionCounter;\n\nCounterManager* CounterManager::impl = NULL;\n\nvoid CounterManager::initialize()\n{\n    InitializeCriticalSection(&CriticalSectionCounter);\n    impl = new CounterManager();\n}\n\nvoid CounterManager::registerRequest(int size, bool success)\n{\n    acquire();\n    impl->doRegisterRequest(size, success);\n    release();\n}\n\nCounter CounterManager::get()\n{\n    Counter ret;\n    acquire();\n    ret = impl->cntr;\n    release();\n    return ret;\n}\n\nvoid CounterManager::acquire()\n{\n    EnterCriticalSection(&CriticalSectionCounter);\n}\n\nvoid CounterManager::release()\n{\n    LeaveCriticalSection(&CriticalSectionCounter);\n}\n\nCounterManager::CounterManager() : cntr()\n{\n}\n\nCounterManager::~CounterManager()\n{\n}\n\nvoid CounterManager::doRegisterRequest(int size, bool success)\n{\n    cntr.registerRequest(size, success);\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/CounterManager.h",
    "content": "#pragma once\n\n#include \"Counter.h\"\n\nclass CounterManager {\n\npublic:\n\n    static void initialize();\n    static void registerRequest(int size, bool success);\n    static Counter get();\n\nprotected:\n\n    static void acquire();\n    static void release();\n\n    CounterManager();\n    virtual ~CounterManager();\n\n    void doRegisterRequest(int size, bool success);\n\n    Counter cntr;\n\nprivate:\n\n    static CounterManager* impl;\n    CounterManager(const CounterManager&);\n    CounterManager& operator = (const CounterManager&);\n\n};\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Error.cpp",
    "content": "#include <windows.h>\n#include <stdio.h>\n\nvoid OutputError(const char* prefix, int err)\n{\n    char* s = NULL;\n    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |\n                  FORMAT_MESSAGE_IGNORE_INSERTS, NULL, (DWORD)err,\n                  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n                  (LPSTR)&s, 0, NULL);\n    fprintf(stderr, \"%s\\ncode: %d\\n%s\\n\", prefix, err, s);\n    LocalFree(s);\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Error.h",
    "content": "#pragma once\n\nvoid OutputError(const char* prefix, int err);\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Makefile",
    "content": "\nASTYLE=astyle\nWINDRES=windres\nCXX = g++\nCC=gcc\nCXX_FLAGS = -g -D_WIN32_IE=0x0900 -Wfatal-errors -Wall -Wextra -Wpedantic -Wconversion -Wshadow\nCFLAGS = -D_WIN32_IE=0x0900 -Wfatal-errors -Wall -Wextra -Wpedantic -Wconversion -Wshadow\n\nSERVER_BIN = server.exe\nCLIENT_BIN = client.exe\n\nBUILD_DIR = ./build\n\nCPP = Aggregator.cpp Counter.cpp CounterManager.cpp Error.cpp Network.cpp\\\n\t  Protocol.cpp server.cpp Slotter.cpp\n\nCLIC = client.c\n\nRES = resources.rc\n\nOBJ = $(CPP:%.cpp=$(BUILD_DIR)/%.o)\nCLIOBJ = $(CLIC:%.c=$(BUILD_DIR)/%.o)\nRESOBJ = $(RES:%.rc=$(BUILD_DIR)/%.o)\n\nLDFLAGS = -static-libgcc -static-libstdc++ -g -lcomctl32 -lws2_32 -Wl,--subsystem,console\nCLILDFLAGS = -s -lws2_32 -Wl,--subsystem,console\n\nDEP = $(OBJ:%.o=%.d)\n\nall: $(SERVER_BIN) $(CLIENT_BIN)\n\n$(SERVER_BIN) : $(BUILD_DIR)/$(SERVER_BIN)\n\n$(CLIENT_BIN) : $(BUILD_DIR)/$(CLIENT_BIN)\n\n$(BUILD_DIR)/$(SERVER_BIN) : $(OBJ) $(RESOBJ)\n\t@echo [Building]: $@\n\t@mkdir -p $(@D)\n\t@$(CXX) $(CXX_FLAGS) $^ -o $@ $(LDFLAGS)\n\n$(BUILD_DIR)/$(CLIENT_BIN) : $(CLIOBJ)\n\t@echo [Building]: $@\n\t@mkdir -p $(@D)\n\t@$(CC) $(C_FLAGS) $^ -o $@ $(CLILDFLAGS)\n\n-include $(DEP)\n\n$(BUILD_DIR)/%.o: %.rc\n\t@echo [Compiling resources]: $<\n\t@mkdir -p $(@D)\n\t@$(WINDRES) -o $@ -i $<\n\n$(BUILD_DIR)/%.o : %.cpp\n\t@echo [Compiling CXX]: $<\n\t@mkdir -p $(@D)\n\t@$(CXX) $(CXX_FLAGS) -MMD -c $< -o $@\n\n$(BUILD_DIR)/%.o: %.c\n\t@echo [Compiling C]: $<\n\t@mkdir -p $(@D)\n\t@$(CC) $(CFLAGS) -o $@ -c $<\n\n.PHONY : clean\nclean :\n\t@echo [Cleaning]\n\t@rm -f $(BUILD_DIR)/$(SERVER_BIN) $(OBJ) $(DEP) $(RESOBJ)\\\n\t\t$(BUILD_DIR)/$(CLIENT_BIN) $(CLIOBJ)\n\nastyle:\n\t$(ASTYLE) --options=astylerc *.c *.cpp *.h\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Network.cpp",
    "content": "#include <windows.h>\n#include <winsock2.h>\n\n#include \"Error.h\"\n#include \"Protocol.h\"\n\n#define PORT 22062\n\nSOCKET acceptSocket;\n\nDWORD WINAPI WorkerThread(LPVOID lpParameter)\n{\n    DWORD ret = 1;\n    Protocol* protHandler = NULL;\n    DWORD i = 0;\n    WSAEVENT events[1];\n    events[0] = (WSAEVENT) lpParameter;\n\n    while (true) {\n        while (true) {\n            i = WSAWaitForMultipleEvents(1, events, FALSE, WSA_INFINITE, TRUE);\n\n            if (i == WSA_WAIT_FAILED) {\n                OutputError(\"WSAWaitForMultipleEvents()\", WSAGetLastError());\n                goto error;\n            }\n\n            if (i != WAIT_IO_COMPLETION) {\n                break;\n            }\n        }\n\n        if (WSAResetEvent(events[i - WSA_WAIT_EVENT_0]) == FALSE) {\n            OutputError(\"WSAWaitForMultipleEvents()\", WSAGetLastError());\n            goto error;\n        }\n\n        protHandler = new Protocol(acceptSocket);\n\n        if (protHandler == NULL) {\n            OutputError(\"GlobalAlloc()\", GetLastError());\n            goto error;\n        }\n\n        if (!protHandler->recv()) {\n            OutputError(\"WSARecv()\", WSAGetLastError());\n            goto error;\n        }\n    }\n\n    // never reached\n    ret = 0;\nerror:\n    return ret;\n}\n\n\nDWORD WINAPI AcceptThread(LPVOID)\n{\n    WSADATA wsaData;\n    SOCKADDR_IN internetAddr;\n    HANDLE threadHandle = NULL;\n    DWORD threadId = 0;\n    SOCKET listenSocket = 0;\n    WSAEVENT acceptEvent;\n    INT ret = WSAStartup(MAKEWORD(2, 2), &wsaData);\n\n    if (ret != 0) {\n        OutputError(\"WSAStartup()\", ret);\n        WSACleanup();\n        return 1;\n    }\n\n    listenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,\n                             WSA_FLAG_OVERLAPPED);\n\n    if (listenSocket == INVALID_SOCKET) {\n        OutputError(\"WSASocket()\", WSAGetLastError());\n        return 1;\n    }\n\n    internetAddr.sin_family = AF_INET;\n    internetAddr.sin_addr.s_addr = htonl(INADDR_ANY);\n    internetAddr.sin_port = htons(PORT);\n    ret = bind(listenSocket, (PSOCKADDR) &internetAddr, sizeof(internetAddr));\n\n    if (ret == SOCKET_ERROR) {\n        OutputError(\"bind()\", WSAGetLastError());\n        return 1;\n    }\n\n    if (listen(listenSocket, 5)) {\n        OutputError(\"listen()\", WSAGetLastError());\n        return 1;\n    }\n\n    acceptEvent = WSACreateEvent();\n\n    if (acceptEvent == WSA_INVALID_EVENT) {\n        OutputError(\"WSACreateEvent()\", WSAGetLastError());\n        return 1;\n    }\n\n    threadHandle = CreateThread(NULL, 0, WorkerThread, (LPVOID) acceptEvent, 0,\n                                &threadId);\n\n    if (threadHandle == NULL) {\n        OutputError(\"CreateThread()\", GetLastError());\n        return 1;\n    }\n\n    while (true) {\n        acceptSocket = accept(listenSocket, NULL, NULL);\n        struct linger so_linger;\n        so_linger.l_onoff = 1;\n        so_linger.l_linger = 0;\n        ret = setsockopt(acceptSocket, SOL_SOCKET, SO_LINGER, (char*)&so_linger,\n                         sizeof so_linger);\n\n        if (ret) {\n            OutputError(\"setsockopt()\", WSAGetLastError());\n            return 1;\n        }\n\n        if (WSASetEvent(acceptEvent) == FALSE) {\n            OutputError(\"WSASetEvent()\", WSAGetLastError());\n            return 1;\n        }\n    }\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Protocol.cpp",
    "content": "#include \"Protocol.h\"\n#include \"Error.h\"\n#include \"Slotter.h\"\n#include \"CounterManager.h\"\n\n#define ENTROPY_SUCCESS ((char)0xFF)\n#define ENTROPY_FAIL ((char)0x00)\n\nvoid CALLBACK WorkerRoutine(DWORD error, DWORD bytesTransferred,\n                            LPWSAOVERLAPPED overlapped, DWORD)\n{\n    Protocol* prot = (Protocol*)overlapped->hEvent;\n\n    if (error != 0 || bytesTransferred == 0) {\n        delete prot;\n        return;\n    }\n\n    prot->handle(bytesTransferred);\n}\n\nProtocol::Protocol(SOCKET accept)\n{\n    socket = accept;\n    ZeroMemory(&(overlapped), sizeof(WSAOVERLAPPED));\n    bytesSEND = 0;\n    bytesRECV = 0;\n    bytesTOBESENT = 0;\n    dataBuf.len = DATABUFSIZE;\n    dataBuf.buf = buffer;\n    overlapped.hEvent = this;\n}\n\nProtocol::~Protocol()\n{\n    closesocket(socket);\n}\n\n\nbool Protocol::recv()\n{\n    DWORD flags = 0;\n    int ret = WSARecv(socket, &dataBuf, 1, NULL, &flags, &overlapped,\n                      WorkerRoutine);\n\n    if ((ret == SOCKET_ERROR) && (WSAGetLastError() != WSA_IO_PENDING)) {\n        return false;\n    }\n\n    return true;\n}\n\n\nvoid Protocol::handle(DWORD bytesTransferred)\n{\n    int ret = 0;\n    DWORD flags = 0;\n\n    if (bytesRECV == 0) {\n        bytesRECV = bytesTransferred;\n        bytesSEND = 0;\n        DWORD bytes = 0;\n        memcpy(&bytes, dataBuf.buf, bytesTransferred);\n        DWORD res = ntohl(bytes);\n        bool succ = Slotter::request((BYTE*)(buffer + 1), res);\n        CounterManager::registerRequest(res, succ);\n\n        if (succ) {\n            buffer[0] = ENTROPY_SUCCESS;\n            bytesTOBESENT = res + 1;\n        }\n        else {\n            buffer[0] = ENTROPY_FAIL;\n            bytesTOBESENT = 1;\n        }\n    }\n    else {\n        bytesSEND += bytesTransferred;\n    }\n\n    if (bytesTOBESENT > bytesSEND) {\n        ZeroMemory(&overlapped, sizeof(WSAOVERLAPPED));\n        overlapped.hEvent = this;\n        dataBuf.buf = buffer + bytesSEND;\n        dataBuf.len = bytesTOBESENT - bytesSEND;\n        ret = WSASend(socket, &dataBuf, 1, NULL, 0, &overlapped, WorkerRoutine);\n\n        if ((ret == SOCKET_ERROR) && (WSAGetLastError() != WSA_IO_PENDING)) {\n            OutputError(\"WSASend()\", WSAGetLastError());\n            return;\n        }\n    }\n    else {\n        bytesRECV = 0;\n        flags = 0;\n        ZeroMemory(&overlapped, sizeof(WSAOVERLAPPED));\n        overlapped.hEvent = this;\n        dataBuf.len = DATABUFSIZE;\n        dataBuf.buf = buffer;\n        ret = WSARecv(socket, &dataBuf, 1, NULL, &flags, &overlapped,\n                      WorkerRoutine);\n\n        if ((ret == SOCKET_ERROR) && (WSAGetLastError() != WSA_IO_PENDING)) {\n            OutputError(\"WSARecv()\", WSAGetLastError());\n            return;\n        }\n    }\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Protocol.h",
    "content": "#pragma once\n\n#include <winsock2.h>\n\n#define DATABUFSIZE 8192\n\nclass Protocol {\n\npublic:\n\n    Protocol(SOCKET);\n    virtual ~Protocol();\n\n    void handle(DWORD bytesTransferred);\n    bool recv();\n\nprotected:\n\n    OVERLAPPED overlapped;\n    SOCKET socket;\n    CHAR buffer[DATABUFSIZE];\n    WSABUF dataBuf;\n    DWORD bytesSEND;\n    DWORD bytesTOBESENT;\n    DWORD bytesRECV;\n\nprivate:\n\n    Protocol(const Protocol&);\n    Protocol& operator = (const Protocol&);\n\n};\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Slotter.cpp",
    "content": "#include \"Slotter.h\"\n\n#include <assert.h>\n#include <cstddef>\n#include <windows.h>\n\n\nCRITICAL_SECTION CriticalSectionSlotter;\n\nSlotter* Slotter::impl = NULL;\n\nvoid Slotter::initialize(int slen)\n{\n    InitializeCriticalSection(&CriticalSectionSlotter);\n    impl = new Slotter(slen);\n}\n\nvoid Slotter::finalize()\n{\n    DeleteCriticalSection(&CriticalSectionSlotter);\n    delete impl;\n    impl = NULL;\n}\n\nvoid Slotter::acquire()\n{\n    EnterCriticalSection(&CriticalSectionSlotter);\n}\n\nvoid Slotter::release()\n{\n    LeaveCriticalSection(&CriticalSectionSlotter);\n}\n\nvoid Slotter::push(BYTE* data)\n{\n    acquire();\n    impl->doPush(data);\n    release();\n}\n\nDWORD Slotter::available()\n{\n    DWORD ret = 0;\n    acquire();\n    ret = impl->dwAvailable;\n    release();\n    return ret;\n}\n\nbool Slotter::request(BYTE* buffer, DWORD count)\n{\n    bool ret = false;\n    acquire();\n    ret = impl->doRequest(buffer, count);\n    release();\n    return ret;\n}\n\nSlotter::Slotter(int slen) : slots()\n{\n    dwAvailable = 0;\n    slice = NULL;\n    slice_avail = 0;\n    slice_length = slen;\n}\n\nSlotter::~Slotter()\n{\n    if (slice != NULL) {\n        LocalFree(slice);\n        slice = NULL;\n    }\n\n    while (!slots.empty()) {\n        slice = slots.front();\n        slots.pop_front();\n        LocalFree(slice);\n        slice = NULL;\n    }\n}\n\nvoid Slotter::doPush(BYTE* data)\n{\n    if (data) {\n        dwAvailable += slice_length;\n        slots.push_back(data);\n    }\n}\n\nvoid Slotter::updateSlice()\n{\n    if (slice_avail == 0) {\n        assert(slice == NULL);\n\n        if (!slots.empty()) {\n            slice = slots.front();\n            slots.pop_front();\n            slice_avail = slice_length;\n        }\n    }\n}\n\nint Slotter::copyFromSlice(BYTE* dst, int required)\n{\n    int tobecopied = slice_avail;\n\n    if (required < slice_avail) {\n        tobecopied = required;\n    }\n\n    if (slice_avail > 0) {\n        assert(slice != NULL);\n        memcpy(dst, slice + (slice_length - slice_avail), tobecopied);\n        slice_avail -= tobecopied;\n    }\n\n    if (slice_avail == 0) {\n        if (slice != NULL) {\n            LocalFree(slice);\n            slice = NULL;\n        }\n    }\n\n    return tobecopied;\n}\n\nbool Slotter::doRequest(BYTE* buffer, DWORD count)\n{\n    if (count > dwAvailable) {\n        return false;\n    }\n\n    if (buffer) {\n        BYTE* tmp = buffer;\n        DWORD gathered = 0;\n\n        while (gathered < count) {\n            updateSlice();\n            int inc = copyFromSlice(tmp, count - gathered);\n            gathered += inc;\n            tmp += inc;\n        }\n\n        dwAvailable -= count;\n    }\n\n    return true;\n}\n\n"
  },
  {
    "path": "common/tools/win32/entropy/Slotter.h",
    "content": "#pragma once\n\n#include <WinDef.h>\n#include <list>\n\nclass Slotter {\n\npublic:\n\n    static void initialize(int);\n    static void finalize();\n    static void push(BYTE*);\n    static DWORD available();\n    static bool request(BYTE* buffer, DWORD count);\n\nprotected:\n\n    static void acquire();\n    static void release();\n\n    Slotter(int);\n    virtual ~Slotter();\n\n    void doPush(BYTE*);\n    bool doRequest(BYTE* buffer, DWORD count);\n    void updateSlice();\n    int copyFromSlice(BYTE* dst, int required);\n\n    DWORD dwAvailable;\n    std::list<BYTE*> slots;\n\n    BYTE* slice;\n    int slice_avail;\n    int slice_length;\n\nprivate:\n\n    static Slotter* impl;\n    Slotter(const Slotter&);\n    Slotter& operator = (const Slotter&);\n\n};\n\n"
  },
  {
    "path": "common/tools/win32/entropy/astylerc",
    "content": "style=stroustrup\nindent=spaces=4\nattach-namespaces\nattach-classes\nattach-inlines\nattach-extern-c\n#attach-closing-while\nindent-cases\nbreak-blocks\npad-oper\npad-comma\npad-header\ndelete-empty-lines\nalign-pointer=type\n#break-closing-braces (-y)\n-y\nbreak-one-line-headers\n#add-braces (-j)\n-j\nconvert-tabs\nlineend=linux\n"
  },
  {
    "path": "common/tools/win32/entropy/client.c",
    "content": "// Command Line Options:\n//    client [-p:x] [-s:IP] [-n:x] [-o]\n//           -p:x      Remote port to send to\n//           -s:IP     Server's IP address or hostname\n//           -n:x      Number of bytes to enquire\n#include <ctype.h>\n#include <winsock2.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <assert.h>\n\n#define DEFAULT_COUNT       20\n#define DEFAULT_PORT        5150\n#define DEFAULT_BUFFER      2048\n\nchar  szServer[128];               // Server to connect to\nint   iPort     = DEFAULT_PORT;    // Port on server to connect to\nDWORD dwCount   = DEFAULT_COUNT;   // Number of bytes to enquire\n\n\nvoid usage()\n{\n    printf(\"client [-p:x] [-s:IP] [-n:x]\\n\\n\");\n    printf(\"       -p:x      Remote port to send to\\n\");\n    printf(\"       -s:IP     Server's IP address or hostname\\n\");\n    printf(\"       -n:x      Number of bytes to enquire\\n\");\n    printf(\"\\n\");\n}\n\n\nvoid OutputError(const char* prefix, int err)\n{\n    char* s = NULL;\n    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |\n                  FORMAT_MESSAGE_IGNORE_INSERTS, NULL, (DWORD)err,\n                  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n                  (LPSTR)&s, 0, NULL);\n    fprintf(stderr, \"%s\\ncode: %d\\n%s\\n\", prefix, err, s);\n    LocalFree(s);\n}\n\n\nvoid ValidateArgs(int argc, char** argv)\n{\n    for (int i = 1; i < argc; i++) {\n        if ((argv[i][0] == '-') || (argv[i][0] == '/')) {\n            switch (tolower(argv[i][1])) {\n            case 'p':        // Remote port\n                if (strlen(argv[i]) > 3) {\n                    iPort = atoi(&argv[i][3]);\n                }\n\n                break;\n\n            case 's':       // Server\n                if (strlen(argv[i]) > 3) {\n                    strcpy(szServer, &argv[i][3]);\n                }\n\n                break;\n\n            case 'n':       // Number of times to send message\n                if (strlen(argv[i]) > 3) {\n                    dwCount = (DWORD)atol(&argv[i][3]);\n                }\n\n                break;\n\n            default:\n                usage();\n                break;\n            }\n        }\n    }\n}\n\nint main(int argc, char** argv)\n{\n    WSADATA wsd;\n    SOCKET sClient;\n    char szBuffer[DEFAULT_BUFFER];\n    int ret;\n    struct sockaddr_in server;\n    struct hostent* host = NULL;\n\n    if (argc < 2) {\n        usage();\n        exit(1);\n    }\n\n    // Parse the command line and load Winsock\n    ValidateArgs(argc, argv);\n\n    if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {\n        OutputError(\"WSAStartup()\", WSAGetLastError());\n        return 1;\n    }\n\n    // Create the socket, and attempt to connect to the server\n    sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);\n\n    if (sClient == INVALID_SOCKET) {\n        OutputError(\"socket()\", WSAGetLastError());\n        return 1;\n    }\n\n    server.sin_family = AF_INET;\n    server.sin_port = htons((unsigned short)iPort);\n    server.sin_addr.s_addr = inet_addr(szServer);\n\n    if (server.sin_addr.s_addr == INADDR_NONE) {\n        host = gethostbyname(szServer);\n\n        if (host == NULL) {\n            printf(\"Unable to resolve server %s\\n\", szServer);\n            return 1;\n        }\n\n        CopyMemory(&server.sin_addr, host->h_addr_list[0], (size_t)host->h_length);\n    }\n\n    ret = connect(sClient, (struct sockaddr*)&server, sizeof(server));\n\n    if (ret == SOCKET_ERROR) {\n        OutputError(\"connect()\", WSAGetLastError());\n        return 1;\n    }\n\n    DWORD out = htonl(dwCount);\n    ret = send(sClient, (const char*)&out, sizeof(out), 0);\n\n    if (ret == 0) {\n        return 1;\n    }\n    else if (ret == SOCKET_ERROR) {\n        OutputError(\"send()\", WSAGetLastError());\n        return 1;\n    }\n\n    ret = recv(sClient, szBuffer, DEFAULT_BUFFER, 0);\n\n    if (ret == 0) {\n        printf(\"It is a graceful close!\\n\");\n    }\n    else if (ret == SOCKET_ERROR) {\n        OutputError(\"recv()\", WSAGetLastError());\n    }\n    else {\n        printf(\"Received %d bytes:\\n\", ret);\n        char resptype = szBuffer[0];\n\n        if (ret == 1) {\n            printf(\"Entroy provider would block\\n\");\n            assert(resptype == 0);\n        }\n        else {\n            assert(resptype == (char)0xFF);\n            assert((unsigned int)ret == (dwCount + 1));\n        }\n\n        for (int i = 1; i < ret; i++) {\n            printf(\"%x:\", (unsigned char)szBuffer[i]);\n        }\n\n        printf(\"\\n\");\n    }\n\n    if (closesocket(sClient) != 0) {\n        OutputError(\"closesocket()\", WSAGetLastError());\n    }\n\n    if (WSACleanup() != 0) {\n        OutputError(\"WSACleanup()\", WSAGetLastError());\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "common/tools/win32/entropy/resources.rc",
    "content": "#include <windows.h>\n\n#define SRV_MOUSE_DLGID 102\n#define SRV_PROGRESS_BARID 1000\n#define SRV_MOUSE_COLLECTID 1002\n#define SRV_MOUSE_STATIC 1003\n#define SRV_TEXT_A 1004\n#define SRV_TEXT_B 1005\n#define SRV_TEXT_C 1006\n\n\n#define MY_WIDTH 495\n#define MY_HEIGHT 278\n\n#define BTN_WIDTH 50\n#define BTN_HEIGHT 14\n#define TXT_HEIGHT 14\n\n#define SPACE 5\n\n#define COLLECT_HEIGHT (MY_HEIGHT - 114)\n\nSRV_MOUSE_DLGID DIALOG DISCARDABLE 0, 0, MY_WIDTH, MY_HEIGHT\nSTYLE DS_MODALFRAME | DS_NOIDLEMSG | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION\nFONT 10, \"MS Sans Serif\"\nBEGIN\n\tLTEXT \"Please use your mouse and keyboard to generate events throughout the whole randomness generation ceremony.\", SRV_MOUSE_STATIC,SPACE,SPACE,MY_WIDTH - 12,14\n\tLTEXT \"\", SRV_TEXT_A, SPACE, SPACE + (1 * TXT_HEIGHT), MY_WIDTH - 12, 14\n\tLTEXT \"\", SRV_TEXT_B, SPACE, SPACE + (2 * TXT_HEIGHT), MY_WIDTH - 12, 14\n\tLTEXT \"\", SRV_TEXT_C, SPACE, SPACE + (3 * TXT_HEIGHT), MY_WIDTH - 12, 14\n\tCONTROL \"\", SRV_MOUSE_COLLECTID, \"Static\", SS_LEFTNOWORDWRAP | SS_SUNKEN | WS_BORDER | WS_GROUP, 5, 63, MY_WIDTH - 12, COLLECT_HEIGHT\n\tCONTROL \"Progress1\", SRV_PROGRESS_BARID, \"msctls_progress32\", PBS_SMOOTH | WS_BORDER, SPACE, COLLECT_HEIGHT + SPACE + 63, MY_WIDTH - 12, 14\n\tPUSHBUTTON \"QUIT\",IDCANCEL,MY_WIDTH - 57,MY_HEIGHT - 21,BTN_WIDTH,BTN_HEIGHT\nEND\n"
  },
  {
    "path": "common/tools/win32/entropy/server.cpp",
    "content": "#include <windows.h>\n#include <commctrl.h>\n#include <stdio.h>\n\n#include \"Aggregator.h\"\n#include \"Error.h\"\n#include \"Slotter.h\"\n#include \"CounterManager.h\"\n\n\n#define SRV_MOUSE_DLGID 102\n#define SRV_PROGRESS_BARID 1000\n#define SRV_MOUSE_COLLECTID 1002\n#define SRV_MOUSE_STATIC 1003\n#define SRV_TEXT_A 1004\n#define SRV_TEXT_B 1005\n#define SRV_TEXT_C 1006\n\nDWORD WINAPI AcceptThread(LPVOID lpParameter);\n\nstatic BOOL CALLBACK KeyboardEntropyProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM)\n{\n    BOOL ret = FALSE;\n\n    if ((uMsg == WM_COMMAND) && (HIWORD(wParam) == BN_CLICKED)) {\n        if (LOWORD(wParam) == IDCANCEL) {\n            BOOL* pQuit = (BOOL*)GetWindowLong(hwndDlg, DWL_USER);\n            DestroyWindow(hwndDlg);\n            *pQuit = TRUE;\n            ret = TRUE;\n        }\n    }\n\n    return ret;\n}\n\n\nHWND DialogInit(HINSTANCE hInstance, HWND hWndParent, BOOL* quit)\n{\n    HWND ret = 0;\n    ret = CreateDialog(hInstance, MAKEINTRESOURCE(SRV_MOUSE_DLGID), hWndParent, KeyboardEntropyProc);\n\n    if (!ret) {\n        goto error;\n    }\n\n    if (hWndParent) {\n        EnableWindow(hWndParent, FALSE);\n    }\n\n    SetWindowLong(ret, DWL_USER, (LONG)quit);\nerror:\n    return ret;\n}\n\nvoid ProgressInit(HWND hwndDlg, int param)\n{\n    SendDlgItemMessage(hwndDlg, SRV_PROGRESS_BARID, PBM_SETRANGE32, 0, param);\n}\n\nvoid ProgressSet(HWND hwndDlg, double param)\n{\n    SendDlgItemMessage(hwndDlg, SRV_PROGRESS_BARID,\n                       PBM_SETPOS, (WPARAM)param, 0);\n}\n\n\nvoid TextSetStr(HWND hwndDlg, DWORD id, TCHAR* txt)\n{\n    int const arraysize = 160;\n    TCHAR pszTmp[arraysize];\n    GetDlgItemText(hwndDlg, id, pszTmp, arraysize);\n\n    if (strcmp(pszTmp, txt) != 0) {\n        SetDlgItemText(hwndDlg, id, txt);\n    }\n}\n\nvoid TextSet(HWND hwndDlg, DWORD avail, Counter cntr)\n{\n    int const arraysize = 160;\n    TCHAR pszDestA[arraysize];\n    TCHAR pszDestB[arraysize];\n    TCHAR pszDestC[arraysize];\n    size_t cbDest = arraysize * sizeof(TCHAR);\n    LPCTSTR pszFormatA = TEXT(\"Available: %d\");\n    LPCTSTR pszFormatB = TEXT(\"All, Current: %d, Max: %d\");\n    LPCTSTR pszFormatC = TEXT(\"Good, Current: %d, Max: %d\");\n    snprintf(pszDestA, cbDest, pszFormatA, avail);\n    snprintf(pszDestB, cbDest, pszFormatB, cntr.lastRequest, cntr.maxRequest);\n    snprintf(pszDestC, cbDest, pszFormatC, cntr.lastSuccessRequest,\n             cntr.maxSuccessRequest);\n    TextSetStr(hwndDlg, SRV_TEXT_A, pszDestA);\n    TextSetStr(hwndDlg, SRV_TEXT_B, pszDestB);\n    TextSetStr(hwndDlg, SRV_TEXT_C, pszDestC);\n}\n\n\nBOOL HandleMouseMove(HWND hwndDlg, MSG msg, Aggregator* dd)\n{\n    MousePosition mpos;\n    mpos.ptMousePos.x = LOWORD(msg.lParam);\n    mpos.ptMousePos.y = HIWORD(msg.lParam);\n    mpos.dwTickCount = GetTickCount();\n    ClientToScreen(hwndDlg, &(mpos.ptMousePos));\n    return dd->handle(&mpos);\n}\n\nBOOL HandleKeyPress(MSG msg, Aggregator* dd)\n{\n    KeyPressed key;\n    key.bScanCode = ((msg.lParam >> 16) & 0x0000000F);\n    key.dwTickCount = GetTickCount();\n    key.isUp = (msg.message == WM_KEYUP);\n    return dd->handle(&key);\n}\n\nBOOL GatherEntropy(HINSTANCE hInstance, HWND hWndParent)\n{\n    MSG msg;\n    BOOL quit = FALSE;\n    BOOL bResult = FALSE;\n    Aggregator* aggr = NULL;\n    aggr = new Aggregator();\n    HWND hwndDlg = DialogInit(hInstance, hWndParent, &quit);\n\n    if (!hwndDlg) {\n        goto error;\n    }\n\n    if (!aggr->init()) {\n        goto error;\n    }\n\n    ProgressInit(hwndDlg, aggr->requested());\n    TextSet(hwndDlg, 0, Counter());\n\n    while ((quit == FALSE) && GetMessage(&msg, 0, 0, 0) > 0) {\n        if (!aggr->enoughEntropy()) {\n            switch (msg.message) {\n            case WM_MOUSEMOVE:\n                if (HandleMouseMove(hwndDlg, msg, aggr)) {\n                    ProgressSet(hwndDlg, aggr->entropy());\n                }\n\n                break;\n\n            case WM_KEYDOWN:\n            case WM_KEYUP:\n                if (HandleKeyPress(msg, aggr)) {\n                    ProgressSet(hwndDlg, aggr->entropy());\n                }\n            }\n        }\n\n        if (!IsDialogMessage(hwndDlg, &msg)) {\n            TranslateMessage(&msg);\n            DispatchMessage(&msg);\n        }\n\n        if (aggr->enoughEntropy()) {\n            aggr->prepareSlice();\n            Slotter::push(aggr->getSlice());\n            delete aggr;\n            aggr = NULL;\n            aggr = new Aggregator();\n            aggr->init();\n            ProgressInit(hwndDlg, aggr->requested());\n        }\n\n        DWORD avail = Slotter::available();\n        Counter cntr = CounterManager::get();\n        TextSet(hwndDlg, avail, cntr);\n    }\n\n    if (hWndParent) {\n        EnableWindow(hWndParent, TRUE);\n    }\n\n    bResult = TRUE;\nerror:\n\n    if (aggr) {\n        delete aggr;\n        aggr = NULL;\n    }\n\n    return bResult;\n}\n\n\nint WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)\n{\n    INITCOMMONCONTROLSEX commonControls;\n    HANDLE threadHandle = NULL;\n    DWORD threadId = 0;\n    commonControls.dwSize = sizeof(commonControls);\n    commonControls.dwICC = ICC_PROGRESS_CLASS;\n    InitCommonControlsEx(&commonControls);\n\n    if (!Aggregator::initialize()) {\n        OutputError(\"Aggregator::initialize\", GetLastError());\n        goto error;\n    }\n\n    CounterManager::initialize();\n    Slotter::initialize(Aggregator::outbytes());\n    threadHandle = CreateThread(NULL, 0, AcceptThread, 0, 0, &threadId);\n\n    if (threadHandle == NULL) {\n        OutputError(\"CreateThread()\", GetLastError());\n        goto error;\n    }\n\n    if (!GatherEntropy(hInstance, 0)) {\n        OutputError(\"GatherEntropy()\", GetLastError());\n    }\n\nerror:\n    Aggregator::finalize();\n    Slotter::finalize();\n    return 0;\n}\n\n"
  },
  {
    "path": "core/.editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\ntrim_trailing_whitespace = true\ninsert_final_newline = true\ncharset = utf-8\nend_of_line = lf\n\n[Makefile]\nindent_style = tab\n\n[{Dockerfile,*.Dockerfile}]\nindent_style = tab\n\n[*.rst]\nindent_style = space\nindent_size = 4\n\n[*.json{,.tmpl}]\nindent_style = space\nindent_size = 4\n\n[*.yml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": "core/.gitignore",
    "content": ".idea/\n"
  },
  {
    "path": "core/.golangci.yml",
    "content": "linters:\n  enable-all: true\n  disable:\n    - depguard # imports are controlled by gofmt\n    - err113 # use fmt.Errorf for formatted errors and errors.New for plain strings\n    - exportloopref # deprecated since Go 1.22\n    - gci # imports are controlled by gofmt\n    - gochecknoinits # init() needed in some cases\n    # 'Accept interface, return implementation' idiom doesn't make always sense, as returning interface\n    # allows API to operate on more abstract level, rather than revealing low-level details\n    - ireturn\n    - lll # there are cases when code lines can be greater than suggested maximum of 120 characters\n    - varnamelen # it is OK to have non-standard variable names in crypto library\nlinters-settings:\n  goimports:\n    local-prefixes: tivi.io\n  gocritic:\n    enable-all: true\n    disabled-checks:\n      - captLocal # func arguments can be capitalized in crypto library\n      #- sloppyReassign\n      - whyNoLint # don't explain //nolint: in a code\n  nolintlint:\n    allow-unused: false\n    require-specific: true\n\nissues:\n  exclude-dirs:\n    - cmd\n    - common\n    - crypto/cades\n    - crypto/cms\n    - crypto/ocsp\n    - crypto/padding\n    - crypto/timestamp\n    - crypto/util\n    - voterlist\n  # Escape all *_test.log files from linter\n  exclude-files:\n    - '(.+)_test\\.go'\n\noutput:\n  show-stats: true\n"
  },
  {
    "path": "core/Makefile",
    "content": "# TIVI Core Go project absolute path dir\nROOTDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\n# golangci-lint version\nGOLANGCI_LINT_VERSION := 1.62.0\n\n# Go test packages to be linted, '/...' denotes \"recursive searching\"\nGO_TEST_PACKAGES := $(ROOTDIR)math/... $(ROOTDIR)crypto/ecdsa/... $(ROOTDIR)crypto/elgamal/... $(ROOTDIR)crypto/x509/...\n\n# Copy everything from that file here\ninclude $(ROOTDIR)govar.mk\n\n.PHONY: help\nhelp:\n\t@echo \"usage: make help       Show this help message\"\n\t@echo \"       make lint       Run code linter\"\n\t@echo \"       make test       Run unit tests\"\n\t@echo \"       make codegen    Auto-generate code\"\n\t@echo \"       make clean      Clean up auto-generated code\"\n\n# Lint Go code inside a Docker\n.PHONY: lint\nlint: codegen\n\tdocker run --rm -v $(ROOTDIR):/linter -w /linter golangci/golangci-lint:v$(GOLANGCI_LINT_VERSION) golangci-lint run\n\n# Test only specific Go packages\n.PHONY: test\ntest: lint\n\tgo test -v $(GO_TEST_PACKAGES)\n\n# Target prefixed with \"--\" are not seen to the call `make [target]`\n.PHONY: --build-errorgen\n--build-errorgen: clean\n\t$(MAKE) -C cmd/errorgen all\n\n# Generate Go code that has a '//go:generate' directive.\n# Generate Go code custom error structs on fly\n.PHONY: codegen\ncodegen: --build-errorgen\n\t$(ROOTDIR)cmd/errorgen/bin/errorgen -base $(ROOTDIR) ./...\n\n# Clean all Go binaries and auto-generated files\n.PHONY: clean\nclean:\n\tfind . -not -path \\*/testdata/\\* \\( \\\n\t\t-name gen_types.go -o \\\n\t\t-name gen_types_test.go -o \\\n\t\t-name gen_import.go -o \\\n\t\t-name gen_import_dev.go \\\n\t\t\\) -delete\n\t -$(GO) clean -i ./...\n\trm -rf bin/\n"
  },
  {
    "path": "core/README.md",
    "content": "# TIVI Core Go library\n\nTIVI Core Go library is a library written in [Go](https://go.dev/). It is a library for providing\ncommon functionality for successful e-voting, mainly targeting cryptographic operations.\n\n**TIVI Core Go's key features are:**\n\n- ElGamal/Lifted ElGamal encryption support\n- Homomorphic ElGamal/Lifted ElGamal encryption support\n- Abstract group based cryptography\n- ModP and elliptic curve support for ElGamal encryption through abstract groups\n- Adaptive X509 certificates and PKCS8 private keys parsing\n\n## Getting started\n\n### Prerequisites\n\nTIVI Core Go requires [Go](https://go.dev/) version [1.23](https://go.dev/doc/devel/release#go1.23.0) or above.\n\n### Getting TIVI Core Go\n\n```sh\nGOPRIVATE='tivi.io' go get tivi.io/core@feature/crypto-api-2\n```\n"
  },
  {
    "path": "core/cmd/cms/main.go",
    "content": "package main\n\nimport (\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n\n\t\"tivi.io/core/crypto/cades\"\n\t\"tivi.io/core/crypto/cms\"\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst (\n\tsuccess int = iota\n\targError\n\tappError\n)\n\nconst appName = \"cms\"\n\nconst (\n\tverifyCmd = \"verify\"\n)\n\nvar cmds []string = []string{verifyCmd}\n\nfunc parseFlag() ([]string, error) {\n\tflags := flag.NewFlagSet(appName, flag.ContinueOnError)\n\tif len(os.Args) < 2 {\n\t\tfmt.Fprintf(flags.Output(), \"No command specified. Valid commands are: %s\\n\", strings.Join(cmds, \", \"))\n\t\treturn nil, errors.New(\"no command\")\n\t}\n\tcommand := os.Args[1]\n\tif command != verifyCmd {\n\t\tfmt.Fprintf(flags.Output(), \"'%s' is not valid command. Valid commands are: %s\\n\", command, strings.Join(cmds, \", \"))\n\t\treturn nil, errors.New(\"invalid command\")\n\t}\n\tflags.Usage = func() {\n\t\tfmt.Fprintf(flags.Output(), \"\\nUsage:\\n\\n\")\n\t\tif command == verifyCmd {\n\t\t\tfmt.Fprintf(flags.Output(), \"\\t%s %s [-data filepath] [-certs filepath(s)] [-cades level]\\n\", appName, verifyCmd)\n\t\t\tfmt.Fprint(flags.Output(), \"\\nDescription:\\n\\n\\tVerifies the signed data with the certificates provided.\\n\")\n\t\t}\n\t\tfmt.Fprintln(flags.Output(), \"\\nFlags:\")\n\t\tflags.PrintDefaults()\n\t}\n\tvar data, certs, cades, detachedData, nonce string\n\tflags.StringVar(&data, \"data\", \"\", \"filepath of the signed data in DER format to be verified\")\n\tflags.StringVar(&certs, \"certs\", \"\", \"filepath(s) (separated by ':') of the certificates to be matched\")\n\tflags.StringVar(&cades, \"cades\", \"\", \"CAdES conformance level (either 'b', 't' or 'lt')\")\n\tflags.StringVar(&detachedData, \"detached\", \"\", \"filepath of the original data, if detached\")\n\tflags.StringVar(&nonce, \"nonce\", \"\", \"nonce used for generating the timestamp (CAdES-t level)\")\n\tif err := flags.Parse(os.Args[2:]); err != nil {\n\t\treturn nil, err\n\t}\n\tif data == \"\" {\n\t\tfmt.Fprintf(flags.Output(), \"%s %s: no data filepath provided\\n\", appName, command)\n\t\tflags.Usage()\n\t\treturn nil, errors.New(\"argument missing\")\n\t}\n\tif certs == \"\" {\n\t\tfmt.Fprintf(flags.Output(), \"%s %s: no certificate filepath provided\\n\", appName, command)\n\t\tflags.Usage()\n\t\treturn nil, errors.New(\"argument missing\")\n\t}\n\targs := []string{command, data, certs, cades, detachedData, nonce}\n\treturn args, nil\n}\n\nfunc verifySignedData(cadesLevel string, data []byte, certs []*x509.Certificate, detachedData, nonce []byte) error {\n\tswitch cadesLevel {\n\tcase \"b\":\n\t\treturn cades.CAdESBVerifySignedData(data, certs, detachedData)\n\tcase \"t\":\n\t\treturn cades.CAdESTVerifySignedData(data, certs, detachedData, nonce)\n\t}\n\treturn cms.VerifySignedData(data, certs, detachedData, map[string]cms.AttrToVerify{}, map[string]cms.AttrToVerify{})\n}\n\nfunc appmain() (result int) {\n\targs, err := parseFlag()\n\tif len(args) != 6 {\n\t\tfmt.Fprintf(os.Stderr, \"not enough arguments\\n\")\n\t\treturn argError\n\t}\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed parsing arguments: %v\\n\", err)\n\t\treturn argError\n\t}\n\tcommand, data, fcts, cades, detached, nc := args[0], args[1], args[2], args[3], args[4], args[5]\n\tbytes, err := ioutil.ReadFile(data)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed reading data file at %s: %v\\n\", data, err)\n\t\treturn appError\n\t}\n\tfcerts := strings.Split(fcts, \":\")\n\tcerts := make([]*x509.Certificate, len(fcerts))\n\tfor i, fcert := range fcerts {\n\t\tpem, err := ioutil.ReadFile(fcert)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed reading PEM file at '%s': %v\\n\", fcert, err)\n\t\t\treturn appError\n\t\t}\n\t\tcert, err := util.GetPEMCertificate(pem)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed getting PEM certificate at '%s': %v\\n\", fcert, err)\n\t\t\treturn appError\n\t\t}\n\t\tcerts[i] = cert\n\t}\n\tvar detachedData []byte\n\tif detached != \"\" {\n\t\tdetachedData, err = ioutil.ReadFile(detached)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed reading detached data file at %s: %v\\n\", data, err)\n\t\t\treturn appError\n\t\t}\n\t}\n\tvar nonce []byte\n\tif nc == \"\" {\n\t\tnonce = nil\n\t} else {\n\t\tnonce = []byte(nc)\n\t}\n\tswitch command {\n\tcase verifyCmd:\n\t\terr := verifySignedData(cades, bytes, certs, detachedData, nonce)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed verifying signed data: %v\\n\", err)\n\t\t\treturn appError\n\t\t}\n\t\tfmt.Fprintf(os.Stdout, \"verification successful\\n\")\n\t}\n\treturn success\n}\n\nfunc main() {\n\tos.Exit(appmain())\n}\n"
  },
  {
    "path": "core/cmd/errorgen/.gitignore",
    "content": "bin/\npkg/\ngentmpl.go\n"
  },
  {
    "path": "core/cmd/errorgen/Makefile",
    "content": "include ../../govar.mk\n\n.PHONY: all\nall: bin/gen\n\nbin/gen: clean\n\t$(GO) generate ./...\n\tenv GOBIN=$(or $(TIVI_GOBIN),$(abspath bin)) \\\n\t\t$(GO) install $(if $(DEVELOPMENT),-tags development )./...\n\n.PHONY: clean\nclean:\n\trm -rf cmd/gen/gentmpl.go\n"
  },
  {
    "path": "core/cmd/errorgen/gen.tmpl",
    "content": "{{/* vim: set ft=gotexttmpl: */}}\npackage {{.Package.Name}}\n\nimport \"fmt\"\n\n{{range .Literals}}\n// {{.Name}} is auto-generated from {{base .Pos.String}}.\ntype {{.Name}} struct {\n\t{{range .Fields}}\n\t{{.}} {{if eq . \"Err\"}}error{{else}}interface{}{{end}}\n\t{{- end}}\n}\n\n// EntryID implements the tivi.io/core/log.Entry interface.\nfunc (x {{.Name}}) EntryID() string { return \"tivi.io/core/{{$.Package.ImportPath}}.{{.Name}}\" }\n\n{{$name := .Name}}\n{{range .Fields}}{{if eq . \"Err\"}}\n// Cause implements the tivi.io/core/errors.Causer interface.\nfunc (x {{$name}}) Cause() error { return x.Err }\n{{end}}{{end}}\n\nfunc (x {{.Name}}) Error() string {\n\treturn fmt.Sprint(x.EntryID(), \"{\",\n\t\t{{- range $i, $e := .Fields -}}\n\t\t{{- if gt $i 0}}\", \",{{end}}\n\t\t\"{{.}}:\", x.{{.}},\n\t\t{{- end -}}\n\t\t\"}\")\n}\n{{end}}\n"
  },
  {
    "path": "core/cmd/errorgen/gen_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"testing\"\n)\n\nconst bin = \"./testgen\"\n\nvar gobin = \"go\"\n\nfunc TestMain(m *testing.M) {\n\t// Prefer go binary from environment.\n\tif goenv, ok := os.LookupEnv(\"GO\"); ok {\n\t\tgobin = goenv\n\t}\n\n\t// Generate the template source file, if not already done.\n\tout, err := exec.Command(gobin, \"generate\").CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, string(out))\n\t\tfmt.Fprintln(os.Stderr, \"go generate failed:\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Build the testing binary.\n\tout, err = exec.Command(gobin, \"build\", \"-o\", bin).CombinedOutput()\n\tif err != nil {\n\t\tfmt.Fprint(os.Stderr, string(out))\n\t\tfmt.Fprintf(os.Stderr, \"building %s failed: %v\\n\", bin, err)\n\t\tos.Exit(1)\n\t}\n\n\t// Run tests.\n\tos.Unsetenv(\"GOPATH\") // Clear for testing.\n\tcode := m.Run()\n\n\t// Remove the test binary.\n\tos.Remove(bin)\n\tos.Exit(code)\n}\n\n// res is the result of running the test binary.\ntype res struct {\n\tt   *testing.T\n\tout []byte\n}\n\n// testrun runs the test binary and assserts that it succeeds.\nfunc testrun(t *testing.T, args ...string) *res {\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err != nil {\n\t\tt.Logf(string(out))\n\t\tt.Fatal(bin, args, \"failed unexpectedly:\", err)\n\t}\n\treturn &res{t, out}\n}\n\n// testfail runs the test binary and assserts that it fails.\nfunc testfail(t *testing.T, args ...string) *res {\n\tout, err := exec.Command(bin, args...).CombinedOutput()\n\tif err == nil {\n\t\tt.Logf(string(out))\n\t\tt.Fatal(bin, args, \"succeeded unexpectedly\")\n\t}\n\treturn &res{t, out}\n}\n\n// expect checks that the result contains the pattern, reporting an error\n// otherwise.\nfunc (r *res) expect(pattern string) {\n\tif !grep(pattern, r.out) {\n\t\tr.t.Error(\"output does not contain\", pattern)\n\t}\n}\n\n// expectnot checks that the result does NOT contain the pattern, reporting an\n// error otherwise.\nfunc (r *res) expectnot(pattern string) {\n\tif grep(pattern, r.out) {\n\t\tr.t.Error(\"output contains\", pattern)\n\t}\n}\n\n// grep checks if b contains the pattern. Matching is performed per-line.\nfunc grep(pattern string, b []byte) bool {\n\tre := regexp.MustCompile(pattern)\n\tfor _, line := range bytes.Split(b, []byte{'\\n'}) {\n\t\tif re.Match(line) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc TestEmpty(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/empty\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^generating testdata/empty/gen_types(_test)\\.go$`)\n}\n\nfunc TestSingle(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/single\")\n\tdefer os.Remove(\"testdata/single/gen_types.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Single{Field Field2} at`)\n\tr.expect(`^generating testdata/single/gen_types\\.go$`)\n\tr.expectnot(`^generating testdata/single/gen_types_test\\.go$`)\n\n\t// Ensure the generated code compiles.\n\tout, err := exec.Command(gobin, \"build\", \"./testdata/single\").CombinedOutput()\n\tif err != nil {\n\t\tt.Logf(string(out))\n\t\tt.Error(\"building ./testdata/single failed:\", err)\n\t}\n}\n\nfunc TestWithTests(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/withtests\")\n\tdefer os.Remove(\"testdata/withtests/gen_types.go\")\n\tdefer os.Remove(\"testdata/withtests/gen_types_test.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Main{Field} at`)\n\tr.expect(`^found Test{Field} at`)\n\tr.expect(`^generating testdata/withtests/gen_types\\.go$`)\n\tr.expect(`^generating testdata/withtests/gen_types_test\\.go$`)\n}\n\nfunc TestExists(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/exists\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Exists{Field} at`)\n\tr.expect(`^error: testdata/exists/gen_types\\.go already exists`)\n\tr.expectnot(`^generating testdata/exists/gen_types(_test)\\.go$`)\n}\n\nfunc TestDeclared(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/declared\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^found Declared{Field} at`)\n\tr.expectnot(`^generating testdata/declared/gen_types(_test)\\.go$`)\n}\n\nfunc TestLocal(t *testing.T) {\n\tr := testrun(t, \"-v\", \"./testdata/local\")\n\tdefer os.Remove(\"testdata/local/gen_types.go\")\n\tr.expect(`^checking \\.$`)\n\tr.expectnot(`^found InnerLocal{Field} at`)\n\tr.expect(`^found OuterLocal{Field} at`)\n\tr.expect(`^generating testdata/local/gen_types\\.go$`)\n\tr.expectnot(`^generating testdata/local/gen_types_test\\.go$`)\n}\n\nfunc TestDuplicate(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/duplicate\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^found Duplicate{Field} at`)\n\tr.expect(`^error: .* duplicate Duplicate`)\n\tr.expectnot(`^generating testdata/duplicate/gen_types(_test)\\.go$`)\n}\n\nfunc TestNoKeys(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/nokeys\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^error: .* NoKeys must have key:value fields$`)\n\tr.expectnot(`^generating testdata/nokeys/gen_types(_test)\\.go$`)\n}\n\nfunc TestUnexportedKeys(t *testing.T) {\n\tr := testfail(t, \"-v\", \"./testdata/unexportedkeys\")\n\tr.expect(`^checking \\.$`)\n\tr.expect(`^error: .* UnexportedKeys must have exported identifier keys$`)\n\tr.expectnot(`^generating testdata/unexportedkeys/gen_types(_test)\\.go$`)\n}\n"
  },
  {
    "path": "core/cmd/errorgen/generate.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/build\"\n\t\"go/format\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\n// header is added to the gen.tmpl template to identify files created by gen.\nconst header = \"// Generated by tivi.io/core/cmd/codegen/bin/gen. DO NOT EDIT!\\n\\n\"\n\n// funcMap contains the helper functions used in gen.tmpl.\nvar funcMap = template.FuncMap{\n\t\"base\": filepath.Base,\n}\n\n// Generate a Go source file which contains the template in gen.tmpl and parses\n// it into the variable gentmpl. The variable name is given as an argument, so\n// that it can easily be changed here, where it will also be used.\n//go:generate ./gentmpl.sh gentmpl\n\n// generate generates type declarations for the struct literals in pkg.\nfunc generate(pkg *build.Package, literals []literal) (ok bool) {\n\tok = true\n\tmain, test := split(literals)\n\tif len(main) > 0 {\n\t\tok = ok && genfile(pkg, *namep+\".go\", main)\n\t}\n\tif len(test) > 0 {\n\t\tok = ok && genfile(pkg, *namep+\"_test.go\", test)\n\t}\n\treturn\n}\n\nfunc genfile(pkg *build.Package, name string, literals []literal) bool {\n\tpath := rel(filepath.Join(pkg.Dir, name))\n\tfp, err := open(path)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", err)\n\t\treturn false\n\t}\n\tdefer fp.Close()\n\n\tif *vp {\n\t\tfmt.Println(\"generating\", path)\n\t}\n\n\tdata := struct {\n\t\tPackage  *build.Package\n\t\tLiterals []literal\n\t}{\n\t\tpkg,\n\t\tliterals,\n\t}\n\n\t// Write to both a memory buffer and the result file: this way have the\n\t// result in memory for formatting, but also any Go formatting errors\n\t// will refer to an actually existing file, greatly reducing debugging\n\t// complexity.\n\tvar buf bytes.Buffer\n\ttee := io.MultiWriter(&buf, fp)\n\tif err = gentmpl.Execute(tee, data); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: template execution:\", err)\n\t\treturn false\n\t}\n\n\tformatted, err := format.Source(buf.Bytes())\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: format generated code: %s:%v\\n\", path, err)\n\t\treturn false\n\t}\n\n\t// Rewind, now writing formatted code.\n\tif _, err = fp.Seek(0, io.SeekStart); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: rewind\", path, \"for formatting:\", err)\n\t\treturn false\n\t}\n\n\tif _, err = fp.Write(formatted); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: write generated code %s: %v\\n\", path, err)\n\t\treturn false\n\t}\n\n\t// Truncate fp in case it already contained a larger file when opened\n\t// or if the formatted code is shorter that unformatted.\n\tif err = fp.Truncate(int64(len(formatted))); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"truncate %s to %d: %v\\n\", path, len(formatted), err)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// split partitions the literals into two sets: ones from regular Go source\n// files and ones from Go test files.\nfunc split(literals []literal) (main []literal, test []literal) {\n\tfor _, l := range literals {\n\t\tif strings.HasSuffix(l.Pos.Filename, \"_test.go\") {\n\t\t\ttest = append(test, l)\n\t\t} else {\n\t\t\tmain = append(main, l)\n\t\t}\n\t}\n\treturn\n}\n\n// open attempts to create the path. If a file already exists there, then it\n// will check if it is safe to overwrite.\nfunc open(path string) (fp *os.File, err error) {\n\t// Attempt to open the path for reading and writing.\n\tvar created bool\nloop:\n\tfp, err = os.OpenFile(path, os.O_RDWR, 0)\n\tif err != nil {\n\t\t// If it does not exist, attempt to create it, otherwise fail.\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn\n\t\t}\n\t\tfp, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)\n\t\tif err != nil {\n\t\t\t// If it now does exist, retry from beginning.\n\t\t\tif os.IsExist(err) {\n\t\t\t\tgoto loop\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tcreated = true\n\t}\n\n\t// If we did not create the file, ensure that it contains our header,\n\t// so it is safe to overwrite, and rewind.\n\tif !created {\n\t\thbuf := make([]byte, len(header))\n\t\t_, err := io.ReadFull(fp, hbuf)\n\t\tif err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {\n\t\t\treturn nil, fmt.Errorf(\"read %s header: %v\", path, err)\n\t\t}\n\t\tif string(hbuf) != header {\n\t\t\treturn nil, fmt.Errorf(\"%s already exists and is not generated, \"+\n\t\t\t\t\"not overwriting\", path)\n\t\t}\n\t\tif _, err = fp.Seek(0, io.SeekStart); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"rewind %s: %v\", path, err)\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/cmd/errorgen/gentmpl.sh",
    "content": "#!/bin/sh -eu\n\n# gentmpl.sh: Generate a Go source file with a template file inlined. This way\n# we can use template-specific syntax-highlighting when editing the template,\n# yet still embed it into the final binary.\n\ntmpl=\"gen.tmpl\"\ngo=\"gentmpl.go\" # NB! if \"gen.tmpl.go\", then go clean will delete gen.tmpl.\nvar=\"$1\"\n\n# Use a variable for the header of the vim modeline used in the generated file\n# so that vim will not pick it up and apply to this file.\nvimhdr=\"vim\"\n\ncat > \"$go\" <<HERE\n// Generated by gentmpl.sh. DO NOT EDIT!\n\npackage main\n\nimport \"text/template\"\n\nvar $var = template.Must(template.New(\"$var\").Funcs(funcMap).Parse(header + \\`\nHERE\n\ncat \"$tmpl\" >> \"$go\"\ncat >> \"$go\" <<HERE\n\\`))\n\n// Override the modeline in the template.\n// $vimhdr: set ft=go:\nHERE\n"
  },
  {
    "path": "core/cmd/errorgen/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"go/build\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n)\n\nfunc usage() {\n\tfmt.Fprintln(os.Stderr, \"usage:\", os.Args[0], `[options] [packages...]\n\ngen searches Go packages for source files with struct literals that have\nundeclared exported types from the same package and attempts to generate the\nstruct declaration. Declarations are put in output files named gen_types.go and\ngen_types_test.go in the same package, configurable via --name.\n\nPackages are either absolute or relative (beginning with a . or .. element)\npaths to package directories, or names of packages that will be searched for in\nGOPATH. If no packages are provided, then the package in the current directory\nis processed. If a package contains one or more \"...\" wildcards, then it will\nemulate the behavior of the go application (see 'go help packages').\n\noptions:`)\n\tflag.PrintDefaults()\n}\n\nvar (\n\tnamep = flag.String(\"name\", \"gen_types\", \"base name of output files\")\n\tvp    = flag.Bool(\"v\", false, \"verbose output\")\n\tbase  = flag.String(\"base\", \"\", \"project root\")\n\n\tpwd string\n)\n\nfunc main() {\n\tflag.Usage = usage\n\tflag.Parse()\n\tpaths := flag.Args()\n\tif len(paths) == 0 {\n\t\t// Default to current directory.\n\t\tpaths = []string{\".\"}\n\t}\n\n\tvar err error\n\tpwd, err = os.Getwd()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error: cannot get current working directory:\", err)\n\t\tos.Exit(1)\n\t}\n\tpkgs, ok := resolve(*base, paths)\n\tif !ok {\n\t\t// Errors have already been logged by resolve.\n\t\tos.Exit(1)\n\t}\n\tif len(pkgs) == 0 {\n\t\t// Wildcards matched no packages: this is not necessarily an\n\t\t// error so just return.\n\t\treturn\n\t}\n\n\t// Simply start a parse-walk-generate goroutine for each package: there\n\t// will not be so many source files that we need to set up a worker\n\t// pool with a pipeline.\n\tvar wg sync.WaitGroup\n\tvar lock sync.Mutex // Used to synchronize access to ok.\n\tfor _, pkg := range pkgs {\n\t\tif *vp {\n\t\t\tfmt.Println(\"checking\", pkg.ImportPath)\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(pkg *build.Package) {\n\t\t\tpok := pwg(pkg)\n\t\t\tlock.Lock()\n\t\t\tok = ok && pok\n\t\t\tlock.Unlock()\n\t\t\twg.Done()\n\t\t}(pkg)\n\t}\n\twg.Wait()\n\tif !ok {\n\t\t// Errors have already been logged by pwg.\n\t\tos.Exit(1)\n\t}\n}\n\n// pwg parses the package AST, walks it looking for struct literals that need\n// generation, and generates the missing type declarations.\nfunc pwg(pkg *build.Package) (ok bool) {\n\tfset, parsed, ok := parse(pkg)\n\tif !ok {\n\t\treturn\n\t}\n\tliterals, ok := walk(pkg.ImportPath, fset, parsed)\n\tif !ok {\n\t\treturn\n\t}\n\tif len(literals) > 0 {\n\t\tok = generate(pkg, literals)\n\t}\n\treturn\n}\n\n// rel attempts to convert the path to a relative path from the current working\n// directory.\nfunc rel(path string) string {\n\tif r, err := filepath.Rel(pwd, path); err == nil {\n\t\treturn r\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "core/cmd/errorgen/resolve.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"go/build\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// resolve resolves package import paths into packages. It emulates the \"...\"\n// wilcard behavior of matchPackages in cmd/go/main.go.\nfunc resolve(base string, paths []string) (pkgs []*build.Package, ok bool) {\n\tok = true\n\tfor _, p := range paths {\n\t\t// Check if local import and if so, make absolute so that\n\t\t// build.Import can determine the package import path.\n\t\tlocal := filepath.IsAbs(p) || build.IsLocalImport(filepath.ToSlash(p))\n\t\tpath := p\n\t\tif local && !filepath.IsAbs(p) {\n\t\t\tpath = filepath.Join(pwd, p)\n\t\t}\n\t\t// Search for packages matching the wildcards.\n\t\tif strings.Contains(p, \"...\") {\n\t\t\t// List of source directories to search.\n\t\t\tsrcdirs := []string{\"\"}\n\t\t\tif !local {\n\t\t\t\tsrcdirs = build.Default.SrcDirs()\n\t\t\t\tpath = filepath.FromSlash(p)\n\t\t\t}\n\t\t\tbefore := len(pkgs)\n\t\t\tfor _, src := range srcdirs {\n\t\t\t\texpanded, expok := expand(path, src, base)\n\t\t\t\tpkgs = append(pkgs, expanded...)\n\t\t\t\tok = ok && expok\n\t\t\t}\n\t\t\tif len(pkgs) == before {\n\t\t\t\tfmt.Fprintln(os.Stderr, \"warning: no packages match\", p)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\t// Attempt to import non-wildcard package.\n\t\timp, src := p, \"\"\n\t\tif local {\n\t\t\timp, src = \".\", path // Import package \".\" from abs(p).\n\t\t}\n\t\tpkg, err := build.Import(imp, src, 0)\n\t\tpkg.ImportPath = strings.TrimPrefix(imp, base)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"error: package %s: %v\\n\", p, err)\n\t\t\tok = false\n\t\t\tcontinue\n\t\t}\n\t\tpkgs = append(pkgs, pkg)\n\t}\n\treturn\n}\n\n// expand expands \"...\" wildcards in the import path p into packages rooted at\n// the given directory.\nfunc expand(p, src, base string) (expanded []*build.Package, ok bool) {\n\tok = true\n\n\t// Clean src and p, and append the non-wildcard prefix of p to src for\n\t// the walking root directory.\n\tif len(src) > 0 {\n\t\tsrc = filepath.Clean(src) + string(filepath.Separator)\n\t}\n\tp = filepath.Clean(p)\n\tdir := src + p[:strings.Index(p, \"...\")]\n\n\t// Compile a regular expression from the slash-separated path with\n\t// \"...\" wildcards.\n\tre := regexp.QuoteMeta(filepath.ToSlash(p))\n\tre = strings.ReplaceAll(re, `\\.\\.\\.`, \".*\")\n\tif strings.HasSuffix(re, \"/.*\") {\n\t\t// Special case: \"foo\" matches \"foo/...\".\n\t\tre = re[:len(re)-len(\"/.*\")] + \"(/.*)?\"\n\t}\n\tmatch := regexp.MustCompile(\"^\" + re + \"$\").MatchString\n\n\t// Walk the directory looking for paths that match.\n\t//\n\t//nolint:errcheck // Only returns nil.\n\tfilepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {\n\t\t// Ignore faulty or non-directory files.\n\t\tif err != nil || !fi.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Ignore \"testdata\" and directories that start with a dot or underscore.\n\t\tname := fi.Name()\n\t\tif strings.HasPrefix(name, \".\") || strings.HasPrefix(name, \"_\") || name == \"testdata\" {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\t// Convert the path to the format expected by match: remove the\n\t\t// src prefix and convert to slash-separated path.\n\t\timp := filepath.ToSlash(path[len(src):])\n\n\t\t// Check if the path matches the pattern and attempt to import\n\t\t// the package, ignoring non-Go directories that match.\n\t\tif match(imp) {\n\t\t\tpkg, err := build.ImportDir(path, 0)\n\n\t\t\tpkg.ImportPath = strings.TrimPrefix(path, base)\n\t\t\tif err != nil {\n\t\t\t\tif _, nogo := err.(*build.NoGoError); nogo {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tfmt.Fprintf(os.Stderr, \"error: matched package %s: %v\\n\", rel(path), err)\n\t\t\t\tok = false\n\t\t\t}\n\t\t\texpanded = append(expanded, pkg)\n\t\t}\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/declared/declared.go",
    "content": "// Package declared tests that gen does not match already declared types.\npackage declared\n\ntype Declared struct {\n\tField int\n}\n\nvar x = Declared{Field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/duplicate/duplicate.go",
    "content": "// Package duplicate tests that gen reports duplicate literals.\npackage duplicate\n\nvar (\n\tx = Duplicate{Field: 0}\n\ty = Duplicate{Field: 0}\n)\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/empty/empty.go",
    "content": "// Package empty tests that gen does not create the output file if no literals\n// were found.\npackage empty\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/exists/exists.go",
    "content": "// Package exists tests that gen does not overwrite already existing files.\npackage exists\n\nvar x = Exists{Field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/local/local.go",
    "content": "// Package local tests that gen does not match locally declared types.\npackage local\n\nfunc x() {\n\ttype InnerLocal struct {\n\t\tField int\n\t}\n\n\ttype OuterLocal struct {\n\t\tField int\n\t}\n\n\t_ = InnerLocal{Field: 0}\n}\n\nvar _ = OuterLocal{Field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/nokeys/nokeys.go",
    "content": "// Package nokeys tests that gen reports literals with non-key:value fields.\npackage nokeys\n\nvar x = NoKeys{0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/single/single.go",
    "content": "// Package single is a simple smoke test that checks if gen works.\npackage single\n\nvar x = Single{Field: 0, Field2: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/unexportedkeys/unexportedkeys.go",
    "content": "// Package unexportedkeys tests that gen reports literals with unexported field\n// identifiers.\npackage unexportedkeys\n\nvar x = UnexportedKeys{field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/withtests/withtests.go",
    "content": "// Package withtests tests that literals found in test files are written to a\n// test file.\npackage withtests\n\nvar x = Main{Field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/testdata/withtests/withtests_test.go",
    "content": "package withtests\n\nvar x = Test{Field: 0}\n"
  },
  {
    "path": "core/cmd/errorgen/walk.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/build\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// parse parses the AST of the package.\nfunc parse(pkg *build.Package) (fset *token.FileSet, parsed *ast.Package, ok bool) {\n\tfset = token.NewFileSet()\n\tpkgmap, err := parser.ParseDir(fset, pkg.Dir, nil, 0)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: could not parse %s: %v\\n\", pkg.ImportPath, err)\n\t\treturn\n\t}\n\tparsed, ok = pkgmap[pkg.Name]\n\tif !ok {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", rel(pkg.Dir), \"does not contain package\", pkg.Name)\n\t}\n\treturn\n}\n\ntype literal struct {\n\tPos    token.Position\n\tName   string\n\tFields []string\n}\n\nfunc (l literal) String() string {\n\treturn fmt.Sprintf(\"%s{%s}\", l.Name, strings.Join(l.Fields, \" \"))\n}\n\n// walk walks the package AST looking for struct literals with undeclared\n// exported local package types and key:value fields.\nfunc walk(path string, fset *token.FileSet, pkg *ast.Package) (literals []literal, ok bool) {\n\tscope := pkg.Scope\n\tif scope == nil {\n\t\t// Collect all file scopes.\n\t\tscope = ast.NewScope(nil)\n\t\tfor name, file := range pkg.Files {\n\t\t\t// Skip the scope of files that are generated by us.\n\t\t\t// Otherwise when a new literal is added, the old ones\n\t\t\t// generated by us will no longer be included.\n\t\t\tbase := filepath.Base(name)\n\t\t\tif base == *namep+\".go\" || base == *namep+\"_test.go\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, obj := range file.Scope.Objects {\n\t\t\t\t// Do not care if the scope already contained\n\t\t\t\t// an object with this name: it still means\n\t\t\t\t// that the name is declared.\n\t\t\t\tscope.Insert(obj)\n\t\t\t}\n\t\t}\n\t}\n\n\tok = true\n\tseen := make(map[string]token.Position)\n\tvar blocklvl int\n\tvar inspector func(ast.Node) bool // Declare for recursion.\n\tinspector = func(node ast.Node) (recurse bool) {\n\t\trecurse = true\n\t\tif node == nil {\n\t\t\treturn\n\t\t}\n\n\t\t// For block statements, create a new block scope, increase\n\t\t// block level, and inspect statements in the block ourselves.\n\t\tif block, match := node.(*ast.BlockStmt); match {\n\t\t\tscope = ast.NewScope(scope)\n\t\t\tblocklvl++\n\t\t\tfor _, stmt := range block.List {\n\t\t\t\tast.Inspect(stmt, inspector)\n\t\t\t}\n\t\t\tblocklvl--\n\t\t\tscope = scope.Outer\n\t\t\treturn false // We already recursed.\n\t\t}\n\n\t\t// If we are inside a block, add type declarations to the scope.\n\t\tif spec, match := node.(*ast.TypeSpec); match && blocklvl > 0 {\n\t\t\tscope.Insert(ast.NewObj(ast.Typ, spec.Name.Name))\n\t\t\treturn\n\t\t}\n\n\t\t// Return as soon as a condition is not met: avoid deep nesting\n\t\t// of control blocks.\n\t\tcomplit, match := node.(*ast.CompositeLit) // Composite literal.\n\t\tif !match {\n\t\t\treturn\n\t\t}\n\t\tident, match := complit.Type.(*ast.Ident) // With simple identifier type.\n\t\tif !match {\n\t\t\treturn\n\t\t}\n\t\tif !ident.IsExported() { // That is exported.\n\t\t\treturn\n\t\t}\n\t\tfor s := scope; s != nil; s = s.Outer { // And does not exist in the scope.\n\t\t\tif s.Lookup(ident.Name) != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// Check if we have already seen this name before.\n\t\tname := ident.Name\n\t\tpos := fset.Position(ident.NamePos)\n\t\tif prev, dup := seen[ident.Name]; dup {\n\t\t\tfmt.Fprintf(os.Stderr, \"error: %s: duplicate %s, \"+\n\t\t\t\t\"previously seen at %s\\n\", relpos(pos), name, prev)\n\t\t\tok = false\n\t\t\treturn\n\t\t}\n\t\tseen[name] = relpos(pos)\n\n\t\t// Check the fields of the literal and add it to the result.\n\t\tl := literal{Pos: pos, Name: name}\n\t\tfor _, el := range complit.Elts {\n\t\t\tkv, match := el.(*ast.KeyValueExpr)\n\t\t\tif !match {\n\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\"must have key:value fields\"})\n\t\t\t\tok = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tkey, match := kv.Key.(*ast.Ident)\n\t\t\tif !match || !key.IsExported() {\n\t\t\t\tfmt.Fprintln(os.Stderr, fieldError{\n\t\t\t\t\trelpos(fset.Position(el.Pos())), ident.Name,\n\t\t\t\t\t\"must have exported identifier keys\"})\n\t\t\t\tok = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tl.Fields = append(l.Fields, key.Name)\n\t\t}\n\t\tliterals = append(literals, l)\n\n\t\tif *vp {\n\t\t\tvar pre string\n\t\t\tif path != \".\" {\n\t\t\t\tpre = path + \".\"\n\t\t\t}\n\t\t\tfmt.Printf(\"found %s%s at %s\\n\", pre, l, relpos(l.Pos))\n\t\t}\n\t\treturn\n\t}\n\tast.Inspect(pkg, inspector)\n\treturn\n}\n\n// relpos sets the filename in p to rel(p).\nfunc relpos(p token.Position) token.Position {\n\tp.Filename = rel(p.Filename)\n\treturn p\n}\n\ntype fieldError struct {\n\tpos  token.Position\n\tname string\n\terr  string\n}\n\nfunc (e fieldError) Error() string {\n\treturn fmt.Sprintf(\"error: %s: undeclared struct literal %s %s\", e.pos, e.name, e.err)\n}\n"
  },
  {
    "path": "core/cmd/ts/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n\n\t\"tivi.io/core/crypto/timestamp\"\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst (\n\tsuccess int = iota\n\targError\n\tappError\n)\n\nconst appName = \"ts\"\n\nconst (\n\trequestCmd = \"request\"\n\tverifyCmd  = \"verify\"\n)\n\nvar cmds []string = []string{requestCmd, verifyCmd}\n\nfunc parseFlag() (string, []string, string, uint64, uint64, string, error) {\n\tflags := flag.NewFlagSet(appName, flag.ContinueOnError)\n\tif len(os.Args) < 2 {\n\t\tfmt.Fprintf(flags.Output(), \"No command specified. Valid commands are: %s\\n\", strings.Join(cmds, \", \"))\n\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"no command\")\n\t}\n\tcommand := os.Args[1]\n\tif command != requestCmd && command != verifyCmd {\n\t\tfmt.Fprintf(flags.Output(), \"'%s' is not valid command. Valid commands are: %s\\n\", command, strings.Join(cmds, \", \"))\n\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"invalid command\")\n\t}\n\tflags.Usage = func() {\n\t\tfmt.Fprintf(flags.Output(), \"\\nUsage:\\n\\n\")\n\t\tcommonFlags := \"[-data filepath] [-cert filepath] [-delay seconds] [-retries number] [-nonce number] [-out filepath]\"\n\t\tif command == requestCmd {\n\t\t\tfmt.Fprintf(flags.Output(), \"\\t%s %s [-tsa url] %s\\n\", appName, requestCmd, commonFlags)\n\t\t\tfmt.Fprint(flags.Output(), \"\\nDescription:\\n\\n\\tSends a timestamp request to the TSA and verifies the response against the data, nonce and certificate provided.\\n\")\n\t\t} else if command == verifyCmd {\n\t\t\tfmt.Fprintf(flags.Output(), \"\\t%s %s [-der filepath] %s\\n\", appName, verifyCmd, commonFlags)\n\t\t\tfmt.Fprint(flags.Output(), \"\\nDescription:\\n\\n\\tVerifies a DER-format timestamp response against the data, nonce and certificate provided.\\n\")\n\t\t}\n\t\tfmt.Fprintln(flags.Output(), \"\\nFlags:\")\n\t\tflags.PrintDefaults()\n\t}\n\tvar tsa, derTimestamp, data, certs, nonce, out string\n\tvar delay, retries uint64\n\tif command == requestCmd {\n\t\tflags.StringVar(&tsa, \"tsa\", \"\", \"the url of the TSA\")\n\t} else if command == verifyCmd {\n\t\tflags.StringVar(&derTimestamp, \"der\", \"\", \"the timestamp response in DER format\")\n\t}\n\tflags.StringVar(&data, \"data\", \"\", \"filepath of the data to be timestamped\")\n\tflags.StringVar(&certs, \"certs\", \"\", \"filepath(s) (separated by ':') of the certificate(s) to be included in both the request and response\")\n\tflags.StringVar(&nonce, \"nonce\", \"\", \"if not provided, a pseudo-random number is included in the request\")\n\tflags.Uint64Var(&delay, \"delay\", 1, \"maximum time that gen time and sign time can differ\")\n\tflags.Uint64Var(&retries, \"retries\", 2, \"number of retries in case of failure\")\n\tflags.StringVar(&out, \"out\", \"\", \"filepath of the output file (default to stdout)\")\n\tif err := flags.Parse(os.Args[2:]); err != nil {\n\t\treturn \"\", nil, \"\", 0, 0, \"\", err\n\t}\n\targs := make([]string, 3)\n\tif command == requestCmd {\n\t\tif tsa == \"\" {\n\t\t\tfmt.Fprintf(flags.Output(), \"%s %s: no TSA url provided\\n\", appName, command)\n\t\t\tflags.Usage()\n\t\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"argument missing\")\n\t\t}\n\t\targs[0] = tsa\n\t} else if command == verifyCmd {\n\t\tif derTimestamp == \"\" {\n\t\t\tfmt.Fprintf(flags.Output(), \"%s %s: no DER timestamp response filepath provided\\n\", appName, command)\n\t\t\tflags.Usage()\n\t\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"argument missing\")\n\t\t}\n\t\targs[0] = derTimestamp\n\t}\n\tif data == \"\" {\n\t\tfmt.Fprintf(flags.Output(), \"%s %s: no data filepath provided\\n\", appName, command)\n\t\tflags.Usage()\n\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"argument missing\")\n\t}\n\targs[1] = data\n\tif certs == \"\" {\n\t\tfmt.Fprintf(flags.Output(), \"%s %s: no certificate filepath provided\\n\", appName, command)\n\t\tflags.Usage()\n\t\treturn \"\", nil, \"\", 0, 0, \"\", errors.New(\"argument missing\")\n\t}\n\targs[2] = certs\n\treturn command, args, nonce, delay, retries, out, nil\n}\n\nfunc appmain() (result int) {\n\tcommand, args, n, delay, retries, out, err := parseFlag()\n\tif err != nil {\n\t\treturn argError\n\t}\n\tdata, err := ioutil.ReadFile(args[1])\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed reading data file at %s: %v\\n\", args[1], err)\n\t\treturn appError\n\t}\n\tfcerts := strings.Split(args[2], \":\")\n\tcerts := make([]*x509.Certificate, len(fcerts))\n\tfor i, fcert := range fcerts {\n\t\tpem, err := ioutil.ReadFile(fcert)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed reading PEM file at '%s': %v\\n\", fcert, err)\n\t\t\treturn appError\n\t\t}\n\t\tcert, err := util.GetPEMCertificate(pem)\n\t\tif err != nil {\n\t\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\t\treturn appError\n\t\t}\n\t\tcerts[i] = cert\n\t}\n\tvar nonce []byte\n\tif n == \"\" {\n\t\tnonce = nil\n\t} else {\n\t\tnonce = []byte(n)\n\t}\n\tif out == \"\" {\n\t\tout = os.Stdout.Name()\n\t}\n\tconf := timestamp.Conf{URL: args[0], Signers: certs, DelayTime: delay, Retries: retries}\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed creating timestamp client: %v\\n\", err)\n\t\treturn appError\n\t}\n\tswitch command {\n\tcase requestCmd:\n\t\tresult, err := timestamp.RequestTimestamp(context.Background(), data, nonce, conf)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed requesting timestamp: %v\\n\", err)\n\t\t\treturn appError\n\t\t}\n\t\tfmt.Fprintf(os.Stdout, \"request successful: \")\n\t\tif out != os.Stdout.Name() {\n\t\t\tfmt.Fprintf(os.Stdout, \"output written to '%s'\\n\", out)\n\t\t} else {\n\t\t\tfmt.Fprintln(os.Stdout)\n\t\t}\n\t\terr = ioutil.WriteFile(out, result, 0600)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed writing output of request timestamp: %v\\n\", err)\n\t\t\treturn appError\n\t\t}\n\tcase verifyCmd:\n\t\tder, err := ioutil.ReadFile(args[0])\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed reading DER response file at %s: %v\\n\", args[0], err)\n\t\t\treturn appError\n\t\t}\n\t\tresult, err := timestamp.VerifyTimestamp(der, data, nonce, conf)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed verifying timestamp: %v\\n\", err)\n\t\t\treturn appError\n\t\t}\n\t\tfmt.Fprintf(os.Stdout, \"verification successful: \")\n\t\tif out != os.Stdout.Name() {\n\t\t\tfmt.Fprintf(os.Stdout, \"output written to '%s'\\n\", out)\n\t\t}\n\t\terr = ioutil.WriteFile(out, []byte(result.String()), 0600)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"failed writing output of verify timestamp: %v\\n\", err)\n\t\t\treturn appError\n\t\t}\n\t}\n\treturn success\n}\n\nfunc main() {\n\tos.Exit(appmain())\n}\n"
  },
  {
    "path": "core/common/files/files.go",
    "content": "package files\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"syscall\"\n)\n\n// Copy copies src file to the dest location.\nfunc Copy(src, dest string) (err error) {\n\tvar in *os.File\n\tif in, err = os.Open(src); err != nil {\n\t\treturn\n\t}\n\tdefer in.Close()\n\treturn WriteFile(in, dest)\n}\n\n// IsFile checks that the path exists, is accessible and is a file.\nfunc IsFile(path string) error {\n\tfi, err := os.Stat(path)\n\tif err == nil && fi != nil && fi.IsDir() {\n\t\terr = fmt.Errorf(\"Path is a directory, not a file: %s\", path)\n\t}\n\treturn err\n}\n\n// IsDir checks if the filesystem path exists and is a directory.\nfunc IsDir(path string) error {\n\tstat, err := os.Stat(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !stat.IsDir() {\n\t\treturn &os.PathError{Op: \"isdir\", Path: path, Err: syscall.ENOTDIR}\n\t}\n\treturn nil\n}\n\n// CleanDir removes the directory at the filesystem path if it exists and then\n// creates it and any missing parents with permission bits 0700 (before umask).\nfunc CleanDir(path string) error {\n\tif err := os.RemoveAll(path); err != nil {\n\t\treturn err\n\t}\n\treturn os.MkdirAll(path, 0700)\n}\n\n// WriteFile reads the data from the reader and writes it into the specified file.\nfunc WriteFile(data io.Reader, path string) (err error) {\n\tif err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {\n\t\treturn\n\t}\n\tvar out *os.File\n\tif out, err = os.Create(path); err != nil {\n\t\treturn\n\t}\n\tdefer out.Close()\n\t_, err = io.Copy(out, data)\n\treturn\n}\n\n// LineByLine calls the specified function for all lines in the file\nfunc LineByLine(path string, f func(string) error) (err error) {\n\tvar file *os.File\n\tif file, err = os.Open(path); err != nil {\n\t\treturn\n\t}\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tif err = f(scanner.Text()); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn scanner.Err()\n}\n"
  },
  {
    "path": "core/common/tar/tar.go",
    "content": "package tar\n\nimport (\n\t\"archive/tar\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"tivi.io/core/common/files\"\n)\n\ntype Exclusions []string\n\nfunc NewExclusions(s ...string) Exclusions {\n\texc := make(Exclusions, len(s))\n\ti := 0\n\tfor _, v := range s {\n\t\texc[i] = v\n\t\ti++\n\t}\n\treturn exc\n}\n\nfunc (e Exclusions) Contains(x string) bool {\n\tfor _, v := range e {\n\t\tif v == x {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc CompressFile(path string, tarball *tar.Writer, info os.FileInfo, prefix string) error {\n\n\theader, err := tar.FileInfoHeader(info, info.Name())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theader.Name = strings.TrimPrefix(path, prefix)\n\n\tif err := tarball.WriteHeader(header); err != nil {\n\t\treturn err\n\t}\n\n\tif info.IsDir() {\n\t\treturn nil\n\t}\n\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\t_, err = io.Copy(tarball, file)\n\treturn err\n\n}\n\nfunc CompressDirectory(inputDir string, tarball *tar.Writer, exc Exclusions) (err error) {\n\treturn filepath.Walk(inputDir,\n\t\tfunc(path string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\textension := filepath.Ext(path)\n\n\t\t\tif exc.Contains(extension) {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn CompressFile(path, tarball, info, inputDir)\n\t\t})\n}\n\nfunc Compress(path string, buf io.Writer, exc Exclusions) (err error) {\n\n\ttarball := tar.NewWriter(buf)\n\tdefer func() {\n\t\terr = tarball.Close()\n\t}()\n\n\tif files.IsDir(path) == nil {\n\t\treturn CompressDirectory(path, tarball, exc)\n\t}\n\n\tvar info os.FileInfo\n\tinfo, err = os.Stat(path)\n\tif err != nil {\n\t\treturn\n\t}\n\t// TODO - exclusion of the single file\n\treturn CompressFile(path, tarball, info, filepath.Dir(path))\n}\n\nfunc Create(inputDir string, tarName string, exc Exclusions) (err error) {\n\n\ttarfile, err := os.Create(tarName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\terr = tarfile.Close()\n\t}()\n\n\treturn Compress(inputDir, tarfile, exc)\n}\n\n// Extract extracts the data as tar into the specified destination.\nfunc Extract(data io.Reader, dest string) (err error) {\n\t// If results directory exists, assume it is already extracted\n\tvar fi os.FileInfo\n\tif fi, err = os.Stat(dest); err == nil && fi.IsDir() {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\t// Cleanup the extracted directory in case of an error\n\t\tif err != nil {\n\t\t\tif tmpErr := os.RemoveAll(dest); tmpErr != nil {\n\t\t\t\terr = fmt.Errorf(\"failed to cleanup after error: %w\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\tif err = os.MkdirAll(dest, 0700); err != nil {\n\t\treturn err\n\t}\n\n\ttr := tar.NewReader(data)\n\tfor {\n\t\tvar hdr *tar.Header\n\t\tif hdr, err = tr.Next(); err == io.EOF {\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\treturn\n\t\t}\n\t\tpath := filepath.Join(dest, hdr.Name)\n\t\tif hdr.FileInfo().IsDir() {\n\t\t\tif err = os.MkdirAll(path, 0700); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t} else if err = files.WriteFile(tr, path); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ExtractToDir(src string, dest string) (err error) {\n\n\tsrcfile, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\terr = srcfile.Close()\n\t}()\n\n\treturn Extract(srcfile, dest)\n}\n"
  },
  {
    "path": "core/common/tar/tar_test.go",
    "content": "package tar\n\nimport (\n\t\"testing\"\n)\n\nfunc TestTarDir(t *testing.T) {\n\n\texc := NewExclusions(\".pfx\", \".json\")\n\n\tif len(exc) != 2 {\n\t\tt.Fatalf(\"Invalid length\")\n\t}\n\n\tif !exc.Contains(\".pfx\") {\n\t\tt.Fatalf(\"Should exclude .pfx\")\n\t}\n\n\tif exc.Contains(\".txt\") {\n\t\tt.Fatalf(\"Should not exclude .txt\")\n\t}\n\n\tif exc.Contains(\"\") {\n\t\tt.Fatalf(\"Should not exclude empty extension\")\n\t}\n}\n"
  },
  {
    "path": "core/crypto/asn1/asn1.go",
    "content": "package asn1\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n)\n\n// identified is an ASN.1 OCTET STRING with an algorithm identifier that identifies data.\ntype identified struct {\n\tAlgorithm pkix.AlgorithmIdentifier\n\tData      asn1.RawValue\n}\n\n// Pack packs multiple byte slices into an ASN.1 structure, so that they can be\n// unambiguously distinguished from each other and avoid message-extension\n// attacks.\n//\n// Pack returns ASN.1 OCTET STRING.\nfunc Pack(data ...[]byte) ([]byte, error) {\n\treturn asn1.Marshal(data)\n}\n\n// AddIdentifier adds identifier to data, DER marshals it along with data and returns it.\nfunc AddIdentifier(identifier asn1.ObjectIdentifier, data []byte) (der []byte, err error) {\n\treturn asn1.Marshal(identified{\n\t\tAlgorithm: pkix.AlgorithmIdentifier{Algorithm: identifier},\n\t\tData:      asn1.RawValue{FullBytes: data},\n\t})\n}\n\n// ParseIdentifier returns identifier that was parsed from data and data without\n// identifier.\nfunc ParseIdentifier(data []byte) (asn1.ObjectIdentifier, []byte, error) {\n\tvar parsed identified\n\trest, err := asn1.Unmarshal(data, &parsed)\n\tif err != nil {\n\t\treturn nil, nil, ParseIdentifierASN1UnmarshalError{Err: err}\n\t}\n\n\tif len(rest) > 0 {\n\t\treturn nil, nil, ParseIdentifierASN1UnmarshalTrailingBytesError{}\n\t}\n\n\tif len(parsed.Algorithm.Parameters.FullBytes) > 0 {\n\t\treturn nil, nil, ParseIdentifierASN1UnmarshalNoParamsExpectedError{}\n\t}\n\n\treturn parsed.Algorithm.Algorithm, parsed.Data.FullBytes, nil\n}\n\n// Concat adds all data bytes into a ASN.1 SEQUENCE OF and returns it.\nfunc Concat(data ...[]byte) (concatenated []byte, err error) {\n\tvar sequenceOf []asn1.RawValue\n\tfor _, item := range data {\n\t\tif len(item) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(sequenceOf) > 0 && item[0] != sequenceOf[0].FullBytes[0] {\n\t\t\treturn nil, ConcatTagMismatch{\n\t\t\t\tTag:      fmt.Sprintf(\"0x%x\", item[0]),\n\t\t\t\tExpected: fmt.Sprintf(\"0x%x\", sequenceOf[0].FullBytes[0]),\n\t\t\t}\n\t\t}\n\n\t\tsequenceOf = append(sequenceOf, asn1.RawValue{FullBytes: item})\n\t}\n\n\tsequenceOfBytes, err := asn1.Marshal(sequenceOf)\n\tif err != nil {\n\t\treturn nil, ConcatASN1MarshalError{Err: err}\n\t}\n\n\treturn sequenceOfBytes, nil\n}\n\n// Split splits ASN.1 SEQUENCE OF into individual data packs.\nfunc Split(concatenated []byte) (split [][]byte, err error) {\n\t// We can't use a slice of RawValues, so take the long way\n\tvar raw asn1.RawValue\n\trest, err := asn1.Unmarshal(concatenated, &raw)\n\tif err != nil {\n\t\treturn nil, SplitASN1UnmarshalError{Err: err}\n\t}\n\n\t// Trailing bytes?\n\tif len(rest) > 0 {\n\t\treturn nil, SplitASN1UnmarshalTrailingBytesError{Err: err}\n\t}\n\n\tsequenceOf := raw.Bytes\n\n\tfor {\n\t\tsequenceOf, err = asn1.Unmarshal(sequenceOf, &raw)\n\t\tif err != nil {\n\t\t\treturn nil, SplitItemError{\n\t\t\t\tIndex: len(split),\n\t\t\t\tErr:   err,\n\t\t\t}\n\t\t}\n\n\t\tif len(split) > 0 && raw.FullBytes[0] != split[0][0] {\n\t\t\treturn nil, SplitTagMismatchError{\n\t\t\t\tTag:      fmt.Sprintf(\"0x%x\", raw.FullBytes[0]),\n\t\t\t\tExpected: fmt.Sprintf(\"0x%x\", split[0][0]),\n\t\t\t}\n\t\t}\n\n\t\tsplit = append(split, raw.FullBytes)\n\n\t\tif len(sequenceOf) == 0 {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc HexN(bytes []byte, n int) string {\n\tif len(bytes) < n {\n\t\tn = len(bytes)\n\t}\n\treturn fmt.Sprintf(\"%x\", bytes[:n])\n}\n"
  },
  {
    "path": "core/crypto/cades/cades-b.go",
    "content": "package cades\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"tivi.io/core/crypto/cms\"\n)\n\nconst idSigningTime = \"1.2.840.113549.1.9.5\"\n\nvar signedAttrs = map[string]cms.AttrToVerify{\n\tidSigningTime: {\n\t\tAttributeID: idSigningTime,\n\t\t// The section 11.3 of the RFC 5652 - Cryptographic Message Syntax\n\t\t// https://www.rfc-editor.org/rfc/rfc5652#section-11.3\n\t\t// specifies the details of the Signing Time attribute, in terms of\n\t\t// its possible encoding formats or values (UTC or Generalized Time).\n\t\t// The golang library asn1.go already parses it according to the\n\t\t// detected type and related constraints. So, everything is already\n\t\t// handled in terms of necessary verifications, just by calling the\n\t\t// asn1.Unmarshal function.\n\t\tVerifyCallback: func(b []byte) error {\n\t\t\tvar signingTime time.Time\n\t\t\trest, err := asn1.Unmarshal(b, &signingTime)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unmarshal signing time: %w\", err)\n\t\t\t}\n\t\t\tif len(rest) > 0 {\n\t\t\t\treturn fmt.Errorf(\"unmarshal signing time excess bytes: %d\", len(rest))\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t},\n}\n\n// VerifySignedData verifies the signed data according to the CAdES-b level,\n// matching it to the signers' certificates provided.\n// Returns the data that was signed.\nfunc CAdESBVerifySignedData(data []byte, certs []*x509.Certificate, detachedData []byte) error {\n\terr := cms.VerifySignedData(data, certs, detachedData, signedAttrs, map[string]cms.AttrToVerify{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cms verify signed data: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc cadesBVerifySignedData(data []byte, certs []*x509.Certificate, detachedData []byte, otherSignedAttrs, otherUnsignedAttrs map[string]cms.AttrToVerify) error {\n\tfor k, v := range otherSignedAttrs {\n\t\tsignedAttrs[k] = v\n\t}\n\terr := cms.VerifySignedData(data, certs, detachedData, signedAttrs, otherUnsignedAttrs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cms verify signed data: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/crypto/cades/cades-t.go",
    "content": "package cades\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\n\t\"tivi.io/core/crypto/cms\"\n\t\"tivi.io/core/crypto/timestamp\"\n)\n\nconst idSignatureTimestamp = \"1.2.840.113549.1.9.16.2.14\"\n\nvar otherUnsignedAttrs = make(map[string]cms.AttrToVerify, 0)\n\nfunc CAdESTVerifySignedData(signedData []byte, certs []*x509.Certificate, detachedData, nonce []byte) error {\n\totherUnsignedAttrs[idSignatureTimestamp] = cms.AttrToVerify{\n\t\tAttributeID: idSignatureTimestamp,\n\t\tVerifyCallback: func(b []byte) error {\n\t\t\tvar ctInfo cms.ContentInfo\n\t\t\trest, err := asn1.Unmarshal(signedData, &ctInfo)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unmarshal content info: %s\", err)\n\t\t\t}\n\t\t\tif len(rest) > 0 {\n\t\t\t\treturn fmt.Errorf(\"unmarshal content info excess bytes: %d\", len(rest))\n\t\t\t}\n\t\t\tfor _, signer := range ctInfo.Content.SignerInfos {\n\t\t\t\t_, err := timestamp.VerifyTimestamp(b, signer.Signature, nonce, timestamp.Conf{Signers: certs})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"verify timestamp: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\treturn cadesBVerifySignedData(signedData, certs, detachedData, map[string]cms.AttrToVerify{}, otherUnsignedAttrs)\n}\n"
  },
  {
    "path": "core/crypto/cades/cades_test.go",
    "content": "package cades\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"io/ioutil\"\n\t\"testing\"\n\n\t\"tivi.io/core/crypto/cms\"\n\t\"tivi.io/core/crypto/util\"\n)\n\nfunc TestCAdESBVerifySignedData(t *testing.T) {\n\ttype test struct {\n\t\tdescription  string\n\t\tcert         string // Filepath of the Certificate\n\t\tsignedData   string // Filepath of the SignedData to be verified\n\t\toriginalData string // Filepath of the original data\n\t\tinvalid      bool\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\tdescription:  \"valid signed data\",\n\t\t\tcert:         \"../cms/testdata/trusted.pem\",\n\t\t\tsignedData:   \"../cms/testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"../cms/testdata/test1.txt\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"not valid signed data\",\n\t\t\tcert:        \"../cms/testdata/trusted.pem\",\n\t\t\tsignedData:  \"../cms/testdata/test2.txt.pk7\",\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"not valid certificate\",\n\t\t\tcert:        \"../cms/testdata/nottrusted.pem\",\n\t\t\tsignedData:  \"../cms/testdata/test1.txt.pk7\",\n\t\t\tinvalid:     true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tsignedData, err := ioutil.ReadFile(tc.signedData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read signed data: %s\", err)\n\t\t\t}\n\t\t\tpem, err := ioutil.ReadFile(tc.cert)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read PEM file: %s\", err)\n\t\t\t}\n\t\t\tcert, err := util.GetPEMCertificate(pem)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"get PEM certificate: %s\", err)\n\t\t\t}\n\t\t\tcerts := []*x509.Certificate{cert}\n\t\t\terr = CAdESBVerifySignedData(signedData, certs, nil)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t\tdata, err := cms.GetDataEContent(signedData)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\toriginalData, err := ioutil.ReadFile(tc.originalData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read original data: %s\", err)\n\t\t\t}\n\t\t\tif !bytes.Equal(originalData, data) {\n\t\t\t\tt.Fatalf(\"original data not equal to data content\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCAdESTVerifySignedData(t *testing.T) {\n\ttype test struct {\n\t\tdescription  string\n\t\tcerts        []string // Filepath of the Certificate\n\t\tsignedData   string   // Filepath of the SignedData to be verified\n\t\toriginalData string   // Filepath of the original data\n\t\tnonce        []byte\n\t\tinvalid      bool\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\tdescription: \"valid signed data\",\n\t\t\tcerts: []string{\n\t\t\t\t\"../timestamp/testdata/DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem\",\n\t\t\t\t\"./testdata/demo-ca-cert.pem\",\n\t\t\t\t\"./testdata/demo-user-cert.pem\",\n\t\t\t},\n\t\t\tnonce:        []byte{1, 128, 179, 189, 0, 230},\n\t\t\tsignedData:   \"./testdata/plain-signed-attr.cms\",\n\t\t\toriginalData: \"./testdata/plain-unsigned.txt\",\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tsignedData, err := ioutil.ReadFile(tc.signedData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read signed data: %s\", err)\n\t\t\t}\n\t\t\tvar certs []*x509.Certificate\n\t\t\tfor _, c := range tc.certs {\n\t\t\t\tpem, err := ioutil.ReadFile(c)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read PEM file: %s\", err)\n\t\t\t\t}\n\t\t\t\tcert, err := util.GetPEMCertificate(pem)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"get PEM certificate: %s\", err)\n\t\t\t\t}\n\t\t\t\tcerts = append(certs, cert)\n\t\t\t}\n\t\t\toriginalData, err := ioutil.ReadFile(tc.originalData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read signed data: %s\", err)\n\t\t\t}\n\t\t\terr = CAdESTVerifySignedData(signedData, certs, originalData, tc.nonce)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/crypto/cades/testdata/demo-ca-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICuzCCAaOgAwIBAgIUXCMHAwbaByKcrMj9/5AYjRkCWc8wDQYJKoZIhvcNAQEL\nBQAwDTELMAkGA1UEAwwCQ0EwHhcNMjIwNTExMTUyMjUxWhcNMzIwNTA4MTUyMjUx\nWjANMQswCQYDVQQDDAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAKv08JOxagaPj6cVjCsTpxCUcPy9OiRtp5pTwvjutkSGQ1KkQWU3Ns3Xo8pZZDg6\n3dXjzbK/9f61yGkBz3RK2MMeWxKwkIjFOwpTZ/vvxQ5HxDe+eG9nJwkB6dP9WY+e\n980dlZj8qemJPx0TYn8v8a2s2EjOc1ngDeoAy1cR/3og7LRGTJBA51LJKZPAHfUc\nGJ/469xp6Ni8z+pJFsaAk/0ZgBE93p95rAbkdMb2yOt9GJ7xISloJePU+y1JEmNx\nIe3uB0W3cy+lU3qm+Zoo2/jubB3l4zq89HPoMFgJFt95gXjrSlzve+G6zLJR4Uny\nUqs44B1A+aVEtgBZzDFlJZkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq\nhkiG9w0BAQsFAAOCAQEApd2RiVlkZeG5RINyACuHJY92/GIi2iQYto8rSD6Mtq5a\nPjiWXXii5hNiglKfR4nVeXNCmcQD+clh6rkxGbaOFO/givsYZShNcN5KwXG3LU0N\nombObKyndrGQ68XXAadmUJhnWN1CHyWwuxJ2ctExRaC6Zcq3l78N5l2lzTrCZpRL\nXYOWMbpTj11+OrnqFUFbLNVNo2Tf03fwgKCtClk3KsyhB7KZuCP/pqu7N/3ku8QA\nksE4PUENPUEmzdRUwIKCn3wjdfi7lAVK/W/VYFTYxCnNlznZCphnHLuOMCjigtZl\ndzDpV/cXkj4eCrMinY9b548pH9mzYXiBHz91MSnDug==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/cades/testdata/demo-user-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC6jCCAdKgAwIBAgIRAMnTJthSzUlNgYPan+T86d4wDQYJKoZIhvcNAQELBQAw\nDTELMAkGA1UEAwwCQ0EwHhcNMjIwNTExMTUyMjUxWhcNMjMwNTExMTUyMjUxWjAR\nMQ8wDQYDVQQDDAZVU0VSIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQDfEYUf4saqfhv/SFnMPV708rJoaIclMCvKG8NXdw452YHnfyQeia4KUW/7Z4gb\no6u3ZTeWkseL0DXuZJ1a4KE1XGtE0SpeP47fktoeOamGxfd75yM8CEuRx1sn+YkN\nmCCVXwzvodaYhEZUlFY5nKWeSNCO1qIs4baogPAtDLhVjrkdmXLXxauTIuDyQIXJ\nLEsi9qepWFapXKVM5fptU3o6dT9hYNiA6VHzqfXq0nu4kpN+gq0OYFKhsM0XiKVK\n6Un4GLZnq2PCfFekCUKUdXFRC62Q1Tg3tIu12ro32futKVktViN7Vhc/6/4GPOgI\nRX9KQ/vtv530dJntymYc7isVAgMBAAGjQTA/MA4GA1UdDwEB/wQEAwIF4DAMBgNV\nHRMBAf8EAjAAMB8GA1UdIwQYMBaAFLddsuryozuGCNaAmlvNgAHNHckPMA0GCSqG\nSIb3DQEBCwUAA4IBAQALoOP18wWn200bYIoyWB2DWgN7gjO7phgELnlWszlkegmq\nvADR+8qNkfCm2KQrEB8sEZ3puFgKummsHrRgV9fSDXzdFLUpGGIVW4B5tQxMSi5u\nr7KLeiqypJUaRNYxnQYfQOmL5wylSHxPJhV/f5assJcK1l0tFYDBAdtwA9Jf8K79\npZPXuefMRiJGGQdYzsPP1VHwSSAyV+t+u8KkVfhE/GCXDWAKT095GDZ74rXCHuj/\nEWxS/bUXvyBIwh5M9SMMn3aO5YSIsv76TVSI37FC+NEm5f+6BeE5UZrK7nhdokLf\nWFquWVfHPCR2r3+FFiQ4xp3Ag7PGmR/8Nbv0FNQ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/cades/testdata/plain-unsigned.txt",
    "content": "content\n"
  },
  {
    "path": "core/crypto/cms/cms.go",
    "content": "package cms\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst (\n\tidSHA256 = \"2.16.840.1.101.3.4.2.1\"\n\tidSHA384 = \"2.16.840.1.101.3.4.2.2\"\n\tidSHA512 = \"2.16.840.1.101.3.4.2.3\"\n)\n\nvar (\n\tidContentTypeData       = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}\n\tidContentTypeSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}\n\tdigestAlgorithms        = map[string]crypto.Hash{\n\t\tidSHA256: crypto.SHA256,\n\t\tidSHA384: crypto.SHA384,\n\t\tidSHA512: crypto.SHA512,\n\t}\n)\n\n// VerifySignedData verifies the signed data, matching it to the signers' certificates provided.\n// It also accepts a set of additional signed and unsigned attributes to be verified.\n// Returns the data that was signed.\nfunc VerifySignedData(derSignedData []byte, certs []*x509.Certificate, detachedData []byte, otherSignedAttrs, otherUnsignedAttrs map[string]AttrToVerify) error {\n\tsignedData, err := unmarshalSignedData(derSignedData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signed data: %w\", err)\n\t}\n\t// There may be multiple independent signers, f.e., in CAdES\n\t// https://datatracker.ietf.org/doc/html/rfc5126#section-5.6\n\tif len(signedData.SignerInfos) == 0 {\n\t\treturn fmt.Errorf(\"missing signers' info\")\n\t}\n\tfor _, signerInfo := range signedData.SignerInfos {\n\t\t// Get the local certificate of the signer\n\t\tcert, err := getCertificate(signerInfo, certs)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"find certificate: %w\", err)\n\t\t}\n\t\t// VerifyAll if the same certificate comes in the response\n\t\tvar certInResponse bool\n\t\tfor _, sCert := range signedData.Certificates {\n\t\t\tif bytes.Equal(cert.Raw, sCert.RawContent) {\n\t\t\t\tcertInResponse = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !certInResponse {\n\t\t\treturn fmt.Errorf(\"missing signed certificate\")\n\t\t}\n\t\tif err = verifySignedAttributes(signerInfo, signedData.EncapContentInfo, cert, detachedData, otherSignedAttrs); err != nil {\n\t\t\treturn fmt.Errorf(\"verify signed attributes: %w\", err)\n\t\t}\n\t\tif err = verifyUnsignedAttributes(signerInfo, otherUnsignedAttrs); err != nil {\n\t\t\treturn fmt.Errorf(\"verify unsigned attributes: %w\", err)\n\t\t}\n\t\tif err = verifySignature(signerInfo, cert); err != nil {\n\t\t\treturn fmt.Errorf(\"verify signature: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetDataEContent unmarshals a DER encoded signedData container and\n// returns the Encapsulated Content, if there is any attached data.\nfunc GetDataEContent(derSignedData []byte) ([]byte, error) {\n\tsignedData, err := unmarshalSignedData(derSignedData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal signed data: %w\", err)\n\t}\n\teContent := signedData.EncapContentInfo.EContent\n\tif eContent == nil {\n\t\treturn nil, fmt.Errorf(\"no attached data\")\n\t}\n\treturn eContent, nil\n}\n\nfunc unmarshalSignedData(derSignedData []byte) (SignedData, error) {\n\tvar ctInfo ContentInfo\n\trest, err := asn1.Unmarshal(derSignedData, &ctInfo)\n\tif err != nil {\n\t\treturn SignedData{}, fmt.Errorf(\"unmarshal content info: %s\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn SignedData{}, fmt.Errorf(\"unmarshal content info excess bytes: %d\", len(rest))\n\t}\n\tif !ctInfo.ContentType.Equal(idContentTypeSignedData) {\n\t\treturn SignedData{}, fmt.Errorf(\"bad content info content type: wanted '%s' got '%s'\", idContentTypeSignedData, ctInfo.ContentType)\n\t}\n\tsignedData := ctInfo.Content\n\tversion := getVersion(signedData.EncapContentInfo)\n\tif signedData.Version != version {\n\t\treturn SignedData{}, fmt.Errorf(\"bad signed data version: wanted '%d' got '%d'\", version, signedData.Version)\n\t}\n\treturn signedData, nil\n}\n\n// https://tools.ietf.org/html/rfc5652#page-10\n// encapContentInfo eContentType is other than id-data, then version MUST be 3\n// else version is 1\nfunc getVersion(encap EncapsulatedContentInfo) int {\n\tif !encap.EContentType.Equal(idContentTypeData) {\n\t\treturn 3\n\t}\n\treturn 1\n}\n\nfunc getCertificate(signer SignerInfo, certs []*x509.Certificate) (*x509.Certificate, error) {\n\t// https://tools.ietf.org/html/rfc5652#page-14\n\tswitch signer.Version {\n\tcase 1:\n\t\tissuer := signer.IssuerAndSerialNumber.Issuer\n\t\tserial := signer.IssuerAndSerialNumber.SerialNumber\n\t\tif len(issuer) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"version 1 missing issuer\")\n\t\t}\n\t\tfor _, cert := range certs {\n\t\t\tif hasIssuerSerial(cert, issuer, serial) {\n\t\t\t\treturn cert, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, fmt.Errorf(\"certificate not found: issuer '%s' serial '%s'\", issuer, serial)\n\tcase 3:\n\t\tif len(signer.SubjectKeyIdentifier) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"version 3 missing subject key identifier\")\n\t\t}\n\t\tfor _, cert := range certs {\n\t\t\tif bytes.Equal(signer.SubjectKeyIdentifier, cert.SubjectKeyId) {\n\t\t\t\treturn cert, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, fmt.Errorf(\"certificate not found: ski '%s'\", string(signer.SubjectKeyIdentifier))\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"bad signer info version: wanted '%d' or '%d' got '%d'\", 1, 3, signer.Version)\n\t}\n}\n\nfunc hasIssuerSerial(cert *x509.Certificate, issuer pkix.RDNSequence, serial *big.Int) bool {\n\t// add all parsed non-standard names to serialized RDN sequence\n\tcert.Issuer.ExtraNames = cert.Issuer.Names\n\treturn cert.SerialNumber.Cmp(serial) == 0 && util.IsRDNSequenceEqual(cert.Issuer.ToRDNSequence(), issuer)\n}\n\nconst (\n\t// https://tools.ietf.org/html/rfc5652#section-11\n\tidContentType   = \"1.2.840.113549.1.9.3\"\n\tidMessageDigest = \"1.2.840.113549.1.9.4\"\n\tidSigningTime   = \"1.2.840.113549.1.9.5\"\n\n\t// https://tools.ietf.org/html/rfc2634#section-5.4\n\tidSigningCert = \"1.2.840.113549.1.9.16.2.12\"\n\n\t// https://tools.ietf.org/html/rfc5035#section-3\n\tidSigningCertV2 = \"1.2.840.113549.1.9.16.2.47\"\n\n\t// https://tools.ietf.org/html/rfc6211#section-2\n\tidCMSAlgorithmProtection = \"1.2.840.113549.1.9.52\"\n)\n\nfunc verifySignedAttributes(signer SignerInfo, encap EncapsulatedContentInfo, cert *x509.Certificate, detachedData []byte, other map[string]AttrToVerify) error {\n\tattributes := make(map[string]bool)\n\tfor _, attr := range signer.SignedAttrs {\n\t\tattributeID := attr.AttrType.String()\n\t\tif attributes[attributeID] {\n\t\t\treturn fmt.Errorf(\"found duplicate signed attribute: %s\", attributeID)\n\t\t}\n\t\tattributes[attributeID] = true\n\t\t// Extract the ASN.1 universal tag\n\t\ttag := attr.AttrValue.FullBytes[0] & 31\n\t\t// All attribute values required to be a SET with a single entry\n\t\tif tag != asn1.TagSet {\n\t\t\treturn fmt.Errorf(\"attribute value not a set: %b\", tag)\n\t\t}\n\t\tvalue := attr.AttrValue.Bytes\n\t\t// 1. The mandatory CMS signed attributes are the content-type and message-digest.\n\t\t// 2. If some optional attribute is in the data, it is up to the user\n\t\t//    of the API to provide a callback. Nothing shall happen if the\n\t\t//    optional attribute is in the data, but the client chose not to\n\t\t//    verify it, f.e., as it can happen with the signingTime.\n\t\t// 3. If the user provides a callback for an attribute that is missing,\n\t\t//    there shall be an error - detected after the switch statement.\n\t\tswitch attributeID {\n\t\tcase other[attributeID].AttributeID:\n\t\t\tif err := other[attributeID].VerifyCallback(value); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify other attribute with id '%s': %w\", attributeID, err)\n\t\t\t}\n\t\tcase idContentType:\n\t\t\tif err := verifyContentType(value, encap.EContentType); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify content type: %w\", err)\n\t\t\t}\n\t\tcase idMessageDigest:\n\t\t\tdata := encap.EContent\n\t\t\tif data == nil {\n\t\t\t\tif detachedData == nil {\n\t\t\t\t\treturn fmt.Errorf(\"missing detached data to be verified\")\n\t\t\t\t}\n\t\t\t\tdata = detachedData\n\t\t\t} else if detachedData != nil {\n\t\t\t\tif len(data) != len(detachedData) {\n\t\t\t\t\treturn fmt.Errorf(\"detached data length mismatches embedded data\")\n\t\t\t\t}\n\t\t\t\tif !bytes.Equal(data, detachedData) {\n\t\t\t\t\treturn fmt.Errorf(\"detached data content mismatches embedded data\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := verifyMessageDigest(value, signer.DigestAlgorithm, data); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify message digest: %w\", err)\n\t\t\t}\n\t\tcase idSigningCert, idSigningCertV2:\n\t\t\tif err := verifySigningCertificate(value, cert, attributeID == idSigningCertV2); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify signing certificate: %w\", err)\n\t\t\t}\n\t\tcase idCMSAlgorithmProtection:\n\t\t\tif err := verifyCMSAlgorithmProtection(value, signer); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify cms algorithm protection: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\tif !attributes[idContentType] {\n\t\treturn fmt.Errorf(\"missing signed content type\")\n\t}\n\tif !attributes[idMessageDigest] {\n\t\treturn fmt.Errorf(\"missing signed message digest\")\n\t}\n\tfor k := range other {\n\t\tif !attributes[k] {\n\t\t\treturn fmt.Errorf(\"missing attribute with id '%s'\", k)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc verifyContentType(value []byte, encapType asn1.ObjectIdentifier) error {\n\tvar oid asn1.ObjectIdentifier\n\trest, err := asn1.Unmarshal(value, &oid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal object identifier: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal object identifier excess bytes: %d\", len(rest))\n\t}\n\tif !oid.Equal(encapType) {\n\t\treturn fmt.Errorf(\"object identifier not equal to encap type: object identifier '%s' encap type '%s'\", oid, encapType)\n\t}\n\treturn nil\n}\n\nfunc verifyMessageDigest(value []byte, alg pkix.AlgorithmIdentifier, data []byte) error {\n\tvar digest []byte\n\trest, err := asn1.Unmarshal(value, &digest)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal message digest: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal message digest excess bytes: %d\", len(rest))\n\t}\n\tcHash, ok := digestAlgorithms[alg.Algorithm.String()]\n\tif !ok {\n\t\treturn fmt.Errorf(\"unsupported digest algorithm: %s\", alg.Algorithm)\n\t}\n\thash := cHash.New()\n\thash.Write(data)\n\tcalculated := hash.Sum(nil)\n\tif !bytes.Equal(digest, calculated) {\n\t\treturn fmt.Errorf(\"different hashed messages with algorithm: %s\", alg.Algorithm)\n\t}\n\treturn nil\n}\n\nfunc verifySigningCertificate(value []byte, cert *x509.Certificate, v2 bool) error {\n\t// SigningCertificateV2 structure is backwards-compatible to V1\n\tvar signingCert SigningCertificateV2\n\trest, err := asn1.Unmarshal(value, &signingCert)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signing certificate: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal signing certificate excess bytes: %d\", len(rest))\n\t}\n\tif len(signingCert.Certs) == 0 {\n\t\treturn fmt.Errorf(\"no signing certificate certs attribute\")\n\t}\n\t// https://tools.ietf.org/html/rfc5035#section-3\n\t// \"The first certificate identified in the sequence of certificate\n\t// identifiers MUST be the certificate used to verify the signature.\"\n\tessCert := signingCert.Certs[0]\n\thashOID := essCert.HashAlgorithm.Algorithm\n\tvar cHash crypto.Hash\n\tif v2 {\n\t\t// SigningCertificateV2 hash algorithm defaults to SHA-256\n\t\t// but can be explicitly provided\n\t\tcHash = crypto.SHA256\n\t\tif hashOID != nil {\n\t\t\tvar ok bool\n\t\t\tif cHash, ok = digestAlgorithms[hashOID.String()]; !ok {\n\t\t\t\treturn fmt.Errorf(\"unsupported digest algorithm: %s\", hashOID.String())\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// SigningCertificateV1 hash algorithm defaults to SHA-1\n\t\t// and no algorithm must be provided\n\t\tcHash = crypto.SHA1\n\t\tif hashOID != nil {\n\t\t\treturn fmt.Errorf(\"(signing certificate V1) no algorithm should be provided: got '%s'\", hashOID.String())\n\t\t}\n\t}\n\thash := cHash.New()\n\thash.Write(cert.Raw)\n\tcertHash := hash.Sum(nil)\n\tif !bytes.Equal(certHash, essCert.CertHash) {\n\t\treturn fmt.Errorf(\"different certificate hashes with algorithm: %s\", hashOID.String())\n\t}\n\tissuer := essCert.IssuerAndSerialNumber.Issuer.DirectoryName\n\tserial := essCert.IssuerAndSerialNumber.SerialNumber\n\t// If present, verify the correct issuer and serial number\n\tif len(issuer) > 0 {\n\t\tif !hasIssuerSerial(cert, issuer, serial) {\n\t\t\treturn fmt.Errorf(\"issuer and serial not found: issuer '%s' serial' '%s'\", issuer, serial)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc verifyCMSAlgorithmProtection(value []byte, signer SignerInfo) error {\n\tvar protection CMSAlgorithmProtection\n\trest, err := asn1.Unmarshal(value, &protection)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal cms algorithm protection: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal cms algorithm protection excess bytes: %d\", len(rest))\n\t}\n\tif !util.AlgorithmIdentifierCmp(signer.DigestAlgorithm, protection.DigestAlgorithm) {\n\t\treturn fmt.Errorf(\"different digest algorithms: signer '%s' protection '%s'\", signer.DigestAlgorithm.Algorithm, protection.DigestAlgorithm.Algorithm)\n\t}\n\tif !util.AlgorithmIdentifierCmp(signer.SignatureAlgorithm, protection.SignatureAlgorithm) {\n\t\treturn fmt.Errorf(\"different signature algorithms: signer '%s' protection '%s'\", signer.SignatureAlgorithm.Algorithm, protection.SignatureAlgorithm.Algorithm)\n\t}\n\treturn nil\n}\n\nfunc verifyUnsignedAttributes(signer SignerInfo, unsignedAttrs map[string]AttrToVerify) error {\n\tattributes := make(map[string]bool)\n\tfor _, attr := range signer.UnsignedAttrs {\n\t\tattributeID := attr.AttrType.String()\n\t\tif attributes[attributeID] {\n\t\t\treturn fmt.Errorf(\"found duplicate unsigned attribute: %s\", attributeID)\n\t\t}\n\t\tattributes[attributeID] = true\n\t\t// Extract the ASN.1 universal tag\n\t\ttag := attr.AttrValue.FullBytes[0] & 31\n\t\t// All attribute values required to be a SET with a single entry\n\t\tif tag != asn1.TagSet {\n\t\t\treturn fmt.Errorf(\"attribute value not a set: %b\", tag)\n\t\t}\n\t\tvalue := attr.AttrValue.Bytes\n\t\tif attr, ok := unsignedAttrs[attributeID]; ok {\n\t\t\tif err := attr.VerifyCallback(value); err != nil {\n\t\t\t\treturn fmt.Errorf(\"verify other attribute with id '%s': %w\", attributeID, err)\n\t\t\t}\n\t\t}\n\t}\n\tfor k := range unsignedAttrs {\n\t\tif !attributes[k] {\n\t\t\treturn fmt.Errorf(\"missing attribute with id '%s'\", k)\n\t\t}\n\t}\n\treturn nil\n}\n\nconst (\n\tidRSAEncryption = \"1.2.840.113549.1.1.1\"\n\n\tidSHA256WithRSAPPS = \"1.2.840.113549.1.1.10\"\n\n\tidSHA256WithRSAEncryption = \"1.2.840.113549.1.1.11\"\n\tidSHA384WithRSAEncryption = \"1.2.840.113549.1.1.12\"\n\tidSHA512WithRSAEncryption = \"1.2.840.113549.1.1.13\"\n\n\tidECDSAWithSHA256 = \"1.2.840.10045.4.3.2\"\n\tidECDSAWithSHA384 = \"1.2.840.10045.4.3.3\"\n\tidECDSAWithSHA512 = \"1.2.840.10045.4.3.4\"\n)\n\nvar (\n\tsignatureAlgorithms = map[string]x509.SignatureAlgorithm{\n\t\tidRSAEncryption: x509.SHA256WithRSA,\n\n\t\tidSHA256WithRSAPPS: x509.SHA256WithRSAPSS,\n\n\t\tidSHA256WithRSAEncryption: x509.SHA256WithRSA,\n\t\tidSHA384WithRSAEncryption: x509.SHA384WithRSA,\n\t\tidSHA512WithRSAEncryption: x509.SHA512WithRSA,\n\n\t\tidECDSAWithSHA256: x509.ECDSAWithSHA256,\n\t\tidECDSAWithSHA384: x509.ECDSAWithSHA384,\n\t\tidECDSAWithSHA512: x509.ECDSAWithSHA512,\n\t}\n\n\tsignatureDigestOIDs = map[string]string{\n\t\tidRSAEncryption: idSHA256,\n\n\t\tidSHA256WithRSAPPS: idSHA256,\n\n\t\tidSHA256WithRSAEncryption: idSHA256,\n\t\tidSHA384WithRSAEncryption: idSHA384,\n\t\tidSHA512WithRSAEncryption: idSHA512,\n\n\t\tidECDSAWithSHA256: idSHA256,\n\t\tidECDSAWithSHA384: idSHA384,\n\t\tidECDSAWithSHA512: idSHA512,\n\t}\n)\n\nfunc verifySignature(signer SignerInfo, cert *x509.Certificate) error {\n\tif len(signer.Signature) == 0 {\n\t\treturn fmt.Errorf(\"no signature found\")\n\t}\n\tsignatureOID := signer.SignatureAlgorithm.Algorithm.String()\n\talgorithm, ok := signatureAlgorithms[signatureOID]\n\tif !ok {\n\t\treturn fmt.Errorf(\"signature algorithm not supported: %s\", signatureOID)\n\t}\n\tif signer.DigestAlgorithm.Algorithm.String() != signatureDigestOIDs[signatureOID] {\n\t\treturn fmt.Errorf(\"different signature algorithms: digest '%s' signature '%s'\", signer.DigestAlgorithm.Algorithm, signatureOID)\n\t}\n\tvar err error\n\tvar content []byte\n\tif content, err = asn1.Marshal(signer.SignedAttrs); err != nil {\n\t\treturn fmt.Errorf(\"marshal signed attributes: %w\", err)\n\t}\n\t// https://tools.ietf.org/html/rfc5652#section-5.4\n\tcontent[0] = 49\n\tif err = cert.CheckSignature(algorithm, content, signer.Signature); err != nil {\n\t\treturn fmt.Errorf(\"check certificate signature: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/crypto/cms/cms_test.go",
    "content": "package cms\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"testing\"\n\t\"time\"\n\n\t\"tivi.io/core/crypto/util\"\n)\n\nfunc TestVerifySignedData(t *testing.T) {\n\ttype test struct {\n\t\tdescription      string\n\t\tcert             string // Filepath of the Certificate\n\t\tsignedData       string // Filepath of the SignedData to be verified\n\t\tdetachedData     string // Filepath of the detached data\n\t\toriginalData     string // Filepath of the original data\n\t\totherSignedAttrs map[string]AttrToVerify\n\t\tunsignedAttrs    map[string]AttrToVerify\n\t\tinvalid          bool\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\tdescription:  \"valid signed data with no callback\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t},\n\t\t{\n\t\t\tdescription:  \"valid signed data with example 'past' callback\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t\totherSignedAttrs: map[string]AttrToVerify{\n\t\t\t\tidSigningTime: {\n\t\t\t\t\tAttributeID:    idSigningTime,\n\t\t\t\t\tVerifyCallback: func(b []byte) error { return verifySigningTimePast(b) }}},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"valid signed data with example 'before' callback\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t\totherSignedAttrs: map[string]AttrToVerify{\n\t\t\t\tidSigningTime: {\n\t\t\t\t\tAttributeID: idSigningTime,\n\t\t\t\t\tVerifyCallback: func(b []byte) error {\n\t\t\t\t\t\treturn verifySigningTimeBefore(b, time.Date(2022, 5, 1, 11, 11, 11, 11, time.UTC))\n\t\t\t\t\t}}},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"valid signed data with example 'between' callback\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t\totherSignedAttrs: map[string]AttrToVerify{\n\t\t\t\tidSigningTime: {\n\t\t\t\t\tAttributeID: idSigningTime,\n\t\t\t\t\tVerifyCallback: func(b []byte) error {\n\t\t\t\t\t\treturn verifySigningTimeBetween(b, time.Date(2002, 5, 1, 11, 11, 11, 11, time.UTC), time.Date(2022, 5, 1, 11, 11, 11, 11, time.UTC))\n\t\t\t\t\t}}},\n\t\t},\n\t\t{\n\t\t\tdescription: \"not valid signed data\",\n\t\t\tcert:        \"./testdata/trusted.pem\",\n\t\t\tsignedData:  \"./testdata/test2.txt.pk7\",\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"not valid certificate\",\n\t\t\tcert:        \"./testdata/nottrusted.pem\",\n\t\t\tsignedData:  \"./testdata/test1.txt.pk7\",\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription:  \"valid signed data but missing attribute\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t\tunsignedAttrs: map[string]AttrToVerify{\n\t\t\t\tidSigningTime: {\n\t\t\t\t\tAttributeID: idSigningTime,\n\t\t\t\t\tVerifyCallback: func(b []byte) error {\n\t\t\t\t\t\treturn verifySigningTimeBetween(b, time.Date(2002, 5, 1, 11, 11, 11, 11, time.UTC), time.Date(2022, 5, 1, 11, 11, 11, 11, time.UTC))\n\t\t\t\t\t}}},\n\t\t\tinvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription:  \"valid signed data but mismatch attached and detached data\",\n\t\t\tcert:         \"./testdata/trusted.pem\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\tdetachedData: \"./testdata/test2.txt.pk7\",\n\t\t\tinvalid:      true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tsignedData, err := ioutil.ReadFile(tc.signedData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read signed data: %s\", err)\n\t\t\t}\n\t\t\tpem, err := ioutil.ReadFile(tc.cert)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read PEM file: %s\", err)\n\t\t\t}\n\t\t\tcert, err := util.GetPEMCertificate(pem)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"get PEM certificate: %s\", err)\n\t\t\t}\n\t\t\tcerts := []*x509.Certificate{cert}\n\t\t\tvar detachedData []byte\n\t\t\tif tc.detachedData != \"\" {\n\t\t\t\tdetachedData, err = ioutil.ReadFile(tc.detachedData)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"read detached data file: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = VerifySignedData(signedData, certs, detachedData, tc.otherSignedAttrs, tc.unsignedAttrs)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t\tdata, err := GetDataEContent(signedData)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\toriginalData, err := ioutil.ReadFile(tc.originalData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read original data: %s\", err)\n\t\t\t}\n\t\t\tif !bytes.Equal(originalData, data) {\n\t\t\t\tt.Fatalf(\"original data not equal to data content\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetDataEContent(t *testing.T) {\n\ttype test struct {\n\t\tdescription  string\n\t\tsignedData   string // Filepath of the SignedData to be verified\n\t\toriginalData string // Filepath of the original data\n\t\tinvalid      bool\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\tdescription:  \"valid signed data\",\n\t\t\tsignedData:   \"./testdata/test1.txt.pk7\",\n\t\t\toriginalData: \"./testdata/test1.txt\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid signed data with no attached data\",\n\t\t\tsignedData:  \"../cades/testdata/plain-signed-attr.cms\",\n\t\t\tinvalid:     true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tsignedData, err := ioutil.ReadFile(tc.signedData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read signed data: %s\", err)\n\t\t\t}\n\t\t\tdata, err := GetDataEContent(signedData)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t\toriginalData, err := ioutil.ReadFile(tc.originalData)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"read original data: %s\", err)\n\t\t\t}\n\t\t\tif !bytes.Equal(originalData, data) {\n\t\t\t\tt.Fatalf(\"original data not equal to data content\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// verifySigningTimePast is an example of a VerifyCallback\n// for an optional AttrToVerify. This method simply verifies\n// if the SigningTime attribute is in the past, related to the moment\n// of the verification of the signed data.\nfunc verifySigningTimePast(value []byte) error {\n\tvar signingTime time.Time\n\trest, err := asn1.Unmarshal(value, &signingTime)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signing time: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal signing time excess bytes: %d\", len(rest))\n\t}\n\tdiff := time.Since(signingTime)\n\tif diff <= 0 {\n\t\treturn fmt.Errorf(\"signing time '%s' in the future\", signingTime)\n\t}\n\treturn nil\n}\n\n// verifySigningTimeBefore is an example of a VerifyCallback\n// for an optional AttrToVerify. This method simply verifies\n// if the SigningTime attribute is before some specific time.\nfunc verifySigningTimeBefore(value []byte, t time.Time) error {\n\tvar signingTime time.Time\n\trest, err := asn1.Unmarshal(value, &signingTime)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signing time: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal signing time excess bytes: %d\", len(rest))\n\t}\n\tif !signingTime.Before(t) {\n\t\treturn fmt.Errorf(\"signing time '%s' not before time '%s'\", signingTime, t)\n\t}\n\treturn nil\n}\n\n// verifySigningTimeBefore is an example of a VerifyCallback\n// for an optional AttrToVerify. This method simply verifies\n// if the SigningTime attribute is between two specific times: aT < signingTime < bT.\nfunc verifySigningTimeBetween(value []byte, aT time.Time, bT time.Time) error {\n\tvar signingTime time.Time\n\trest, err := asn1.Unmarshal(value, &signingTime)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signing time: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal signing time excess bytes: %d\", len(rest))\n\t}\n\tif !(aT.Before(signingTime) && signingTime.Before(bT)) {\n\t\treturn fmt.Errorf(\"signing time '%s' not between '%s' and '%s'\", signingTime, aT, bT)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/crypto/cms/conf.go",
    "content": "package cms\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n)\n\n// https://www.rfc-editor.org/rfc/rfc5652#section-3\n// ContentInfo\n//     -- contentType is id-signedData\n//     -- content is SignedData\ntype ContentInfo struct {\n\tRaw         asn1.RawContent\n\tContentType asn1.ObjectIdentifier\n\tContent     SignedData `asn1:\"explicit,tag:0\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.1\ntype SignedData struct {\n\tVersion          int\n\tDigestAlgorithms []pkix.AlgorithmIdentifier `asn1:\"set\"`\n\tEncapContentInfo EncapsulatedContentInfo\n\tCertificates     []CertificateChoices   `asn1:\"set,tag:0,optional\"`\n\tCrls             []pkix.CertificateList `asn1:\"set,tag:1,optional\"`\n\tSignerInfos      []SignerInfo           `asn1:\"set\"`\n}\n\n//https://tools.ietf.org/html/rfc5652#section-5.2\ntype EncapsulatedContentInfo struct {\n\tRawContent   asn1.RawContent\n\tEContentType asn1.ObjectIdentifier\n\tEContent     []byte `asn1:\"explicit,tag:0,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.2\ntype CertificateChoices struct {\n\tRawContent asn1.RawContent\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype SignerInfo struct {\n\tVersion int\n\t// Golang doesn't support ASN.1 CHOICE, so make 2 optional fields\n\tIssuerAndSerialNumber IssuerAndSerialNumber `asn1:\"optional\"`\n\tSubjectKeyIdentifier  []byte                `asn1:\"tag:0,optional\"`\n\tDigestAlgorithm       pkix.AlgorithmIdentifier\n\tSignedAttrs           []Attribute `asn1:\"set,tag:0,optional\"`\n\tSignatureAlgorithm    pkix.AlgorithmIdentifier\n\tSignature             []byte\n\tUnsignedAttrs         []Attribute `asn1:\"set,tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype Attribute struct {\n\tAttrType  asn1.ObjectIdentifier\n\tAttrValue asn1.RawValue\n}\n\n// AttrToVerify represents an additional attribute\n// that needs verification. It can also act as a replacement\n// for the default verification callback of the given attribute, f.e.,\n// when verifying the signing time of timestamp responses.\ntype AttrToVerify struct {\n\t// AttributeID is the string representation\n\t// of the object identifier of the attribute\n\tAttributeID string\n\n\t// VerifyCallback encapsulates the call to\n\t// a function that verifies the attribute value\n\t// when being parsed.\n\tVerifyCallback func([]byte) error\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.4\ntype IssuerAndSerialNumber struct {\n\tIssuer       pkix.RDNSequence\n\tSerialNumber *big.Int\n}\n\n// https://tools.ietf.org/html/rfc5035#section-3\ntype SigningCertificateV2 struct {\n\tCerts    []ESSCertIDv2\n\tPolicies asn1.RawValue `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5035#section-4\ntype ESSCertIDv2 struct {\n\tHashAlgorithm         pkix.AlgorithmIdentifier `asn1:\"optional\"`\n\tCertHash              []byte\n\tIssuerAndSerialNumber IssuerSerial `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5035#section-4\ntype IssuerSerial struct {\n\tIssuer       GeneralName\n\tSerialNumber *big.Int\n}\n\n// https://tools.ietf.org/html/rfc5280#page-38\ntype GeneralName struct {\n\tDirectoryName pkix.RDNSequence `asn1:\"explicit,tag:4\"`\n}\n\n// https://tools.ietf.org/html/rfc6211#section-2\ntype CMSAlgorithmProtection struct {\n\tDigestAlgorithm    pkix.AlgorithmIdentifier\n\tSignatureAlgorithm pkix.AlgorithmIdentifier `asn1:\"tag:1\"`\n}\n"
  },
  {
    "path": "core/crypto/cms/testdata/nottrusted.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFLDCCBBSgAwIBAgISBJm9iTCsKngt325MsromM5zGMA0GCSqGSIb3DQEBCwUA\nMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD\nEwJSMzAeFw0yMjA1MDQxOTI0NDhaFw0yMjA4MDIxOTI0NDdaMB0xGzAZBgNVBAMM\nEiouaXZvdGluZ2NlbnRyZS5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBANSRj7vB+jy3v9HNMT1tGqImUGYinsv5mwWzUqmPFh69WaI3z1OFphlNZRKF\nQ/isYNo66ssXQrIteFHTfX4W9xpB5shyCUffWus+vUhCV6MnpTLnpWl6+kawBzfe\nPhtILzrvCNL4siRlTU+jhGOuTkXIUy+cXIQCZVheDHrcw+LdO/KtKytuAAwf+LpX\nc7D7qgrexU0gNUdaO8nzDpgNJQTzXCYpfqFfY7Cw7Av2CWhlRFMnIYNadyow8omm\nn/cVHQjc0a/V/ymLxQlDi8P44oO4QVkVKj5s+sK65vvlvR51d81lpV1FoPe0boct\nkYxSTBaJ5dJuyyyn+WwpF/uCrMsCAwEAAaOCAk8wggJLMA4GA1UdDwEB/wQEAwIF\noDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd\nBgNVHQ4EFgQU9rUBurzqo9obngGToWlkBsPtZTkwHwYDVR0jBBgwFoAUFC6zF7dY\nVsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRw\nOi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNy\nLm9yZy8wHQYDVR0RBBYwFIISKi5pdm90aW5nY2VudHJlLmVlMEwGA1UdIARFMEMw\nCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9j\ncHMubGV0c2VuY3J5cHQub3JnMIIBBgYKKwYBBAHWeQIEAgSB9wSB9ADyAHcA36Ve\nq2iCTx9sre64X04+WurNohKkal6OOxLAIERcKnMAAAGAkL8ocwAABAMASDBGAiEA\n/76yR80N9L+v6hUxzty0wqRwX4P/zpMjqjBjYNw2nrsCIQCMoKWc2lXVJNXvzf0f\nmW/m5Gt4cF5/b9Zdw+IIkC/mmgB3AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr\n8vxw/m1HAAABgJC/KJcAAAQDAEgwRgIhAIx/3GDrhblEa3XnjbOgV92BIcSEd4P+\nNAGPGQKtC0woAiEA5+d+ndesmlSpLuuM8xF9L4PJwnp5p0VPnQeUCgnk1v0wDQYJ\nKoZIhvcNAQELBQADggEBAJ1KBBx4IaEJUD4UIWUiUWBbY6OSPUgjLlVzwWmJ5Di0\ndqjMEbb3ozq+WSALCMWXSI8CwG/YnAmuVeQS7aB4w0/jLeS/5Y40goV+Ub6W2lOX\nRpBAFRLW/ZEH4gyLhx8V6CkcCgUuRJeff/zguypGR+qKyR/vSXoQKA2sRfjV1il5\nfvgDw/dWv+RYKPRCbd2EQpqN/f1Eyrn4lYX2Y5UW+psO65HLDOX7e3wPrXqzIq8D\nM0eKMUMhVx784CV5DnV8CmwDJMsz6VfvaFuIIbN5hWrleB7K0sNc0fMYDek+HqKE\nk8vpRZm7J0BXehERW03gu49ue+EJMphdgnYvP0mupVY=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/cms/testdata/test1.txt",
    "content": "ewfwef"
  },
  {
    "path": "core/crypto/cms/testdata/trusted.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIErDCCA5SgAwIBAgIIDQ6goPoHl9AwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UE\nBhMCREUxMzAxBgNVBAoMKkRHTiBEZXV0c2NoZXMgR2VzdW5kaGVpdHNuZXR6IFNl\ncnZpY2UgR21iSDEiMCAGA1UEAwwZZGduc2VydmljZSBDQSAyIFR5cGUgRTpQTjAe\nFw0yMTA4MDkwODMzMDJaFw0yMjA4MDkwODMzMDJaMIGJMQswCQYDVQQGEwJERTEh\nMB8GA1UEBRMYNDAwMDAwMDA2MTEwZWE5YjJhOTAyODA0MTQwMgYDVQQDDCtUZXN0\nIENlcnRpZmljYXRlIC0gRk9SIFRFU1QgUFVSUE9TRVMgT05MWSAtMSEwHwYJKoZI\nhvcNAQkBFhJzdXBwb3J0QHNlY3J5cHQuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQCnTlYywSD9LhQHa3IG3ICmWQEGmO6BOgs20o3/m2Bu7MyrQTOg\nxT5nYb4dmucw2QaG/0scerJGSBc+2wCw2BuWhUBpYhV9HBZRqVzQ4UuRRtS0gJwn\nyrZNVba7GmcPtSNQFXlfKP4TinZasxg338rpFlwDHXBg+OGnBV/1ulIc1FwPHWfH\nY7NHKVuoKFf5zQ3z2hZNaL/zTKXPhGoNgBA+ykOR0oTkz/Rg6tITts2qFVcJK/ve\neW9lyinNPn5Ci+hE//Ka/vCKBNgNuaEE1xqRPZy+vuhDcbiudGnD6KHEyluOGphr\neqwOBQ/tyHVLBR8lMENpxG7Tr1TQDpBmk2cPAgMBAAGjggE4MIIBNDAdBgNVHQ4E\nFgQUulZMZO/UymwaWvnsrnanr81iJ4cwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAW\ngBTpxpPR1Q8GZHLqapY+uhDyVFSyeTBWBgNVHSAETzBNMEsGDCsGAQQB+ysCAQMC\nCDA7MDkGCCsGAQUFBwIBFi1odHRwOi8vc2VjNS5kZ25zZXJ2aWNlLmRlL3BvbGlj\naWVzL2luZGV4Lmh0bWwwPgYDVR0fBDcwNTAzoDGgL4YtaHR0cDovL3NlYzUuZGdu\nc2VydmljZS5kZS9jcmwvY3JsMi10eXBlLWUuY3JsMA4GA1UdDwEB/wQEAwIEsDAd\nBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwHQYDVR0RBBYwFIESc3VwcG9y\ndEBzZWNyeXB0LmRlMA0GCSqGSIb3DQEBCwUAA4IBAQDR2UmPPeQ1P8m1dzWZoaVM\naVy4r4DNYZeym8RLYypZzVWPhSWl2hkwgT/2gELycn0tBS9J4gTpsDH3T4sisdK7\ns3eK4HnXWIaUVyQ+TemRn/G0CxGJoYQiRUO0I4naTO/XT0jYYYP/Ytj8soMk3lKb\nssRJT+5CNWTrA213xZzniG8FFvEQbL/6eFTOJMxk11zjhMsHrDgGS7DNttT6rsnY\nRvE9QaM81BQGwjGSdORf2nxieF733G5NQrjb9vsSbmicED8LEii/R8HPjEhmYJ1u\ndNQNa+bNTzx3O4ecCHBdn7u1Yyjnr8QFY4R6DMPuJfwtSjd8yNJiNKcQeu4I77Uu\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/crypto.go",
    "content": "package crypto\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/math/group\"\n)\n\nvar (\n\t// ElGamalEncryptionOID https://datatracker.ietf.org/doc/html/draft-rfced-info-pgutmann-00#section-2.\n\tElGamalEncryptionOID = func() asn1.ObjectIdentifier { return asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 3029, 2, 1} }\n\t// DistributedElGamalEncryptionOID is a custom OID, doesn't collide with existing OIDs.\n\tDistributedElGamalEncryptionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 3029, 2, 1, 9999}\n\t// https://datatracker.ietf.org/doc/rfc8692/\n\tEcdsaWithSHAKE256 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 6, 33}\n)\n\n// ASN1Marshaller is an interface for marshalling any data to ASN.1 bytes.\ntype ASN1Marshaller interface {\n\t// Marshal marshals this instance into ASN.1 bytes.\n\tMarshal() (asn_1.DER, error)\n}\n\n// AlgorithmIdentifierParameters is an interface for accessing key parameters' data.\ntype AlgorithmIdentifierParameters interface {\n\t// Group returns abstract group parsed from key parameters.\n\tGroup() group.Group\n\n\t// Algorithm returns OID parsed from key parameters.\n\tAlgorithm() asn1.ObjectIdentifier\n\n\t// ToPKIXAlgorithmIdentifier converts this key parameters into PKIX format.\n\tToPKIXAlgorithmIdentifier() (pkix.AlgorithmIdentifier, error)\n}\n\n// KeyInfo is an interface for accessing key metadata.\ntype KeyInfo interface {\n\tASN1Marshaller\n\n\t// Parameters returns this key parameters.\n\tParameters() AlgorithmIdentifierParameters\n\n\t// Fingerprint returns this key fingerprint.\n\tFingerprint() (uint64, error)\n}\n\n// EncryptionKey is an interface for a key that is used for data encryption.\ntype EncryptionKey interface {\n\tKeyInfo\n\tEncrypter\n\tEncryptionVerifier\n\tProofVerifier\n}\n\n// Encrypter is an interface for plaintext encryption.\ntype Encrypter interface {\n\tKeyInfo\n\n\t// Encrypt uses random group scalar s to encrypt a plaintext.\n\t// Returns an ASN.1 marshalled ciphertext.\n\tEncrypt(rand *group.Scalar, plaintext []byte) ([]byte, error)\n}\n\ntype DecrypterWithRandomness interface {\n\tKeyInfo\n\n\tDecryptWithRandomness(r *group.Scalar, ciphertext []byte, checkDecodable bool) (Decryption, error)\n}\n\n// EncryptionVerifier is an interface\ntype EncryptionVerifier interface {\n\tKeyInfo\n\tVerify(proof []byte, salt []byte) error\n}\n\n// DecryptionKey is an interface for a key that is used in ciphertext decryption.\ntype DecryptionKey interface {\n\tKeyInfo\n\tDecrypter\n\tProver\n\tProvableDecrypter\n\tProofProver\n\n\t// EncryptionKey returns an encryption key that belongs to this decryption key.\n\tEncryptionKey() EncryptionKey\n}\n\n// Decryption is an interface for any decrypted data.\ntype Decryption interface {\n\tASN1Marshaller\n\n\t// Value returns result of a decryption.\n\tValue() group.Element\n\n\t// Plaintext converts result of a decryption into UTF-8 byte slice.\n\tPlaintext() ([]byte, error)\n}\n\n// Decrypter is an interface for ciphertext decryption.\ntype Decrypter interface {\n\tKeyInfo\n\n\t// Decrypt decrypts a ciphertext. If checkDecodable is true then ciphertext\n\t// is also validated for decodability property, which allows to detect\n\t// malformed plaintexts. Use checkDecodable, when plaintext length\n\t// varies, otherwise keep it false (e.g. homomorphic encryption).\n\tDecrypt(ciphertext []byte, checkDecodable bool) (Decryption, error)\n}\n\n// ProvableDecrypter is an interface for ciphertext decryption with a proof of correct decryption.\ntype ProvableDecrypter interface {\n\tKeyInfo\n\n\t// ProvableDecrypt decrypts a ciphertext and provides a proof of correct decryption.\n\t// It uses cryptographic salt if there is a need to add uniqueness to the proof,\n\t// otherwise keep it nil. See Decrypter for details.\n\tProvableDecrypt(s *group.Scalar, ciphertext []byte, salt []byte, checkDecodable bool) (dec Decryption, proof []byte, err error)\n}\n\ntype Prover interface {\n\tKeyInfo\n\tProve(rand *group.Scalar, ciphertext []byte, dec Decryption, salt []byte) (proof []byte, err error)\n}\n\n// ProofOpts is options that both prover and verifier should agree on in order\n// to determine which set of proofs will be generated.\ntype ProofOpts any\n\n// KeyPairProofOpts is a proof that ensures verifier that public key, used in vote encryption\n// is actually a pair of a private key used in vote decryption.\ntype KeyPairProofOpts struct{}\n\nfunc NewKeyPairProofOpts() KeyPairProofOpts {\n\treturn KeyPairProofOpts{}\n}\n\n// CiphertextProofOpts is a proof that ensures verifier that ciphertext (encrypted vote) produced\n// by public key is actually a decrypted vote produced by a private key.\ntype CiphertextProofOpts struct {\n\t// Ciphertext is an encrypted vote.\n\tciphertext []byte\n\n\t// Decrypted is a decrypted vote.\n\tdecrypted []byte\n}\n\nfunc NewCiphertextProofOpts(ciphertext []byte, decrypted []byte) CiphertextProofOpts {\n\treturn CiphertextProofOpts{\n\t\tciphertext: ciphertext,\n\t\tdecrypted:  decrypted,\n\t}\n}\n\nfunc (cpo *CiphertextProofOpts) Ciphertext() []byte {\n\treturn cpo.ciphertext\n}\n\nfunc (cpo *CiphertextProofOpts) Decrypted() []byte {\n\treturn cpo.decrypted\n}\n\n// ProofProver proves to verifier different kind of proofs, governed by ProofOpts.\ntype ProofProver interface {\n\t// ProofProve proves to verifier different kind of proofs, governed by opts.\n\t// Salt is optional parameter and if not nil, then is used as cryptographic salt\n\t// to make a proof unique among others.\n\tProofProve(rand *group.Scalar, salt []byte, opts []ProofOpts) (proof []byte, err error)\n}\n\n// ProofVerifier verifies prover's different kind of proofs, governed by ProofOpts.\ntype ProofVerifier interface {\n\t// ProofVerify verifiers prover's different kind of proofs, governed by opts.\n\t// As for ProofProver, salt is optional parameter and if not nil, then is used as\n\t// cryptographic salt to make a proof unique among others.\n\tProofVerify(proof []byte, salt []byte, opts []ProofOpts) (err error)\n}\n\n// DecryptionSharesCombiner is an interface of distributed encryption scheme\n// to combine all partial Decryptions into a single Decryption. Only after\n// combining all Decryptions you may run Plaintext() on a resulting Decryption.\ntype DecryptionSharesCombiner interface {\n\t// DecryptionSharesCombine combines all Decryptions decs of a ciphertext\n\t// into a single Decryption.\n\tDecryptionSharesCombine(ciphertext []byte, decs ...[]byte) (Decryption, error)\n}\n\ntype EncryptionPrivateKeyShare interface {\n\tDecryptionKey\n\tIndex() *group.Scalar\n\tShare() *group.Scalar\n}\n\n// EncryptionPrivateKeyShareRegenerator is an interface of a distributed\n// encryption to regenerate a single private key share.\ntype EncryptionPrivateKeyShareRegenerator interface {\n\t// RegeneratePrivateKeyShare regenerates a single private key share by only\n\t// index of the missing private key share and threshold amount of other\n\t// shares given.\n\tRegeneratePrivateKeyShare(index *group.Scalar, shares []EncryptionPrivateKeyShare) (EncryptionPrivateKeyShare, error)\n}\n\n// HomomorphicEncrypter is an interface for homomorphic encryption.\n//\n// In homomorphic encryption, a plaintext is called as marks. marks is an\n// ordered list of candidates with a bool value representing is particular\n// candidate is chosen or not e.g. {Alice, Bob, Eve} can be represented as\n// {false, true, false}, if Bob is chosen. Mark is therefore is a single candidate.\n//\n// In homomorphic encryption, a ciphertext is a choice.\ntype HomomorphicEncrypter interface {\n\tEncrypt(pk EncryptionKey, marks []bool) (choice []byte, err error)\n}\n\n// HomomorphicAggregator is an interface for aggregating all choices into a single\n// choice without decrypting each choice. It is possible due to homomorphic property\n// of each choice.\n//\n// During aggregation, it is also possible to check each choice proofs:\n//\n//\ta) range proof\n//\tb) mark proof\n//\n// Where range proof confirms that a single choice's marks' sum equals to 1\n// (range proof), and each mark is either 0 or 1 (mark proof).\n//\n// After aggregation a single choice is sent to the HomomorphicTallier for a\n// decryption.\ntype HomomorphicAggregator interface {\n\t// Aggregate sums up all DER marshalled choices into a single DER\n\t// marshalled aggregated choice.\n\t//\n\t// Use withVerify=true when you sum up choices that has not been aggregated yet,\n\t// and use withVerify=false otherwise. That is because aggregated choice\n\t// has already range proof > 1 and therefore verification of this proof\n\t// will fail.\n\tAggregate(pk EncryptionKey, withVerify bool, choices ...[]byte) (aggregated []byte, err error)\n}\n\n// HomomorphicVerifier is an interface for verifying a choice proof.\ntype HomomorphicVerifier interface {\n\tVerify(pk EncryptionKey, proof []byte, salt []byte) error\n}\n\n// HomomorphicDecryption is a result of homomorphic tally.\n//\n// Result of a homomorphic tally is a decrypted choice. a choice holds\n// aggregated marks inside. marks is an ordered list of candidates.\ntype HomomorphicDecryption interface {\n\tASN1Marshaller\n\n\t// Count is an amount of a particular mark per a choice, e.g. suppose\n\t// we have candidate list (marks): list={Alice, Bob, Eve}. Then two voters\n\t// John and Michelle. They have voted as follows: John's is {true, false, false}\n\t// and Michelle's is {true, false, false}. After HomomorphicAggregator we\n\t// got {true(x2), false(x0), false(x0)}. After HomomorphicTallier we got\n\t// {2, 0, 0}, so Count() for a list[0] (candidate Alice) will be 2, and\n\t// for list[1] (candidate Bob) will be 0.\n\tCount() uint64\n\n\t// Proofs are all proofs per mark. Consider the same example in Count(),\n\t// the Proofs() for candidate Alice will be all proofs during HomomorphicTallier\n\t// for candidate Alice (list[0]).\n\tProofs() (proofs [][]byte, err error)\n}\n\n// HomomorphicTallier is an interface for tallying choices, i.e. providing\n// following info per a single choice:\n//\n//\ta) how many times this choice has been chosen?\n//\tb) proofs of all marks for this choice\ntype HomomorphicTallier interface {\n\t// Tally decrypts a choice with attached DER marshalled proof for each\n\t// DecryptionKey sk, and verifies that proof with each EncryptionKey\n\t// pk, i.e. provable decryption is done by each sk[i] and proof is verified\n\t// by each pk[i].\n\t//\n\t// MaxCount is used to restrict iterations for HomomorphicDecryption.\n\tTally(key []DecryptionKey, pkey []EncryptionKey, choice []byte, maxCount int, salt []byte) (tallies []HomomorphicDecryption, err error)\n}\n\n// HomomorphicEncryption is an interface to support homomorphic encryption\n// for regular/distributed encryption schemes.\ntype HomomorphicEncryption interface {\n\tHomomorphicEncrypter\n\tHomomorphicAggregator\n\tHomomorphicVerifier\n\tHomomorphicTallier\n}\n\ntype SignatureVerifier interface {\n\tKeyInfo\n\tVerify(rand *group.Scalar, signature, data []byte) error\n}\n\ntype SigningPublicKey interface {\n\tKeyInfo\n\tSignatureVerifier\n}\n\ntype Signer interface {\n\tKeyInfo\n\tSign(rand *group.Scalar, data []byte) ([]byte, error)\n}\n\ntype SigningPrivateKey interface {\n\tKeyInfo\n\tSigner\n\tPublicKey() SigningPublicKey\n}\n"
  },
  {
    "path": "core/crypto/ecdsa/ecdsa.go",
    "content": "package ecdsa\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/asn1\"\n\t\"encoding/binary\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"golang.org/x/crypto/sha3\"\n\n\tcrypto2 \"tivi.io/core/crypto\"\n\t\"tivi.io/core/math/group\"\n)\n\n// https://datatracker.ietf.org/doc/rfc8692/\nvar ecdsaWithSHAKE256 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 6, 33}\n\nfunc init() {\n\tcrypto2.RegisterPrivateKeyASN1Unmarshaller(ecdsaWithSHAKE256, func(g group.Group, key []byte) (crypto2.SigningPrivateKey, error) {\n\t\treturn ASN1UnmarshalPrivateKey(g, key)\n\t})\n\t//crypto2.RegisterPublicKeyASN1Unmarshaller(ecdsaWithSHAKE256, func(g group.Group, key []byte) (crypto2.SigningPublicKey, error) {\n\t//\treturn ASN1UnmarshalPublicKey(g, key)\n\t//})\n}\n\n// New generates ECDSA private key.\nfunc New(g group.Group) (PrivateKey, error) {\n\tsk, err := group.RandomScalar(g.Order())\n\tif err != nil {\n\t\treturn PrivateKey{}, GenerateRandomScalarError{Err: err}\n\t}\n\tgen := g.Generator()\n\tpub, err := gen.Scale(sk.val)\n\tif err != nil {\n\t\treturn PrivateKey{}, GenerateScaleGeneratorByValError{Err: err}\n\t}\n\tpr := PrivateKey{\n\t\tp: sk,\n\t\tpub: PublicKey{\n\t\t\tprm: g,\n\t\t\tpub: pub,\n\t\t},\n\t}\n\treturn pr, nil\n}\n\ntype PublicKey struct {\n\tprm group.Group\n\tpub group.Element\n}\n\nfunc (pk PublicKey) MarshalASN1() ([]byte, error) {\n\tpubBytes, err := pk.pub.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPublicKeyASN1MarshalPublicError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(pubBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPublicKeyBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\nfunc ASN1UnmarshalPublicKey(g group.Group, data []byte) (PublicKey, error) {\n\ts := cryptobyte.String(data)\n\n\tvar inner cryptobyte.String\n\tvar pubBytes cryptobyte.String\n\n\tif !s.ReadASN1(&inner, asn_1.SEQUENCE) {\n\t\treturn PublicKey{}, ASN1UnmarshalPublicKeyNotASequenceError{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&pubBytes, nil) {\n\t\treturn PublicKey{}, ASN1UnmarshalPublicKeyNoPublicError{}\n\t}\n\n\tif !s.Empty() || !inner.Empty() {\n\t\treturn PublicKey{}, ASN1UnmarshalPublicKeyTrailingBytesError{}\n\t}\n\n\tpub, err := g.ElementOf(pubBytes)\n\tif err != nil {\n\t\treturn PublicKey{}, ASN1UnmarshalElementOfASN1Error{Err: err}\n\t}\n\n\treturn PublicKey{g, pub}, nil\n}\n\nfunc (pk PublicKey) Parameters() crypto2.AlgorithmIdentifierParameters {\n\treturn nil //pk.prm\n}\n\nfunc (pk PublicKey) Algorithm() asn1.ObjectIdentifier {\n\treturn ecdsaWithSHAKE256\n}\n\n// Fingerprint returns first 24 bytes of SHA256(pk), or 0 if hashing fails.\nfunc (pk PublicKey) Fingerprint() uint64 {\n\tpubBytes, err := pk.pub.Marshal()\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\tsum := sha256.Sum256(pubBytes)\n\treturn binary.BigEndian.Uint64(sum[24:])\n}\n\ntype PrivateKey struct {\n\tpub PublicKey\n\tp   group.Scalar\n}\n\nfunc (sk PrivateKey) Parameters() crypto2.AlgorithmIdentifierParameters {\n\treturn nil //sk.pub.\n}\n\nfunc (sk PrivateKey) Algorithm() asn1.ObjectIdentifier {\n\treturn ecdsaWithSHAKE256\n}\n\n// Fingerprint is unimplemented for sk.\nfunc (sk PrivateKey) Fingerprint() uint64 {\n\treturn 0\n}\n\nfunc (sk PrivateKey) MarshalASN1() ([]byte, error) {\n\tskBytes, err := sk.p.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPrivateKeyASN1MarshalPrivateError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(skBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPrivateKeyBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\nfunc ASN1UnmarshalPrivateKey(g group.Group, data []byte) (PrivateKey, error) {\n\ts := cryptobyte.String(data)\n\n\tvar inner cryptobyte.String\n\tvar privBytes cryptobyte.String\n\n\tif !s.ReadASN1(&inner, asn_1.SEQUENCE) {\n\t\treturn PrivateKey{}, ASN1UnmarshalPrivateKeyNotASequenceError{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&privBytes, nil) {\n\t\treturn PrivateKey{}, ASN1UnmarshalPrivateKeyNoPrivateError{}\n\t}\n\n\tif !s.Empty() || !inner.Empty() {\n\t\treturn PrivateKey{}, ASN1UnmarshalPrivateKeyTrailingBytesError{}\n\t}\n\n\tsk, err := group.UnmarshalScalar(privBytes, g.Order())\n\tif err != nil {\n\t\treturn PrivateKey{}, ASN1UnmarshalASN1UnmarshalScalarError{Err: err}\n\t}\n\n\tpub, err := g.Generator().Scale(sk.val)\n\tif err != nil {\n\t\treturn PrivateKey{}, ASN1UnmarshalPrivateKeyRestorePublicError{Err: err}\n\t}\n\tpr := PrivateKey{\n\t\tp: sk,\n\t\tpub: PublicKey{\n\t\t\tprm: g,\n\t\t\tpub: pub,\n\t\t},\n\t}\n\treturn pr, nil\n}\n\ntype M struct {\n\tR, S group.Scalar\n}\n\nfunc (m M) MarshalASN1() ([]byte, error) {\n\trBytes, err := m.R.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMRError{Err: err}\n\t}\n\n\tsBytes, err := m.S.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMSError{Err: err}\n\t}\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(rBytes)\n\t\tc.AddBytes(sBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// ASN1UnmarshalCiphertext unmarshalls ElGamal Cipher.\nfunc ASN1UnmarshalM(g group.Group, data []byte) (M, error) {\n\tc := cryptobyte.String(data)\n\n\tvar inner cryptobyte.String\n\tvar r, s cryptobyte.String\n\n\tif !c.ReadASN1(&inner, asn_1.SEQUENCE) {\n\t\treturn M{}, ASN1UnmarshalMNotASequenceError{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&r, nil) {\n\t\treturn M{}, ASN1UnmarshalMNoRError{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&s, nil) {\n\t\treturn M{}, ASN1UnmarshalMNoSError{}\n\t}\n\n\tif !inner.Empty() || !c.Empty() {\n\t\treturn M{}, ASN1UnmarshalMTrailingBytesError{}\n\t}\n\n\tvar err error\n\n\tR, err := group.UnmarshalScalar(r, g.Order())\n\tif err != nil {\n\t\treturn M{}, ASN1UnmarshalMASN1UnmarshalRError{Err: err}\n\t}\n\n\tS, err := group.UnmarshalScalar(s, g.Order())\n\tif err != nil {\n\t\treturn M{}, ASN1UnmarshalMASN1UnmarshalSError{Err: err}\n\t}\n\n\treturn M{R, S}, nil\n}\n\nfunc (sk PrivateKey) PublicKey() crypto2.SigningPublicKey {\n\treturn sk.pub\n}\n\nfunc (sk PrivateKey) Sign(data []byte) ([]byte, error) {\n\t// privkey\n\tsc := group.NewScalar(sk.p.val, sk.pub.prm.Order())\n\n\t// rand scalar k\n\tk, err := group.RandomScalar(sk.pub.prm.Order())\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyRandomScalarError{Err: err}\n\t}\n\t// x = Gk\n\tX, err := sk.pub.prm.Generator().Scale(k.val)\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyScaleGeneratorByValError{Err: err}\n\t}\n\n\t// r = Xx\n\tr1 := X.(ecp.NistElement).X\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyCastToNistElementError{Err: err}\n\t}\n\tr := group.NewScalar(r1, sk.pub.prm.Order())\n\n\t// SHAKE256(h)\n\tshake256 := sha3.NewShake256()\n\tshake256.Write(data) //nolint: errcheck\n\n\t// c = SHAKE56(H(text))\n\tc, err := group.ScalarValueOfReader(shake256, sk.pub.prm.Order())\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyScalarValueOfReaderError{Err: err}\n\t}\n\n\t// kN = k^-1\n\tkN := k.Inverse()\n\n\t// s = kN * (c + sc * r)\n\tsc2, err := sc.Mul(r)\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyMulError{Err: err}\n\t}\n\tsc3, err := c.Add(sc2)\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyAddError{Err: err}\n\t}\n\tkN, err = kN.Mul(sc3)\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyMul2Error{Err: err}\n\t}\n\n\tmm := M{r, kN}\n\tMBytes, err := mm.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, SignPrivateKeyASN1MarshalError{Err: err}\n\t}\n\n\treturn MBytes, nil\n}\n\nfunc (pk PublicKey) Verify(signature, signed []byte) error {\n\tdata, err := ASN1UnmarshalM(pk.prm, signature)\n\tif err != nil {\n\t\treturn VerifyPublicKeyASN1UnmarshalMError{Err: err}\n\t}\n\n\t// SHAKE256(h)\n\tshake256 := sha3.NewShake256()\n\tshake256.Write(signed) //nolint: errcheck\n\n\t// c = SHAKE56(H(text))\n\tc, err := group.ScalarValueOfReader(shake256, pk.prm.Order())\n\tif err != nil {\n\t\treturn VerifyPublicKeyScalarValueOfReaderError{Err: err}\n\t}\n\n\t// w = s^-1\n\tnews := group.NewScalar(data.S.val, pk.prm.Order())\n\tw := news.Inverse()\n\n\ta, err := c.Mul(w)\n\tif err != nil {\n\t\treturn VerifyPublicKeyNewScalarError{Err: err}\n\t}\n\tr := group.NewScalar(data.R.val, pk.prm.Order())\n\n\tb, err := r.Mul(w)\n\tif err != nil {\n\t\treturn VerifyPublicKeyMulError{Err: err}\n\t}\n\tX1, err := pk.prm.Generator().Scale(a.val)\n\tif err != nil {\n\t\treturn VerifyPublicKeyScaleGeneratorByValError{Err: err}\n\t}\n\tX2, err := pk.pub.Scale(b.val)\n\tif err != nil {\n\t\treturn VerifyPublicKeyScalePublicByValError{Err: err}\n\t}\n\n\tX, err := X1.Op(X2)\n\tif err != nil {\n\t\treturn VerifyPublicKeyOpError{Err: err}\n\t}\n\txc1 := X.(ecp.NistElement).X\n\tif err != nil {\n\t\treturn VerifyPublicKeyCastToNistElementError{Err: err}\n\t}\n\txc := group.NewScalar(xc1, pk.prm.Order())\n\tsc1, err := xc.Marshal()\n\tif err != nil {\n\t\treturn VerifyPublicKeyNewScalar2Error{Err: err}\n\t}\n\n\tsc2, err := data.R.Marshal()\n\tif err != nil {\n\t\treturn VerifyPublicKeyASN1MarshalError{Err: err}\n\t}\n\n\tif !bytes.Equal(sc1, sc2) {\n\t\treturn VerifyPublicKeyEqualError{}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/crypto/ecdsa/ecdsa_test.go",
    "content": "package ecdsa\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/x509\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc storePKCS8PrivateKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509.PEMEncodePKCS8PrivateKey(key)\n}\n\nfunc storeX509PublicKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509.X509MarshalPEM(key)\n}\n\nfunc sign(signer crypto.Signer, data []byte) ([]byte, error) {\n\treturn signer.Sign(data)\n}\n\nfunc verify(verifier crypto.SignatureVerifier, signature, data []byte) error {\n\treturn verifier.Verify(signature, data)\n}\n\nfunc TestSignAndVerify(t *testing.T) {\n\tdata := []byte(\"Hello World!\")\n\tfor _, g := range group.All() {\n\t\t// Skip non-NIST groups\n\t\tif !strings.Contains(string(g.Name()), \"NIST\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\t// Generate new ECDSA private key with only Group given, btw,\n\t\t\t// we derive public key from a private\n\t\t\tpriv, err := New(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t// Cast to interface\n\t\t\tvar privI crypto.SigningPrivateKey = priv\n\t\t\tvar pubI = priv.PublicKey()\n\n\t\t\t// Since generated keys is likely to be marshalled after creation (and\n\t\t\t// later reused, on unmarshal), we simulate storing in I/O\n\t\t\tprivBytes, err := storePKCS8PrivateKey(privI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tos.WriteFile(string(g.Name()), privBytes, 0777)\n\n\t\t\tpubBytes, err := storeX509PublicKey(pubI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Simulate reading private key from I/O to concrete implementation\n\t\t\t// first\n\t\t\t_, privIRestored, err := x509.DecodePKCS8PrivateKey[crypto.SigningPrivateKey](privBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// SignAll custom data\n\t\t\tsigned, err := sign(privIRestored, data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Simulate reading public key from I/O to concrete implementation\n\t\t\t// first\n\t\t\t_, pubIRestored, err := x509.X509Unmarshal[crypto.SigningPublicKey](pubBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// VerifyAll signature\n\t\t\terr = verify(pubIRestored, signed, data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSignAndVerifyManyData(t *testing.T) {\n\tdata1 := []byte(\"Hello World!\")\n\tdata2 := []byte(\"I'm Alice\")\n\tdata3 := []byte(\"Good weather\")\n\tdata4 := []byte(\"Happy coding :)\")\n\tdataSet := [][]byte{data1, data2, data3, data4}\n\n\tfor _, g := range group.All() {\n\t\t// Skip non-NIST groups\n\t\tif !strings.Contains(string(g.Name()), \"NIST\") {\n\t\t\tcontinue\n\t\t}\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\t// Generate new ECDSA signing key\n\t\t\tvar signingKeyI crypto.SigningPrivateKey\n\t\t\tsigningKeyI, err := New(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// SignAll many data\n\t\t\tsignature, err := crypto.SignAll(signingKeyI, dataSet...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// VerifyAll many data\n\t\t\terr = crypto.VerifyAll(signingKeyI.PublicKey(), signature, dataSet...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/crypto/elgamal/ciphertext.go",
    "content": "package elgamal\n\nimport (\n\t\"fmt\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/math/group\"\n)\n\n// Ciphertext is ElGamal ciphertext.\ntype Ciphertext struct {\n\ta group.Element\n\tb group.Element\n}\n\n// NewCiphertext creates new ElGamal ciphertext with provided a and b group elements.\nfunc NewCiphertext(A, B group.Element) *Ciphertext {\n\treturn &Ciphertext{\n\t\ta: A,\n\t\tb: B,\n\t}\n}\n\nfunc (ct *Ciphertext) A() group.Element {\n\treturn ct.a\n}\n\nfunc (ct *Ciphertext) B() group.Element {\n\treturn ct.b\n}\n\n// ASN1Marshal ASN.1 marshals ElGamal ciphertext as\n//\n//\tct ::= SEQUENCE {\n//\t\ta\tGROUP ELEMENT\n//\t\tb\tGROUP ELEMENT\n//\t}\n//\n//\tGROUP ELEMENT ::= CHOICE {\n//\t\tModqPElement\tINTEGER\n//\t\tECqPElement\tOCTET STRING\n//\t\tEdwards25519Element\tOCTET STRING\n//\t}\nfunc (ct *Ciphertext) MarshalASN1() (der []byte, err error) {\n\tA, err := ct.a.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal ciphertext a element: %v\", err)\n\t}\n\n\tB, err := ct.b.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal ciphertext b element: %v\", err)\n\t}\n\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddBytes(A)\n\t\tbuilder.AddBytes(B)\n\t})\n\n\tder, err = builder.Bytes()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal ciphertext: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1UnmarshalCiphertext ASN.1 unmarshalls ElGamal ciphertext.\nfunc ASN1UnmarshalCiphertext(g group.Group, data []byte) (*Ciphertext, error) {\n\tder := cryptobyte.String(data)\n\tvar sequence, ADer, BDer cryptobyte.String\n\n\tif !der.ReadASN1(&sequence, asn1.SEQUENCE) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal ciphertext ASN.1 SEQUENCE\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&ADer, nil) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal ciphertext a element\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&BDer, nil) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal ciphertext b element\")\n\t}\n\n\tif !der.Empty() || !sequence.Empty() {\n\t\treturn nil, fmt.Errorf(\"trailing bytes left while ASN.1 unmarshalling ElGamal ciphertext\")\n\t}\n\n\tA, err := g.ElementOf(asn_1.DER(ADer))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group element from ASN.1 marshalled ElGamal ciphertext a element: %v\", err)\n\t}\n\n\tB, err := g.ElementOf(asn_1.DER(BDer))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group element from ASN.1 marshalled ElGamal ciphertext b element: %v\", err)\n\t}\n\n\treturn NewCiphertext(A, B), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/decryption.go",
    "content": "package elgamal\n\nimport (\n\t\"fmt\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/math/group\"\n)\n\n// Decryption is ElGamal decrypted group element.\ntype Decryption struct {\n\tg       group.Group\n\telement group.Element\n}\n\n// NewDecryption creates new ElGamal decrypted group element.\nfunc NewDecryption(g group.Group, element group.Element) *Decryption {\n\treturn &Decryption{\n\t\tg:       g,\n\t\telement: element,\n\t}\n}\n\n// Value returns ElGamal decrypted group element.\nfunc (d *Decryption) Value() group.Element {\n\treturn d.element\n}\n\n// Plaintext decodes a plaintext from ElGamal decrypted group element.\nfunc (d *Decryption) Plaintext() (plaintext []byte, err error) {\n\tunpadded, err := d.element.Decode()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode padded plaintext from ElGamal decrypted group element: %v\", err)\n\t}\n\n\tplaintext, err = d.g.UnpadBytes(unpadded)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unpad plaintext from padded plaintext: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1Marshal ASN.1 marshals ElGamal decrypted group element as\n//\n//\td ::= GROUP ELEMENT\n//\n//\tGROUP ELEMENT ::= CHOICE {\n//\t\tModqPElement\tINTEGER\n//\t\tECqPElement\tOCTET STRING\n//\t\tEdwards25519Element\tOCTET STRING\n//\t}\nfunc (d *Decryption) Marshal() (der asn_1.DER, err error) {\n\tder, err = d.element.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal decrypted group element: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1UnmarshalDecryption ASN.1 unmarshalls ElGamal decrypted group element.\nfunc ASN1UnmarshalDecryption(g group.Group, data []byte) (*Decryption, error) {\n\tE, err := g.ElementOf(data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal decrypted group element: %v\", err)\n\t}\n\n\treturn NewDecryption(g, E), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/distributed/distributed.go",
    "content": "package distributed\n\nimport (\n\t\"crypto/sha256\"\n\tasn_1 \"encoding/asn1\"\n\t\"encoding/binary\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"math/big\"\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\tasn_11 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/polynomial\"\n)\n\n// PublicKeyShare is distributed ElGamal public key share.\ntype PublicKeyShare struct {\n\tparams *Parameters\n\t// EncryptionKey is a generated ElGamal public key for this public key share.\n\t//EncryptionKey elgamal.EncryptionKey\n\tpublic group.Element\n}\n\nfunc (pk *PublicKeyShare) ProofVerify(proof []byte, salt []byte, opts []crypto.ProofOpts) (err error) {\n\treturn nil\n}\n\nfunc NewPublicKeyShare(params *Parameters, pubkey group.Element) *PublicKeyShare {\n\treturn &PublicKeyShare{\n\t\tparams: params,\n\t\tpublic: pubkey,\n\t}\n}\n\nfunc (pk *PublicKeyShare) Parameters() crypto.AlgorithmIdentifierParameters {\n\treturn pk.params\n}\n\n// Fingerprint returns first 24 bytes of SHA256(pk).\nfunc (pk *PublicKeyShare) Fingerprint() (uint64, error) {\n\tpubBytes, err := pk.public.Marshal()\n\tif err != nil {\n\t\treturn 0, FingerprintPublicKeyShareError{Err: err}\n\t}\n\n\tsum := sha256.Sum256(pubBytes)\n\treturn binary.BigEndian.Uint64(sum[24:]), nil\n}\n\n// Encrypt is unimplemented for distributed ElGamal, use elgamal.EncryptionKey instead.\nfunc (pk *PublicKeyShare) Encrypt(_ *group.Scalar, _ []byte) (ciphertext []byte, err error) {\n\treturn nil, EncryptPublicKeyShareUnimplementedError{}\n}\n\n// Verify verifies DER marshalled DecryptionShareProof proof.\n//\n// Extra is the same as in ProvableDecrypt.\nfunc (pk *PublicKeyShare) Verify(proof, extra []byte) error {\n\tp, err := ASN1UnmarshalDecryptionProofShare(pk.params.g, proof)\n\tif err != nil {\n\t\treturn VerifyPublicKeyShareASN1UnmarshalDecryptionProofShareError{Err: err}\n\t}\n\n\terr = Verify(pk, p, extra)\n\tif err != nil {\n\t\treturn VerifyPublicKeyShareError{Err: err}\n\t}\n\n\treturn nil\n}\n\n// ASN1Marshal as\n//\n//\tpk ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tpk.EncryptionKey\n//\t}\nfunc (pk *PublicKeyShare) Marshal() (asn_11.DER, error) {\n\tpubKeyBytes, err := pk.public.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPublicKeyShareASN1MarshalPublicKeyError{Err: err}\n\t}\n\n\treturn pubKeyBytes, nil\n}\n\n// PrivateKeyShare is distributed ElGamal private key share.\ntype PrivateKeyShare struct {\n\t// Index is an identifier of a polynomial that this private key share belongs to.\n\t//Index group.Scalar\n\tparams *Parameters\n\t// PrivateKey is a generated ElGamal private key for this private key share.\n\t//PrivateKey elgamal.PrivateKey\n\tpriv *group.Scalar\n\t// pkey is a public key share derived from this private key share.\n\tpkey *PublicKeyShare\n}\n\nfunc (p *PrivateKeyShare) Prove(rand *group.Scalar, ciphertext []byte, dec crypto.Decryption, salt []byte) (proof []byte, err error) {\n\tskk, err := elgamal.NewPrivateKey(elgamal.NewParameters(p.Parameters().Group()), p.priv)\n\n\tproof, err = skk.Prove(rand, ciphertext, dec, salt)\n\tif err != nil {\n\t\treturn nil, ProvableDecryptImplPrivateKeyShareProveError{Err: err}\n\t}\n\treturn proof, nil\n}\n\nfunc (p *PrivateKeyShare) ProofProve(rand *group.Scalar, salt []byte, opts []crypto.ProofOpts) (proof []byte, err error) {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (p *PrivateKeyShare) Index() *group.Scalar {\n\treturn p.params.Index()\n}\n\nfunc (p *PrivateKeyShare) Share() *group.Scalar {\n\treturn p.priv\n}\n\n// NewPrivateKeyShares returns new distributed ElGamal private key shares and\n// ElGamal public key. NB! ElGamal public key is not distributed!\nfunc NewPrivateKeyShares(g group.Group, parties, threshold uint64) ([]*PrivateKeyShare, *elgamal.PublicKey, error) {\n\tif threshold == 0 {\n\t\treturn nil, nil, NewPrivateKeySharesThresholdIsZeroError{}\n\t}\n\n\t// Quorum check\n\tquorum := (parties / 2) + 1\n\tif threshold < quorum {\n\t\treturn nil, nil, NewPrivateKeySharesThresholdNotInQuorumError{\n\t\t\tParties:   parties,\n\t\t\tThreshold: threshold,\n\t\t\tQuorum:    quorum,\n\t\t}\n\t}\n\n\tprivShares := make([]*PrivateKeyShare, parties)\n\n\t// Holds polynomials, the amount of polynomials stored in this variable\n\t// equals to parties amount\n\tprivPreShares := make([]*preshare, parties)\n\n\t// Index has two purposes:\n\t//\ta) polynomial identifier\n\t//\tb) x coordinate of a point on polynomial\n\tindices := make([]*group.Scalar, parties)\n\n\t// shareevs, map's key is indices[i] and value - is evs[i], iteration is done\n\t// over privPreShares\n\tshareevs := make(map[string][]*preshareEvaluation)\n\n\t// Each commitments[i] is a list of\n\t// commitments[i] = [for coefficient in coefficients {group^(coefficient)}],\n\t// where coefficients are the coefficients of the given polynomial and\n\t// group is a group generator\n\tcommitments := make([]*preshareCommitment, parties)\n\n\t// evs holds (x,y) coordinates of each polynomial. Amount of coordinates\n\t// per polynomial == parties amount, but to restore a polynomial it is enough\n\t// to have only threshold amount of (x,y) coordinates\n\tevs := make([][]*preshareEvaluation, parties)\n\n\tvar err error\n\n\t// Create polynomials, amount of polynomials created == parties amount\n\tfor i := range privPreShares {\n\t\t// Random Index will be used for each polynomial, to make it unique\n\t\tindices[i], err = group.RandomScalar(g.Order())\n\t\tif err != nil {\n\t\t\treturn nil, nil, NewPrivateKeySharesCreateShareIndexError{Err: err}\n\t\t}\n\t}\n\n\t// Loop over all polynomials\n\tfor i := range privPreShares {\n\t\t// Generate new N-degree polynomial, where N=threshold and assign Index\n\t\t// to it (to distinguish between different polynomials)\n\t\tprivPreShares[i], err = randomPreshare(g, threshold, indices[i])\n\t\tif err != nil {\n\t\t\treturn nil, nil, NewPrivateKeySharesGeneratePolynomialError{Err: err}\n\t\t}\n\n\t\t// Each commitment hold an array of\n\t\t// commitments[i] = [for coefficient in coefficients {group^(coefficient)}]\n\t\t// where coefficient is a polynomial coefficient\n\t\tcommitments[i], err = commit(privPreShares[i])\n\t\tif err != nil {\n\t\t\treturn nil, nil, NewPrivateKeySharesCommitError{Err: err}\n\t\t}\n\n\t\t// Get all (x,y) coordinates of a polynomial.\n\t\t//\n\t\t// Note, that each evs[i] will have exactly parties amount of (x,y)\n\t\t// coordinates. To restore a polynomial we only need threshold amount\n\t\t// of points, but here we get parties amount of them - that is what we\n\t\t// call a key shares.\n\t\t//\n\t\t// Indices are x coordinates of a polynomial. And as you can see, that\n\t\t// each polynomial will have exactly the same set of x coordinates\n\t\tevs[i], err = evaluate(privPreShares[i], indices...)\n\t\tif err != nil {\n\t\t\treturn nil, nil, NewPrivateKeySharesEvaluateError{Err: err}\n\t\t}\n\n\t\t// Loop over all (x,y) coordinates of a polynomial.\n\t\t//\n\t\t// What we are doing here is following:\n\t\t//\ta) Take X1 coordinate of a polynomial\n\t\t//\tb) Set that X1 coordinate as a key inside a shareevs map\n\t\t//\tc) Take (X1,Y1) coordinates and append it to the array.\n\t\t//\n\t\t// Note that each \"privPreShares\" iteration we will, here (inside evs[i]\n\t\t// loop) go exactly over the same set of x coordinates.\n\t\t//\n\t\t// Let me clarify what shareevs[i] will hold inside,\n\t\t// Suppose you have 2 polynomials, P1={(X1,Y1),(X2,Y2)} and\n\t\t// P2={(X1,Y11),(X2,Y22)}. Note, that X1 and X2 are the same for\n\t\t// both polynomials. Alright, then shareevs[i] will hold\n\t\t// shareevs[X1]={(X1,Y1),(X1,Y11)} and shareevs[X2]={(X2,Y2),(X2,Y22)}.\n\t\t//\n\t\t// Hope now it is clear\n\t\tfor j, ev := range evs[i] {\n\t\t\t// Take latest (x,y) coordinates\n\t\t\tpoint := shareevs[ev.point.X.Value().String()]\n\n\t\t\t// Append new (x,y) coordinates to the latest\n\t\t\tpoint = append(point, evs[i][j])\n\t\t\tshareevs[ev.point.X.Value().String()] = point\n\t\t}\n\t}\n\n\t// Create private key shares - which is sum of all y coordinates of a\n\t// given x coordinate. Recall, each polynomial has exactly the same set of\n\t// x coordinates, but evaluations (y coordinates) are different.\n\t// So, we take x coordinate in all polynomials and sum up all y coordinate\n\t// values, i.e. P1=(x1,y1), P2=(x1,y2), P3=(x1,y3), then sum1=y1+y2+y3,\n\t// then we take P1=(x2,y1), P2=(x2,y2), P3=(x2,y3), and sum2=y1+y2+y3\n\tfor i, preshare := range privPreShares {\n\t\t// privShare[i] = sum of all y coordinates\n\t\tprivShares[i], err = privateKeyShareFromPreshare(g, preshare, commitments, shareevs[preshare.index.Value().String()])\n\t\tif err != nil {\n\t\t\treturn nil, nil, NewPrivateKeySharesCreatePrivateKeyShareError{Err: err}\n\t\t}\n\t}\n\n\t// Generate ElGamal public key\n\tzeroIndex := group.ZeroScalar(g.Order())\n\tpk, err := generationPublicKeyShare(g, threshold, commitments, zeroIndex)\n\tif err != nil {\n\t\treturn nil, nil, NewPrivateKeySharesCreatePublicKeyError{Err: err}\n\t}\n\n\tparams := NewParameters(g, zeroIndex)\n\tpubkeyShare := NewPublicKeyShare(params, pk)\n\n\telGamalParams := elgamal.NewParameters(g)\n\tpubkey := elgamal.NewPublicKey(elGamalParams, pubkeyShare.public)\n\n\treturn privShares, pubkey, nil\n}\n\n// NewPrivateKeyShareWithSecret returns new PrivateKeyShare from provided secret.\nfunc NewPrivateKeyShareWithSecret(params *Parameters, secret *group.Scalar) (*PrivateKeyShare, error) {\n\telgamalParams := elgamal.NewParameters(params.Group())\n\tsk, err := elgamal.NewPrivateKey(elgamalParams, secret)\n\tif err != nil {\n\t\treturn nil, NewPrivateKeyShareFromSecretError{Err: err}\n\t}\n\n\tpubShare := NewPublicKeyShare(params, sk.Public().Public())\n\n\treturn &PrivateKeyShare{\n\t\tparams: params,\n\t\tpriv:   secret,\n\t\tpkey:   pubShare,\n\t}, nil\n}\n\nfunc (sk *PrivateKeyShare) Parameters() crypto.AlgorithmIdentifierParameters {\n\treturn sk.params\n}\n\n// Fingerprint is unimplemented for sk.\nfunc (sk *PrivateKeyShare) Fingerprint() (uint64, error) {\n\treturn 0, nil\n}\n\n// Decrypt partially decrypts a ciphertext. Use Combiner for combining\n// decrypted parts.\nfunc (sk *PrivateKeyShare) Decrypt(ciphertext []byte, checkDecodable bool) (crypto.Decryption, error) {\n\tct, err := elgamal.ASN1UnmarshalCiphertext(sk.params.g, ciphertext)\n\tif err != nil {\n\t\treturn nil, DecryptPrivateKeyShareASN1UnmarshalCiphertextError{Err: err}\n\t}\n\n\tdecryptedShare, err := Decrypt(sk, ct, checkDecodable)\n\tif err != nil {\n\t\treturn nil, DecryptPrivateKeyShareError{Err: err}\n\t}\n\n\treturn decryptedShare, nil\n}\n\n// ProvableDecrypt partially decrypts a ciphertext and provides a proof for\n// that partial decryption.\n//\n// Extra is used as \"salt\" for proof challenge.\nfunc (sk *PrivateKeyShare) ProvableDecrypt(y *group.Scalar, ciphertext, extra []byte, checkDecodable bool) (decryptedPart crypto.Decryption, proofForPart []byte, err error) {\n\t//ct, err := elgamal.ASN1UnmarshalCiphertext(sk.params.g, ciphertext)\n\t//if err != nil {\n\t//\treturn nil, nil, ProvableDecryptPrivateKeyShareASN1UnmarshalCiphertextError{Err: err}\n\t//}\n\n\tdecryptedShare, proof, err := ProvableDecrypt(y, sk, ciphertext, extra, checkDecodable)\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptPrivateKeyShareError{Err: err}\n\t}\n\n\tproofBytes, err := proof.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptPrivateKeyShareASN1MarshalProofError{Err: err}\n\t}\n\n\treturn decryptedShare, proofBytes, nil\n}\n\n// PublicKey returns PublicKeyShare of a given PrivateKeyShare.\nfunc (sk *PrivateKeyShare) EncryptionKey() crypto.EncryptionKey {\n\treturn sk.pkey\n}\n\n// ASN1Marshal as\n//\n//\tsk ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tINTEGER\n//\t}\nfunc (sk *PrivateKeyShare) Marshal() (asn_11.DER, error) {\n\t//indexBytes, err := sk.Index.Marshal()\n\t//if err != nil {\n\t//\treturn nil, ASN1MarshalPrivateKeyShareASN1MarshalIndexError{Err: err}\n\t//}\n\n\t//privBytes, err := sk.PrivateKey.Marshal()\n\t//if err != nil {\n\t//\treturn nil, ASN1MarshalPrivateKeyShareASN1MarshalPrivateKeyError{Err: err}\n\t//}\n\t//\n\t//var c cryptobyte.Builder\n\t//c.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t//\tc.AddBytes(indexBytes)\n\t//\tc.AddBytes(privBytes)\n\t//})\n\n\t//b, err := c.Bytes()\n\t//if err != nil {\n\t//\treturn nil, ASN1MarshalPrivateKeyShareError{Err: err}\n\t//}\n\n\treturn sk.priv.Marshal()\n}\n\n// DecryptionShare is a partially decrypted value.\n//\n// DecryptionShare is not intended to be initialized as\n//\n//\tDecryptionShare{}\n//\n// instead use a constructor\n//\n//\tNewDecryptionShare()\n//\n// if you do so, you will prevent code panics, when some fields are uninitialized.\ntype DecryptionShare struct {\n\tindex     *group.Scalar\n\tdecrypted *elgamal.Decryption\n}\n\nfunc NewDecryptionShare(index *group.Scalar, decryption *elgamal.Decryption) *DecryptionShare {\n\treturn &DecryptionShare{\n\t\tindex:     index,\n\t\tdecrypted: decryption,\n\t}\n}\n\nfunc (s DecryptionShare) Value() group.Element {\n\treturn s.decrypted.Value()\n}\n\nfunc (s DecryptionShare) Plaintext() ([]byte, error) {\n\treturn s.decrypted.Plaintext()\n}\n\n// ASN1Marshal as\n//\n//\ts ::= SEQUENCE {\n//\t\tINTEGER\n//\t\ts.decrypted\n//\t}\nfunc (s DecryptionShare) Marshal() (asn_11.DER, error) {\n\tindexBytes, err := s.index.Marshal()\n\tif err != nil {\n\t\treturn nil, DecryptionShareIndexASN1MarshalError{Err: err}\n\t}\n\n\tproofBytes, err := s.decrypted.Value().Marshal()\n\tif err != nil {\n\t\treturn nil, DecryptionShareProofASN1MarshalError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(indexBytes)\n\t\tc.AddBytes(proofBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, DecryptionShareBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// DecryptionShareProof is not intended to be initialized as\n//\n//\tDecryptionShareProof{}\n//\n// instead use a constructor\n//\n//\tNewDecryptionProofShare()\n//\n// if you do so, you will prevent code panics, when some fields are uninitialized.\ntype DecryptionShareProof struct {\n\tindex *group.Scalar\n\tproof *elgamal.DecryptionProof\n}\n\nfunc NewDecryptionProofShare(index *group.Scalar, proof *elgamal.DecryptionProof) *DecryptionShareProof {\n\treturn &DecryptionShareProof{\n\t\tindex: index,\n\t\tproof: proof,\n\t}\n}\n\n// ASN1Marshal as\n//\n//\td ::= SEQUENCE {\n//\t\tINTEGER\n//\t\td.proof\n//\t}\nfunc (d *DecryptionShareProof) MarshalASN1() ([]byte, error) {\n\tindexBytes, err := d.index.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalDecryptionProofShareASN1MarshalError{Err: err}\n\t}\n\n\tproofBytes, err := d.proof.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalDecryptionProofShareASN1MarshalProofError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(indexBytes)\n\t\tc.AddBytes(proofBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalDecryptionProofShareBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// Combiner combines all DecryptionShare into a single decrypted value.\n//\n// Combiner is not intended to be initialized as\n//\n//\tCombiner{}\n//\n// instead use a constructor\n//\n//\tNewDecryptionShareCombiner()\n//\n// if you do so, you will prevent code panics, when some fields are uninitialized.\ntype Combiner struct {\n\tg         group.Group\n\tthreshold uint64\n}\n\nfunc NewDecryptionShareCombiner(g group.Group, threshold uint64) *Combiner {\n\tif threshold < 2 {\n\t\tthreshold = 1\n\t}\n\treturn &Combiner{\n\t\tg:         g,\n\t\tthreshold: threshold,\n\t}\n}\n\n// DecryptionSharesCombine combines DER marshalled DecryptionShare\n// sharesBytes into a single decrypted value.\nfunc (c *Combiner) DecryptionSharesCombine(ciphertext []byte, sharesBytes ...[]byte) (crypto.Decryption, error) {\n\t// Skip if non-distributed encryption\n\tif len(sharesBytes) == 1 || c.threshold < 2 {\n\t\tdecrypted, err := elgamal.ASN1UnmarshalDecryption(c.g, sharesBytes[0])\n\t\tif err != nil {\n\t\t\treturn nil, DecryptionSharesCombineCombinerASN1UnmarshalDecryptionError{Err: err}\n\t\t}\n\n\t\treturn decrypted, nil\n\t}\n\n\tct, err := elgamal.ASN1UnmarshalCiphertext(c.g, ciphertext)\n\tif err != nil {\n\t\treturn nil, DecryptionSharesCombineCombinerASN1UnmarshalCiphertextError{Err: err}\n\t}\n\n\tif len(sharesBytes) < int(c.threshold) {\n\t\treturn nil, CombineDecryptedSharesSharesLessThanThresholdError{\n\t\t\tSharesCount: len(sharesBytes),\n\t\t\tThreshold:   int(c.threshold),\n\t\t}\n\t}\n\n\tshares := make([]*DecryptionShare, len(sharesBytes))\n\tindices := make([]*group.Scalar, len(shares))\n\tfor i, share := range sharesBytes {\n\t\tshares[i], err = ASN1UnmarshalDecryptionShare(c.g, share)\n\t\tif err != nil {\n\t\t\treturn nil, DecryptionSharesCombineCombinerASN1UnmarshalDecryptionShareError{Err: err}\n\t\t}\n\t\tindices[i] = shares[i].index\n\t}\n\n\tzero := group.ZeroScalar(c.g.Order())\n\tdenom := c.g.Identity()\n\tvar tmp group.Element\n\n\tfor _, share := range shares {\n\t\ts, err := polynomial.LagrangeBasisPolynomial(c.g, zero, indices, share.index)\n\t\tif err != nil {\n\t\t\treturn nil, DecryptionSharesCombineCombinerLagrangeBasisPolynomialError{Err: err}\n\t\t}\n\n\t\ttmp, err = share.Value().Scale(s)\n\t\tif err != nil {\n\t\t\treturn nil, DecryptionSharesCombineCombinerShareScaleError{Err: err}\n\t\t}\n\n\t\tdenom, err = denom.Op(tmp)\n\t\tif err != nil {\n\t\t\treturn nil, DecryptionSharesCombineCombinerOpError{Err: err}\n\t\t}\n\n\t}\n\n\ttmp2, err := ct.B().Op(denom.Inverse())\n\tif err != nil {\n\t\treturn nil, DecryptionSharesCombineCombinerBlindedMessageOpError{Err: err}\n\t}\n\n\tdecrypted := elgamal.NewDecryption(c.g, tmp2)\n\n\treturn decrypted, nil\n}\n\n// Regenerator regenerates a private key share by only share Index given.\ntype Regenerator struct {\n\tg group.Group\n\t//group     group.group\n\tthreshold uint64\n}\n\nfunc NewRegenerator(g group.Group, threshold uint64) *Regenerator {\n\treturn &Regenerator{g: g, threshold: threshold}\n}\n\nfunc (r *Regenerator) RegeneratePrivateKeyShare(index *group.Scalar, privKeySharesBytes []crypto.EncryptionPrivateKeyShare) (crypto.EncryptionPrivateKeyShare, error) {\n\tvar err error\n\n\t// DER unmarshalled private key shares\n\tprivKeyShares := make([]*PrivateKeyShare, len(privKeySharesBytes))\n\n\tfor i, privKeyShareBytes := range privKeySharesBytes {\n\n\t\t//ska, err := UnmarshalPrivateElementShare(r.g, privKeyShareBytes.Share())\n\t\teparams := elgamal.NewParameters(r.g)\n\t\tparams := NewParameters(r.g, privKeyShareBytes.Index())\n\t\tskz, err := elgamal.NewPrivateKey(eparams, privKeyShareBytes.Share())\n\t\tprivKeyShares[i] = &PrivateKeyShare{\n\t\t\tparams: params,\n\t\t\tpriv:   privKeyShareBytes.Share(),\n\t\t\tpkey:   NewPublicKeyShare(params, skz.Public().Public()),\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, RegeneratePrivateKeyShareByIndexASN1UnmarshalPrivateKeyShareError{Err: err}\n\t\t}\n\t}\n\n\tregblinds := make([][]*regenerationBlindShare, r.threshold)\n\n\t// Loop exactly over #threshold amount of shares\n\tfor i := uint64(0); i < r.threshold; i++ {\n\t\tregblinds[i], err = randomRegenerationBlindShares(r.g, r.threshold, privKeyShares[i].params.Index(), privKeyShares[:r.threshold])\n\t\tif err != nil {\n\t\t\treturn nil, RegeneratePrivateKeyShareByIndexImplRandomRegenerateBlindSharesError{Err: err}\n\t\t}\n\t}\n\n\trecvregblinds := make([][]*regenerationBlindShare, r.threshold)\n\tfor i := uint64(0); i < r.threshold; i++ {\n\t\trecvregblinds[i] = make([]*regenerationBlindShare, r.threshold)\n\t}\n\n\tfor i := uint64(0); i < r.threshold; i++ {\n\t\tfor j := uint64(0); j < r.threshold; j++ {\n\t\t\trecvregblinds[j][i] = regblinds[i][j]\n\t\t}\n\t}\n\n\tregshares := make([]*regenerationShare, r.threshold)\n\n\tfor i := uint64(0); i < r.threshold; i++ {\n\t\tregshares[i], err = regenerationFromShare(r.g, r.threshold, privKeyShares[i], index, recvregblinds[i])\n\t\tif err != nil {\n\t\t\treturn nil, RegeneratePrivateKeyShareByIndexImplRegenerationFromShareError{Err: err}\n\t\t}\n\t}\n\n\tregeneratedShare, err := combineRegShares(r.g, r.threshold, regshares)\n\tif err != nil {\n\t\treturn nil, RegeneratePrivateKeyShareByIndexImplCombineRegSharesError{Err: err}\n\t}\n\n\treturn regeneratedShare, nil\n}\n\n// preshare is the private input of a party taking part in a distributed Elgamal\n// key generation.\n//\n// Or simply put, preshare is a polynomial with identifier shareIndex.\ntype preshare struct {\n\tindex *group.Scalar\n\t//params Parameters\n\tpoly *polynomial.Polynomial\n}\n\n// preshareCoefficientCommitment is a commitment to a coefficient of a preshare.\ntype preshareCoefficientCommitment struct {\n\t// commitmentIndex is a value from [0, polynomial degree+1).\n\tcommitmentIndex uint64\n\t// commitment is a value of\n\t//\tgroup {^,*} (polynomial coefficient)\n\t// where group is a group generator\n\tcommitment group.Element\n}\n\n// preshareCommitment is a commitment to a preshare.\ntype preshareCommitment struct {\n\t// shareIndex has two purposes:\n\t//\ta) serve as identifier for a polynomial (distinguish)\n\t//\tb) x coordinate of all polynomials. So, shareIndex will present in\n\t// each polynomial as x coordinate\n\tshareIndex *group.Scalar\n\t// coefficientCommitments are all preshareCoefficientCommitment for\n\t// a given polynomial\n\tcoefficientCommitments []*preshareCoefficientCommitment\n}\n\n// preshareEvaluation is an evaluation of the polynomial representing the preshare.\ntype preshareEvaluation struct {\n\t// shareIndex has two purposes:\n\t//\ta) serve as identifier for a polynomial (distinguish)\n\t//\tb) x coordinate of all polynomials. So, shareIndex will present in\n\t// each polynomial as x coordinate\n\tshareIndex *group.Scalar\n\t// point is polynomial point (x,y) coordinates, where x == shareIndex.\n\tpoint *polynomial.Point\n}\n\n// regenerationShare is the regeneration share for recovering a lost private keyshare.\ntype regenerationShare struct {\n\t// shareIndex is the Index of the party which generated this share\n\tshareIndex *group.Scalar\n\t// regenerationIndex is the Index of the party recovering the private keyshare\n\tregenerationIndex *group.Scalar\n\t// share is the value of the regeneration share.\n\tshare *group.Scalar\n}\n\n// regenerationBlindShare is the share of the additive secret share used to\n// blind the regeneration share.\ntype regenerationBlindShare struct {\n\t// shareIndex is the Index of the party which generated this share\n\tshareIndex *group.Scalar\n\t// targetIndex is the Index of the intended recipient of this share\n\ttargetIndex *group.Scalar\n\t// blindShare is the value of the this share.\n\tblindShare *group.Scalar\n}\n\n// commit constructs a commitment to this preshare.\nfunc commit(s *preshare) (*preshareCommitment, error) {\n\tcoms := make([]*preshareCoefficientCommitment, s.poly.Degree()+1)\n\n\t// Loop over all polynomial coefficients\n\t// and do coms[i] = group^(polynomial coefficient)\n\tfor i := uint64(0); i < s.poly.Degree()+1; i++ {\n\t\tgen := s.poly.Group.Generator()\n\n\t\t// com = group^(polynomial coefficient)\n\t\tcom, err := gen.Scale(s.poly.Coefficient(i))\n\t\tif err != nil {\n\t\t\treturn nil, CommitScaleCoefficientError{Err: err}\n\t\t}\n\n\t\t// i is a polynomial coefficient Index, i.e. poly coefficient {a, b, C},\n\t\t// then a Index is 0, b Index is 1, C Index is 2 (just like in an array)\n\t\tcoms[i] = &preshareCoefficientCommitment{\n\t\t\tcommitmentIndex: i,\n\t\t\tcommitment:      com,\n\t\t}\n\t}\n\n\t// s is a random scalar that we use to distinguish polynomials between each\n\t// other\n\treturn &preshareCommitment{\n\t\tshareIndex:             s.index,\n\t\tcoefficientCommitments: coms,\n\t}, nil\n}\n\n// evaluate evaluates (gets (x,y) coordinates) an s polynomial (preshare).\n//\n// Indicies is a list of x coordinates that we evaluate s polynomial for.\nfunc evaluate(s *preshare, indices ...*group.Scalar) ([]*preshareEvaluation, error) {\n\tevs := make([]*preshareEvaluation, len(indices))\n\n\t// Index is a polynomial identifier to distinguish between each other\n\tfor i := range indices {\n\t\t// Get (Index,y) coordinate of a point on a polynomial, where x coordinate\n\t\t// is given x=Index.\n\t\tpoint, err := s.poly.Evaluate(indices[i])\n\t\tif err != nil {\n\t\t\treturn nil, EvaluatePolynomialError{Err: err}\n\t\t}\n\n\t\t// From now on, Index is used both to:\n\t\t//\ta) distinguish between different polynomials\n\t\t//\tb) as x coordinate of a point on a polynomial\n\t\tevs[i] = &preshareEvaluation{\n\t\t\tshareIndex: s.index,\n\t\t\tpoint:      point,\n\t\t}\n\t}\n\n\treturn evs, nil\n}\n\n// privateKeyShareFromPreshare verifies that the preshare commitments correspond to preshare\n// evaluations and returns the private key share.\nfunc privateKeyShareFromPreshare(g group.Group, s *preshare, commits []*preshareCommitment, evs []*preshareEvaluation) (*PrivateKeyShare, error) {\n\terr := shareVerify(g, s, commits, evs)\n\tif err != nil {\n\t\treturn nil, PrivateKeyShareFromPreshareShareVerifyError{Err: err}\n\t}\n\n\ty := group.ZeroScalar(g.Order())\n\n\t// Loop over all polynomials points (note that x coordinates are the same\n\t// for each polynomial)\n\tfor _, e := range evs {\n\t\t// Sum all y coordinates of all polynomials\n\t\ty, err = y.Add(e.point.Y)\n\t\tif err != nil {\n\t\t\treturn nil, PrivateKeyShareFromPreshareShareAddError{Err: err}\n\t\t}\n\t}\n\n\t// Return ElGamal private key share by given secret\n\tparams := NewParameters(g, s.index)\n\tprivkey, err := NewPrivateKeyShareWithSecret(params, y)\n\tif err != nil {\n\t\treturn nil, PrivateKeyShareFromPreshareNewPrivateKeyShareFromSecretError{Err: err}\n\t}\n\n\treturn privkey, nil\n}\n\nfunc shareVerify(g group.Group, preshare *preshare, commits []*preshareCommitment, shareevs []*preshareEvaluation) error {\n\t// shareevs[i] holds all (x,y) coordinates of all polynomials (preshares),\n\t// where x == preshare.shareIndex,\n\t// So, e.g. suppose X1 == preshare.shareIndex, then\n\t// shareevs[X1] = {(X1,Y1), (X1,Y2), (X1,Y3)}\n\tfor _, shareev := range shareevs {\n\t\t// There should not be any other than preshare.shareIndex x coordinates\n\t\t// of a polynomial\n\t\terr := shareev.point.X.Equal(preshare.index)\n\t\tif err != nil {\n\t\t\treturn ShareVerifyPreshareEqualError{Err: err}\n\t\t}\n\t}\n\n\tfor _, cms := range commits {\n\t\t// Amount of commitments should be the same as polynomial degree+1\n\t\tif len(cms.coefficientCommitments) != int(preshare.poly.Degree())+1 {\n\t\t\treturn ShareVerifyCommitmentsAmountNotEqualToPolynomialDegreeError{\n\t\t\t\tExpected: int(preshare.poly.Degree()) + 1,\n\t\t\t\tGot:      len(cms.coefficientCommitments),\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, ev := range shareevs {\n\t\tgen := g.Generator()\n\n\t\t// group {*,^} point.y\n\t\tlhs, err := gen.Scale(ev.point.Y)\n\t\tif err != nil {\n\t\t\treturn ShareVerifyScaleGeneratorByPolynomialPointYValueError{Err: err}\n\t\t}\n\n\t\tcmap := make(map[uint64]*preshareCoefficientCommitment)\n\t\t// Copy coefficient commitments by shareIndex into a tmp \"cmap\" map\n\t\tfor _, cms := range commits {\n\t\t\tif cms.shareIndex.Equal(ev.shareIndex) == nil {\n\t\t\t\tfor _, cm := range cms.coefficientCommitments {\n\t\t\t\t\tcmap[cm.commitmentIndex] = cm\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Amount of cmap keys should be the same as polynomial degree+1\n\t\tif len(cmap) != int(preshare.poly.Degree())+1 {\n\t\t\treturn ShareVerifyCommitmentsAmountError{\n\t\t\t\tExpected: int(preshare.poly.Degree()) + 1,\n\t\t\t\tGot:      len(cmap),\n\t\t\t}\n\t\t}\n\n\t\trhs := g.Identity()\n\n\t\texponent := group.ZeroScalar(g.Order())\n\n\t\tvar tmp group.Element\n\n\t\tfor _, cm := range cmap {\n\t\t\t// exponent = preshare.shareIndex^commitmentIndex, where\n\t\t\t// preshare.shareIndex is polynomial x coordinate, and\n\t\t\t// commitmentIndex is number from [0, polynomial degree+1)\n\t\t\texponent = preshare.index.Exp(new(big.Int).SetUint64(cm.commitmentIndex))\n\n\t\t\t// tmp is a group element\n\t\t\ttmp, err = cm.commitment.Scale(exponent)\n\t\t\tif err != nil {\n\t\t\t\treturn ShareVerifyScaleCommitmentError{Err: err}\n\t\t\t}\n\n\t\t\t// rhs is next group element (right after the tmp)\n\t\t\trhs, err = rhs.Op(tmp)\n\t\t\tif err != nil {\n\t\t\t\treturn ShareVerifyOpCommitmentError{Err: err}\n\t\t\t}\n\t\t}\n\n\t\terr = lhs.Equal(rhs)\n\t\tif err != nil {\n\t\t\treturn ShareVerifyEqualError{Err: err}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// randomPreshare generates new threshold degree random polynomial.\nfunc randomPreshare(g group.Group, threshold uint64, index *group.Scalar) (*preshare, error) {\n\t// Random polynomial of N degree, where N=threshold\n\tpol, err := polynomial.NewRandom(g, threshold-1)\n\tif err != nil {\n\t\treturn nil, RandomPreshareNewPolynomialError{Err: err}\n\t}\n\n\treturn &preshare{index: index, poly: pol}, nil\n}\n\n// generationPublicKeyShare returns ElGamal public key that is generated from\n// commitments.\nfunc generationPublicKeyShare(g group.Group, threshold uint64, commits []*preshareCommitment, index *group.Scalar) (group.Element, error) {\n\tres := g.Identity()\n\n\tfor _, cms := range commits {\n\t\tfound := make(map[uint64]struct{})\n\n\t\texp := group.ZeroScalar(g.Order())\n\n\t\tvar tmp group.Element\n\n\t\tvar err error\n\n\t\t// Loop over all polynomial group^coefficient values\n\t\tfor _, cm := range cms.coefficientCommitments {\n\t\t\tfound[cm.commitmentIndex] = struct{}{}\n\n\t\t\t// exp is number from [0...Polynomial degree+1)\n\t\t\texp = index.Exp(new(big.Int).SetUint64(cm.commitmentIndex))\n\n\t\t\t// tmp = (group^coefficient)^exp\n\t\t\ttmp, err = cm.commitment.Scale(exp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, GenerationPublicKeyShareCommitmentScaleError{Err: err}\n\t\t\t}\n\n\t\t\t// Each iteration do res = res {*,+} tmp\n\t\t\tres, err = res.Op(tmp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, GenerationPublicKeyShareCommitmentOpError{Err: err}\n\t\t\t}\n\t\t}\n\n\t\t// Check that iterations amount == threshold\n\t\tif len(found) != int(threshold) {\n\t\t\treturn nil, GenerationPublicKeyShareNotEnoughCommitmentsError{\n\t\t\t\tExpected: int(threshold),\n\t\t\t\tGot:      len(found),\n\t\t\t}\n\t\t}\n\t}\n\n\t// Create new ElGamal public key, where public element is res\n\t//pubkey := elgamal.NewPublicKey(g, res)\n\n\treturn res, nil\n}\n\n// RandomRegenerationBlindShares returns the shares for additive secret shares.\n// The argument Index is the Index of the party calling the method and\n// targets is a list of ShareIndices for whom to generate the shares.\n//\n// The size of argument target must be at least threshold and the elements must\n// be unique. For statelessness, Index must be included in targets. The method\n// ensures that the sum of the shares is 0.\n//\n// Every returned shares must be transmitted to the party indicated by\n// targetIndex in an authenticated and encrypted channel.\nfunc randomRegenerationBlindShares(g group.Group, threshold uint64, index *group.Scalar, shares []*PrivateKeyShare) ([]*regenerationBlindShare, error) {\n\t// Shares should be >= threshold\n\tif len(shares) < int(threshold) {\n\t\treturn nil, RandomRegenerationBlindSharesSharesLessThanThresholdError{\n\t\t\tShares:    len(shares),\n\t\t\tThreshold: int(threshold),\n\t\t}\n\t}\n\n\tvar seen bool\n\n\t// Index, i.e. x coordinate of polynomial, must present in a share,\n\t// otherwise that Index is outside the polynomial\n\t//\n\t// Loop over all polynomials and seek for the x coordinate (Index)\n\tfor _, target := range shares {\n\t\tif target.params.Index().Equal(index) == nil {\n\t\t\tseen = true\n\t\t}\n\t}\n\n\t// Index is not found in any polynomial\n\tif !seen {\n\t\treturn nil, RandomRegenerationBlindSharesIndexNotInSharesError{}\n\t}\n\n\tblindShares := make([]*regenerationBlindShare, len(shares))\n\n\tblind1 := group.ZeroScalar(g.Order())\n\n\tfor i := 0; i < len(shares)-1; i++ {\n\t\tblind2, err := group.RandomScalar(g.Order())\n\t\tif err != nil {\n\t\t\treturn nil, RandomRegenerationBlindSharesRandomScalarError{Err: err}\n\t\t}\n\n\t\tblindShares[i] = &regenerationBlindShare{\n\t\t\tshareIndex:  index,\n\t\t\ttargetIndex: shares[i].params.Index(),\n\t\t\tblindShare:  blind2,\n\t\t}\n\n\t\t// Sum up all random scalars\n\t\tblind1, err = blind1.Add(blind2)\n\t\tif err != nil {\n\t\t\treturn nil, RandomRegenerationBlindSharesAddError{Err: err}\n\t\t}\n\t}\n\n\tblindShares[len(shares)-1] = &regenerationBlindShare{\n\t\tshareIndex:  index,\n\t\ttargetIndex: shares[len(shares)-1].params.Index(),\n\t\tblindShare:  blind1.Negate(),\n\t}\n\n\treturn blindShares, nil\n}\n\nfunc combineRegenerationBlindShares(g group.Group, threshold uint64, index *group.Scalar, shares []*regenerationBlindShare) (*group.Scalar, []*group.Scalar, error) {\n\t// Shares must be >= threshold\n\tif len(shares) < int(threshold) {\n\t\treturn nil, nil, CombineRegenerationBlindSharesSharesLessThanThresholdError{\n\t\t\tExpected: int(threshold),\n\t\t\tGot:      len(shares),\n\t\t}\n\t}\n\n\tvar seenIndices []*group.Scalar\n\n\tfor _, share := range shares {\n\t\tfor _, seen := range seenIndices {\n\t\t\terr := share.shareIndex.Equal(seen)\n\t\t\tif err == nil {\n\t\t\t\t// Multiple shares from same Index\n\t\t\t\treturn nil, nil, CombineRegenerationBlindSharesShareAlreadySeenError{}\n\t\t\t}\n\t\t}\n\n\t\tseenIndices = append(seenIndices, share.shareIndex)\n\t}\n\n\tblind := group.ZeroScalar(g.Order())\n\n\tvar err error\n\tfor _, share := range shares {\n\t\t// Each share should be exactly of the Index, i.e.\n\t\t// x coordinate of share == Index\n\t\terr = share.targetIndex.Equal(index)\n\t\tif err != nil {\n\t\t\treturn nil, nil, CombineRegenerationBlindSharesIndexAlreadyInsideSharesError{}\n\t\t}\n\n\t\t// Sum up all blinds\n\t\tblind, err = blind.Add(share.blindShare)\n\t\tif err != nil {\n\t\t\treturn nil, nil, CombineRegenerationBlindSharesAddBlindShareError{Err: err}\n\t\t}\n\t}\n\n\treturn blind, seenIndices, nil\n}\n\n// regenerationFromShare uses the private keyshare sk of the party and received\n// shares to generate the regeneration share for the party indicated by\n// newIndex. The returned regeneration share must be transmitted to the party\n// with newIndex over an authenticated and encrypted channel.\nfunc regenerationFromShare(g group.Group, threshold uint64, sk *PrivateKeyShare, newIndex *group.Scalar, shares []*regenerationBlindShare) (*regenerationShare, error) {\n\tblind, indices, err := combineRegenerationBlindShares(g, threshold, sk.params.Index(), shares)\n\tif err != nil {\n\t\treturn nil, RegenerationFromShareCombineRegenerationBlindSharesError{Err: err}\n\t}\n\n\tpriv := sk.priv //group.NewScalar(sk.private, sk.params.group.Order())\n\n\teval, err := polynomial.LagrangeBasisPolynomial(g, newIndex, indices, sk.params.Index())\n\tif err != nil {\n\t\treturn nil, RegenerationFromShareLagrangeBasisPolynomialError{Err: err}\n\t}\n\n\tpriv, err = priv.Mul(eval)\n\tif err != nil {\n\t\treturn nil, RegenerationFromShareMulError{Err: err}\n\t}\n\n\tpriv, err = priv.Add(blind)\n\tif err != nil {\n\t\treturn nil, RegenerationFromShareAddError{Err: err}\n\t}\n\n\treturn &regenerationShare{\n\t\tshareIndex:        sk.params.Index(),\n\t\tregenerationIndex: newIndex,\n\t\tshare:             priv,\n\t}, nil\n}\n\n// combineRegShares uses the received regeneration shares to generate a new private keyshare.\nfunc combineRegShares(g group.Group, threshold uint64, shares []*regenerationShare) (*PrivateKeyShare, error) {\n\tif len(shares) < int(threshold) {\n\t\treturn nil, CombineRegSharesSharesLessThanThresholdError{\n\t\t\tExpected: int(threshold),\n\t\t\tGot:      len(shares),\n\t\t}\n\t}\n\n\tregenIndex := shares[0].regenerationIndex\n\tvar seenIndices []*group.Scalar\n\tfor _, share := range shares {\n\t\tif share.regenerationIndex.Equal(regenIndex) != nil {\n\t\t\treturn nil, CombineRegSharesInconsistentRegIndexError{}\n\t\t}\n\n\t\tfor _, seen := range seenIndices {\n\t\t\terr := share.shareIndex.Equal(seen)\n\t\t\tif err == nil {\n\t\t\t\treturn nil, CombineRegSharesMultipleSharesFromSingleIndex{}\n\t\t\t}\n\t\t}\n\n\t\tseenIndices = append(seenIndices, share.shareIndex)\n\t}\n\n\tsk := group.ZeroScalar(g.Order())\n\n\tvar err error\n\tfor _, share := range shares {\n\t\tsk, err = sk.Add(share.share)\n\t\tif err != nil {\n\t\t\treturn nil, CombineRegSharesAddError{Err: err}\n\t\t}\n\t}\n\n\tprivkey, err := NewPrivateKeyShareWithSecret(NewParameters(g, regenIndex), sk)\n\tif err != nil {\n\t\treturn nil, CombineRegSharesNewPrivateKeyShareFromSecretError{Err: err}\n\t}\n\n\treturn privkey, nil\n}\n\nfunc Verify(pk *PublicKeyShare, proof *DecryptionShareProof, extra []byte) error {\n\terr := pk.params.Index().Equal(proof.index)\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareEqualIndexError{Err: err}\n\t}\n\n\textraBytes, err := asn1MarshalShareExtraBytes(pk.params.Index(), extra)\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareASN1MarshalShareExtraBytesError{Err: err}\n\t}\n\n\tkeyCommitment, err := nizkp.Verify(pk.params.g.Generator(), pk.public, proof.proof.Proof().Challenge(), proof.proof.Proof().Response())\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareKeyCommittmentVerifyLogProofError{Err: err}\n\t}\n\n\tmsgCommitment, err := nizkp.Verify(proof.proof.Ciphertext().A(), proof.proof.Decryption().Value(), proof.proof.Proof().Challenge(), proof.proof.Proof().Response())\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareMsgCommittmentVerifyLogProofError{Err: err}\n\t}\n\n\tpkey := elgamal.NewPublicKey(elgamal.NewParameters(pk.Parameters().Group()), pk.public)\n\tchallenge, err := pkey.ProofChallenge(proof.proof.Ciphertext(), proof.proof.Decryption().Value(), msgCommitment, keyCommitment, extraBytes)\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareProofChallengeError{Err: err}\n\t}\n\n\terr = challenge.Equal(proof.proof.Proof().Challenge())\n\tif err != nil {\n\t\treturn VerifyImplPublicKeyShareEqualChallengeError{Err: err}\n\t}\n\n\treturn nil\n}\n\nfunc Decrypt(sk *PrivateKeyShare, ciphertext *elgamal.Ciphertext, _ bool) (*DecryptionShare, error) {\n\tE, err := ciphertext.A().Scale(sk.priv)\n\tif err != nil {\n\t\treturn nil, DecryptImplPrivateKeyShareEphemeralScaleError{Err: err}\n\t}\n\n\tdecrypted := elgamal.NewDecryption(sk.params.g, E)\n\n\tdecryptedShare := NewDecryptionShare(sk.params.Index(), decrypted)\n\n\treturn decryptedShare, nil\n}\nfunc ProvableDecrypt(y *group.Scalar, sk *PrivateKeyShare, ciphertext []byte, extra []byte, checkDecodable bool) (decryptedPart *DecryptionShare, proofForPart *DecryptionShareProof, err error) {\n\tdecrypted, err := sk.Decrypt(ciphertext, checkDecodable) //Decrypt(sk, ct)\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptPrivateKeyShareDecryptImplError{Err: err}\n\t}\n\n\textraBytes, err := asn1MarshalShareExtraBytes(sk.params.Index(), extra)\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptPrivateKeyShareASN1MarshalShareExtraBytesError{Err: err}\n\t}\n\n\tskk, err := elgamal.NewPrivateKey(elgamal.NewParameters(sk.Parameters().Group()), sk.priv)\n\n\tproof, err := skk.Prove(y, ciphertext, decrypted, extraBytes)\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptImplPrivateKeyShareProveYError{Err: err}\n\t}\n\n\tproof2, err := elgamal.ASN1UnmarshalDecryptionProof(sk.params.g, proof)\n\tif err != nil {\n\t\treturn nil, nil, ProvableDecryptImplASN1UnmarshalProof{Err: err}\n\t}\n\n\tdecProof := NewDecryptionProofShare(sk.params.Index(), proof2)\n\n\treturn decrypted.(*DecryptionShare), decProof, nil\n}\n\nfunc UnmarshalPublicElementShare(oid asn_1.ObjectIdentifier, g group.Group, pk []byte) (group.Element, error) { //nolint:dupl\n\t//pkBytes := cryptobyte.String(pk)\n\t//\n\t//var outerSequence, pubBytes cryptobyte.String\n\t//\n\t//if !pkBytes.ReadASN1(&outerSequence, asn1.SEQUENCE) {\n\t//\treturn PublicKeyShare{}, ASN1UnmarshalPublicKeyShareNotASequenceError{}\n\t//}\n\t//\n\t//if !outerSequence.ReadAnyASN1Element(&indexBytes, nil) {\n\t//\treturn PublicKeyShare{}, ASN1UnmarshalPublicKeyShareNoIndexError{}\n\t//}\n\n\t//if !outerSequence.ReadAnyASN1Element(&pubBytes, nil) {\n\t//\treturn PublicKeyShare{}, ASN1UnmarshalPublicKeyShareNoPublicError{}\n\t//}\n\t//\n\t//if !pkBytes.Empty() || !outerSequence.Empty() {\n\t//\treturn PublicKeyShare{}, ASN1UnmarshalPublicKeyShareTrailingBytesError{}\n\t//}\n\n\t//Index, err := group.UnmarshalScalar(indexBytes, g.Order())\n\t//if err != nil {\n\t//\treturn PublicKeyShare{}, ASN1UnmarshalPublicKeyShareASN1UnmarshalIndexError{Err: err}\n\t//}\n\n\tpub, err := elgamal.ASN1UnmarshalPublicElement(g, pk)\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalPublicKeyShareASN1UnmarshalPublicKeyError{Err: err}\n\t}\n\t//\n\t//pkey := elgamal.NewPublicKey(g, pub)\n\n\t//pubkey := NewPublicKeyShare(Group{group: g, Index: Index}, pkey)\n\n\treturn pub, nil\n}\n\nfunc UnmarshalPrivateElementShare(g group.Group, sk []byte) (*group.Scalar, error) {\n\t//skBytes := cryptobyte.String(sk)\n\t//\n\t//var outerSequence, indexBytes, privBytes cryptobyte.String\n\t//\n\t//if !skBytes.ReadASN1(&outerSequence, asn1.SEQUENCE) {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareNotASN1SequenceError{}\n\t//}\n\t//\n\t//if !outerSequence.ReadAnyASN1Element(&indexBytes, nil) {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareNoIndexError{}\n\t//}\n\t//\n\t//if !outerSequence.ReadAnyASN1Element(&privBytes, nil) {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareNoPrivError{}\n\t//}\n\t//\n\t//if !skBytes.Empty() || !outerSequence.Empty() {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareTrailingBytesError{}\n\t//}\n\t//\n\t//Index, err := group.UnmarshalScalar(indexBytes, g.Order())\n\t//if err != nil {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareASN1UnmarshalIndexError{Err: err}\n\t//}\n\t//\n\t//priv, err := group.UnmarshalScalar(privBytes, g.Order())\n\t//if err != nil {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareASN1UnmarshalPrivError{Err: err}\n\t//}\n\t//\n\t//privkey, err := NewPrivateKeyShareWithSecret(g, Index, priv)\n\t//if err != nil {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareNewPrivateKeyShareFromSecretError{Err: err}\n\t//}\n\n\tska, err := group.UnmarshalScalar(sk, g.Order())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t//privkey, err := NewPrivateKeyShareWithSecret(params, ska)\n\t//if err != nil {\n\t//\treturn nil, ASN1UnmarshalPrivateKeyShareNewPrivateKeyShareFromSecretError{Err: err}\n\t//}\n\n\treturn ska, nil\n}\n\nfunc ASN1UnmarshalDecryptionShare(g group.Group, data []byte) (*DecryptionShare, error) { //nolint:dupl\n\tdataBytes := cryptobyte.String(data)\n\n\tvar outerSequence, shareBytes, pubBytes cryptobyte.String\n\n\tif !dataBytes.ReadASN1(&outerSequence, asn1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalDecryptionShareNotASequenceError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&shareBytes, nil) {\n\t\treturn nil, ASN1UnmarshalDecryptionShareNoIndexError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&pubBytes, nil) {\n\t\treturn nil, ASN1UnmarshalDecryptionShareNoPublicError{}\n\t}\n\n\tif !dataBytes.Empty() || !outerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalDecryptionShareTrailingBytesError{}\n\t}\n\n\tindex, err := group.UnmarshalScalar(asn_11.DER(shareBytes), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalDecryptionShareIndexError{Err: err}\n\t}\n\n\tpub, err := elgamal.ASN1UnmarshalDecryption(g, pubBytes)\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalDecryptionShareASN1UnmarshalDecryptionError{Err: err}\n\t}\n\n\tproof := NewDecryptionShare(index, pub)\n\n\treturn proof, nil\n}\n\nfunc ASN1UnmarshalDecryptionProofShare(g group.Group, data []byte) (*DecryptionShareProof, error) { //nolint:dupl\n\tdataBytes := cryptobyte.String(data)\n\n\tvar outerSequence, shareBytes, pubBytes cryptobyte.String\n\n\tif !dataBytes.ReadASN1(&outerSequence, asn1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareNotASequenceError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&shareBytes, nil) {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareNoIndexError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&pubBytes, nil) {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareNoPublicError{}\n\t}\n\n\tif !dataBytes.Empty() || !outerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareTrailingBytesError{}\n\t}\n\n\tindex, err := group.UnmarshalScalar(asn_11.DER(shareBytes), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareIndexError{Err: err}\n\t}\n\n\tpub, err := elgamal.ASN1UnmarshalDecryptionProof(g, pubBytes)\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalDecryptionProofShareASN1UnmarshalDecryptionProofError{Err: err}\n\t}\n\n\tproof := NewDecryptionProofShare(index, pub)\n\n\treturn proof, nil\n}\n\n// asn1MarshalShareExtraBytes as\n//\n//\textra ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tOCTET STRING\n//\t\tOCTET STRING\n//\t\t...\n//\t}\nfunc asn1MarshalShareExtraBytes(shareIndex *group.Scalar, extra ...[]byte) ([]byte, error) {\n\tshareIndexBytes, err := shareIndex.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalShareExtraBytesShareIndexASN1Marshal{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\n\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(shareIndexBytes)\n\t\tif len(extra) > 0 {\n\t\t\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\t\t\tfor _, ex := range extra {\n\t\t\t\t\tc.AddASN1OctetString(ex)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalShareExtraBytesShareBytes{Err: err}\n\t}\n\n\treturn b, nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/distributed/distributed_test.go",
    "content": "package distributed\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/util\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc TestDistributed(t *testing.T) {\n\tthreshold, parties := uint64(3), uint64(5)\n\ttestMessage := []byte(\"test-message\")\n\ttestExtra := []byte(\"test-extra\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\t// User should provide:\n\t\t\t// - threshold\n\t\t\t// - parties\n\t\t\t// - group name\n\n\t\t\t// User generates private key shares and EncryptionKey key\n\t\t\tprivKeyShares, pkey, err := NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t// Encrypt msg\n\t\t\trnd, err := group.RandomScalar(pkey.Parameters().Group().Order())\n\t\t\tciphertext, err := pkey.Encrypt(rnd, testMessage)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// DecryptAll ciphertext by parts\n\t\t\tdecshares := make([]crypto.Decryption, parties)\n\n\t\t\t// DecryptAll ciphertext by parts and with proof for each part\n\t\t\tdecshares2 := make([]crypto.Decryption, parties)\n\n\t\t\tdecshares3 := make([][]byte, parties)\n\t\t\tdecshares4 := make([][]byte, parties)\n\t\t\tproofs := make([][]byte, parties)\n\n\t\t\tfor i, share := range privKeyShares {\n\t\t\t\t// Simulate simple decrypted\n\t\t\t\tdecshares[i], err = share.Decrypt(ciphertext, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\t// Simulate decrypted with proofs\n\t\t\t\trnd, err := group.RandomScalar(pkey.Parameters().Group().Order())\n\t\t\t\tdecshares2[i], proofs[i], err = share.ProvableDecrypt(rnd, ciphertext, testExtra, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecshares4[i], err = decshares[i].Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecshares3[i], err = decshares2[i].Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdecryptionShareCombiner := NewDecryptionShareCombiner(g, threshold)\n\n\t\t\tcombinedDecrypted, err := decryptionShareCombiner.DecryptionSharesCombine(ciphertext, decshares4...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tcombinedDecrypted2, err := decryptionShareCombiner.DecryptionSharesCombine(ciphertext, decshares3...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Plaintext after DecryptAll\n\t\t\tdecmsg, err := combinedDecrypted.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Plaintext after ProvableDecryptAll\n\t\t\tdecmsg2, err := combinedDecrypted2.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Check that plaintexts are equal to initial msg\n\t\t\tif !bytes.Equal(decmsg, testMessage) {\n\t\t\t\tt.Fatalf(\"%x %x\\n\", decmsg, testMessage)\n\t\t\t}\n\t\t\tif !bytes.Equal(decmsg2, testMessage) {\n\t\t\t\tt.Fatalf(\"%x %x\\n\", decmsg2, testMessage)\n\t\t\t}\n\n\t\t\t// VerifyAll proofs from ProvableDecryptAll by public key shares.\n\t\t\t// With extra bytes\n\t\t\tfor i, decshare := range proofs {\n\t\t\t\terr = privKeyShares[i].pkey.Verify(decshare, testExtra)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Without extra bytes, should fail, because proof challenge uses\n\t\t\t// testExtra in a hash creation, and since we used testExtra in\n\t\t\t// ProvableDecryptAll, we should also use testExtra in verify.\n\t\t\t//\n\t\t\t// However, if you use nil for extra in ProvableDecryptAll, then\n\t\t\t// VerifyAll extra should also be nil\n\t\t\tfor i, decshare := range proofs {\n\t\t\t\terr = privKeyShares[i].pkey.Verify(decshare, nil)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestShareRegeneration(t *testing.T) {\n\tthreshold, parties := uint64(3), uint64(5)\n\tfor _, g := range group.All() {\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\t// User generates private key shares and EncryptionKey key\n\t\t\tprivKeyShares, _, err := NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tprivSH := make([]crypto.EncryptionPrivateKeyShare, len(privKeyShares))\n\t\t\tfor i, pp := range privKeyShares {\n\t\t\t\tprivSH[i] = pp\n\t\t\t}\n\n\t\t\tindex := parties - 1\n\n\t\t\t// Suppose we only know an Index of a share, but not share itself\n\t\t\tshareIndex := privKeyShares[index].params.Index()\n\t\t\t//\n\t\t\t//// Simulate storing private key shares in I/O\n\t\t\t//privKeySharesBytes := make([][]byte, len(privKeyShares))\n\t\t\t//for i, privKeyShare := range privKeyShares {\n\t\t\t//\tprivKeySharesBytes[i], err = privKeyShare.Marshal()\n\t\t\t//\tif err != nil {\n\t\t\t//\t\tt.Fatal(err)\n\t\t\t//\t}\n\t\t\t//}\n\n\t\t\t// Then we wish to restore that private key share\n\t\t\t// Given only Index of private key share of interest and\n\t\t\t// #threshold amount of private key shares\n\t\t\tregenerator := NewRegenerator(g, threshold)\n\t\t\tvar regeneratorI crypto.EncryptionPrivateKeyShareRegenerator = regenerator\n\t\t\trestoredSinglePrivKeyShare, err := regeneratorI.RegeneratePrivateKeyShare(shareIndex, privSH)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ASN1 marshal just to compare by bytes\n\t\t\trestoredSinglePrivKeyShareBytes, err := restoredSinglePrivKeyShare.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ASN1 marshal just to compare by bytes\n\t\t\tprivKeySharesRestoredBytes, err := privKeyShares[index].Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif !bytes.Equal(restoredSinglePrivKeyShareBytes, privKeySharesRestoredBytes) {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptDecryptManyData(t *testing.T) {\n\tdata1 := []byte(\"Hello World!\")\n\tdata2 := []byte(\"I'm Alice\")\n\tdata3 := []byte(\"Good weather\")\n\tdata4 := []byte(\"Happy coding :)\")\n\tdataSet := [][]byte{data1, data2, data3, data4}\n\n\tfor _, g := range group.All() {\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\tvar parties uint64 = 5\n\t\t\tvar threshold uint64 = 3\n\n\t\t\t// Create new private key\n\t\t\tvar pubKeyI crypto.EncryptionKey\n\t\t\tprivKeyShares, pubKeyI, err := NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// All private key shares should have exactly same algorithm\n\t\t\tprivKeyShare0Algo := privKeyShares[0].Parameters().Algorithm()\n\n\t\t\t// Encrypt all ciphertexts, so at output we get ciphertext={ct1,ct2}\n\t\t\trnd, err := group.RandomScalar(pubKeyI.Parameters().Group().Order())\n\t\t\tciphertext, err := util.EncryptAll(rnd, pubKeyI, privKeyShare0Algo, dataSet...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tdecryptionSharesPerCiphertext := make([][]crypto.Decryption, threshold)\n\t\t\tdecryptionSharesPerCiphertextBytes := make([][][]byte, threshold)\n\t\t\tcts := make([][]byte, len(dataSet))\n\t\t\t// Each private key share should decrypt a ciphertext, i.e.\n\t\t\t// since ciphertext={c1,c2}, so we got decrypted={dec1,dec2}\n\t\t\tfor i, privKeyShare := range privKeyShares[:threshold] {\n\t\t\t\t// DecryptAll ciphertext -> {dec1,dec2}\n\t\t\t\tcts, decryptionSharesPerCiphertext[i], err = util.DecryptAll(privKeyShare, privKeyShare.Parameters().Algorithm(), ciphertext, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecryptionSharesBytes := make([][]byte, len(dataSet))\n\n\t\t\t\t// Range over all decrypted ({dec1,dec2}) and ASN1 marshal each\n\t\t\t\tfor j, decryptionShare := range decryptionSharesPerCiphertext[i] {\n\t\t\t\t\tdecryptionSharesBytes[j], err = decryptionShare.Marshal()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdecryptionSharesPerCiphertextBytes[i] = make([][]byte, len(dataSet))\n\t\t\t\t// Store ASN1 marshalled decrypted ciphertext pair {dec1B,dec2B}\n\t\t\t\tdecryptionSharesPerCiphertextBytes[i] = decryptionSharesBytes\n\t\t\t}\n\n\t\t\t// Time to combine decrypted shares\n\t\t\t//\n\t\t\t// We combine it as follows, suppose we have ciphertexts={ct1,ct2}\n\t\t\t// and 3 private key shares then combining is done as follows:\n\t\t\t// plain1={dec1,dec11,dec111}, plain2={dec2,dec22,dec222}\n\t\t\tdecryptionShareCombiner := NewDecryptionShareCombiner(g, threshold)\n\n\t\t\tfor i := range dataSet {\n\t\t\t\tdecryptionSharesBytesPerSpecificCiphertext := make([][]byte, threshold)\n\t\t\t\tfor j := range decryptionSharesPerCiphertextBytes {\n\t\t\t\t\tdecryptionSharesBytesPerSpecificCiphertext[j] = append(decryptionSharesBytesPerSpecificCiphertext[j], decryptionSharesPerCiphertextBytes[j][i]...)\n\t\t\t\t}\n\n\t\t\t\t// Combine decryption shares for ciphertext ct[i]\n\t\t\t\tcombinedDecrypted, err := decryptionShareCombiner.DecryptionSharesCombine(cts[i], decryptionSharesBytesPerSpecificCiphertext...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tplaintext, err := combinedDecrypted.Plaintext()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif !bytes.Equal(plaintext, dataSet[i]) {\n\t\t\t\t\tt.Fatal(\"decrypted not equal to initial data\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptProvableDecryptManyData(t *testing.T) {\n\textra := []byte(\"Reji\")\n\tdata1 := []byte(\"Hello World!\")\n\tdata2 := []byte(\"I'm Alice\")\n\tdataSet := [][]byte{data1, data2}\n\n\tfor _, g := range group.All() {\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\tvar parties uint64 = 3\n\t\t\tvar threshold uint64 = 2\n\n\t\t\t// Create new private key\n\t\t\tvar pubKeyI crypto.EncryptionKey\n\t\t\tprivKeyShares, pubKeyI, err := NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// All private key shares should have exactly same algorithm\n\t\t\tprivKeyShare0Algo := privKeyShares[0].Parameters().Algorithm()\n\n\t\t\t// Encrypt all ciphertexts, so at output we get ciphertext={ct1,ct2}\n\t\t\trnd, err := group.RandomScalar(pubKeyI.Parameters().Group().Order())\n\t\t\tciphertext, err := util.EncryptAll(rnd, pubKeyI, privKeyShare0Algo, dataSet...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tdecryptionSharesProofsPerCiphertext := make([][][]byte, threshold)\n\t\t\tdecryptionSharesPerCiphertext := make([][]crypto.Decryption, threshold)\n\t\t\tdecryptionSharesPerCiphertextBytes := make([][][]byte, threshold)\n\t\t\tcts := make([][]byte, len(dataSet))\n\n\t\t\t// Each private key share should decrypt a ciphertext, i.e.\n\t\t\t// since ciphertext={c1,c2}, so we got decrypted={dec1,dec2}\n\t\t\tfor i, privKeyShare := range privKeyShares[:threshold] {\n\t\t\t\t// DecryptAll ciphertext -> {dec1,dec2}\n\t\t\t\trnd, err := group.RandomScalar(pubKeyI.Parameters().Group().Order())\n\t\t\t\tcts, decryptionSharesPerCiphertext[i], decryptionSharesProofsPerCiphertext[i], err = util.ProvableDecryptAll(rnd, privKeyShare, privKeyShare.Parameters().Algorithm(), ciphertext, extra, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecryptionSharesBytes := make([][]byte, len(dataSet))\n\n\t\t\t\t// Range over all decrypted ({dec1,dec2}) and ASN1 marshal each\n\t\t\t\tfor j, decryptionShare := range decryptionSharesPerCiphertext[i] {\n\t\t\t\t\tdecryptionSharesBytes[j], err = decryptionShare.Marshal()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdecryptionSharesPerCiphertextBytes[i] = make([][]byte, len(dataSet))\n\t\t\t\t// Store ASN1 marshalled decrypted ciphertext pair {dec1B,dec2B}\n\t\t\t\tdecryptionSharesPerCiphertextBytes[i] = decryptionSharesBytes\n\t\t\t}\n\n\t\t\t// Time to combine decrypted shares\n\t\t\t//\n\t\t\t// We combine it as follows, suppose we have ciphertexts={ct1,ct2}\n\t\t\t// and 3 private key shares then combining is done as follows:\n\t\t\t// plain1={dec1,dec11,dec111}, plain2={dec2,dec22,dec222}\n\t\t\tdecryptionShareCombiner := NewDecryptionShareCombiner(g, threshold)\n\n\t\t\tfor i := range dataSet {\n\t\t\t\tdecryptionSharesBytesPerSpecificCiphertext := make([][]byte, threshold)\n\t\t\t\tfor j := range decryptionSharesPerCiphertextBytes {\n\t\t\t\t\terr = privKeyShares[j].EncryptionKey().Verify(decryptionSharesProofsPerCiphertext[j][i], extra)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tdecryptionSharesBytesPerSpecificCiphertext[j] = append(decryptionSharesBytesPerSpecificCiphertext[j], decryptionSharesPerCiphertextBytes[j][i]...)\n\t\t\t\t}\n\n\t\t\t\t// Combine decryption shares for ciphertext ct[i]\n\t\t\t\tcombinedDecrypted, err := decryptionShareCombiner.DecryptionSharesCombine(cts[i], decryptionSharesBytesPerSpecificCiphertext...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tplaintext, err := combinedDecrypted.Plaintext()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif !bytes.Equal(plaintext, dataSet[i]) {\n\t\t\t\t\tt.Fatal(\"decrypted not equal to initial data\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/crypto/elgamal/distributed/parameters.go",
    "content": "package distributed\n\nimport (\n\t\"crypto/x509/pkix\"\n\tasn_1 \"encoding/asn1\"\n\tasn_11 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/math/group\"\n)\n\n// Parameters are ElGamal key parameters.\ntype Parameters struct {\n\t// g is a group that ElGamal key belongs to.\n\tg     group.Group\n\tindex *group.Scalar\n}\n\nfunc NewParameters(g group.Group, index *group.Scalar) *Parameters {\n\treturn &Parameters{\n\t\tg:     g,\n\t\tindex: index,\n\t}\n}\n\nfunc (p *Parameters) Group() group.Group {\n\treturn p.g\n}\n\nfunc (p *Parameters) Index() *group.Scalar {\n\treturn p.index\n}\n\nfunc (p *Parameters) Algorithm() asn_1.ObjectIdentifier {\n\treturn crypto.DistributedElGamalEncryptionOID\n}\n\nfunc (p *Parameters) ToPKIXAlgorithmIdentifier() (algorithm pkix.AlgorithmIdentifier, err error) {\n\tparameters, err := group.Marshal(p.g)\n\tif err != nil {\n\t\treturn pkix.AlgorithmIdentifier{}, ToPKIXAlgorithmIdentifierGroupError{Err: err}\n\t}\n\n\tindex, err := p.index.Marshal()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar builder cryptobyte.Builder\n\n\tbuilder.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(parameters)\n\t\tc.AddBytes(index)\n\t})\n\n\tbb, err := builder.Bytes()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\talgorithm = pkix.AlgorithmIdentifier{\n\t\tAlgorithm: p.Algorithm(),\n\t\tParameters: asn_1.RawValue{\n\t\t\tFullBytes: bb,\n\t\t},\n\t}\n\n\treturn\n}\n\nfunc OfPKIXAlgorithmIdentifier(algorithm pkix.AlgorithmIdentifier) (crypto.AlgorithmIdentifierParameters, error) {\n\tc := cryptobyte.String(algorithm.Parameters.FullBytes)\n\n\tvar cc cryptobyte.String\n\tvar cc1 cryptobyte.String\n\tvar cc2 cryptobyte.String\n\tif !c.ReadASN1(&cc, asn1.SEQUENCE) {\n\t\tpanic(\"SEQ\")\n\t}\n\tif !cc.ReadAnyASN1Element(&cc1, nil) {\n\t\tpanic(\"1111\")\n\t}\n\tif !cc.ReadAnyASN1Element(&cc2, nil) {\n\t\tpanic(\"2222\")\n\t}\n\n\tg, err := group.Unmarshal(asn_11.DER(cc1))\n\tif err != nil {\n\t\treturn nil, OfPKIXAlgorithmIdentifierGroupError{Err: err}\n\t}\n\n\tindex, err := group.UnmarshalScalar(asn_11.DER(cc2), g.Order())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewParameters(g, index), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/distributed/x509/pkcs8.go",
    "content": "package x509\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\tx_509 \"tivi.io/core/crypto/x509/marshal\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\ntype UnmarshallerPKCS8 struct{}\n\nfunc NewUnmarshallerPKCS8() x_509.KeyUnmarshaller[*group.Scalar] {\n\treturn UnmarshallerPKCS8{}\n}\n\nfunc (pkcs8 UnmarshallerPKCS8) UnmarshalKeyParameters(algoID pkix.AlgorithmIdentifier) (oid asn1.ObjectIdentifier, params crypto.AlgorithmIdentifierParameters, err error) {\n\tparams, err = distributed.OfPKIXAlgorithmIdentifier(algoID)\n\toid = algoID.Algorithm\n\treturn\n}\n\nfunc (pkcs8 UnmarshallerPKCS8) UnmarshalKeyElement(g group.Group, der asn_1.DER) (*group.Scalar, error) {\n\treturn elgamal.ASN1UnmarshalPrivateElement(g, der)\n}\n"
  },
  {
    "path": "core/crypto/elgamal/distributed/x509/x509.go",
    "content": "package x509\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\ntype Unmarshaller struct{}\n\nfunc NewUnmarshaller() Unmarshaller {\n\treturn Unmarshaller{}\n}\n\nfunc (x509 Unmarshaller) UnmarshalKeyParameters(algoID pkix.AlgorithmIdentifier) (oid asn1.ObjectIdentifier, params crypto.AlgorithmIdentifierParameters, err error) {\n\tparams, err = distributed.OfPKIXAlgorithmIdentifier(algoID)\n\toid = algoID.Algorithm\n\treturn\n}\n\nfunc (x509 Unmarshaller) UnmarshalKeyElement(g group.Group, der asn_1.DER) (group.Element, error) {\n\treturn elgamal.ASN1UnmarshalPublicElement(g, der)\n}\n"
  },
  {
    "path": "core/crypto/elgamal/elgamal.go",
    "content": "package elgamal\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"golang.org/x/crypto/sha3\"\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\t\"tivi.io/core/crypto/elgamal/nizkp/ciphertext\"\n\t\"tivi.io/core/crypto/elgamal/nizkp/key\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/math/group\"\n)\n\n// PublicKey is ElGamal public key.\ntype PublicKey struct {\n\t// parameters are ElGamal key parameters.\n\tparameters *Parameters\n\t// public is a public component of ElGamal key.\n\tpublic group.Element\n}\n\n// NewPublicKey returns new ElGamal public key.\nfunc NewPublicKey(parameters *Parameters, public group.Element) *PublicKey {\n\treturn &PublicKey{\n\t\tparameters: parameters,\n\t\tpublic:     public,\n\t}\n}\n\nfunc (pkey *PublicKey) Public() group.Element {\n\treturn pkey.public\n}\n\n// Parameters returns ElGamal public key parameters.\nfunc (pkey *PublicKey) Parameters() crypto.AlgorithmIdentifierParameters {\n\treturn pkey.parameters\n}\n\n// Fingerprint returns ElGamal public key fingerprint.\nfunc (pkey *PublicKey) Fingerprint() (uint64, error) {\n\tder, err := pkey.Public().Marshal()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to ASN.1 marshal ElGamal public element: %v\", err)\n\t}\n\n\tdigest := sha256.Sum256(der)\n\n\t// Output only first 24 bytes of SHA256 digest\n\treturn binary.BigEndian.Uint64(digest[24:]), nil\n}\n\n// Encrypt creates ASN.1 marshalled ElGamal ciphertext from plaintext.\n// Process of creation is following:\n//  1. pad a plaintext\n//  2. encode a padded plaintext into group element\n//  3. encrypt group element and return ElGamal ciphertext\n//  4. ASN.1 marshal ElGamal ciphertext\n//\n// s is considered to be a random scalar.\nfunc (pkey *PublicKey) Encrypt(s *group.Scalar, plaintext []byte) (der []byte, err error) {\n\t// PadBytes a plaintext\n\tpadded, err := pkey.Parameters().Group().PadBytes(plaintext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to pad a plaintext: %v\", err)\n\t}\n\n\t// Encode a padded plaintext\n\tencoded, err := pkey.Parameters().Group().Encode(padded)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encode a padded plaintext: %v\", err)\n\t}\n\n\t// Encrypt encoded group element and return ElGamal ciphertext\n\tct, err := pkey.EncryptGroupElement(s, encoded)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encrypt encoded group element: %v\", err)\n\t}\n\n\t// ASN.1 marshal ElGamal ciphertext\n\tder, err = ct.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal ciphertext: %v\", err)\n\t}\n\treturn\n}\n\n// EncryptGroupElement creates ASN.1 marshalled ElGamal ciphertext from group element.\n//\n// s is considered to be a random scalar.\nfunc (pkey *PublicKey) EncryptGroupElement(s *group.Scalar, msg group.Element) (*Ciphertext, error) {\n\t// g {^,*} s\n\tA, err := pkey.Parameters().Group().Generator().Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create ElGamal ciphertext a element: %v\", err)\n\t}\n\n\t// y {^,*} s\n\tB, err := pkey.Public().Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to scale ElGamal public element by random scalar: %v\", err)\n\t}\n\n\t// b {*,+} msg\n\tB, err = B.Op(msg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create ElGamal ciphertext b element: %v\", err)\n\t}\n\n\treturn NewCiphertext(A, B), nil\n}\n\n// Verify verifies ASN.1 marshalled ElGamal proof of correct decryption.\n//\n// See ProofChallenge for salt description.\nfunc (pkey *PublicKey) Verify(proof, salt []byte) error {\n\t// decryption proof is a non-interactive zero-knowledge proof\n\tpr, err := ASN1UnmarshalDecryptionProof(pkey.Parameters().Group(), proof)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal decryption proof: %v\", err)\n\t}\n\n\t// Recreate prover's key commitment\n\tkeyc, err := nizkp.Verify(pkey.Parameters().Group().Generator(), pkey.public, pr.Proof().Challenge(), pr.Proof().Response())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to recreate prover's key commitment: %v\", err)\n\t}\n\n\tdec := pr.decryption.Value().Inverse()\n\n\t// Create message commitment from ElGamal ciphertext b element\n\tmsgc2, err := pr.ciphertext.B().Op(dec)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create message commitment from ElGamal ciphertext b element: %v\", err)\n\t}\n\n\t// Recreate prover's message commitment\n\tmsgc, err := nizkp.Verify(pr.ciphertext.A(), msgc2, pr.Proof().Challenge(), pr.Proof().Response())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to recreate prover's message commitment: %v\", err)\n\t}\n\n\t// Recreate prover's ElGamal proof challenge\n\tchallenge, err := pkey.ProofChallenge(pr.ciphertext, pr.decryption.Value(), msgc, keyc, salt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to recreate prover's ElGamal proof challenge: %v\", err)\n\t}\n\n\t// Recreated prover's challenge should be equal to the challenge that prover has actually calculated\n\terr = challenge.Equal(pr.Proof().Challenge())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"recreated challenge on verifier side doesn't match the one generated on a prover side: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// ProofChallenge computes the non-interactive challenge (hash).\n// Prover generates a challenge and verifier recomputes the same\n// challenge on its side and then compares two challenges.\n//\n// Hash is created using Shake256 and output is always non-deterministically variable length.\n//\n// Salt is used to add uniqueness to the challenge. So that you can create different challenges\n// for the same input to the ProofChallenge, just by providing different salt for each invocation.\nfunc (pkey *PublicKey) ProofChallenge(ct *Ciphertext, dec, msgc, keyc group.Element, salt ...[]byte) (*group.Scalar, error) {\n\tpubDer, err := pkey.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal public key: %v\", err)\n\t}\n\n\tctDer, err := ct.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal ciphertext: %v\", err)\n\t}\n\n\tdecDer, err := dec.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal decryption: %v\", err)\n\t}\n\n\tmsgcDer, err := msgc.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal proof message commitment: %v\", err)\n\t}\n\n\tkeycDer, err := keyc.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal proof key commitment: %v\", err)\n\t}\n\n\t// Custom string, acts as a challenge header\n\thashID := []byte(\"DECRYPTION\")\n\n\t// If salt == nil, then\n\t// digest = HASH(hashID || pubDer || ctDer ... || keycDer)\n\t//\n\t// otherwise,\n\t// digest = HASH(hashID || pubDer || ctDer ... || keycDer || salt)\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddASN1(asn1.PrintableString, func(builder *cryptobyte.Builder) {\n\t\t\tbuilder.AddBytes(hashID)\n\t\t})\n\t\tbuilder.AddBytes(pubDer)\n\t\tbuilder.AddBytes(ctDer)\n\t\tbuilder.AddBytes(decDer)\n\t\tbuilder.AddBytes(msgcDer)\n\t\tbuilder.AddBytes(keycDer)\n\t\tif len(salt) > 0 {\n\t\t\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\t\t\tfor _, ex := range salt {\n\t\t\t\t\tbuilder.AddASN1OctetString(ex)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tder, err := builder.Bytes()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal proof challenge: %v\", err)\n\t}\n\n\t// Variable length hash\n\thash := sha3.NewShake256()\n\n\t// Shake instance does not fail\n\thash.Write(der) //nolint: errcheck\n\n\t// Create group scalar from hash bytes\n\tchallenge, err := group.ScalarValueOfReader(hash, pkey.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group scalar from hashed ASN.1 marshalled ElGamal proof challenge: %v\", err)\n\t}\n\n\treturn challenge, nil\n}\n\n// ASN1Marshal ASN.1 marshals ElGamal public key as\n//\n//\tpkey ::= GROUP ELEMENT\n//\n//\tGROUP ELEMENT ::= CHOICE {\n//\t\tModqPElement\tINTEGER\n//\t\tECqPElement\tOCTET STRING\n//\t\tEdwards25519Element\tOCTET STRING\n//\t}\nfunc (pkey *PublicKey) Marshal() (der asn_1.DER, err error) {\n\tder, err = pkey.Public().Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal public element: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1UnmarshalPublicElement ASN.1 unmarshalls ElGamal public element.\nfunc ASN1UnmarshalPublicElement(g group.Group, der asn_1.DER) (pub group.Element, err error) {\n\tpub, err = g.ElementOf(der)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal public element: %v\", err)\n\t}\n\treturn\n}\n\n// PrivateKey is ElGamal private key.\ntype PrivateKey struct {\n\t// private is a private component of the key.\n\tprivate   *group.Scalar\n\tpublicKey *PublicKey\n}\n\n// NewPrivateKey returns new ElGamal private key.\n//\n// s is considered to be a random scalar.\nfunc NewPrivateKey(params *Parameters, s *group.Scalar) (*PrivateKey, error) {\n\tG := params.Group().Generator()\n\n\t// g {^,*} s\n\tY, err := G.Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create ElGamal public element: %v\", err)\n\t}\n\n\treturn &PrivateKey{\n\t\tprivate:   s,\n\t\tpublicKey: NewPublicKey(params, Y),\n\t}, nil\n}\n\n// Parameters returns ElGamal private key parameters.\nfunc (key *PrivateKey) Parameters() crypto.AlgorithmIdentifierParameters {\n\treturn key.publicKey.parameters\n}\n\n// Fingerprint is unimplemented for ElGamal private key.\nfunc (key *PrivateKey) Public() *PublicKey {\n\treturn key.publicKey\n}\n\n// Fingerprint is unimplemented for ElGamal private key.\nfunc (key *PrivateKey) Fingerprint() (uint64, error) {\n\treturn 0, nil\n}\n\n// PublicKey returns ElGamal public key.\nfunc (key *PrivateKey) EncryptionKey() crypto.EncryptionKey {\n\treturn key.publicKey\n}\n\n// Decrypt decrypts ASN.1 marshalled ElGamal ciphertext into a group element.\nfunc (key *PrivateKey) Decrypt(ciphertext []byte, checkDecodable bool) (crypto.Decryption, error) {\n\tct, err := ASN1UnmarshalCiphertext(key.Public().Parameters().Group(), ciphertext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal ciphertext: %v\", err)\n\t}\n\n\treturn decrypt(key.Parameters().Group(), ct.A(), key.private, ct, checkDecodable)\n}\n\nfunc (key *PublicKey) DecryptWithRandomness(r *group.Scalar, ciphertext []byte, checkDecodable bool) (crypto.Decryption, error) {\n\tct, err := ASN1UnmarshalCiphertext(key.Parameters().Group(), ciphertext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal ciphertext: %v\", err)\n\t}\n\n\tgr, err := key.Parameters().Group().Generator().Scale(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = ct.A().Equal(gr); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn decrypt(key.Parameters().Group(), key.Public(), r, ct, checkDecodable)\n}\n\nfunc decrypt(g group.Group, base group.Element, s *group.Scalar, ct *Ciphertext, checkDecodable bool) (crypto.Decryption, error) {\n\t// Y {^,*} x\n\tA, err := base.Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot scale ElGamal public key element: %v\", err)\n\t}\n\n\t// -A\n\tA = A.Inverse()\n\n\t// B {*,+} AInv\n\tB, err := ct.B().Op(A)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot recreate ElGamal ciphertext B element: %v\", err)\n\t}\n\n\tif checkDecodable {\n\t\tif _, err = B.Decode(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"ElGamal ciphertext B group element is not decodable: %v\", err)\n\t\t}\n\t}\n\n\treturn NewDecryption(g, B), nil\n}\n\n// ProvableDecrypt decrypts ASN.1 marshalled ElGamal ciphertext into a group element, and\n// also provides proofs of correct decryption.\n//\n// s is considered to be a random scalar.\n//\n// See ProofChallenge for salt description.\nfunc (key *PrivateKey) ProvableDecrypt(s *group.Scalar, ciphertext, salt []byte, checkDecodable bool) (dec crypto.Decryption, proof []byte, err error) {\n\tdec, err = key.Decrypt(ciphertext, checkDecodable)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to decrypt an ElGamal ciphertext: %v\", err)\n\t}\n\n\tproof, err = key.Prove(s, ciphertext, dec, salt)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create an ElGamal proof: %v\", err)\n\t}\n\treturn\n}\n\n// Prove proves that given ASN.1 marshalled ElGamal ciphertext and corresponding to it\n// decryption are correct.\n//\n// s is considered to be a random scalar.\n//\n// See ProofChallenge for salt description.\nfunc (key *PrivateKey) Prove(s *group.Scalar, ciphertext []byte, dec crypto.Decryption, salt []byte) ([]byte, error) {\n\tct, err := ASN1UnmarshalCiphertext(key.Parameters().Group(), ciphertext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal ciphertext: %v\", err)\n\t}\n\n\t// Create msg commitment a {^,*} s\n\tmsgc, err := ct.A().Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create message commitment: %v\", err)\n\t}\n\n\t// Create key commitment g {^,*} s\n\tkeyc, err := key.Parameters().Group().Generator().Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create key commitment: %v\", err)\n\t}\n\n\t// Create proof challenge (h) for verifier\n\tchallenge, err := key.publicKey.ProofChallenge(ct, dec.Value(), msgc, keyc, salt)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create an ElGamal proof challenge: %v\", err)\n\t}\n\n\t// r = h * x\n\tresponse, err := challenge.Mul(key.private)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to multiply ElGamal proof challenge by ElGamal private element: %v\", err)\n\t}\n\n\t// r = r + s\n\tresponse, err = response.Add(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create an ElGamal proof response: %v\", err)\n\t}\n\n\tder, err := NewDecryptionProof(ct, NewDecryption(key.Parameters().Group(), dec.Value()), nizkp.NewProof(challenge, response)).MarshalASN1()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn der, nil\n}\n\nfunc (pkey *PublicKey) NewCommitments(g group.Group, opts []crypto.ProofOpts) ([]nizkp.Commitment, uint64, error) {\n\tvar count uint64\n\tcommitments := make([]nizkp.Commitment, len(opts))\n\n\tfor i, opt := range opts {\n\t\tswitch op := opt.(type) {\n\t\tcase crypto.KeyPairProofOpts:\n\t\t\tcommitments[i] = key.NewKeyPairCommitment(g, pkey.Public())\n\t\t\tcount += commitments[i].Count()\n\t\tcase crypto.CiphertextProofOpts:\n\t\t\tct, err := ASN1UnmarshalCiphertext(g, op.Ciphertext())\n\t\t\tdec, err := ASN1UnmarshalDecryption(g, op.Decrypted())\n\t\t\tcommitments[i], err = ciphertext.NewCiphertextCommitment(g, ct.A(), ct.B(), dec.Value())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, fmt.Errorf(\"failed to create ciphertext commitments: %v\", err)\n\t\t\t}\n\t\t\tcount += commitments[i].Count()\n\t\tdefault:\n\t\t\treturn nil, 0, fmt.Errorf(\"unsupported proof type\")\n\t\t}\n\t}\n\n\treturn commitments, count, nil\n}\n\nfunc (key *PrivateKey) ProofProve(s *group.Scalar, salt []byte, opts []crypto.ProofOpts) (der []byte, err error) {\n\toptz, count, err := key.Public().NewCommitments(key.Parameters().Group(), opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prove key pair: %v\", err)\n\t}\n\n\t// Size of array\n\tcommitments := make([]group.Element, count)\n\tvar added int\n\n\tfor _, optzz := range optz {\n\t\tcommit, err := optzz.Create(s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to prove key pair: %v\", err)\n\t\t}\n\n\t\tfor j, commitment := range commit {\n\t\t\tcommitments[added+j] = commitment\n\t\t}\n\n\t\tadded++\n\t}\n\n\t// Create proof challenge (h) for verifier\n\tchallenge, err := nizkp.Challenge(key.Parameters().Group(), commitments, salt)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create an ElGamal proof challenge: %v\", err)\n\t}\n\n\t// r = h * x\n\tresponse, err := challenge.Mul(key.private)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to multiply ElGamal proof challenge by ElGamal private element: %v\", err)\n\t}\n\n\t// r = r + s\n\tresponse, err = response.Add(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create an ElGamal proof response: %v\", err)\n\t}\n\n\tt := nizkp.NewProof(challenge, response)\n\n\tder, err = t.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal key pair proof: %v\", err)\n\t}\n\treturn\n}\n\nfunc (pkey *PublicKey) ProofVerify(proof []byte, salt []byte, opts []crypto.ProofOpts) (err error) {\n\t//// decryption proof is a non-interactive zero-knowledge proof\n\tt, err := nizkp.ASN1UnmarshalProof(pkey.Parameters().Group(), proof)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal decryption proof: %v\", err)\n\t}\n\n\toptz, count, err := pkey.NewCommitments(pkey.Parameters().Group(), opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to prove key pair: %v\", err)\n\t}\n\n\t// Size of array\n\tcommitments := make([]group.Element, count)\n\tvar added int\n\n\tfor _, optzz := range optz {\n\t\tcommitmentz, err := optzz.Verify(t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to prove key pair: %v\", err)\n\t\t}\n\t\tfor j, commitment := range commitmentz {\n\t\t\tcommitments[added+j] = commitment\n\t\t}\n\t\tadded++\n\t}\n\n\t// Create proof challenge (h) for verifier\n\tchallenge, err := nizkp.Challenge(pkey.Parameters().Group(), commitments, salt)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create an ElGamal proof challenge: %v\", err)\n\t}\n\n\t// Recreated prover's challenge should be equal to the challenge that prover has actually calculated\n\terr = challenge.Equal(t.Challenge())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"recreated challenge on verifier side doesn't match the one generated on a prover side: %v\", err)\n\t}\n\treturn\n}\n\n// Marshal ASN.1 marshals ElGamal private key as\n//\n//\tkey ::= INTEGER\nfunc (key *PrivateKey) Marshal() (der asn_1.DER, err error) {\n\tder, err = key.private.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal private element: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1UnmarshalPrivateElement ASN.1 unmarshalls ElGamal private element.\nfunc ASN1UnmarshalPrivateElement(g group.Group, data []byte) (priv *group.Scalar, err error) {\n\tpriv, err = group.UnmarshalScalar(data, g.Order())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal private element: %v\", err)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "core/crypto/elgamal/elgamal_test.go",
    "content": "package elgamal\n\nimport (\n\t\"bytes\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"testing\"\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\t\"tivi.io/core/test/fixtures\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/util\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc TestPrivateKeyElementASN1MarshalAndUnmarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tvar keyI crypto.DecryptionKey\n\t\t\tparameters := NewParameters(g)\n\n\t\t\t// private key element is a random scalar\n\t\t\trnd, err := group.RandomScalar(parameters.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Create private key\n\t\t\tkeyI, err = NewPrivateKey(parameters, rnd)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ASN.1 marshal private key element\n\t\t\tder, err := keyI.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ASN.1 unmarshal private key element\n\t\t\trnd2, err := ASN1UnmarshalPrivateElement(g, der)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif rnd.Equal(rnd2) != nil {\n\t\t\t\tt.Fatal(\"unequal private key elements\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPublicKeyElementASN1MarshalAndUnmarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tvar keyI crypto.DecryptionKey\n\t\t\tparameters := NewParameters(g)\n\n\t\t\t// private key element is a random scalar\n\t\t\trnd, err := group.RandomScalar(parameters.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Create private key\n\t\t\tkeyI, err = NewPrivateKey(parameters, rnd)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tvar pkeyI crypto.EncryptionKey\n\n\t\t\t// Derive public key element from private key\n\t\t\tpkeyI = keyI.EncryptionKey()\n\n\t\t\t// ASN.1 marshal public key element\n\t\t\tder, err := pkeyI.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ASN.1 unmarshal public key element\n\t\t\tpub, err := ASN1UnmarshalPublicElement(g, der)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tpub2 := pkeyI.(*PublicKey).Public()\n\n\t\t\tif pub.Equal(pub2) != nil {\n\t\t\t\tt.Fatal(\"unequal public key elements\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDecryptionProofASN1MarshalAndUnmarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tplaintext := []byte(\"Hello World!\")\n\n\t\t\tchallenge, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tresponse, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal ciphertext a\n\t\t\tA, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal ciphertext b\n\t\t\tB, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Plaintext as group element\n\t\t\tE, err := g.Encode(plaintext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tciphertext := NewCiphertext(A, B)\n\n\t\t\tdec := NewDecryption(g, E)\n\n\t\t\ttranscript := nizkp.NewProof(challenge, response)\n\n\t\t\tproof := NewDecryptionProof(ciphertext, dec, transcript)\n\n\t\t\tder, err := proof.MarshalASN1()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tproof2, err := ASN1UnmarshalDecryptionProof(g, der)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif proof.ciphertext.A().Equal(proof2.ciphertext.A()) != nil {\n\t\t\t\tt.Fatal(\"ElGamal proof ciphertext a elements differ\")\n\t\t\t}\n\n\t\t\tif proof.ciphertext.B().Equal(proof2.ciphertext.B()) != nil {\n\t\t\t\tt.Fatal(\"ElGamal proof ciphertext b elements differ\")\n\t\t\t}\n\n\t\t\tif proof.decryption.Value().Equal(proof2.decryption.Value()) != nil {\n\t\t\t\tt.Fatal(\"ElGamal decryptions differ\")\n\t\t\t}\n\n\t\t\tif proof.Proof().Challenge().Equal(proof2.Proof().Challenge()) != nil {\n\t\t\t\tt.Fatal(\"ElGamal transcripts differ\")\n\t\t\t}\n\n\t\t\tif proof.Proof().Response().Equal(proof2.Proof().Response()) != nil {\n\t\t\t\tt.Fatal(\"ElGamal proofs differ\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptDecrypt(t *testing.T) {\n\tplaintext := []byte(\"Imagine plaintext =]\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\t// private key element\n\t\t\tx, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tparameters := NewParameters(g)\n\t\t\tkey, err := NewPrivateKey(parameters, x)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Encryption randomness\n\t\t\trnd, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tct, err := key.publicKey.Encrypt(rnd, plaintext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Decrypt\n\t\t\tdec, err := key.Decrypt(ct, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tplaintext2, err := dec.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif !bytes.Equal(plaintext, plaintext2) {\n\t\t\t\tt.Fatal(\"decrypted and initial plaintexts differ\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptProvableDecryptVerify(t *testing.T) {\n\tplaintext := []byte(\"0000.101\")\n\tsalt := []byte(\"Unique ID for a proof of a given decryption, that allows you to differ among other decryptions\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\t// ElGamal parameters\n\t\t\tparameters := NewParameters(g)\n\n\t\t\t// private key element is a random scalar\n\t\t\tx, err := group.RandomScalar(parameters.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tkey, err := NewPrivateKey(parameters, x)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Randomness used in encryption\n\t\t\trnd, err := group.RandomScalar(key.Parameters().Group().Order())\n\n\t\t\t// Encrypt\n\t\t\tct, err := key.publicKey.Encrypt(rnd, plaintext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Randomness used in ElGamal proof creation\n\t\t\trnd2, err := group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tdecrypted, err := key.Decrypt(ct, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tdecrypted2, err := key.Public().DecryptWithRandomness(rnd, ct, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Decrypt with proofs returned\n\t\t\tproof, err := key.Prove(rnd2, ct, decrypted, salt)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tproof2, err := key.Prove(rnd2, ct, decrypted2, salt)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// decrypted plaintext\n\t\t\tplaintext2, err := decrypted.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tplaintext3, err := decrypted2.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Should be the same\n\t\t\tif !bytes.Equal(plaintext, plaintext2) {\n\t\t\t\tt.Fatal(\"decrypted and initial plaintexts differ\")\n\t\t\t}\n\n\t\t\t// Should be the same\n\t\t\tif !bytes.Equal(plaintext, plaintext3) {\n\t\t\t\tt.Fatal(\"decrypted and initial plaintexts differ\")\n\t\t\t}\n\n\t\t\t// Should fail, because ProvableDecryptAll used salt and therefore\n\t\t\t// VerifyAll should use as well\n\t\t\tif err = key.publicKey.Verify(proof, nil); err == nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// VerifyAll with salt should succeed, since ProvableDecrypted uses\n\t\t\t// salt\n\t\t\tif err = key.publicKey.Verify(proof, salt); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif err = key.publicKey.Verify(proof2, salt); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestE2E is a complete process of ElGamal encryption scheme:\n//  1. ElGamal key parameters generation\n//  2. ElGamal private key element generation\n//  3. ElGamal private key generation behind the crypto.DecryptionKey interface\n//  4. Random scalar generation that will be used in encryption\n//  5. Plaintext encryption, result is a byte slice that can be stored in I/O\n//  6. Ciphertext decryption, decrypted value is crypto.Decryption interface\n//  7. Right after the decryption we extract an initial plaintext from it\n//  8. We may or may not immediately generate proofs of a correct decryption,\n//     in case we don't do that right away, we store decrypted values in I/O\n//  9. Admin chooses predefined and agreed with auditor proof options that TIVI\n//     Core provides, and composes a chain to be verified later by auditor\n//  10. Admin also creates a random scalar to be used in proofs generation\n//  11. Admin generates a proof chain\n//  12. Admin stores proof chain in I/O along with necessary data that is somehow\n//     linked to the proof chain generated. NB! It is a responsibility of admin\n//     to store proof chain and its related data correctly\n//  13. Auditor imports proof chain and its related data from I/O\n//  14. Auditor composes the same proof chain options as admin did\n//     (remember that these options are already been discussed and agreed on)\n//  15. Auditor verifies proof chain\nfunc TestE2E(t *testing.T) {\n\tplaintext := []byte(\"0000.101\")\n\tsalt := []byte(\"Unique ID for a proof of a given decryption, that allows you to differ among other decryptions\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(fmt.Sprintf(\"Testing e2e encryption scheme for %v group\", g.Name()), func(t *testing.T) {\n\t\t\tt.Logf(\"Group %s is chosen for ElGamal key parameters\", g.Name())\n\n\t\t\tparams := NewParameters(g)\n\t\t\tt.Log(\"ElGamal key parameters generation\")\n\n\t\t\tpriv, err := group.RandomScalar(params.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"ElGamal private key element generation\")\n\n\t\t\tvar key crypto.DecryptionKey\n\t\t\tkey, err = NewPrivateKey(params, priv)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"ElGamal private key generation behind the crypto.DecryptionKey interface\")\n\n\t\t\ts, err := group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tt.Log(\"Random scalar generation that will be used in encryption\")\n\n\t\t\tct, err := key.EncryptionKey().Encrypt(s, plaintext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Plaintext encryption, result is a byte slice that can be stored in I/O\")\n\n\t\t\tdecrypted, err := key.Decrypt(ct, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Ciphertext decryption, decrypted value is crypto.Decryption interface\")\n\n\t\t\tplaintext2, err := decrypted.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Right after the decryption we extract an initial plaintext from it\")\n\n\t\t\t// Compare that initial plaintext and decrypted one match\n\t\t\tif !bytes.Equal(plaintext, plaintext2) {\n\t\t\t\tt.Fatal(\"initial and decrypted plaintexts differ\")\n\t\t\t}\n\n\t\t\tdecryptedDer, err := decrypted.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(`We may or may not immediately generate proofs of a correct decryption,\n                         in case we don't do that right away, we store decrypted values in I/O`)\n\n\t\t\topts := []crypto.ProofOpts{\n\t\t\t\tcrypto.NewKeyPairProofOpts(),                    // key pair proof\n\t\t\t\tcrypto.NewCiphertextProofOpts(ct, decryptedDer), // ciphertext proof\n\t\t\t}\n\t\t\tt.Log(`Admin chooses available proof options that TIVI Core provides, and\n\t\t\t             composes a chain to be verified by auditor`)\n\n\t\t\ts, err = group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Admin also creates a random scalar to be used in proofs generation\")\n\n\t\t\tproof, err := key.ProofProve(s, salt, opts)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Admin generates a proof chain\")\n\n\t\t\tproofFixture := fixtures.CiphertextProof{\n\t\t\t\tCiphertext: ct,\n\t\t\t\tDecryption: decryptedDer,\n\t\t\t\tProof:      proof,\n\t\t\t}\n\t\t\tproofDer, err := asn1.Marshal(proofFixture)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(`Admin stores proof chain in I/O along with necessary data that is somehow\n\t\t\t             linked to the proof chain generated. NB! It is a responsibility of admin\n\t\t\t             to store proof chain and its related data correctly`)\n\n\t\t\tproofFixture = fixtures.CiphertextProof{}\n\t\t\t_, err = asn1.Unmarshal(proofDer, &proofFixture)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Auditor imports proof chain and its related data from I/O\")\n\n\t\t\topts = []crypto.ProofOpts{\n\t\t\t\tcrypto.NewKeyPairProofOpts(),\n\t\t\t\tcrypto.NewCiphertextProofOpts(proofFixture.Ciphertext, proofFixture.Decryption),\n\t\t\t}\n\t\t\tt.Log(`Auditor composes the same proof chain options as admin did\n                         (remember that these options are already been discussed and agreed on)`)\n\n\t\t\tif err = key.EncryptionKey().ProofVerify(proof, salt, opts); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tt.Log(\"Auditor verifies proof chain\")\n\n\t\t\t// This steps should fail, since salt hasn't been used during proof chain generation\n\t\t\tif err = key.EncryptionKey().ProofVerify(proof, nil, opts); err == nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptDecryptManyData(t *testing.T) {\n\tplaintext1 := []byte(\"Hello World!\")\n\tplaintext2 := []byte(\"I'm Alice\")\n\tplaintext3 := []byte(\"Good weather\")\n\tplaintext4 := []byte(\"Happy coding :)\")\n\tplaintexts := [][]byte{plaintext1, plaintext2, plaintext3, plaintext4}\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\t// ElGamal parameters\n\t\t\tparameters := NewParameters(g)\n\n\t\t\t// ElGamal private key element\n\t\t\trnd, err := group.RandomScalar(parameters.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal private key\n\t\t\tkey, err := NewPrivateKey(parameters, rnd)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\trnd, err = group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tciphertext, err := util.EncryptAll(rnd, key.EncryptionKey(), nil, plaintexts...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t_, decs, err := util.DecryptAll(key, nil, ciphertext, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tfor i, dec := range decs {\n\t\t\t\tplaintext, err := dec.Plaintext()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tif !bytes.Equal(plaintext, plaintexts[i]) {\n\t\t\t\t\tt.Fatal(\"decrypted plaintext is not equal to initial plaintext\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEncryptProvableDecryptManyData(t *testing.T) {\n\tsalt := []byte(\":p\")\n\tplaintext1 := []byte(\"Hello World!\")\n\tplaintext2 := []byte(\"I'm Alice\")\n\tplaintexts := [][]byte{plaintext1, plaintext2}\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tparameter := NewParameters(g)\n\t\t\trnd, err := group.RandomScalar(parameter.Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tvar key crypto.DecryptionKey\n\n\t\t\tkey, err = NewPrivateKey(parameter, rnd)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\trnd, err = group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tciphertext, err := util.EncryptAll(rnd, key.EncryptionKey(), nil, plaintexts...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\trnd2, err := group.RandomScalar(key.Parameters().Group().Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t_, decs, proofs, err := util.ProvableDecryptAll(rnd2, key, nil, ciphertext, salt, true)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tfor i, dec := range decs {\n\t\t\t\tplaintext, err := dec.Plaintext()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tif !bytes.Equal(plaintext, plaintexts[i]) {\n\t\t\t\t\tt.Fatal(\"decrypted ElGamal plaintext doesn't match initial one\")\n\t\t\t\t}\n\n\t\t\t\terr = key.EncryptionKey().Verify(proofs[i], salt)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/crypto/elgamal/homomorphic/homomorphic.go",
    "content": "package homomorphic\n\nimport (\n\t\"encoding/asn1\"\n\t\"reflect\"\n\t\"strconv\"\n\tasn_11 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"golang.org/x/crypto/sha3\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\t\"tivi.io/core/math/group\"\n)\n\n// ElGamalHomomorphicEncryption is an implementation of crypto.HomomorphicEncryption.\n//\n// Both regular and distributed encryption schemes are supported under a single\n// implementation.\ntype ElGamalHomomorphicEncryption struct {\n\tg         group.Group\n\tthreshold uint64\n}\n\n// NewElGamalHomomorphicEncryption is a constructor for ElGamalHomomorphicEncryption.\n//\n// Don't initialize ElGamalHomomorphicEncryption as\n//\n//\tenc := NewElGamalHomomorphicEncryption{}\n//\n// instead use this constructor\n//\n//\tenc := NewElGamalHomomorphicEncryption()\n//\n// By doing so, you can be always sure, that code will not panic due to uninitialized\n// fields.\n//\n// Also note, that ElGamalHomomorphicEncryption is not a pointer, why? That is\n// because we don't modify ElGamalHomomorphicEncryption outside the\n// NewElGamalHomomorphicEncryption constructor and, in addition to, it has\n// struct fields that are either relatively small values or pointers as well.\nfunc NewElGamalHomomorphicEncryption(g group.Group, threshold uint64) *ElGamalHomomorphicEncryption {\n\treturn &ElGamalHomomorphicEncryption{\n\t\tg:         g,\n\t\tthreshold: threshold,\n\t}\n}\n\n// Encrypt implements crypto.HomomorphicEncrypter.\n//\n// Encrypt uses public key pk to assist in homomorphic encryption of marks, it\n// returns an DER marshalled Choice.\nfunc (d *ElGamalHomomorphicEncryption) Encrypt(pk crypto.EncryptionKey, marks []bool) ([]byte, error) {\n\telgamalPk, ok := pk.(*elgamal.PublicKey)\n\tif !ok {\n\t\treturn nil, EncryptElGamalHomomorphicEncryptionCastTypeError{\n\t\t\tExpected: reflect.TypeOf(new(elgamal.PublicKey)),\n\t\t\tGot:      reflect.TypeOf(pk),\n\t\t}\n\t}\n\n\tchoice, err := Encrypt(elgamalPk, marks)\n\tif err != nil {\n\t\treturn nil, EncryptElGamalHomomorphicEncryptionError{Err: err}\n\t}\n\n\tchoiceBytes, err := choice.Marshal()\n\tif err != nil {\n\t\treturn nil, EncryptElGamalHomomorphicEncryptionASN1MarshalChoiceError{Err: err}\n\t}\n\n\treturn choiceBytes, nil\n}\n\n// Aggregate implements crypto.HomomorphicAggregator.\n//\n// Aggregate will use homomorphic property of choices and sum them together into\n// a resulting DER marshalled Choice. When withVerify is true, then each\n// choices[i] will be verified. Verification of a choice consists of:\n//\n// a) check that each mark is either 0 or 1\n//\n// b) check that sum of all marks per this particular choice is 1\nfunc (d *ElGamalHomomorphicEncryption) Aggregate(pk crypto.EncryptionKey, withVerify bool, choices ...[]byte) ([]byte, error) {\n\telgamalPk, ok := pk.(*elgamal.PublicKey)\n\tif !ok {\n\t\treturn nil, AggregateElGamalHomomorphicEncryptionCastTypeError{\n\t\t\tExpected: reflect.TypeOf(new(elgamal.PublicKey)),\n\t\t\tGot:      reflect.TypeOf(pk),\n\t\t}\n\t}\n\n\taggregated, err := Aggregate(elgamalPk, withVerify, choices...)\n\tif err != nil {\n\t\treturn nil, AggregateElGamalHomomorphicEncryptionError{Err: err}\n\t}\n\n\taggregatedBytes, err := aggregated.Marshal()\n\tif err != nil {\n\t\treturn nil, AggregateElGamalHomomorphicEncryptionASN1MarshalAggregatedError{Err: err}\n\t}\n\n\treturn aggregatedBytes, nil\n}\n\n// Verify implements crypto.HomomorphicVerifier.\n//\n// Verify uses pk to verify a choice.\nfunc (d *ElGamalHomomorphicEncryption) Verify(pk crypto.EncryptionKey, choice, extra []byte) error {\n\terr := pk.Verify(choice, extra)\n\tif err != nil {\n\t\treturn VerifyElGamalHomomorphicEncryptionError{Err: err}\n\t}\n\n\treturn nil\n}\n\n// Tally implements crypto.HomomorphicTallier.\n//\n// Privs and Pubs are intentionally passed as slices in order to support both\n// regular and distributed encryption schemes, when with distributed you pass\n// private and public key shares, whereas with distributed you only pass a\n// single private and a public keys.\n//\n// Aggregated is an DER marshalled Choice, that has already been processed\n// by Aggregate.\n//\n// MaxCount is an amount of iterations for DiscreteLog.\n//\n// Extra is bytes that we can add to the proof challenge (hash) creation in\n// order to distinguish between different set of proofs, i.e. set of proofs,\n// where identifier is hashed inside (in cryptography we call it\n// \"to add a salt\").\n//\n// Tally returns an ordered slice (order is predefined on aggregation and is\n// consistent due to Go slice order guarantees). Each element inside a slice\n// has:\n//\n// a) count() - which represent how many times this candidate was marked\n//\n// b) Proofs() - which represent DER marshalled proofs of each mark for this candidate\nfunc (d *ElGamalHomomorphicEncryption) Tally(privs []crypto.DecryptionKey, pubs []crypto.EncryptionKey, aggregated []byte, maxCount int, extra []byte) ([]crypto.HomomorphicDecryption, error) {\n\tchoice, err := ASN1UnmarshalChoice(d.g, aggregated)\n\tif err != nil {\n\t\treturn nil, TallyElGamalHomomorphicEncryptionASN1UnmarshalChoiceError{Err: err}\n\t}\n\n\thomomorphicDecrypted := make([]crypto.HomomorphicDecryption, len(choice.marks))\n\tproofsBytes := make([][]byte, len(privs))\n\tdecryptedBytes := make([][]byte, len(privs))\n\n\tvar combinedDecrypted crypto.Decryption\n\n\tfor i, mark := range choice.marks {\n\t\tcipherBytes, err := mark.ciphertext.MarshalASN1()\n\t\tif err != nil {\n\t\t\treturn nil, TallyElGamalHomomorphicEncryptionASN1MarshalCipherError{Err: err}\n\t\t}\n\n\t\tfor j, priv := range privs {\n\t\t\tvar decrypted crypto.Decryption\n\t\t\trand, err := group.RandomScalar(d.g.Order())\n\t\t\tdecrypted, proofsBytes[j], err = priv.ProvableDecrypt(rand, cipherBytes, extra, false)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, TallyElGamalHomomorphicEncryptionProvableDecryptError{Err: err}\n\t\t\t}\n\n\t\t\tdecryptedBytes[j], err = decrypted.Marshal()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, TallyElGamalHomomorphicEncryptionASN1MarshalDecryptionError{Err: err}\n\t\t\t}\n\n\t\t\terr = pubs[j].Verify(proofsBytes[j], nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, TallyElGamalHomomorphicEncryptionVerifyError{Err: err}\n\t\t\t}\n\t\t}\n\n\t\t// This will have no effect for non-distributed encryption scheme\n\t\tdecryptionCombiner := distributed.NewDecryptionShareCombiner(d.g, d.threshold)\n\n\t\t// This will have no effect for non-distributed encryption scheme.\n\t\t//\n\t\t// But for distributed one, it will combine all partial decryptions\n\t\t// into a single one. That single one can actually be decoded to a\n\t\t// meaningful plaintext.\n\t\tcombinedDecrypted, err = decryptionCombiner.DecryptionSharesCombine(cipherBytes, decryptedBytes...)\n\t\tif err != nil {\n\t\t\treturn nil, TallyElGamalHomomorphicEncryptionCombineDecryptedSharesError{Err: err}\n\t\t}\n\n\t\t// Find such exponent that can satisfy an equation:\n\t\t//\tgroup^exponent = combinedDecrypted.value()\n\t\t// where group is a group generator\n\t\texponent, err := DiscreteLog(d.g, maxCount, combinedDecrypted.Value())\n\t\tif err != nil {\n\t\t\treturn nil, TallyDistributedPrivateKeyDiscreteLogError{Err: err}\n\t\t}\n\n\t\thomomorphicDecrypted[i] = &DistributedResult{\n\t\t\tcount: uint64(exponent),\n\t\t\tproof: proofsBytes,\n\t\t}\n\t}\n\n\treturn homomorphicDecrypted, nil\n}\n\n// Choice holds voter's encrypted preferences, or simply put, Choice is an\n// ordered list of candidates (marks), where mark is a binary value 0 (candidate is\n// not chosen) or 1 (candidate is chosen). Each mark has a proof, that confirms\n// to the prover, that this mark can only be 0 or 1. Also, each choice has a\n// range proof, which confirms to the prover, that sum of all marks is exactly 1.\n// This means, that each choice could only have a single 1 mark and rest marks\n// should be zeroes, i.e. {0, 0, 0, 1}, but not {0, 1, 0, 1}, and also not\n// {0, 0, 0, 0}.\ntype Choice struct {\n\t// marks is a list of encrypted marks and proofs, which confirms to the\n\t// prover that a mark can only be 0 or 1.\n\tmarks []*Mark\n\t// proof holds a range proof that confirms to the prover that sum of all\n\t// marks is exactly 1.\n\tproof *RangeProof\n}\n\n// NewChoice is a constructor for Choice.\n//\n// Don't initialize Choice as\n//\n//\tchoice := &nil\n//\n// instead use this constructor\n//\n//\tchoice := NewChoice()\n//\n// By doing so, you can be always sure, that code will not panic due to uninitialized\n// fields.\nfunc NewChoice(marks []*Mark, rangeProof *RangeProof) (*Choice, error) {\n\tif marks == nil {\n\t\treturn nil, NewChoiceMarksIsNilError{}\n\t}\n\n\treturn &Choice{\n\t\tmarks: marks,\n\t\tproof: rangeProof,\n\t}, nil\n}\n\n// Verify verifies the correctness of a homomorphic Choice:\n//\n// a) check that each mark is either 0 or 1\n//\n// b) check that range proof is a sum of all marks\nfunc (ch *Choice) Verify(pub *elgamal.PublicKey) (err error) {\n\tvar markSumA = pub.Parameters().Group().Identity()\n\tvar markSumB = pub.Parameters().Group().Identity()\n\n\tfor i, mark := range ch.marks {\n\t\t// VerifyAll that mark is either 0 or 1\n\t\tif err = mark.Verify(pub); err != nil {\n\t\t\treturn VerifyChoiceVerifyMarkError{Err: err, Index: strconv.Itoa(i)}\n\t\t}\n\n\t\t// Sum all mark ephemeral values, in terms of abstract algebra\n\t\t// we do following:\n\t\t// Element = Element {*} Element, where {*} is a group operation.\n\t\t//\n\t\t// As you can see the result of sum is always a group element\n\t\tmarkSumA, err = markSumA.Op(mark.ciphertext.A())\n\t\tif err != nil {\n\t\t\treturn VerifyChoiceEphemeralOpError{Err: err, Index: strconv.Itoa(i)}\n\t\t}\n\n\t\t// Same for mark blinded message - sum all them up\n\t\tmarkSumB, err = markSumB.Op(mark.ciphertext.B())\n\t\tif err != nil {\n\t\t\treturn VerifyChoiceBlindedMsgOpError{Err: err, Index: strconv.Itoa(i)}\n\t\t}\n\t}\n\n\t// Create a group element by given generator group and range proof.\n\t// Please note, that range proof is a sum of all marks, and generator\n\t// is a group element, that is used for generating any other group\n\t// element.\n\t// This actually means, that rangeProofElement is an element which is\n\t// obtained by summing all marks.\n\trangeProofElement, err := pub.EncryptGroupElement(ch.proof.value, pub.Parameters().Group().Generator())\n\tif err != nil {\n\t\treturn VerifyChoiceEncryptWithEncodedMsgAndEphemeralError{Err: err}\n\t}\n\n\t// Obviously, since range proof element is a sum of all marks per choice, then\n\t// ephemeral value of mark, should be the same as ephemeral value of a range\n\t// proof element.\n\t// NB! If it doesn't make sense, read this function comments again with\n\t// patience.\n\terr = rangeProofElement.A().Equal(markSumA)\n\tif err != nil {\n\t\treturn VerifyChoiceEphemeralEqualError{Err: err}\n\t}\n\n\t// Same for blinded message of a mark, it should be the same as for range\n\t// proof element\n\terr = rangeProofElement.B().Equal(markSumB)\n\tif err != nil {\n\t\treturn VerifyChoiceBlindedMsgEqualError{Err: err}\n\t}\n\n\treturn nil\n}\n\n// ASN1Marshal marshals ch as\n//\n//\tch ::= SEQUENCE {\n//\t\tSEQUENCE {\n//\t\t\tch.marks[0]\n//\t\t\tch.marks[1]\n//\t\t\t...\n//\t\t}\n//\t\tch.RangeProof\n//\t}\nfunc (ch *Choice) Marshal() (asn_11.DER, error) {\n\tvar err error\n\n\tvar c cryptobyte.Builder\n\n\trangeProofBytes, err := ch.proof.value.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalChoiceASN1MarshalRangeProofError{Err: err}\n\t}\n\n\tmarksBytes := make([][]byte, len(ch.marks))\n\tfor i, mark := range ch.marks {\n\t\tmarksBytes[i], err = mark.MarshalASN1()\n\t\tif err != nil {\n\t\t\treturn nil, ASN1MarshalChoiceASN1MarshalMarkError{Err: err}\n\t\t}\n\t}\n\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\t\tfor _, markBytes := range marksBytes {\n\t\t\t\tc.AddBytes(markBytes)\n\t\t\t}\n\t\t})\n\t\tc.AddBytes(rangeProofBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalChoiceBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// Mark is a candidate that can be either chosen (true) or not (false).\ntype Mark struct {\n\t// ciphertext holds the encrypted value of a mark.\n\tciphertext *elgamal.Ciphertext\n\t// binaryProof holds the proof that the encrypted value is either 0 or 1.\n\tbinaryProof *BinaryValueProof\n}\n\n// NewMark is a constructor for Mark.\n//\n// Don't initialize Mark as\n//\n//\tmark := &Mark{}\n//\n// instead use this constructor\n//\n//\tmark := NewMark()\n//\n// By doing so, you can be always sure, that code will not panic due to uninitialized\n// fields.\nfunc NewMark(pubkey *elgamal.PublicKey, value bool) (*Mark, *group.Scalar, error) {\n\t// M is a mark value, initialized as 0\n\tvar M = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\tif value {\n\t\t// This mark (candidate) has been chosen (1)\n\t\tM = group.OneScalar(pubkey.Parameters().Group().Order())\n\t}\n\n\t// group^M\n\tgM, err := pubkey.Parameters().Group().Generator().Scale(M)\n\tif err != nil {\n\t\treturn nil, nil, NewMarkEncodeMessageError{Err: err}\n\t}\n\n\tR, err := group.RandomScalar(pubkey.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, nil, NewMarkCreateEphemeralError{Err: err}\n\t}\n\n\t// Encrypt mark\n\t// ct = (ct1, ct2) = (generator^response, y^response generator^bigM)\n\tct, err := pubkey.EncryptGroupElement(R, gM)\n\tif err != nil {\n\t\treturn nil, nil, NewMarkEncryptWithEncodedMsgAndEphemeralError{Err: err}\n\t}\n\n\t// Create binary proof for a mark in order to later prove that\n\t// this mark has only either false or true\n\tproof, err := NewBinaryValueProof(pubkey, value, ct, R)\n\tif err != nil {\n\t\treturn nil, nil, NewMarkNewBinaryValueProofError{Err: err}\n\t}\n\n\treturn &Mark{\n\t\tciphertext:  ct,\n\t\tbinaryProof: proof,\n\t}, R, nil\n}\n\n// Verify verifies the correctness of binary value proof.\nfunc (m *Mark) Verify(pub *elgamal.PublicKey) (err error) {\n\tvar a, b [2]group.Element\n\n\ta[0], b[0], err = getPreCommitment(m, false, pub)\n\tif err != nil {\n\t\treturn VerifyMarkGetPreCommitmentIfMarkIsNotChosenError{Err: err}\n\t}\n\n\ta[1], b[1], err = getPreCommitment(m, true, pub)\n\tif err != nil {\n\t\treturn VerifyMarkGetPreCommitmentIfMarkIsChosenError{Err: err}\n\t}\n\n\tcandidateChallenge, err := proofChallenge(pub.Parameters().Group(), pub, m.ciphertext, a, b)\n\tif err != nil {\n\t\treturn VerifyMarkProofChallengeError{Err: err}\n\t}\n\n\tvar challenge = group.ZeroScalar(pub.Parameters().Group().Order())\n\tchallenge, err = challenge.Add(m.binaryProof.challenge0)\n\tif err != nil {\n\t\treturn VerifyMarkChallengeAddBinaryProofChallenge0Error{Err: err}\n\t}\n\n\tchallenge, err = challenge.Add(m.binaryProof.challenge1)\n\tif err != nil {\n\t\treturn VerifyMarkChallengeAddBinaryProofChallenge1Error{Err: err}\n\t}\n\n\tif err = candidateChallenge.Equal(challenge); err != nil {\n\t\treturn VerifyMarkEqualError{Err: err}\n\t}\n\n\treturn nil\n}\n\n// ASN1Marshal marshals m as\n//\n//\tm ::= SEQUENCE {\n//\t\tm.ciphertext\n//\t\tm.binaryProof\n//\t}\nfunc (m *Mark) MarshalASN1() ([]byte, error) {\n\tcipherBytes, err := m.ciphertext.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMarkASN1MarshalCipherError{Err: err}\n\t}\n\n\tbinaryProofBytes, err := m.binaryProof.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMarkASN1MarshalBinaryProofError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(cipherBytes)\n\t\tc.AddBytes(binaryProofBytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalMarkBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\ntype BinaryValueProof struct {\n\tchallenge0 *group.Scalar\n\tresponse0  *group.Scalar\n\tchallenge1 *group.Scalar\n\tresponse1  *group.Scalar\n}\n\n// NewBinaryValueProof represents OR proof.\n// For a primer on OR composition of proof, see Section 5.2.4 of Berry Schoenmakers' lectures notes\n// on cryptographic protocols: https://www.win.tue.nl/~berry/CryptographicProtocols/LectureNotes.pdf\nfunc NewBinaryValueProof(opts *elgamal.PublicKey, value bool, ct *elgamal.Ciphertext, ephemeral *group.Scalar) (*BinaryValueProof, error) {\n\t// cast `value` to uint64 and big.Int for later use\n\tvar M uint64\n\tbigM := group.ZeroScalar(opts.Parameters().Group().Order())\n\t// value=1 == voter has chosen this mark (candidate)\n\tif value {\n\t\tM = 1\n\t\tbigM = group.OneScalar(opts.Parameters().Group().Order())\n\t}\n\n\t// There are two of each variable:\n\t// one for the true Mark, and one for the opposite Mark.\n\tvar a [2]group.Element // commitments (elements)\n\tvar b [2]group.Element // commitments (elements)\n\tvar c [2]*group.Scalar // challenges (scalars)\n\tvar r [2]*group.Scalar // responses (scalars)\n\n\t// Sample the ephemeral proof randomness.\n\ts, err := group.RandomScalar(opts.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofEphemeralProofError{Err: err}\n\t}\n\n\t// Compute the commitments for the case for which we have a witness.\n\n\t// a[M] = group^s\n\ta[M], err = opts.Parameters().Group().Generator().Scale(s)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScaleGeneratorByEphemeralError{Err: err}\n\t}\n\n\t// b[M] = y^s\n\tb[M], err = opts.Public().Scale(s)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScalePublicByEphemeralError{Err: err}\n\t}\n\n\t// For the ‘wrong’ case we do not have a witness.\n\t// We must therefore simulate the transcript for this case. The standard\n\t// technique is to take (or generate) a challenge, randomly sample a response,\n\t// and compute the commitment using the challenge and response such that the\n\t// simulated transcript would be accepting.\n\n\t// Compute a random challenge for opposite value proof.\n\tc[1-M], err = group.RandomScalar(opts.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofChallenge1MError{Err: err}\n\t}\n\n\t// Sample a random response for the opposite value proof.\n\tr[1-M], err = group.RandomScalar(opts.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofResponse1MError{Err: err}\n\t}\n\n\t// Compute the simulated commitments.\n\t// a must satisfy: a = group^r / ct1^c\n\t// b must satisfy:\n\t// - b = y^r / ct2^c if M = 1, or\n\t// - b = y^r / (ct2^c / group)^c if M = 0\n\t// We can combine both with: b = y^r / (ct2 / (group^(1-M))^c\n\t// and simplify it to b = y^r / ct2^c * (group^(1-M))^c\n\n\t// a[1-M] = group^r[1-M] / (ct1^c[1-M])\n\n\t// - group^r[1-M]\n\taSim, err := opts.Parameters().Group().Generator().Scale(r[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScaleGeneratorByResponse1MError{Err: err}\n\t}\n\n\t// - 1 / (ct1^c[1-M])\n\ttmp, err := ct.A().Scale(c[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScaleCipherEphemeralByChallenge1MError{Err: err}\n\t}\n\ttmp = tmp.Inverse()\n\n\t// - group^r[1-M] / (ct1^c[1-M])\n\ta[1-M], err = aSim.Op(tmp)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofASimOpError{Err: err}\n\t}\n\n\t// b[1-M] = y^r[1-M] / ct2^c[1-M] * group^((1-M) * c[1-M])\n\n\t// - y^r[1-M]\n\tbSim, err := opts.Public().Scale(r[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScalePublicByResponse1MError{Err: err}\n\t}\n\n\t// - 1 / (ct2^c[1-M])\n\ttmp, err = ct.B().Scale(c[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScaleCipherBlindedMsgByChallenge1MError{Err: err}\n\t}\n\ttmp = tmp.Inverse()\n\n\t// - y^r[1-m] / (ct2^c[1-M])\n\tbSim, err = bSim.Op(tmp)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofBSimOpError{Err: err}\n\t}\n\n\t// - (1-M) * c[1-M]\n\ttmp2 := group.OneScalar(opts.Parameters().Group().Order())\n\ttmp2, err = tmp2.Sub(bigM)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofSubMError{Err: err}\n\t}\n\n\ttmp2, err = tmp2.Mul(c[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofMulByChallenge1MError{Err: err}\n\t}\n\n\t// - group^((1-M) * c[1-M])\n\ttmp, err = opts.Parameters().Group().Generator().Scale(tmp2)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofScaleGeneratorByTmpError{Err: err}\n\t}\n\n\t// - y^r[1-M] / (ct2^c[1-M]) * (group^(1-M))^c[1-M]\n\tb[1-M], err = bSim.Op(tmp)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofBSimOp2Error{Err: err}\n\t}\n\n\t// Compute the challenge for the OR-proof using the Fiat-Shamir heuristic.\n\tC, err := proofChallenge(opts.Parameters().Group(), opts, ct, a, b)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofChallengeError{Err: err}\n\t}\n\n\t// Compute the challenge for the correct case s.t. challenge = c[M] + c[1-M].\n\t// c[M] = (challenge - c[1-M]) % Q\n\tc[M], err = C.Sub(c[1-M])\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofSubChallenge1MFromChallengeError{Err: err}\n\t}\n\n\t// Compute the response for the correct case.\n\t// r[M] = (s + c[M] * t) % Q, with t the encryption randomness.\n\tr[M], err = c[M].Mul(ephemeral)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofMulChallengeMByEphemeralError{Err: err}\n\t}\n\n\tr[M], err = r[M].Add(s)\n\tif err != nil {\n\t\treturn nil, NewBinaryValueProofAddResponseMError{Err: err}\n\t}\n\n\treturn &BinaryValueProof{\n\t\tchallenge0: c[0], response0: r[0],\n\t\tchallenge1: c[1], response1: r[1],\n\t}, nil\n}\n\n// ASN1Marshal marshals b as\n//\n//\tb ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tINTEGER\n//\t\tINTEGER\n//\t\tINTEGER\n//\t}\nfunc (bvp *BinaryValueProof) MarshalASN1() ([]byte, error) {\n\tchallenge0Bytes, err := bvp.challenge0.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalBinaryValueProofASN1MarshalChallenge0Error{Err: err}\n\t}\n\n\tresponse0Bytes, err := bvp.response0.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalBinaryValueProofASN1MarshalResponse0Error{Err: err}\n\t}\n\n\tchallenge1Bytes, err := bvp.challenge1.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalBinaryValueProofASN1MarshalChallenge1Error{Err: err}\n\t}\n\n\tresponse1Bytes, err := bvp.response1.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalBinaryValueProofASN1MarshalResponse1Error{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(challenge0Bytes)\n\t\tc.AddBytes(response0Bytes)\n\t\tc.AddBytes(challenge1Bytes)\n\t\tc.AddBytes(response1Bytes)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalBinaryValueProofBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// RangeProof is a proof to confirm that sum of all marks per choice == 1.\ntype RangeProof struct {\n\t// value is the sum of ephemeral random values in the ciphertexts.\n\tvalue *group.Scalar\n}\n\nfunc NewRangeProof(value *group.Scalar) *RangeProof {\n\treturn &RangeProof{value: value}\n}\n\n// Result holds a Count and decryption proof for a single Mark.\ntype Result struct {\n\t// Count is amount of a given Mark has been chosen,\n\t// e.g. choice={2, 4}, then mark(2) count=2, and mark(4) count=4\n\tcount uint64\n\t// DecryptionProof holds the decryption proof that the Mark is correctly\n\t// computed.\n\tproof []*elgamal.DecryptionProof\n}\n\n// Count returns count.\nfunc (r *Result) Count() uint64 {\n\treturn r.count\n}\n\n// Proofs returns DER marshalled proofs.\n// Amount of proofs == amount of marks for a given choice.\nfunc (r *Result) Proofs() ([][]byte, error) {\n\tvar err error\n\n\tproofsBytes := make([][]byte, len(r.proof))\n\tfor i, proof := range r.proof {\n\t\tproofsBytes[i], err = proof.MarshalASN1()\n\t\tif err != nil {\n\t\t\treturn nil, ProofsResultError{Err: err}\n\t\t}\n\t}\n\n\treturn proofsBytes, nil\n}\n\n// ASN1Marshal marshals r to\n//\n//\tr ::= SEQUENCE {\n//\t\tSEQUENCE {\n//\t\t\tr.proof[0]\n//\t\t\tr.proof[1]\n//\t\t\t...\n//\t\t}\n//\t\tINTEGER\n//\t}\n//\n// Amount of proofs == amount of marks for a given choice.\nfunc (r *Result) MarshalASN1() ([]byte, error) {\n\tvar err error\n\n\tvar c cryptobyte.Builder\n\n\tproofBytes := make([][]byte, len(r.proof))\n\tfor i, proof := range r.proof {\n\t\tproofBytes[i], err = proof.MarshalASN1()\n\t\tif err != nil {\n\t\t\treturn nil, ASN1MarshalResultASN1MarshalProofError{Err: err}\n\t\t}\n\t}\n\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\t\tfor _, proof := range proofBytes {\n\t\t\t\tc.AddBytes(proof)\n\t\t\t}\n\t\t})\n\t\tc.AddASN1Int64(int64(r.count))\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalResultBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// DistributedResult is a Result of homomorphic tally.\ntype DistributedResult struct {\n\t// count is amount of a given Mark has been chosen,\n\t// e.g. choice={2, 4}, then mark(2) count=2, and mark(4) count=4\n\tcount uint64\n\t// DecryptionProof holds the decryption proof that the Mark is correctly\n\t// computed.\n\tproof [][]byte\n}\n\n// Count returns count.\nfunc (r *DistributedResult) Count() uint64 {\n\treturn r.count\n}\n\n// Proofs returns DER marshalled proofs.\n// Amount of proofs == amount of marks for a given choice.\nfunc (r *DistributedResult) Proofs() ([][]byte, error) {\n\tvar err error\n\n\tproofsBytes := make([][]byte, len(r.proof))\n\tfor i, proof := range r.proof {\n\t\tproofsBytes[i] = proof\n\t\tif err != nil {\n\t\t\treturn nil, ProofsDistributedResultError{Err: err}\n\t\t}\n\t}\n\n\treturn proofsBytes, nil\n}\n\n// ASN1Marshal marshals r to\n//\n//\tr ::= SEQUENCE {\n//\t\tSEQUENCE {\n//\t\t\tr.proof[0]\n//\t\t\tr.proof[1]\n//\t\t\t...\n//\t\t}\n//\t\tINTEGER\n//\t}\n//\n// Amount of proofs == amount of marks for a given choice.\nfunc (r *DistributedResult) Marshal() (asn_11.DER, error) {\n\tvar err error\n\n\tvar c cryptobyte.Builder\n\n\tproofBytes := make([][]byte, len(r.proof))\n\tfor i, proof := range r.proof {\n\t\tproofBytes[i] = proof\n\t\tif err != nil {\n\t\t\treturn nil, ASN1MarshalDistributedResultASN1MarshalProofError{Err: err}\n\t\t}\n\t}\n\n\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddASN1(asn_1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\t\tfor _, proof := range proofBytes {\n\t\t\t\tc.AddBytes(proof)\n\t\t\t}\n\t\t})\n\t\tc.AddASN1Int64(int64(r.count))\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalDistributedResultBytesError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// Encrypt returns encrypted marks (candidates) as a Choice.\nfunc Encrypt(pubKey *elgamal.PublicKey, marks []bool) (*Choice, error) {\n\t// Range proof for each choice, should be 1\n\trangeProof := NewRangeProof(group.ZeroScalar(pubKey.Parameters().Group().Order()))\n\n\t// Create new choice from all marks per voter\n\tchoice, err := NewChoice(make([]*Mark, len(marks)), rangeProof)\n\tif err != nil {\n\t\treturn nil, EncryptNewChoiceError{Err: err}\n\t}\n\n\t// Convert from bool mark to Mark\n\tfor i, mark := range marks {\n\t\tvar ephemeral *group.Scalar\n\n\t\t// Create new encrypted mark, which shows that voter has (true) or\n\t\t//  hasn't (false) chosen a candidate, as well as attach proofs to the\n\t\t// mark, showing, that value could only be either true or false\n\t\tchoice.marks[i], ephemeral, err = NewMark(pubKey, mark)\n\t\tif err != nil {\n\t\t\treturn nil, EncryptNewMarkError{Err: err}\n\t\t}\n\n\t\t// Sum range proof, remember, the final value should only be 1\n\t\tchoice.proof.value, err = choice.proof.value.Add(ephemeral)\n\t\tif err != nil {\n\t\t\treturn nil, EncryptRangeProofAddError{Err: err}\n\t\t}\n\t}\n\n\treturn choice, nil\n}\n\n// Aggregate will sum up all choices into a single choice using homomorphic\n// property of an encrypted choices.\n//\n// NB! Aggregate is a long-running process, and passing all choices at once\n// could cost a lost (imagine if len(choices) == 1 million).\n//\n// Therefore, it is worth to perform Aggregate in batches.\nfunc Aggregate(pubkey *elgamal.PublicKey, withVerify bool, choices ...[]byte) (*Choice, error) {\n\t// Nothing to Aggregate\n\tif len(choices) == 0 {\n\t\treturn nil, AggregateChoicesSizeIsZeroError{}\n\t}\n\n\t// Summed up choices will be stored here\n\taggregated := &Choice{}\n\n\t// len(marks) within each choices[i] should be the same\n\tchoice0, err := ASN1UnmarshalChoice(pubkey.Parameters().Group(), choices[0])\n\tif err != nil {\n\t\treturn nil, AggregateASN1UnmarshalChoice0Error{Err: err}\n\t}\n\tmarksCount := len(choice0.marks)\n\n\t// Zero initialization\n\taggregated.marks = make([]*Mark, marksCount)\n\t//for i := range aggregated.marks {\n\t//\taggregated.marks[i] = new(Mark)\n\t//\taggregated.marks[i].ciphertext = new(elgamal.ciphertext)\n\t//\taggregated.marks[i].binaryProof = new(BinaryValueProof)\n\t//\taggregated.marks[i].binaryProof.challenge0 = group.ZeroScalar(pubkey.Group().Group().Order())\n\t//\taggregated.marks[i].binaryProof.challenge1 = group.ZeroScalar(pubkey.Group().Group().Order())\n\t//\taggregated.marks[i].binaryProof.response0 = group.ZeroScalar(pubkey.Group().Group().Order())\n\t//\taggregated.marks[i].binaryProof.response1 = group.ZeroScalar(pubkey.Group().Group().Order())\n\t//}\n\taggregated.proof = new(RangeProof)\n\taggregated.proof.value = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\n\tfor i := range aggregated.marks {\n\t\tciphertext, err := newEncryptedMessage(pubkey, pubkey.Parameters().Group(), pubkey.Parameters().Group().Identity(), group.ZeroScalar(pubkey.Parameters().Group().Order()))\n\t\tif err != nil {\n\t\t\treturn nil, AggregateNewEncryptedMessageError{Err: err}\n\t\t}\n\t\taggregated.marks[i] = new(Mark)\n\t\taggregated.marks[i].ciphertext = ciphertext\n\t\taggregated.marks[i].binaryProof = new(BinaryValueProof)\n\t\taggregated.marks[i].binaryProof.challenge0 = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\t\taggregated.marks[i].binaryProof.challenge1 = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\t\taggregated.marks[i].binaryProof.response0 = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\t\taggregated.marks[i].binaryProof.response1 = group.ZeroScalar(pubkey.Parameters().Group().Order())\n\t}\n\n\tfor _, ch := range choices {\n\t\tchoice, err := ASN1UnmarshalChoice(pubkey.Parameters().Group(), ch)\n\t\tif err != nil {\n\t\t\treturn nil, AggregateASN1UnmarshalChoiceError{Err: err}\n\t\t}\n\n\t\t// All choices should have exactly the same amount of marks\n\t\tif len(choice.marks) != marksCount {\n\t\t\treturn nil, AggregateIncompatibleMarksSizeError{\n\t\t\t\tExpected: marksCount,\n\t\t\t\tGot:      len(choice.marks),\n\t\t\t}\n\t\t}\n\n\t\t// If choices are already aggregated then withVerify should be false\n\t\t// otherwise that verification will fail, since range proof expects\n\t\t// to be exactly = 1 (sum of all marks for a given choice == 1, and\n\t\t// this can only be true if we have choice={0,0,1}, but not {1,0,1})\n\t\tif withVerify {\n\t\t\terr = choice.Verify(pubkey)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, AggregateChoiceVerifyError{Err: err}\n\t\t\t}\n\t\t}\n\n\t\tfor i := range aggregated.marks {\n\t\t\tct1 := aggregated.marks[i].ciphertext\n\t\t\tct2 := choice.marks[i].ciphertext\n\n\t\t\ta, err := ct1.A().Op(ct2.A())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, AggregateEphemeralOpError{Err: err}\n\t\t\t}\n\n\t\t\tb, err := ct1.B().Op(ct2.B())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, AggregateBlindedMessageOpError{Err: err}\n\t\t\t}\n\n\t\t\taggregated.marks[i].ciphertext = elgamal.NewCiphertext(a, b)\n\t\t}\n\t}\n\n\treturn aggregated, nil\n}\n\n// DiscreteLog is a brute force method to find an exponent of a decrypted\n// element by looping over all non-negative whole numbers Z+, starting from 0,\n// but not more iterations than maxCount.\nfunc DiscreteLog(g group.Group, maxCount int, decrypted group.Element) (exponent int, err error) {\n\tfor element := g.Identity(); exponent <= maxCount; exponent++ {\n\t\tif element.Equal(decrypted) == nil {\n\t\t\t// Found an exponent\n\t\t\treturn exponent, nil\n\t\t}\n\n\t\t// element * group, i.e. try next group element\n\t\telement, err = element.Op(g.Generator())\n\t\tif err != nil {\n\t\t\treturn 0, DiscreteLogOpError{Err: err}\n\t\t}\n\t}\n\n\treturn 0, DiscreteLogOverflowError{Max: strconv.Itoa(maxCount)}\n}\n\nfunc ASN1UnmarshalChoice(g group.Group, ch []byte) (*Choice, error) {\n\tchBytes := cryptobyte.String(ch)\n\n\tvar outerSequence, innerSequence, rangeProofBytes, privBytes cryptobyte.String\n\n\tif !chBytes.ReadASN1(&outerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalChoiceNotAnOuterSequenceError{}\n\t}\n\n\tif !outerSequence.ReadASN1(&innerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalChoiceNotAnInnerSequenceError{}\n\t}\n\n\tmarks := make([]*Mark, 0)\n\n\t// TODO is it vulnerable to read in infinite for loop here,\n\t//  what about infinite large DER SEQUENCE?\n\tfor innerSequence.ReadAnyASN1Element(&privBytes, nil) {\n\t\tmark, err := ASN1UnmarshalMark(g, privBytes)\n\t\tif err != nil {\n\t\t\treturn nil, ASN1UnmarshalChoiceASN1UnmarshalMarkError{Err: err}\n\t\t}\n\n\t\tmarks = append(marks, mark)\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&rangeProofBytes, nil) {\n\t\treturn nil, ASN1UnmarshalChoiceNoRangeProofError{}\n\t}\n\n\trangeProof, err := group.UnmarshalScalar(asn_11.DER(rangeProofBytes), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalChoiceASN1UnmarshalRangeProofError{Err: err}\n\t}\n\n\tif !chBytes.Empty() || !outerSequence.Empty() || !innerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalChoiceTrailingBytesError{Err: err}\n\t}\n\n\treturn &Choice{marks: marks, proof: NewRangeProof(rangeProof)}, nil\n}\n\nfunc ASN1UnmarshalMark(g group.Group, mk []byte) (*Mark, error) {\n\tmkBytes := cryptobyte.String(mk)\n\n\tvar outerSequence, cipherBytes, binaryProofBytes cryptobyte.String\n\n\tif !mkBytes.ReadASN1(&outerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalMarkNotASequenceError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&cipherBytes, nil) {\n\t\treturn nil, ASN1UnmarshalMarkNoCiphertextError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&binaryProofBytes, nil) {\n\t\treturn nil, ASN1UnmarshalMarkNoBinaryProofError{}\n\t}\n\n\tif !mkBytes.Empty() || !outerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalMarkTrailingBytesError{}\n\t}\n\n\tct, err := elgamal.ASN1UnmarshalCiphertext(g, cipherBytes)\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalMarkASN1UnmarshalCiphertextError{}\n\t}\n\n\tbp, err := ASN1UnmarshalBinaryValueProof(g, binaryProofBytes)\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalMarkASN1UnmarshalBinaryValueProofError{}\n\t}\n\n\treturn &Mark{ciphertext: ct, binaryProof: bp}, nil\n}\n\nfunc ASN1UnmarshalBinaryValueProof(g group.Group, binProof []byte) (*BinaryValueProof, error) {\n\tbinProofBytes := cryptobyte.String(binProof)\n\n\tvar outerSequence, challengeBytes0, responseBytes0, challenge1Bytes, response1Bytes cryptobyte.String\n\n\tif !binProofBytes.ReadASN1(&outerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofNotASequenceError{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&challengeBytes0, nil) {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofASN1ReadChallenge0Error{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&responseBytes0, nil) {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofASN1ReadResponse0Error{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&challenge1Bytes, nil) {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofASN1ReadChallenge1Error{}\n\t}\n\n\tif !outerSequence.ReadAnyASN1Element(&response1Bytes, nil) {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofASN1ReadResponse1Error{}\n\t}\n\n\tif !binProofBytes.Empty() || !outerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofTrailingBytesError{}\n\t}\n\n\tchallenge0, err := group.UnmarshalScalar(asn_11.DER(challengeBytes0), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofChallenge0Error{}\n\t}\n\n\tresponse0, err := group.UnmarshalScalar(asn_11.DER(responseBytes0), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofResponse0Error{}\n\t}\n\n\tchallenge1, err := group.UnmarshalScalar(asn_11.DER(challenge1Bytes), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofChallenge1Error{}\n\t}\n\n\tresponse1, err := group.UnmarshalScalar(asn_11.DER(response1Bytes), g.Order())\n\tif err != nil {\n\t\treturn nil, ASN1UnmarshalBinaryValueProofResponse1Error{}\n\t}\n\n\treturn &BinaryValueProof{\n\t\tchallenge0: challenge0,\n\t\tresponse0:  response0,\n\t\tchallenge1: challenge1,\n\t\tresponse1:  response1,\n\t}, nil\n}\n\nfunc ASN1UnmarshalResult(g group.Group, res []byte) (*Result, error) {\n\tresBytes := cryptobyte.String(res)\n\n\tvar err error\n\tvar count int\n\n\tvar outerSequence, innerSequence, proofsBytes cryptobyte.String\n\n\tif !resBytes.ReadASN1(&outerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalResultNotAnOuterSequenceError{}\n\t}\n\n\tif !outerSequence.ReadASN1(&innerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalResultNotAnInnerSequenceError{}\n\t}\n\n\tproofs := make([]*elgamal.DecryptionProof, 0)\n\n\t// Read infinitely from DER SEQUENCE until all proofs are read\n\t// TODO vulnerability? What if infinite amount of proofs are inside DER SEQUENCE\n\tfor innerSequence.ReadAnyASN1Element(&proofsBytes, nil) {\n\t\tproof, err := elgamal.ASN1UnmarshalDecryptionProof(g, proofsBytes)\n\t\tif err != nil {\n\t\t\treturn nil, ASN1UnmarshalResultASN1UnmarshalDecryptionProofShareError{Err: err}\n\t\t}\n\n\t\tproofs = append(proofs, proof)\n\t}\n\n\tif !outerSequence.ReadASN1Integer(&count) {\n\t\treturn nil, ASN1UnmarshalResultASN1ReadCountError{Err: err}\n\t}\n\n\tif !resBytes.Empty() || !outerSequence.Empty() || !innerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalResultTrailingBytesError{Err: err}\n\t}\n\n\treturn &Result{count: uint64(count), proof: proofs}, nil\n}\n\nfunc ASN1UnmarshalDistributedResult(res []byte) (*DistributedResult, error) {\n\tresBytes := cryptobyte.String(res)\n\n\tvar err error\n\tvar count int\n\n\tvar outerSequence, innerSequence, proofsBytes cryptobyte.String\n\n\tif !resBytes.ReadASN1(&outerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalDistributedResultNotAnOuterSequenceError{}\n\t}\n\n\tif !outerSequence.ReadASN1(&innerSequence, asn_1.SEQUENCE) {\n\t\treturn nil, ASN1UnmarshalDistributedResultNotAnInnerSequenceError{}\n\t}\n\n\tproofs := make([][]byte, 0)\n\t// Read infinitely from DER SEQUENCE until all proofs are read\n\t// TODO vulnerability? What if infinite amount of proofs are inside DER SEQUENCE\n\tfor innerSequence.ReadAnyASN1Element(&proofsBytes, nil) {\n\t\tproofs = append(proofs, proofsBytes)\n\t}\n\n\tif !outerSequence.ReadASN1Integer(&count) {\n\t\treturn nil, ASN1UnmarshalDistributedResultASN1ReadCountError{Err: err}\n\t}\n\n\tif !resBytes.Empty() || !outerSequence.Empty() || !innerSequence.Empty() {\n\t\treturn nil, ASN1UnmarshalDistributedResultTrailingBytesError{Err: err}\n\t}\n\n\treturn &DistributedResult{count: uint64(count), proof: proofs}, nil\n}\n\nfunc newEncryptedMessage(pub *elgamal.PublicKey, g group.Group, message group.Element, ephemeral *group.Scalar) (*elgamal.Ciphertext, error) {\n\t// group^ephemeral\n\ta, err := g.Generator().Scale(ephemeral)\n\tif err != nil {\n\t\treturn nil, NewEncryptedMessageScaleGeneratorByEphemeralError{Err: err}\n\t}\n\n\t// y^ephemeral\n\tb, err := pub.Public().Scale(ephemeral)\n\tif err != nil {\n\t\treturn nil, NewEncryptedMessageScalePublicByEphemeralError{Err: err}\n\t}\n\n\t// b * message\n\tb, err = b.Op(message)\n\tif err != nil {\n\t\treturn nil, NewEncryptedMessageOpError{Err: err}\n\t}\n\n\tct := elgamal.NewCiphertext(a, b)\n\n\treturn ct, nil\n}\n\nfunc getPreCommitment(mark *Mark, isChosen bool, pub *elgamal.PublicKey) (group.Element, group.Element, error) {\n\tvar challenge, response, bigValue *group.Scalar\n\n\t// If this mark (candidate) is not chosen by a voter\n\tif !isChosen {\n\t\tbigValue = group.ZeroScalar(pub.Parameters().Group().Order())\n\t\tchallenge = mark.binaryProof.challenge0\n\t\tresponse = mark.binaryProof.response0\n\t} else {\n\t\tbigValue = group.OneScalar(pub.Parameters().Group().Order())\n\t\tchallenge = mark.binaryProof.challenge1\n\t\tresponse = mark.binaryProof.response1\n\t}\n\n\t// Action 1.\n\t//\ta = (group^response) / (ct1^challenge))\n\n\t// group^response\n\ta, err := pub.Parameters().Group().Generator().Scale(response)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentScaleGeneratorByResponseError{Err: err}\n\t}\n\n\t// ephemeral^challenge\n\ttmp, err := mark.ciphertext.A().Scale(challenge)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentScaleEphemeralByChallengeError{Err: err}\n\t}\n\n\t// -tmp\n\ttmp = tmp.Inverse()\n\n\t// a * tmp\n\t// Note that tmp is now inverse, which means, for example, with multiplication\n\t// a / b == a * (1/b)\n\ta, err = a.Op(tmp)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentOp1Error{Err: err}\n\t}\n\n\t// Action 2.\n\t//\tb = (y^response) / (blindedMsg^challenge) * group^(value * challenge)\n\n\t// y^response\n\tb, err := pub.Public().Scale(response)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentScalePublicByResponseError{Err: err}\n\t}\n\n\t// blindedMsg^challenge\n\ttmp, err = mark.ciphertext.B().Scale(challenge)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentScaleBlindedMsgByChallengeError{Err: err}\n\t}\n\n\t// -tmp\n\ttmp = tmp.Inverse()\n\n\t// b / tmp\n\t// Note that tmp is inverse\n\tb, err = b.Op(tmp)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentOp2Error{Err: err}\n\t}\n\n\t// value * challenge\n\ttmp2, err := bigValue.Mul(challenge)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentMulValueByChallengeError{Err: err}\n\t}\n\n\t// group^tmp2\n\ttmp, err = pub.Parameters().Group().Generator().Scale(tmp2)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentScaleGeneratorByTmpValueError{Err: err}\n\t}\n\n\t// b * tmp\n\tb, err = b.Op(tmp)\n\tif err != nil {\n\t\treturn nil, nil, GetPreCommitmentOp3Error{Err: err}\n\t}\n\n\treturn a, b, nil\n}\n\n// proofChallenge creates a hash as\n//\n//\th = SHAKE256(\n//\t\tBinaryValue\" ||\n//\t\tg ||\n//\t\tpub ||\n//\t\tct.A() ||\n//\t\tct.B() ||\n//\t\tcommitmentsA[0] ||\n//\t\tcommitmentsA[1] ||\n//\t\tcommitmentsB[0] ||\n//\t\tcommitmentsB[1]\n//\t)\n//\n// and then produces a scalar from h as\n//\n//\tC = h mod [0, g.Order())\nfunc proofChallenge(g group.Group, pub *elgamal.PublicKey, ct *elgamal.Ciphertext, commitmentsA, commitmentsB [2]group.Element) (C *group.Scalar, err error) {\n\tgBytes, err := group.Marshal(g)\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalGroupError{Err: err}\n\t}\n\n\tpkBytes, err := pub.Public().Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalPublicError{Err: err}\n\t}\n\n\tephemeralBytes, err := ct.A().Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalEphemeralError{Err: err}\n\t}\n\n\tblindedMsgBytes, err := ct.B().Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalBlindedMsgError{Err: err}\n\t}\n\n\tA0Bytes, err := commitmentsA[0].Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalChallenge0Error{Err: err}\n\t}\n\n\tA1Bytes, err := commitmentsA[1].Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalChallenge1Error{Err: err}\n\t}\n\n\tB0Bytes, err := commitmentsB[0].Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalResponse0Error{Err: err}\n\t}\n\n\tB1Bytes, err := commitmentsB[1].Marshal()\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalResponse1Error{Err: err}\n\t}\n\n\tseed := struct {\n\t\tDomain     string\n\t\tParams     []byte\n\t\tPubElement []byte\n\t\tEphemeral  []byte\n\t\tBlindedMsg []byte\n\t\tA0         []byte\n\t\tA1         []byte\n\t\tB0         []byte\n\t\tB1         []byte\n\t}{Domain: \"BinaryValue\",\n\t\tParams:     gBytes,\n\t\tPubElement: pkBytes,\n\t\tEphemeral:  ephemeralBytes,\n\t\tBlindedMsg: blindedMsgBytes,\n\t\tA0:         A0Bytes,\n\t\tA1:         A1Bytes,\n\t\tB0:         B0Bytes,\n\t\tB1:         B1Bytes,\n\t}\n\n\tseedBytes, err := asn1.Marshal(seed)\n\tif err != nil {\n\t\treturn nil, ProofChallengeASN1MarshalSeedError{Err: err}\n\t}\n\n\t// Hash(seed bytes)\n\treader := sha3.NewShake256()\n\treader.Write(seedBytes) //nolint:errcheck\n\n\tC, err = group.ScalarValueOfReader(reader, g.Order())\n\tif err != nil {\n\t\treturn nil, ProofChallengeScalarValueOfReaderError{Err: err}\n\t}\n\n\treturn C, nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/homomorphic/homomorphic_test.go",
    "content": "package homomorphic\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"tivi.io/core/crypto\"\n\toelgamal \"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc TestMarkCompleteness(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tvalue bool\n\t}{\n\t\t{\"zero\", false},\n\t\t{\"one\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tfor _, g := range group.All() {\n\t\t\topts := generateOptions(t, g)\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tmark, _, err := NewMark(opts.Public(), tc.value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"new Mark error: %s\", err)\n\t\t\t\t}\n\t\t\t\tverified := mark.Verify(opts.Public())\n\t\t\t\tif verified != nil {\n\t\t\t\t\tt.Fatalf(\"Mark verification error: %s\", verified)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestChoiceCompleteness(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\ttests := []struct {\n\t\t\tname  string\n\t\t\tmarks []bool\n\t\t\tfails bool\n\t\t}{\n\t\t\t{\"no_marks\", []bool{}, true},\n\t\t\t{\"no_true_mark\", []bool{false, false, false, false, false}, true},\n\t\t\t{\"single_mark\", []bool{false, false, false, true, false}, false},\n\t\t\t{\"two_marks\", []bool{true, false, true, false, false}, true},\n\t\t}\n\t\topts := generateOptions(t, g)\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tch, err := Encrypt(opts.Public(), tc.marks)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"error constructing Choice\", err)\n\t\t\t\t}\n\t\t\t\tif err := ch.Verify(opts.Public()); (err != nil) != tc.fails {\n\t\t\t\t\tfmt.Println(err)\n\t\t\t\t\tt.Fatalf(\"expected verification: %t, got %t\", tc.fails, err != nil)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestChoiceAdd(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\ttests := []struct {\n\t\t\tname  string\n\t\t\tmarks [][]bool\n\t\t\tfails bool\n\t\t}{\n\t\t\t{\"compatible_choices\", [][]bool{{false, false, true}, {true, false, false}}, false},\n\t\t\t{\"incompatible_choices\", [][]bool{{false, false, true, false}, {false, true, false}}, true},\n\t\t\t{\"many_choices\", [][]bool{{false, true}, {true, false}, {false, true}, {true, false}, {false, true}}, false},\n\t\t\t{\"no_choices\", [][]bool{}, true},\n\t\t}\n\t\topts := generateOptions(t, g)\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tvar err error\n\t\t\t\tchs := make([][]byte, len(tc.marks))\n\t\t\t\tfor i, marks := range tc.marks {\n\t\t\t\t\ttmp, err := Encrypt(opts.Public(), marks)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(\"error creating Choice\", err)\n\t\t\t\t\t}\n\t\t\t\t\tchs[i], err = tmp.MarshalASN1()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(\"error creating Choice\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_, err = Aggregate(opts.Public(), true, chs...)\n\t\t\t\tif (err != nil) != tc.fails {\n\t\t\t\t\tt.Fatalf(\"expected fails %t, got %t\", tc.fails, err != nil)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestDiscreteLog(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tmaxCount := 1024\n\n\t\tdl0, err := g.Identity().Scale(group.OneScalar(g.Order()))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tdl1 := g.Generator()\n\n\t\tdl1024, err := g.Generator().Scale(group.NewScalar(big.NewInt(1024), g.Order()))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tdl1025, err := g.Generator().Scale(group.NewScalar(big.NewInt(1025), g.Order()))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tdlrand, err := group.RandomElement(g)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\ttests := []struct {\n\t\t\tname  string\n\t\t\tdec   group.Element\n\t\t\tcount int\n\t\t\tfails bool\n\t\t}{\n\t\t\t{\"DL-0\", dl0, 0, false},\n\t\t\t{\"DL-1\", dl1, 1, false},\n\t\t\t{\"DL-1024\", dl1024, 1024, false},\n\t\t\t{\"DL-1025\", dl1025, 0, true},\n\t\t\t{\"nil\", nil, 0, true},\n\t\t\t{\"random\", dlrand, 0, true},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tcount, err := DiscreteLog(g, maxCount, tc.dec)\n\t\t\t\tif (err != nil) != tc.fails {\n\t\t\t\t\tt.Fatalf(\"expected failure %t, got %t\", tc.fails, (err != nil))\n\t\t\t\t}\n\t\t\t\tif count != tc.count {\n\t\t\t\t\tt.Fatalf(\"expected count %d, got %d\", tc.count, count)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc generateOptionsWithParameters2(t *testing.T, gr group.Group) *oelgamal.PrivateKey {\n\tt.Helper()\n\tparams := oelgamal.NewParameters(gr)\n\trnd, err := group.RandomScalar(params.Group().Order())\n\tpriv, err := oelgamal.NewPrivateKey(params, rnd)\n\tif err != nil {\n\t\tt.Fatalf(\"error generating public key: %s\", err)\n\t}\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn priv\n}\n\nfunc generateOptions(t *testing.T, g group.Group) *oelgamal.PrivateKey {\n\treturn generateOptionsWithParameters2(t, g)\n}\nfunc generateNonStandardOptions(t *testing.T, g group.Group) *oelgamal.PrivateKey {\n\treturn generateOptionsWithParameters2(t, g)\n}\n\nfunc testDistributedHomomorphicEncryption(t *testing.T, g group.Group, _ *oelgamal.PrivateKey) {\n\ttests := []struct {\n\t\tname     string\n\t\tmarks    [][]bool\n\t\texpected []int\n\t}{\n\t\t{\"single_choice\", [][]bool{{false, false, true}}, []int{0, 0, 1}},\n\t\t{\"multiple_choices-single_mark\", [][]bool{{true, false, false}, {false, true, false}, {false, false, true}}, []int{1, 1, 1}},\n\t\t{\"multiple_choices-multiple_marks\", [][]bool{{true, false, false}, {true, false, false}}, []int{2, 0, 0}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// First of all, we start with a workflow, when user generates private\n\t\t\t// key automatically\n\t\t\t//\n\t\t\t// User decides which private key to create:\n\t\t\t//\ta) Regular\n\t\t\t//\tb) Distributed\n\n\t\t\t// This time we decided to create Distributed one.\n\t\t\t//\n\t\t\t// Note that we say explicitly, that Distributed encryption is\n\t\t\t// ElGamal. However, further in a code we cast these concrete types\n\t\t\t// to the interfaces.\n\t\t\t// When, for example, you don't want to use ElGamal in a future,\n\t\t\t// you easily could replace Distributed ElGamal with, for example,\n\t\t\t// Distributed Paillier or Distributed RSA, if such exist.\n\t\t\t//\n\t\t\t// Keeping that in mind, it is strongly advised to use concrete types\n\t\t\t// in a specific place inside a code where initialization is done, i.e.:\n\t\t\t//\ta) init() func\n\t\t\t//\tb) main() func\n\t\t\t//\tc) REST controller/service\n\t\t\t//\n\t\t\t// Rule of thumb - don't let concrete types to flow deeply into a code,\n\t\t\t// keep them in a one single place and always pass inside the interfaces.\n\t\t\tprivSs, pk, err := distributed.NewPrivateKeyShares(g, 3, 2)\n\n\t\t\t// Cast to interfaces\n\t\t\t//\tvar elgamalPubKey crypto.EncryptionKey = pk\n\t\t\tprivShares := make([]crypto.DecryptionKey, len(privSs))\n\t\t\tfor i, privS := range privSs {\n\t\t\t\tprivShares[i] = privS\n\t\t\t}\n\n\t\t\t// Derive public key shares from private key shares\n\t\t\tpubShares := make([]crypto.EncryptionKey, len(privShares))\n\t\t\tfor i, share := range privShares {\n\t\t\t\tpubShares[i] = share.EncryptionKey()\n\t\t\t}\n\n\t\t\t// This is the place where we have finished with Distributed ElGamal\n\t\t\t// private key creation, and it is a time to store the material in I/O,\n\t\t\t// since most likely, it will be returned to the client via either\n\t\t\t// disk storing or Web call.\n\n\t\t\t// Store private key shares in I/O\n\t\t\tprivSharesBytes := make([][]byte, len(privShares))\n\t\t\tfor i, share := range privShares {\n\t\t\t\tprivSharesBytes[i], err = share.Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Store public key shares in I/O\n\t\t\tpubSharesBytes := make([][]byte, len(pubShares))\n\t\t\tfor i, share := range pubShares {\n\t\t\t\tpubSharesBytes[i], err = share.Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tzk := &ElGamalHomomorphicEncryption{g: g, threshold: 3}\n\t\t\t// Cast to interface\n\t\t\tvar pubkeyRestored crypto.HomomorphicEncryption = zk\n\n\t\t\t// Homomorphic ciphertexts == choices\n\t\t\tchoices := make([][]byte, len(tc.marks))\n\t\t\tfor i, marks := range tc.marks {\n\n\t\t\t\t// Homomorphically encrypt user choices\n\t\t\t\tchoices[i], err = pubkeyRestored.Encrypt(pk, marks)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Aggregate choices in parallel\n\t\t\twg := new(sync.WaitGroup)\n\t\t\tbatchSize := 1000\n\t\t\tvar aggregated []byte\n\t\t\tlock := new(sync.Mutex)\n\n\t\t\t// Use homomorphic property of each choice to aggregate them together\n\t\t\tfor i := 0; i < len(choices); i += batchSize {\n\t\t\t\tend := i + batchSize\n\t\t\t\tif end > len(choices) {\n\t\t\t\t\tend = len(choices)\n\t\t\t\t}\n\t\t\t\tbatch := choices[i:end]\n\n\t\t\t\t// Each goroutine call will be recorded and using wg.Wait()\n\t\t\t\t// will be awaited\n\t\t\t\twg.Add(1)\n\n\t\t\t\t// Use goroutines to make it parallel\n\t\t\t\tgo func() {\n\t\t\t\t\t// Aggregate batches (use as large batch as you can)\n\t\t\t\t\taggregated, err = pubkeyRestored.Aggregate(pk, true, batch...)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t// Log error, continue\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// This is a blocking goroutine\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tlock.Lock()\n\t\t\t\t\t\tdefer lock.Unlock()\n\n\t\t\t\t\t\t// Aggregate aggregated batches\n\t\t\t\t\t\taggregated, err = pubkeyRestored.Aggregate(pk, false, aggregated)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t// Log error, continue\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Mark goroutine as finished\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t}()\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\t// Block and wait for all unfinished goroutines (code didn't reach\n\t\t\t// wg.Done() statement\n\t\t\twg.Wait()\n\n\t\t\t// DecryptAll+tally aggregated choices\n\t\t\t//\n\t\t\t// Tallies contain voting result\n\t\t\ttalllies, err := pubkeyRestored.Tally(privShares, pubShares, aggregated, len(choices), nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tfor i, count := range tc.expected {\n\t\t\t\tif talllies[i].Count() != uint64(count) {\n\t\t\t\t\tt.Fatalf(\"Mark nr.%d count expected %d, got %d\", i, count, talllies[i].Count())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Store tally in I/O for later auditing (proofs)\n\t\t\ttalliesBytes := make([][]byte, len(talllies))\n\t\t\tfor i, tally := range talllies {\n\t\t\t\ttalliesBytes[i], err = tally.Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, tally := range talllies {\n\t\t\t\ttallyProofsBytes, err := tally.Proofs()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tfor i, pubShare := range pubShares {\n\t\t\t\t\terr = pubkeyRestored.Verify(pubShare, tallyProofsBytes[i], nil)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc testHomomorphicEncryption(t *testing.T, g group.Group, _ *oelgamal.PrivateKey) {\n\ttests := []struct {\n\t\tname     string\n\t\tmarks    [][]bool\n\t\texpected []int\n\t}{\n\t\t{\"single_choice\", [][]bool{{false, false, true}}, []int{0, 0, 1}},\n\t\t{\"multiple_choices-single_mark\", [][]bool{{true, false, false}, {false, true, false}, {false, false, true}}, []int{1, 1, 1}},\n\t\t{\"multiple_choices-multiple_marks\", [][]bool{{true, false, false}, {true, false, false}}, []int{2, 0, 0}},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// User decides which private key to create:\n\t\t\t//\ta) Regular\n\t\t\t//\tb) Distributed\n\n\t\t\t// This time\n\t\t\tparams := oelgamal.NewParameters(g)\n\t\t\trnd, err := group.RandomScalar(params.Group().Order())\n\t\t\tprivKey, err := oelgamal.NewPrivateKey(params, rnd)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// User decides which homomorphic public key to create\n\t\t\t// based on existing private key\n\t\t\thomoKey := NewElGamalHomomorphicEncryption(g, 1)\n\t\t\tchoices := make([][]byte, len(tc.marks))\n\t\t\tfor i, marks := range tc.marks {\n\t\t\t\t// Homomorphically Encrypt user choices\n\t\t\t\tchoices[i], err = homoKey.Encrypt(privKey.EncryptionKey(), marks)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Aggregate choices in parallel\n\t\t\twg := new(sync.WaitGroup)\n\t\t\tbatchSize := 1000\n\t\t\tvar aggregated []byte\n\t\t\tlock := new(sync.Mutex)\n\n\t\t\t// Use homomorphic property of each Choice to Aggregate them together\n\t\t\tfor i := 0; i < len(choices); i += batchSize {\n\t\t\t\tend := i + batchSize\n\t\t\t\tif end > len(choices) {\n\t\t\t\t\tend = len(choices)\n\t\t\t\t}\n\t\t\t\tbatch := choices[i:end]\n\n\t\t\t\t// Each goroutine call will be recorded and using wg.Wait()\n\t\t\t\t// will be awaited\n\t\t\t\twg.Add(1)\n\n\t\t\t\t// Use goroutines to make it parallel\n\t\t\t\tgo func() {\n\t\t\t\t\t// Aggregate batches (use as large batch as you can)\n\t\t\t\t\taggregated, err = homoKey.Aggregate(privKey.EncryptionKey(), true, batch...)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t}\n\n\t\t\t\t\t// This is a blocking goroutine\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tlock.Lock()\n\t\t\t\t\t\tdefer lock.Unlock()\n\n\t\t\t\t\t\t// Aggregate aggregated batches\n\t\t\t\t\t\taggregated, err = homoKey.Aggregate(privKey.EncryptionKey(), false, aggregated)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Mark goroutine as finished\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t}()\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\t// Block and wait for all unfinished goroutines (code didn't reach\n\t\t\t// wg.Done() statement\n\t\t\twg.Wait()\n\n\t\t\t// DecryptAll+tally aggregated choices\n\t\t\t//\n\t\t\t// Tally contains decrypted Result and proof\n\t\t\tprivKeys := []crypto.DecryptionKey{privKey}\n\t\t\tpubKeys := []crypto.EncryptionKey{privKey.Public()}\n\t\t\ttally, err := homoKey.Tally(privKeys, pubKeys, aggregated, len(choices), nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tfor j, count := range tc.expected {\n\t\t\t\tif tally[j].Count() != uint64(count) {\n\t\t\t\t\tt.Fatalf(\"Mark %d count expected %d, got %d\", j, count, tally[j].Count())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Store tally in I/O for later auditing (proof)\n\n\t\t\tfor _, a := range tally {\n\t\t\t\tgo func(a crypto.HomomorphicDecryption) {\n\t\t\t\t\tproofsPerTally, err := a.Proofs()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t// Log error, continue\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfor _, proofPerTally := range proofsPerTally {\n\t\t\t\t\t\tgo func(proofPerTally []byte) {\n\t\t\t\t\t\t\terr = homoKey.Verify(privKey.EncryptionKey(), proofPerTally, nil)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t// Log error, continue\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}(proofPerTally)\n\t\t\t\t\t}\n\t\t\t\t}(a)\n\t\t\t}\n\t\t})\n\t}\n\n}\n\nfunc TestTallyCompletenessDistributedHomo(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tpriv := generateOptions(t, g)\n\t\ttestDistributedHomomorphicEncryption(t, g, priv)\n\t}\n}\n\nfunc TestTallyCompletenessRegularHomo(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tpriv := generateOptions(t, g)\n\t\ttestHomomorphicEncryption(t, g, priv)\n\t}\n}\n\nfunc TestTallyCompletenessNonStandard(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tpriv := generateNonStandardOptions(t, g)\n\t\ttestDistributedHomomorphicEncryption(t, g, priv)\n\t}\n}\n"
  },
  {
    "path": "core/crypto/elgamal/internal/x509/x509.go",
    "content": "// Package x509 is for internal usage of ElGamal implementations and contains common logic\n// for any x509/PKCS8 operations.\npackage x509\n\nimport (\n\t\"crypto/x509/pkix\"\n\n\t\"tivi.io/core/crypto\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n)\n\n// MarshalKeyParameters provides common logic for ElGamal key parameters PKIX marshalling.\nfunc MarshalKeyParameters(params crypto.AlgorithmIdentifierParameters) (pkix.AlgorithmIdentifier, error) {\n\treturn params.ToPKIXAlgorithmIdentifier()\n}\n\n// MarshalKeyElement provides common logic for ElGamal key marshalling.\nfunc MarshalKeyElement(info crypto.KeyInfo) (asn_1.DER, error) {\n\treturn info.Marshal()\n}\n"
  },
  {
    "path": "core/crypto/elgamal/nizkp/ciphertext/ciphertext.go",
    "content": "// Package ciphertext provides implementation for ElGamal non-interactive zero-knowledge proof\n// ciphertext commitment. Ciphertext commitment ensures a verifier, that ciphertext and decrypted\n// ciphertext have a relationship.\npackage ciphertext\n\nimport (\n\t\"fmt\"\n\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\t\"tivi.io/core/math/group\"\n)\n\nconst (\n\tciphertextCommitmentsCount = 4\n)\n\n// Commitment is a ciphertext commitment that ensures a verifier, that ciphertext and decrypted\n// ciphertext have a relationship.\ntype Commitment struct {\n\t// g is an abstract group that both ciphertext and decrypted ciphertext belong to.\n\tg group.Group\n\n\t// a is an ElGamal ciphertext A parameter.\n\ta group.Element\n\n\t// b is an ElGamal ciphertext B parameter.\n\tb group.Element\n\n\t// dec is an ElGamal decrypted ciphertext.\n\tdec group.Element\n}\n\n// NewCiphertextCommitment returns new ElGamal non-interactive zero-knowledge proof ciphertext commitment.\nfunc NewCiphertextCommitment(g group.Group, a group.Element, b group.Element, dec group.Element) (*Commitment, error) {\n\treturn &Commitment{\n\t\tg:   g,\n\t\ta:   a,\n\t\tb:   b,\n\t\tdec: dec,\n\t}, nil\n}\n\n// Count returns 4.\nfunc (cc *Commitment) Count() uint64 {\n\treturn ciphertextCommitmentsCount\n}\n\n// Create creates prover's ElGamal non-interactive zero-knowledge proof ciphertext commitment.\nfunc (cc *Commitment) Create(s *group.Scalar) ([]group.Element, error) {\n\tcipherc, err := cc.a.Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create prover's ElGamal non-interactive zero-knowledge proof ciphertext commitment: %v\", err)\n\t}\n\n\treturn []group.Element{cipherc, cc.a, cc.b, cc.dec}, nil\n}\n\nfunc (cc *Commitment) Verify(proof *nizkp.Proof) ([]group.Element, error) {\n\tdec := cc.dec.Inverse()\n\n\t// Parse encoded message out of ElGamal decryption\n\tmsg, err := cc.b.Op(dec)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse encoded message out of ElGamal decryption: %v\", err)\n\t}\n\n\t// Recreate prover's ciphertext commitment and verify it\n\tcipherc, err := nizkp.Verify(cc.a, msg, proof.Challenge(), proof.Response())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to verify prover's ElGamal non-interactive zero-knowledge proof ciphertext commitment: %v\", err)\n\t}\n\n\treturn []group.Element{cipherc, cc.a, cc.b, cc.dec}, nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/nizkp/key/key.go",
    "content": "// Package key provides implementation for ElGamal non-interactive zero-knowledge proof\n// key pair commitment. Key pair commitment ensures a verifier, that encryption key is actually\n// a valid pair to decryption key.\npackage key\n\nimport (\n\t\"fmt\"\n\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\t\"tivi.io/core/math/group\"\n)\n\nconst (\n\tkeyPairCommitmentsCount = 1\n)\n\n// Commitment is a key pair commitment that ensures a verifier, that encryption key is actually\n// a valid pair to decryption key.\ntype Commitment struct {\n\t// g is an abstract group that both keys belong to.\n\tg group.Group\n\n\t// pub is a public element of a key.\n\tpub group.Element\n}\n\n// NewKeyPairCommitment returns new ElGamal non-interactive zero-knowledge proof key pair commitment.\nfunc NewKeyPairCommitment(g group.Group, pub group.Element) *Commitment {\n\treturn &Commitment{\n\t\tg:   g,\n\t\tpub: pub,\n\t}\n}\n\n// Count returns 1.\nfunc (kpc *Commitment) Count() uint64 {\n\treturn keyPairCommitmentsCount\n}\n\n// Create creates prover's ElGamal non-interactive zero-knowledge proof key pair commitment.\nfunc (kpc *Commitment) Create(s *group.Scalar) ([]group.Element, error) {\n\tkeyc, err := kpc.g.Generator().Scale(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create prover's ElGamal non-interactive zero-knowledge proof key pair commitment: %v\", err)\n\t}\n\n\treturn []group.Element{keyc}, nil\n}\n\n// Verify verifies prover's ElGamal non-interactive zero-knowledge proof key pair commitment.\nfunc (kpc *Commitment) Verify(t *nizkp.Proof) ([]group.Element, error) {\n\t// Recreate prover's key commitment and verify it\n\tkeyc, err := nizkp.Verify(kpc.g.Generator(), kpc.pub, t.Challenge(), t.Response())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to verify prover's ElGamal non-interactive zero-knowledge proof key pair commitment: %v\", err)\n\t}\n\n\treturn []group.Element{keyc}, nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/nizkp/nizkp.go",
    "content": "// Package nizkp implements ElGamal non-interactive zero-knowledge proof.\n// ElGamal non-interactive zero-knowledge proof is a collection of commitments\n// that prover provides to verifier in order to check data integrity and completeness.\n// Commitment is a unit of something that is being either proved (prover) or verified (verifier).\npackage nizkp\n\nimport (\n\t\"fmt\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"golang.org/x/crypto/sha3\"\n\n\t\"tivi.io/core/math/group\"\n)\n\n// Commitment is a unit of something that is being either proved (prover) or verified (verifier).\ntype Commitment interface {\n\t// Count returns an amount of commitments to be either created (prover) or verified (verifier).\n\t// Verifier should generate exactly the same amount of commitments as prover does.\n\tCount() uint64\n\n\t// Create is used by a prover to create a set of commitments to verifier.\n\t// Random scalar s is used in a creation of a commitments set.\n\tCreate(s *group.Scalar) (commitments []group.Element, err error)\n\n\t// Verify is used by a verifier to parse ElGamal non-interactive zero-knowledge proof into\n\t// a set of commitments, that were generated by a prover. After parsing, commitments\n\t// are also verified and returned.\n\tVerify(nizkp *Proof) (commitments []group.Element, err error)\n}\n\n// Challenge creates ElGamal non-interactive zero-knowledge proof challenge by summing up all\n// commitments into a resulting one, and then hashing it as SHAKE256(ID || sum).\n// If salt non-nil then hashing is SHAKE256(ID || sum || salt). Salt is used to add a\n// uniqueness to a set of commitments.\nfunc Challenge(g group.Group, commitments []group.Element, salt []byte) (challenge *group.Scalar, err error) {\n\tsum := g.Identity()\n\n\t// Sum up all commitments, this will ensure that even if order of elements in a commitments\n\t// slice changes, we still get the same challenge\n\tfor _, commitment := range commitments {\n\t\tsum, err = sum.Op(commitment)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to sum up ElGamal non-interactive zero-knowledge proof commitments: %v\", err)\n\t\t}\n\t}\n\n\t// ASN.1 marshal sum\n\tsumDer, err := sum.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal summed ElGamal non-interactive zero-knowledge proof commitment: %v\", err)\n\t}\n\n\t// If salt is nil, then\n\t// digest = SHAKE256([]byte(\"NIZKP\") || sumDer)\n\t//\n\t// otherwise,\n\t// digest = SHAKE256([]byte(\"NIZKP\") || sumDer || salt)\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddASN1(asn1.PrintableString, func(builder *cryptobyte.Builder) {\n\t\t\tbuilder.AddBytes([]byte(\"NIZKP\"))\n\t\t})\n\t\tbuilder.AddBytes(sumDer)\n\t\tbuilder.AddASN1OctetString(salt)\n\t})\n\n\tder, err := builder.Bytes()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal non-interactive zero-knowledge proof challenge: %v\", err)\n\t}\n\n\t// Variable length hash\n\tdigest := sha3.NewShake256()\n\n\t// Shake instance does not fail\n\tdigest.Write(der) //nolint: errcheck\n\n\t// Create group scalar from hash bytes\n\tchallenge, err = group.ScalarValueOfReader(digest, g.Order())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group scalar from ASN.1 marshalled ElGamal non-interactive zero-knowledge proof challenge: %v\", err)\n\t}\n\treturn\n}\n\n// Verify parses non-interactive zero-knowledge proof (challenge, response) for a specific\n// commitment, verifies and returns it. Specific commitment is extracted by providing correct\n// base and element.\nfunc Verify(base, element group.Element, challenge, response *group.Scalar) (commitment group.Element, err error) {\n\tc1, err := element.Scale(challenge)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to scale element by ElGamal non-interactive zero-knowledge proof challenge: %v\", err)\n\t}\n\n\tc2, err := base.Scale(response)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to scale base by ElGamal non-interactive zero-knowledge proof response: %v\", err)\n\t}\n\n\tc3 := c1.Inverse()\n\n\tcommitment, err = c2.Op(c3)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compute a ElGamal non-interactive zero-knowledge proof commitment: %v\", err)\n\t}\n\n\tlhs, err := base.Scale(response)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to calculate ElGamal non-interactive zero-knowledge proof lhs expression: %v\", err)\n\t}\n\n\trhs, err := c1.Op(commitment)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to calculate ElGamal non-interactive zero-knowledge proof rhs expression: %v\", err)\n\t}\n\n\tif err = lhs.Equal(rhs); err != nil {\n\t\treturn nil, fmt.Errorf(\"ElGamal non-interactive zero-knowledge proof lhs not equal to rhs: %v\", err)\n\t}\n\treturn\n}\n\n// Proof is ElGamal non-interactive zero-knowledge proof.\ntype Proof struct {\n\t// challenge is the hash that both prover and verifier create on their sides\n\t// and then compare them to be equal.\n\tchallenge *group.Scalar\n\n\t// response is the prover response, that verifier verifies.\n\tresponse *group.Scalar\n}\n\n// NewProof returns new ElGamal non-interactive zero-knowledge proof.\nfunc NewProof(challenge, response *group.Scalar) *Proof {\n\treturn &Proof{\n\t\tchallenge: challenge,\n\t\tresponse:  response,\n\t}\n}\n\n// Challenge returns ElGamal non-interactive zero-knowledge proof challenge.\nfunc (p *Proof) Challenge() *group.Scalar {\n\treturn p.challenge\n}\n\n// Response returns ElGamal non-interactive zero-knowledge proof response.\nfunc (p *Proof) Response() *group.Scalar {\n\treturn p.response\n}\n\n// ASN1Marshal ASN.1 marshals ElGamal non-interactive zero-knowledge proof as\n//\n//\tp ::= SEQUENCE {\n//\t\tchallenge\tINTEGER\n//\t\tresponse\tINTEGER\n//\t}\nfunc (p *Proof) MarshalASN1() (der []byte, err error) {\n\tchallengeDer, err := p.challenge.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal non-interactive zero-knowledge proof challenge: %v\", err)\n\t}\n\n\tresponseDer, err := p.response.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal non-interactive zero-knowledge proof response: %v\", err)\n\t}\n\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddBytes(challengeDer)\n\t\tbuilder.AddBytes(responseDer)\n\t})\n\n\tder, err = builder.Bytes()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal ElGamal non-interactive zero-knowledge proof: %v\", err)\n\t}\n\treturn\n}\n\n// ASN1UnmarshalProof ASN.1 unmarshalls ElGamal non-interactive zero-knowledge proof.\nfunc ASN1UnmarshalProof(g group.Group, data []byte) (*Proof, error) {\n\tder := cryptobyte.String(data)\n\tvar sequence, challengeDer, responseDer cryptobyte.String\n\n\tif !der.ReadASN1(&sequence, asn1.SEQUENCE) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal non-interactive zero-knowledge proof SEQUENCE\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&challengeDer, nil) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal non-interactive zero-knowledge proof challenge\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&responseDer, nil) {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal ElGamal non-interactive zero-knowledge proof response\")\n\t}\n\n\tif !der.Empty() || !sequence.Empty() {\n\t\treturn nil, fmt.Errorf(\"trailing bytes left while ASN.1 unmarshalling ElGamal non-interactive zero-knowledge proof\")\n\t}\n\n\tchallenge, err := group.UnmarshalScalar(asn_1.DER(challengeDer), g.Order())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group scalar from ElGamal non-interactive zero-knowledge proof challenge: %v\", err)\n\t}\n\n\tresponse, err := group.UnmarshalScalar(asn_1.DER(responseDer), g.Order())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create group scalar from ElGamal non-interactive zero-knowledge proof response: %v\", err)\n\t}\n\n\treturn NewProof(challenge, response), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/parameters.go",
    "content": "package elgamal\n\nimport (\n\t\"crypto/x509/pkix\"\n\tasn_1 \"encoding/asn1\"\n\t\"fmt\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/math/group\"\n)\n\n// Parameters are ElGamal key parameters.\ntype Parameters struct {\n\t// g is a group that ElGamal key belongs to.\n\tg group.Group\n}\n\n// NewParameters returns new ElGamal key parameters.\nfunc NewParameters(g group.Group) *Parameters {\n\treturn &Parameters{g: g}\n}\n\n// Group returns ElGamal group that this key belongs to.\nfunc (p *Parameters) Group() group.Group {\n\treturn p.g\n}\n\n// Algorithm returns ElGamal OID as defined in\n// https://datatracker.ietf.org/doc/html/draft-rfced-info-pgutmann-00#section-2\nfunc (p *Parameters) Algorithm() asn_1.ObjectIdentifier {\n\treturn crypto.ElGamalEncryptionOID()\n}\n\n// ToPKIXAlgorithmIdentifier converts ElGamal parameters into PKIX parameters.\nfunc (p *Parameters) ToPKIXAlgorithmIdentifier() (algorithm pkix.AlgorithmIdentifier, err error) {\n\tparameters, err := group.Marshal(p.g)\n\tif err != nil {\n\t\treturn pkix.AlgorithmIdentifier{}, fmt.Errorf(\"failed to convert ElGamal parameters to PKIX parameters: %v\", err)\n\t}\n\n\talgorithm = pkix.AlgorithmIdentifier{\n\t\tAlgorithm: p.Algorithm(),\n\t\tParameters: asn_1.RawValue{\n\t\t\tFullBytes: parameters,\n\t\t},\n\t}\n\treturn\n}\n\n// OfPKIXAlgorithmIdentifier converts PKIX parameters into ElGamal parameters.\nfunc OfPKIXAlgorithmIdentifier(algorithm pkix.AlgorithmIdentifier) (crypto.AlgorithmIdentifierParameters, error) {\n\tparameters, err := group.Unmarshal(algorithm.Parameters.FullBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 unmarshal group from PKIX parameters: %v\", err)\n\t}\n\n\treturn NewParameters(parameters), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/proof.go",
    "content": "package elgamal\n\nimport (\n\t\"fmt\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/crypto/elgamal/nizkp\"\n\t\"tivi.io/core/math/group\"\n)\n\n// DecryptionProof holds an ElGamal non-interactive zero-knowledge proof and its related metadata.\ntype DecryptionProof struct {\n\tciphertext *Ciphertext\n\tdecryption *Decryption\n\tproof      *nizkp.Proof\n}\n\nfunc NewDecryptionProof(ciphertext *Ciphertext, decryption *Decryption, proof *nizkp.Proof) *DecryptionProof {\n\treturn &DecryptionProof{\n\t\tciphertext: ciphertext,\n\t\tdecryption: decryption,\n\t\tproof:      proof,\n\t}\n}\n\nfunc (p *DecryptionProof) Ciphertext() *Ciphertext {\n\treturn p.ciphertext\n}\n\nfunc (p *DecryptionProof) Decryption() *Decryption {\n\treturn p.decryption\n}\n\nfunc (p *DecryptionProof) Proof() *nizkp.Proof {\n\treturn p.proof\n}\n\n// MarshalASN1 ASN.1 marshals ElGamal non-interactive zero-knowledge proof as\n//\n//\tp ::= SEQUENCE {\n//\t\tCiphertext\tCIPHERTEXT\n//\t\tDecryption\tDECRYPTION\n//\t\tProof\tPROOF\n//\t}\n//\n//\tCIPHERTEXT ::= {\n//\t\ta\tGROUP ELEMENT\n//\t\tb\tGROUP ELEMENT\n//\t}\n//\n//\tDECRYPTION ::= GROUP ELEMENT\n//\n//\tPROOF ::= {\n//\t\tchallenge\tINTEGER\n//\t\tresponse\tINTEGER\n//\t}\n//\n//\tGROUP ELEMENT ::= CHOICE {\n//\t\tModPElement\tINTEGER\n//\t\tNistElement\tOCTET STRING\n//\t\tEdwards25519Element\tOCTET STRING\n//\t}\nfunc (p *DecryptionProof) MarshalASN1() (der []byte, err error) {\n\tctDer, err := p.ciphertext.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 marshal ElGamal proof ciphertext: %v\", err)\n\t}\n\n\tdecDer, err := p.decryption.Value().Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 marshal ElGamal proof decryption: %v\", err)\n\t}\n\n\ttransDer, err := p.proof.MarshalASN1()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 marshal ElGamal proof transcript: %v\", err)\n\t}\n\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddBytes(ctDer)\n\t\tbuilder.AddBytes(decDer)\n\t\tbuilder.AddBytes(transDer)\n\t})\n\n\tder, err = builder.Bytes()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 marshal ElGamal proof: %v\", err)\n\t}\n\n\treturn\n}\n\nfunc ASN1UnmarshalDecryptionProof(g group.Group, data []byte) (*DecryptionProof, error) {\n\tder := cryptobyte.String(data)\n\tvar sequence, ctDer, decDer, transDer cryptobyte.String\n\n\tif !der.ReadASN1(&sequence, asn1.SEQUENCE) {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal proof SEQUENCE\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&ctDer, nil) {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal proof ciphertext\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&decDer, nil) {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal proof decryption\")\n\t}\n\n\tif !sequence.ReadAnyASN1Element(&transDer, nil) {\n\t\treturn nil, fmt.Errorf(\"cannot ASN.1 unmarshal ElGamal proof transcript\")\n\t}\n\n\tif !der.Empty() || !sequence.Empty() {\n\t\treturn nil, fmt.Errorf(\"trailing bytes while ASN.1 unmarshalling ElGamal proof\")\n\t}\n\n\tciphertext, err := ASN1UnmarshalCiphertext(g, ctDer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot create ElGamal ciphertext from ASN.1 unmarshalled ElGamal proof ciphertext: %v\", err)\n\t}\n\n\tdecrypted, err := ASN1UnmarshalDecryption(g, decDer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot create ElGamal decryption from ASN.1 unmarshalled ElGamal proof decryption: %v\", err)\n\t}\n\n\ttranscript, err := nizkp.ASN1UnmarshalProof(g, transDer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot create ElGamal transcript from ASN.1 unmarshalled ElGamal proof transcript: %v\", err)\n\t}\n\n\treturn NewDecryptionProof(ciphertext, decrypted, transcript), nil\n}\n"
  },
  {
    "path": "core/crypto/elgamal/shamir/doc.go",
    "content": "/*\nPackage shamir defines distributed ElGamal that follows Shamir scheme, i.e.\nprivate key shares are split during private key initialization, and when\ndecryption is needed to be performed - these splitted key shares are put together\nto create a complete private key.\n*/\npackage shamir\n"
  },
  {
    "path": "core/crypto/elgamal/x509/pkcs8.go",
    "content": "package x509\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\tinternal \"tivi.io/core/crypto/elgamal/internal/x509\"\n\tx_509 \"tivi.io/core/crypto/x509/marshal\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\ntype UnmarshallerPKCS8 struct{}\n\nfunc NewUnmarshallerPKCS8() x_509.KeyUnmarshaller[*group.Scalar] {\n\treturn UnmarshallerPKCS8{}\n}\n\nfunc (pkcs8 UnmarshallerPKCS8) UnmarshalKeyParameters(algoID pkix.AlgorithmIdentifier) (oid asn1.ObjectIdentifier, params crypto.AlgorithmIdentifierParameters, err error) {\n\tparams, err = elgamal.OfPKIXAlgorithmIdentifier(algoID)\n\toid = algoID.Algorithm\n\treturn\n}\n\nfunc (pkcs8 UnmarshallerPKCS8) UnmarshalKeyElement(params group.Group, der asn_1.DER) (*group.Scalar, error) {\n\treturn elgamal.ASN1UnmarshalPrivateElement(params, der)\n}\n\ntype MarshallerPKCS8 struct{}\n\nfunc NewMarshallerPKCS8() x_509.KeyMarshaller {\n\treturn MarshallerPKCS8{}\n}\n\nfunc (pkcs8 MarshallerPKCS8) MarshalKeyParameters(params crypto.AlgorithmIdentifierParameters) (pkix.AlgorithmIdentifier, error) {\n\treturn internal.MarshalKeyParameters(params)\n}\n\nfunc (pkcs8 MarshallerPKCS8) MarshalKeyElement(info crypto.KeyInfo) (asn_1.DER, error) {\n\treturn internal.MarshalKeyElement(info)\n}\n"
  },
  {
    "path": "core/crypto/elgamal/x509/x509.go",
    "content": "// Package x509 provides adapters and other ElGamal specific implementations for crypto/x509.\npackage x509\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"tivi.io/core/crypto/elgamal\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\tx509e \"tivi.io/core/crypto/elgamal/internal/x509\"\n\tx_509 \"tivi.io/core/crypto/x509/marshal\"\n\t\"tivi.io/core/math/group\"\n)\n\ntype Unmarshaller struct{}\n\nfunc NewUnmarshaller() x_509.KeyUnmarshaller[group.Element] {\n\treturn Unmarshaller{}\n}\n\nfunc (x509 Unmarshaller) UnmarshalKeyParameters(algoID pkix.AlgorithmIdentifier) (oid asn1.ObjectIdentifier, params crypto.AlgorithmIdentifierParameters, err error) {\n\tparams, err = elgamal.OfPKIXAlgorithmIdentifier(algoID)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to ASN.1 unmarshal PKIX parameters to ElGamal public key parameters: %v\", err)\n\t}\n\toid = algoID.Algorithm\n\treturn\n}\n\nfunc (x509 Unmarshaller) UnmarshalKeyElement(g group.Group, der asn_1.DER) (group.Element, error) {\n\treturn elgamal.ASN1UnmarshalPublicElement(g, der)\n}\n\ntype Marshaller struct{}\n\nfunc NewMarshaller() x_509.KeyMarshaller {\n\treturn Marshaller{}\n}\n\nfunc (x509 Marshaller) MarshalKeyParameters(params crypto.AlgorithmIdentifierParameters) (pkix.AlgorithmIdentifier, error) {\n\treturn x509e.MarshalKeyParameters(params)\n}\n\nfunc (x509 Marshaller) MarshalKeyElement(info crypto.KeyInfo) (asn_1.DER, error) {\n\treturn x509e.MarshalKeyElement(info)\n}\n"
  },
  {
    "path": "core/crypto/ocsp/conf.go",
    "content": "package ocsp\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\t\"time\"\n)\n\n// Conf contains the configurable options for the OCSP api.\ntype Conf struct {\n\t// URL is the address of the OCSP responder.\n\tURL string\n\n\t// Retries is the number of times a OCSP request is retried in case of failure.\n\t// Defaults to 2 retries.\n\tRetries uint64\n}\n\n// REQUEST\n\n// https://tools.ietf.org/html/rfc6960#section-4.1.1\ntype ocspRequest struct {\n\tTBSRequest tbsRequest\n}\n\ntype tbsRequest struct {\n\tVersion           int `asn1:\"explicit,tag:0,optional,default:0\"`\n\tRequestList       []request\n\tRequestExtensions []pkix.Extension `asn1:\"explicit,tag:2,optional\"`\n}\n\ntype request struct {\n\tReqCert certID\n}\n\ntype certID struct {\n\tHashAlgorithm  pkix.AlgorithmIdentifier\n\tIssuerNameHash []byte\n\tIssuerKeyHash  []byte\n\tSerialNumber   *big.Int\n}\n\n// RESPONSE\n\n// CertStatus is the return value for OCSP commands containing all the\n// relevant information about the OCSP response.\ntype CertStatus struct {\n\tProducedAt       time.Time // The time this response was produced at\n\tNonce            []byte    // The nonce used in the response\n\tGood             bool      // If the status of the requested certificate is good\n\tUnknown          bool      // If the status of the certificate is unknown\n\tRevocationReason int       // If the certificate is revoked, the reason of revocation.\n}\n\n// LiveCertStatus is an extension of CertStatus returned from live OCSP queries\n// which also contains the raw basic response received from the server.\ntype LiveCertStatus struct {\n\tRawResponse []byte // The raw ASN.1 DER-encoded basic response from the server.\n\tCertStatus\n}\n\n// https://tools.ietf.org/html/rfc6960#section-4.2.1\ntype ocspResponse struct {\n\tResponseStatus asn1.Enumerated\n\tResponseBytes  responseBytes `asn1:\"explicit,tag:0,optional\"`\n}\n\nconst ocspResponseStatusSuccessful = 0\n\nvar ocspResponseStatus = map[int]string{\n\t0: \"successful\",\n\t1: \"malformedRequest\",\n\t2: \"internalError\",\n\t3: \"tryLater\",\n\t// 4: unused,\n\t5: \"sigRequired\",\n\t6: \"unauthorized\",\n}\n\ntype responseBytes struct {\n\tResponseType asn1.ObjectIdentifier\n\tResponse     []byte\n}\n\ntype basicOCSPResponse struct {\n\tRaw                asn1.RawContent\n\tTBSResponseData    responseData\n\tSignatureAlgorithm pkix.AlgorithmIdentifier\n\tSignature          asn1.BitString\n\tCerts              []asn1.RawValue `asn1:\"explicit,tag:0,optional\"`\n}\n\ntype responseData struct {\n\tRaw     asn1.RawContent\n\tVersion int `asn1:\"explicit,tag:0,optional,default:0\"`\n\n\t// The asn1 module does not support choices, so use 2 optional fields\n\t// instead of ResponderID.\n\tResponderIDByName pkix.RDNSequence `asn1:\"explicit,tag:1,optional\"`\n\tResponderIDByKey  []byte           `asn1:\"explicit,tag:2,optional\"`\n\n\tProducedAt         time.Time\n\tResponses          []singleResponse\n\tResponseExtensions []pkix.Extension `asn1:\"explicit,tag:1,optional\"`\n}\n\ntype singleResponse struct {\n\tCertID certID\n\n\t// The asn1 module does not support choices, so use 3 optional fields\n\t// instead of CertStatus.\n\tCertStatusGood        asn1.Flag   `asn1:\"explicit,tag:0,optional\"`\n\tCertStatusRevokedTime time.Time   `asn1:\"explicit,tag:1,optional\"`\n\tCertStatusRevokedInfo revokedInfo `asn1:\"explicit,tag:1,optional\"`\n\tCertStatusUnknown     asn1.Flag   `asn1:\"explicit,tag:2,optional\"`\n\n\tThisUpdate       time.Time\n\tNextUpdate       time.Time        `asn1:\"explicit,tag:0,optional\"`\n\tSingleExtensions []pkix.Extension `asn1:\"explicit,tag:1,optional\"`\n}\n\n// Alternative type for CertStatusRevoked in singleResponse\ntype revokedInfo struct {\n\tRevocationTime   time.Time\n\tRevocationReason asn1.Enumerated `asn1:\"explicit,tag:0,optional\"`\n}\n\n// The following constants define the many possible reasons a certificate may\n// have been revoked. These constants can be used to check the revocation reason\n// returned inside the CertStatus structure\n// https://tools.ietf.org/html/rfc5280#section-5.3.1\nconst (\n\tReasonUnspecified = iota\n\tReasonKeyCompromise\n\tReasonCACompromise\n\tReasonAffiliationChanged\n\tReasonSuperseded\n\tReasonCessationOfOperation\n\tReasonCertificateHold\n\t_\n\tReasonRemoveFromCRL\n\tReasonPrivilegeWithdrawn\n\tReasonAACompromise\n)\n"
  },
  {
    "path": "core/crypto/ocsp/ocsp.go",
    "content": "package ocsp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"math\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst (\n\t// The maximum amount the response thisUpdate can be set in the future\n\t// allowing for correction for system clock inconsistencies\n\tmaxSkew = 300 * time.Millisecond\n\n\t// The maximum amount the response thisUpdate can differ from\n\t// the current time at the time of response validation\n\tmaxAge = 1 * time.Minute\n\n\t// Maximum size for the ocsp server response.\n\tmaxResponseSize = 10240 // 10 KiB.\n)\n\nvar (\n\t// https://tools.ietf.org/html/rfc6960#section-4.4.1\n\tidPKIXOCSPNonce = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 2}\n\n\t// https://tools.ietf.org/html/rfc6960#section-4.2.1\n\tidPKIXOCSPBasic = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 1}\n\n\t// Map of signature algorithms\n\tsigMap = map[string]x509.SignatureAlgorithm{\n\t\t\"1.2.840.113549.1.1.11\": x509.SHA256WithRSA,\n\t\t\"1.2.840.113549.1.1.12\": x509.SHA384WithRSA,\n\t\t\"1.2.840.113549.1.1.13\": x509.SHA512WithRSA,\n\t}\n\n\t// OID of the hash function used to calculate CertID fields\n\tcertIDHashOID = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}\n)\n\n// VerifyOCSPCertificate verifies the status of the certificate against\n// the configured OCSP server. If nonce is not nil, then that value will\n// be used as the nonce in the request, otherwise no nonce is used.\nfunc VerifyOCSPCertificate(ctx context.Context, cert, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte, conf Conf) (status LiveCertStatus, err error) {\n\tif len(conf.URL) == 0 {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"no OCSP URL specified\")\n\t}\n\tretries := util.DefaultUint64(conf.Retries, 1)\n\treqCert, err := newCertIDRequest(cert)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"new cert id request: %w\", err)\n\t}\n\tbasicResponse, err := sendOCSPRequest(ctx, conf.URL, reqCert, nonce, retries)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"send ocsp request: %w\", err)\n\t}\n\trespNonce, err := verifyOCSPResponse(basicResponse, reqCert, issuer, responders, nonce)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"verify ocsp response: %w\", err)\n\t}\n\tsingle := basicResponse.TBSResponseData.Responses[0]\n\treturn LiveCertStatus{\n\t\tRawResponse: basicResponse.Raw,\n\t\tCertStatus: CertStatus{\n\t\t\tProducedAt:       basicResponse.TBSResponseData.ProducedAt,\n\t\t\tGood:             bool(single.CertStatusGood),\n\t\t\tUnknown:          bool(single.CertStatusUnknown),\n\t\t\tNonce:            respNonce,\n\t\t\tRevocationReason: int(single.CertStatusRevokedInfo.RevocationReason),\n\t\t},\n\t}, nil\n}\n\n// VerifyResponse verifies a DER-encoded full OCSP response. a full OCSP\n// response envelopes a basic OCSP response with the status of the server\n// response and the OID of the response type. VerifyResponse unpacks the\n// basic response and calls VerifyBasicResponse.\nfunc VerifyResponse(bytes []byte, cert, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte) (status LiveCertStatus, err error) {\n\tresp, err := unmarshalOCSPResponse(bytes)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"unmarshal ocsp response: %w\", err)\n\t}\n\treturn VerifyBasicResponse(resp.ResponseBytes.Response, cert, issuer, responders, nonce)\n}\n\n// VerifyBasicResponse verifies a stored DER-encoded basic OCSP response. If nonce is\n// not nil, then the nonce in the response must match that value.\nfunc VerifyBasicResponse(response []byte, cert, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte) (status LiveCertStatus, err error) {\n\tbasicResponse, err := unmarshalBasicResponse(response)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"unmarshal basic response: %w\", err)\n\t}\n\treqCert, err := newCertIDRequest(cert)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"new cert id request: %w\", err)\n\t}\n\trespNonce, err := verifyStoredResponse(basicResponse, reqCert, issuer, responders, nonce)\n\tif err != nil {\n\t\treturn LiveCertStatus{}, fmt.Errorf(\"verify stored response: %w\", err)\n\t}\n\tsingle := basicResponse.TBSResponseData.Responses[0]\n\treturn LiveCertStatus{\n\t\tRawResponse: basicResponse.Raw,\n\t\tCertStatus: CertStatus{\n\t\t\tProducedAt:       basicResponse.TBSResponseData.ProducedAt,\n\t\t\tGood:             bool(single.CertStatusGood),\n\t\t\tUnknown:          bool(single.CertStatusUnknown),\n\t\t\tNonce:            respNonce,\n\t\t\tRevocationReason: int(single.CertStatusRevokedInfo.RevocationReason)},\n\t}, nil\n}\n\n// ParseTime parses a DER-encoded basic OCSP response and returns the time it\n// was produced at. ParseTime does not check the validity of the response, it only\n// returns the producedAt time value.\nfunc ParseTime(response []byte) (time.Time, error) {\n\tbasicResponse, err := unmarshalBasicResponse(response)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal basic response: %w\", err)\n\t}\n\treturn basicResponse.TBSResponseData.ProducedAt, nil\n}\n\nconst ocspReply = \"application/ocsp-response\"\n\n// sendOCSPRequest creates, marshals and sends the HTTP POST request\n// to the OCSP server, retrying in case of failure. If successful,\n// it unmarshals and returns the basic OCSP response.\nfunc sendOCSPRequest(ctx context.Context, url string, certID *certID, nonce []byte, retries uint64) (basicOCSPResponse, error) {\n\t// Create the request\n\trequest := ocspRequest{\n\t\tTBSRequest: tbsRequest{\n\t\t\tRequestList: []request{{ReqCert: *certID}},\n\t\t},\n\t}\n\tif nonce != nil {\n\t\trequest.TBSRequest.RequestExtensions = []pkix.Extension{{\n\t\t\tId:    idPKIXOCSPNonce,\n\t\t\tValue: nonce,\n\t\t}}\n\t}\n\t// ASN11Marshal the request\n\treqBytes, err := asn1.Marshal(request)\n\tif err != nil {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"marshal request: %w\", err)\n\t}\n\t// Create the HTTP POST request\n\thttpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"new http request: %w\", err)\n\t}\n\thttpReq.Header.Set(\"Content-Type\", \"application/ocsp-request\")\n\tvar httpRespBody []byte\n\t// Send the HTTP request\n\t// In case of (server) failure, retry the number of times specified\n\t// with Exponential Backoff sleep time in between\n\tfor tries := uint64(0); ; tries++ {\n\t\tif tries > retries {\n\t\t\treturn basicOCSPResponse{}, err\n\t\t}\n\t\tsleep := time.Duration(math.Pow(2, float64(tries))) * time.Second\n\t\tvar httpResp *http.Response\n\t\thttpResp, err = http.DefaultClient.Do(httpReq)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"send http request: %w\", err)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tdefer func() {\n\t\t\tif closeErr := httpResp.Body.Close(); closeErr != nil && err == nil {\n\t\t\t\terr = closeErr\n\t\t\t}\n\t\t}()\n\t\tif httpResp.StatusCode != http.StatusOK {\n\t\t\terr = fmt.Errorf(\"bad http status code: wanted '%d' got '%d'\", http.StatusOK, httpResp.StatusCode)\n\t\t\tif httpResp.StatusCode/100 != 5 {\n\t\t\t\treturn basicOCSPResponse{}, err\n\t\t\t}\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tif ct := httpResp.Header.Get(\"Content-Type\"); ct != ocspReply {\n\t\t\terr = fmt.Errorf(\"bad http response content type: wanted '%s' got '%s'\", ocspReply, ct)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\thttpRespBody, err = ioutil.ReadAll(io.LimitReader(httpResp.Body, maxResponseSize))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"http response body: %w\", err)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tresp, err := unmarshalOCSPResponse(httpRespBody)\n\tif err != nil {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"unmarshal ocsp response: %w\", err)\n\t}\n\tbasicResponse, err := unmarshalBasicResponse(resp.ResponseBytes.Response)\n\tif err != nil {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"unmarshal basic response: %w\", err)\n\t}\n\treturn basicResponse, nil\n}\n\n// unmarshalOCSPResponse unmarshals a full OCSP response, verifies the\n// response status from the OCSP server, and returns the unmarshalled content.\nfunc unmarshalOCSPResponse(bytes []byte) (ocspResponse, error) {\n\tvar resp ocspResponse\n\trest, err := asn1.Unmarshal(bytes, &resp)\n\tif err != nil {\n\t\treturn ocspResponse{}, fmt.Errorf(\"unmarshal response: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn ocspResponse{}, fmt.Errorf(\"unmarshal response excess bytes: %d\", len(rest))\n\t}\n\tif resp.ResponseStatus != ocspResponseStatusSuccessful {\n\t\tstatus, ok := ocspResponseStatus[int(resp.ResponseStatus)]\n\t\tif !ok {\n\t\t\tstatus = fmt.Sprint(resp.ResponseStatus)\n\t\t}\n\t\treturn ocspResponse{}, fmt.Errorf(\"bad ocsp status code: wanted '%d' got '%d' (meaning '%s')\", ocspResponseStatusSuccessful, resp.ResponseStatus, status)\n\t}\n\tif !resp.ResponseBytes.ResponseType.Equal(idPKIXOCSPBasic) {\n\t\treturn ocspResponse{}, fmt.Errorf(\"bad ocsp response type: wanted '%s' got '%s'\", idPKIXOCSPBasic, resp.ResponseBytes.ResponseType)\n\t}\n\treturn resp, nil\n}\n\n// unmarshalBasicResponse unmarshals a basic OCSP response and\n// returns the unmarshalled content.\nfunc unmarshalBasicResponse(response []byte) (basicOCSPResponse, error) {\n\tvar basicResponse basicOCSPResponse\n\trest, err := asn1.Unmarshal(response, &basicResponse)\n\tif err != nil {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"unmarshal basic response: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn basicOCSPResponse{}, fmt.Errorf(\"unmarshal basic response excess bytes: %d\", len(rest))\n\t}\n\treturn basicResponse, err\n}\n\n// verifyOCSPResponse verifies if the given response should be accepted, based on\n// the certID, the presence of the correct nonce, the responder signature (verifyResponseCommon),\n// and the validity of the thisUpdate and producedAt times. Returns the response nonce on success.\nfunc verifyOCSPResponse(resp basicOCSPResponse, cert *certID, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte) ([]byte, error) {\n\trespNonce, err := verifyResponseCommon(resp, cert, issuer, responders, nonce)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify response common: %w\", err)\n\t}\n\tthisUpdate := resp.TBSResponseData.Responses[0].ThisUpdate\n\tcurrent := time.Now()\n\tskewed := current.Add(maxSkew)\n\tif thisUpdate.After(skewed) {\n\t\treturn nil, fmt.Errorf(\"thisUpdate set in the future: thisUpdate '%s' skewed '%s'\", thisUpdate, skewed)\n\t}\n\tif age := current.Sub(thisUpdate); age > maxAge {\n\t\treturn nil, fmt.Errorf(\"thisUpdate too old: thisUpdate '%s' age '%s'\", thisUpdate, age)\n\t}\n\tif pat := resp.TBSResponseData.ProducedAt; pat.Before(thisUpdate) || pat.After(skewed) {\n\t\treturn nil, fmt.Errorf(\"producedAt wrong time: wanted after thisUpdate '%s' and before skewed '%s', got '%s'\", thisUpdate, skewed, pat)\n\t}\n\treturn respNonce, nil\n}\n\n// verifyStoredResponse verifies if the given stored response should be accepted, based on\n// the certID, the presence of the correct nonce, the responder signature (verifyResponseCommon),\n// and the validity of the thisUpdate and producedAt times. Returns the response nonce on success.\nfunc verifyStoredResponse(resp basicOCSPResponse, cert *certID, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte) ([]byte, error) {\n\trespNonce, err := verifyResponseCommon(resp, cert, issuer, responders, nonce)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify response common: %w\", err)\n\t}\n\tthisUpdate := resp.TBSResponseData.Responses[0].ThisUpdate\n\tpat := resp.TBSResponseData.ProducedAt\n\tif pat.Before(thisUpdate) {\n\t\treturn nil, fmt.Errorf(\"producedAt wrong time: producedAt '%s' before thisUpdate '%s'\", pat, thisUpdate)\n\t}\n\tif age := pat.Sub(thisUpdate); age > maxAge {\n\t\treturn nil, fmt.Errorf(\"producedAt too old: producedAt '%s' age '%s'\", pat, age)\n\t}\n\treturn respNonce, nil\n}\n\n// verifyResponseCommon verifies if the given response should be accepted, based on\n// the certID, the presence of the correct nonce, and the responder signature.\n// Returns the response nonce on success.\nfunc verifyResponseCommon(basicResponse basicOCSPResponse, cert *certID, issuer *x509.Certificate, responders []*x509.Certificate, nonce []byte) ([]byte, error) {\n\tif n := len(basicResponse.TBSResponseData.Responses); n != 1 {\n\t\treturn nil, fmt.Errorf(\"bad ocsp responses number: wanted '%d' got '%d'\", 1, n)\n\t}\n\tsingleResponse := basicResponse.TBSResponseData.Responses[0]\n\tif !cert.equal(&singleResponse.CertID) {\n\t\treturn nil, fmt.Errorf(\"cert ids not equal\")\n\t}\n\tvar respNonce []byte\n\tfor _, extension := range basicResponse.TBSResponseData.ResponseExtensions {\n\t\tif extension.Id.Equal(idPKIXOCSPNonce) {\n\t\t\trespNonce = extension.Value\n\t\t}\n\t}\n\tif nonce != nil && !bytes.Equal(nonce, respNonce) {\n\t\treturn nil, fmt.Errorf(\"bad ocsp response nonce: wanted '%s' got '%s'\", nonce, respNonce)\n\t}\n\tresponder, err := getResponderCertificate(basicResponse, issuer, responders)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get responder certificate: %w\", err)\n\t}\n\talgo, ok := sigMap[basicResponse.SignatureAlgorithm.Algorithm.String()]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"signature algorithm not supported: %s\", basicResponse.SignatureAlgorithm.Algorithm)\n\t}\n\tif err = responder.CheckSignature(algo, basicResponse.TBSResponseData.Raw, basicResponse.Signature.RightAlign()); err != nil {\n\t\treturn nil, fmt.Errorf(\"responder certificate check signature: %w\", err)\n\t}\n\treturn respNonce, nil\n}\n\n// getResponderCertificate returns the basic OCSP responder certificate, if it matches\n// one of the certificates in the responders list of if it exists within the\n// basic OCSP response.\nfunc getResponderCertificate(basicResponse basicOCSPResponse, issuer *x509.Certificate, responders []*x509.Certificate) (*x509.Certificate, error) {\n\tresponderName := basicResponse.TBSResponseData.ResponderIDByName\n\t// Verify if the response is signed by a configured responder\n\tfor _, responder := range responders {\n\t\tresponder.Subject.ExtraNames = responder.Subject.Names\n\t\tif util.IsRDNSequenceEqual(responder.Subject.ToRDNSequence(), responderName) {\n\t\t\treturn responder, nil\n\t\t}\n\t}\n\t// Otherwise verify if the certificate is in the response, if it is issued by\n\t// the same issuer, and if it is allowed for OCSP signing.\n\tif issuer != nil {\n\t\tcertPool := x509.NewCertPool()\n\t\tcertPool.AddCert(issuer)\n\t\topts := x509.VerifyOptions{\n\t\t\tRoots:     certPool,\n\t\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},\n\t\t}\n\t\tfor _, der := range basicResponse.Certs {\n\t\t\tresponder, err := x509.ParseCertificate(der.FullBytes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"x509 parse certificate: %w\", err)\n\t\t\t}\n\t\t\tresponder.Subject.ExtraNames = responder.Subject.Names\n\t\t\tif !util.IsRDNSequenceEqual(responder.Subject.ToRDNSequence(), responderName) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, err = responder.Verify(opts); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"responder verify: %w\", err)\n\t\t\t}\n\t\t\treturn responder, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"responder not found: %s\", responderName)\n}\n\n// newCertIDRequest returns a new certID object from the provided certificate.\nfunc newCertIDRequest(cert *x509.Certificate) (id *certID, err error) {\n\t// Since certIDHash is SHA-1 then skip calculating the IssuerKeyHash\n\t// and simply use AuthorityKeyId.\n\tif len(cert.AuthorityKeyId) == 0 {\n\t\treturn nil, fmt.Errorf(\"authority key id missing\")\n\t}\n\tnameHash := sha1.Sum(cert.RawIssuer)\n\treturn &certID{\n\t\tHashAlgorithm: pkix.AlgorithmIdentifier{\n\t\t\tAlgorithm: certIDHashOID,\n\t\t},\n\t\tIssuerNameHash: nameHash[:],\n\t\tIssuerKeyHash:  cert.AuthorityKeyId,\n\t\tSerialNumber:   cert.SerialNumber,\n\t}, nil\n}\n\nfunc (c *certID) equal(other *certID) bool {\n\treturn util.AlgorithmIdentifierCmp(c.HashAlgorithm, other.HashAlgorithm) &&\n\t\tbytes.Equal(c.IssuerNameHash, other.IssuerNameHash) &&\n\t\tbytes.Equal(c.IssuerKeyHash, other.IssuerKeyHash) &&\n\t\tc.SerialNumber.Cmp(other.SerialNumber) == 0\n}\n"
  },
  {
    "path": "core/crypto/ocsp/ocsp_test.go",
    "content": "package ocsp\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst testURL = \"http://demo.sk.ee/ocsp\"\n\nvar testCertGood, testCertRevoked, testCertUnknown, testIssuer, testResponder *x509.Certificate\n\nconst (\n\tgood = iota\n\trevoked\n\tunknown\n)\n\nfunc init() {\n\tpem, err := ioutil.ReadFile(\"./testdata/TESTCERT_GOOD_12345678901-sign.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\ttestCertGood, err = util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n\tpem, err = ioutil.ReadFile(\"./testdata/TESTCERT_REVOKED_12345678902-sign.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\ttestCertRevoked, err = util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n\tpem, err = ioutil.ReadFile(\"./testdata/TESTCERT_UNKNOWN_12345678903-sign.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\ttestCertUnknown, err = util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n\tpem, err = ioutil.ReadFile(\"./testdata/intermediate.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\ttestIssuer, err = util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n\tpem, err = ioutil.ReadFile(\"./testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\ttestResponder, err = util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc TestVerifyOCSPCertificate(t *testing.T) {\n\ttype test struct {\n\t\tdescription    string\n\t\turl            string\n\t\ttestCert       *x509.Certificate\n\t\ttestIssuer     *x509.Certificate\n\t\ttestResponders []*x509.Certificate\n\t\tnonce          []byte\n\t\tstatus         int\n\t\tinvalid        bool\n\t}\n\ttests := []test{\n\t\t{\n\t\t\tdescription:    \"good certificate\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertGood,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         good,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"revoked certificate\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertRevoked,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         revoked,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"unknown certificate\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         unknown,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong certificate\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testIssuer,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         good,\n\t\t\ttestResponders: []*x509.Certificate{testIssuer},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong issuer\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testResponder,\n\t\t\tstatus:         good,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong responder\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         good,\n\t\t\ttestResponders: []*x509.Certificate{testIssuer},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong status\",\n\t\t\turl:            testURL,\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\tstatus:         good,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tinvalid:        true,\n\t\t},\n\t}\n\tctx := context.Background()\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tif tc.nonce == nil {\n\t\t\t\ttc.nonce = make([]byte, 20)\n\t\t\t\tif _, err := rand.Read(tc.nonce); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstatus, err := VerifyOCSPCertificate(ctx, tc.testCert, tc.testIssuer, tc.testResponders, tc.nonce, Conf{URL: tc.url})\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tst := getStatusInt(status)\n\t\t\tif tc.invalid && st == tc.status {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t\tif !tc.invalid && st != tc.status {\n\t\t\t\tt.Fatalf(\"bad certificate status: wanted '%d' got '%d'\", tc.status, st)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVerifyResponse(t *testing.T) {\n\ttype test struct {\n\t\tdescription    string\n\t\tderResponse    string\n\t\ttestCert       *x509.Certificate\n\t\ttestIssuer     *x509.Certificate\n\t\ttestResponders []*x509.Certificate\n\t\tnonce          []byte\n\t\tstatus         int\n\t\tinvalid        bool\n\t}\n\ttests := []test{\n\t\t{\n\t\t\tdescription:    \"good certificate\",\n\t\t\tderResponse:    \"./testdata/response-good.der\",\n\t\t\ttestCert:       testCertGood,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         good,\n\t\t\tnonce:          []byte{105, 170, 130, 62, 49, 32, 51, 136, 220, 146, 100, 163, 65, 252, 87, 24, 240, 170, 169, 47},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"revoked certificate\",\n\t\t\tderResponse:    \"./testdata/response-revoked.der\",\n\t\t\ttestCert:       testCertRevoked,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         revoked,\n\t\t\tnonce:          []byte{4, 61, 203, 172, 247, 222, 81, 169, 206, 108, 252, 160, 85, 175, 45, 255, 150, 95, 149, 160},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"unknown certificate\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         unknown,\n\t\t\tnonce:          []byte{101, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong certificate\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertGood,\n\t\t\ttestIssuer:     testResponder,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         unknown,\n\t\t\tnonce:          []byte{100, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong issuer\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testResponder,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         unknown,\n\t\t\tnonce:          []byte{100, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong responder\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testResponder,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         unknown,\n\t\t\tnonce:          []byte{100, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong status\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         good,\n\t\t\tnonce:          []byte{101, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t\tinvalid:        true,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"wrong nonce\",\n\t\t\tderResponse:    \"./testdata/response-unknown.der\",\n\t\t\ttestCert:       testCertUnknown,\n\t\t\ttestIssuer:     testIssuer,\n\t\t\ttestResponders: []*x509.Certificate{testResponder},\n\t\t\tstatus:         unknown,\n\t\t\tnonce:          []byte{100, 131, 166, 243, 5, 130, 23, 208, 40, 148, 249, 244, 79, 10, 5, 248, 117, 182, 142, 87},\n\t\t\tinvalid:        true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tbytes, err := ioutil.ReadFile(tc.derResponse)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tstatus, err := VerifyResponse(bytes, tc.testCert, tc.testIssuer, tc.testResponders, tc.nonce)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tst := getStatusInt(status)\n\t\t\tif tc.invalid && st == tc.status {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t\tif !tc.invalid && st != tc.status {\n\t\t\t\tt.Fatalf(\"bad certificate status: wanted '%d' got '%d'\", tc.status, st)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc getStatusInt(status LiveCertStatus) int {\n\tif status.Good {\n\t\treturn good\n\t}\n\tif !status.Unknown {\n\t\treturn revoked\n\t}\n\treturn unknown\n}\n"
  },
  {
    "path": "core/crypto/ocsp/testdata/TESTCERT_GOOD_12345678901-sign.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICbzCCAdCgAwIBAgIDAI1lMAoGCCqGSM49BAMCMFkxCzAJBgNVBAYTAkVFMQ0w\nCwYDVQQKDARUSVZJMRowGAYDVQQLDBFUZXN0IENlcnRpZmljYXRlczEfMB0GA1UE\nAwwWUGVyc29uIENBIEludGVybWVkaWF0ZTAgFw0yMjA1MjQxNjI3NDhaGA8yMTIy\nMDQzMDE2Mjc0OFowbzELMAkGA1UEBhMCRUUxIjAgBgNVBAMMGVRFU1RDRVJULEdP\nT0QsMTIzNDU2Nzg5MDExETAPBgNVBAQMCFRFU1RDRVJUMQ0wCwYDVQQqDARHT09E\nMRowGAYDVQQFExFQTk9FRS0xMjM0NTY3ODkwMTB2MBAGByqGSM49AgEGBSuBBAAi\nA2IABFG1Eh03W/5OzECaLcjfYdgjgjQAMRE+Tr+4ygVSv/xeU45zrjf/Acs80ESJ\nyxYuZhR2PdbqNB8ugjvC2GI6aFzk98vTWk0KjCKY5JEBSNKnDyinhMOGKBFCO6Lg\nb1aUKKNSMFAwHQYDVR0OBBYEFIzaA0RGSHoiV0HVWd9Xkv9IhekTMB8GA1UdIwQY\nMBaAFG0KRcif63bvExlLGKu4Vu1tNKqMMA4GA1UdDwEB/wQEAwIGQDAKBggqhkjO\nPQQDAgOBjAAwgYgCQgEnc90qkCKy8wD4DdvyflqIqUiGmiawjPRG6ZOGdl+NnSN0\nDzT6h3iFBfi7wjT+rgRB7PMI6up2MGPvcZT2PxfLDwJCAftNf2HAZShd6YOFupgU\nDspHDekNQD3J0UrsiebTFepp01FaAxwI/pWUUuKLNDiX7H5bvIdsWnsIMWkL3ecY\nYS/q\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/ocsp/testdata/TESTCERT_REVOKED_12345678902-sign.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICdTCCAdagAwIBAgIDAI18MAoGCCqGSM49BAMCMFkxCzAJBgNVBAYTAkVFMQ0w\nCwYDVQQKDARUSVZJMRowGAYDVQQLDBFUZXN0IENlcnRpZmljYXRlczEfMB0GA1UE\nAwwWUGVyc29uIENBIEludGVybWVkaWF0ZTAgFw0yMjA1MjQxNjI3NDhaGA8yMTIy\nMDQzMDE2Mjc0OFowdTELMAkGA1UEBhMCRUUxJTAjBgNVBAMMHFRFU1RDRVJULFJF\nVk9LRUQsMTIzNDU2Nzg5MDIxETAPBgNVBAQMCFRFU1RDRVJUMRAwDgYDVQQqDAdS\nRVZPS0VEMRowGAYDVQQFExFQTk9FRS0xMjM0NTY3ODkwMjB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABAaFP/XkwpiwEZkEB3AYMux4v+2CtucKDzMMMZiTqVkjBf42/msA\nrGfF/ntmcvIA6Fds45mFbvVNrYG77E7VmXR5FGQ54QVl5OY86AwzOJPC5kl2zT+k\n+tlQ3dO6V3BIWaNSMFAwHQYDVR0OBBYEFJV0fk/vIDZIgumXybHhLpvjlOZwMB8G\nA1UdIwQYMBaAFG0KRcif63bvExlLGKu4Vu1tNKqMMA4GA1UdDwEB/wQEAwIGQDAK\nBggqhkjOPQQDAgOBjAAwgYgCQgHK/jimFH/cWbfPctUnvv6qJ58wYhonxN5TmSCN\nnIvUMDSQ6yqVe5ZHe4akleLCPMIJxRDftfxqH+M/MNR4m2LpXAJCAcqo0Nxye2ki\nR8NaNrUa8ASKDEz7nku41L4QbYRrpsruUSyaV8T8vE0OMKiV974aPH5hwCuYGSKH\n15KefFQDyfer\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/ocsp/testdata/TESTCERT_UNKNOWN_12345678903-sign.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICdTCCAdagAwIBAgIDAI2TMAoGCCqGSM49BAMCMFkxCzAJBgNVBAYTAkVFMQ0w\nCwYDVQQKDARUSVZJMRowGAYDVQQLDBFUZXN0IENlcnRpZmljYXRlczEfMB0GA1UE\nAwwWUGVyc29uIENBIEludGVybWVkaWF0ZTAgFw0yMjA1MjQxNjI3NDhaGA8yMTIy\nMDQzMDE2Mjc0OFowdTELMAkGA1UEBhMCRUUxJTAjBgNVBAMMHFRFU1RDRVJULFVO\nS05PV04sMTIzNDU2Nzg5MDMxETAPBgNVBAQMCFRFU1RDRVJUMRAwDgYDVQQqDAdV\nTktOT1dOMRowGAYDVQQFExFQTk9FRS0xMjM0NTY3ODkwMzB2MBAGByqGSM49AgEG\nBSuBBAAiA2IABFyP7eEQSOMSPEAnhsPfpAuj9q1Ex2ywFk7jj9359uXSiFVkbH26\nHsDKWAr4qSCGlcC+Z5ourhzKFuAeTjOygG3/a90CzNJI6uNOl6HuqxcTadJxOvda\nCtvvUrafnznIBqNSMFAwHQYDVR0OBBYEFPbgime7UUrTCKOJMgaIFskT7LjKMB8G\nA1UdIwQYMBaAFG0KRcif63bvExlLGKu4Vu1tNKqMMA4GA1UdDwEB/wQEAwIGQDAK\nBggqhkjOPQQDAgOBjAAwgYgCQgHXsboGPO8ZY+uJpo7MqA2/MJ+Yt5EORfRHBlC4\nQ+GFKzxTmjg6D94oCKonTZi6yQK6fT0CqyLcDN7px2ki+3G1pgJCAcWb1MNzrq2e\nDTqoHzVdYqN64fep59rYYT1wH6DJfk67hRvgAaADs/MxghM1FZ40OMv7Og0MS+9u\n4Z+lIo/CkhEq\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/ocsp/testdata/TEST_of_SK_OCSP_RESPONDER_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzjCCA7agAwIBAgIQa7w4iGoiIOtfrn0fG/hc1zANBgkqhkiG9w0BAQUFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTEzMTIzMzM1WhcN\nMjQwNjEzMTEzMzM1WjCBgzELMAkGA1UEBhMCRUUxIjAgBgNVBAoMGUFTIFNlcnRp\nZml0c2VlcmltaXNrZXNrdXMxDTALBgNVBAsMBE9DU1AxJzAlBgNVBAMMHlRFU1Qg\nb2YgU0sgT0NTUCBSRVNQT05ERVIgMjAyMDEYMBYGCSqGSIb3DQEJARYJcGtpQHNr\nLmVlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz6U1uMvi5P6bycik\ngOFp1QdIdt2R/x/+WbRVNLNjDTMS0t70BVl6+Z7c5jqZUNIBZ5qlr3K8v5bIv0rd\nr1H/By0wFMWsWksZnQLIsb/lU+HeuSIDY2ESs0YzvZW4AB3tDrMFOrtuImmsUxhs\nz00KcRt9o+/o0RD9v5qxhJaqj6+Pr/8fZJK67Wuiqli2vVtuStaTb5zpjA1MJtu9\nOM4jk/FaL1FaST72XPTzpMVNJR/Rk63t0wL4l4f4s3y0ZI+JPzXu3jyeH+g3ZVLb\nwB2ccwgqfDPKXoxfNtcDxjUZz16OQQp2Rp14h/n8If0jyHfiNHHCDKaSPFyyJJMg\nRrQkiwIDAQABo4IBQTCCAT0wEwYDVR0lBAwwCgYIKwYBBQUHAwkwHQYDVR0OBBYE\nFIGteMcJzpGYrEl+MRkb+QpBx6XFMIGgBgNVHSAEgZgwgZUwgZIGCisGAQQBzh8D\nAQEwgYMwWAYIKwYBBQUHAgIwTB5KAEEAaQBuAHUAbAB0ACAAdABlAHMAdABpAG0A\naQBzAGUAawBzAC4AIABPAG4AbAB5ACAAZgBvAHIAIAB0AGUAcwB0AGkAbgBnAC4w\nJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2suZWUvYWphdGVtcGVsLzAfBgNVHSME\nGDAWgBS1NAqdpS8QxechDr7EsWVHGwN2/jBDBgNVHR8EPDA6MDigNqA0hjJodHRw\nczovL3d3dy5zay5lZS9yZXBvc2l0b3J5L2NybHMvdGVzdF9lZWNjcmNhLmNybDAN\nBgkqhkiG9w0BAQUFAAOCAQEAKR+ssgVTDDkGl+sLwz5OwaBMUOPEscr7DcCXmjmR\naC+KjTe8kCuXZwnMH7tMf0mDyF22USJ/o2m0MFW1k8zjH1yr1/2JghttRfi5mCvo\nMHNXVM/ST1C/6rrymaYA27RxIj201USwTQp35YvhUUIZO3Xby/60yXZyt7wCS7xA\nnH65U/0LnkT5w5DLC8EdXlH3QF600Z74fm8z54lY80IoSgIEPmFZlLe4YR822G24\nmawGRQKIbhPK2DO6sGtLZDAfee4B6TGmPcunztsYaUoc1spfCKrx5EBthieSgAp0\ndh0kMBAR/AGh7fSwl5zyASFgYmtVP4FZS6w6ETlXU7Bg3g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/ocsp/testdata/intermediate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICVjCCAbigAwIBAgIDAIv+MAoGCCqGSM49BAMCMFExCzAJBgNVBAYTAkVFMQ0w\nCwYDVQQKDARUSVZJMRowGAYDVQQLDBFUZXN0IENlcnRpZmljYXRlczEXMBUGA1UE\nAwwOUGVyc29uIENBIFJvb3QwIBcNMjIwNTI0MTYyNzQ2WhgPMjEyMjA0MzAxNjI3\nNDZaMFkxCzAJBgNVBAYTAkVFMQ0wCwYDVQQKDARUSVZJMRowGAYDVQQLDBFUZXN0\nIENlcnRpZmljYXRlczEfMB0GA1UEAwwWUGVyc29uIENBIEludGVybWVkaWF0ZTCB\nmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAHjYE0ncz4dTEbOfzbQCTdef/6+73zz/\nvOnuqo27rlZWaPDqtE9k1ZV1uLBbF4bLovNxJFrhsYJKxlPy5uLzhYxMAZyfzXN/\nipEu+ZSTkvoj1T5HM8s1+nUtsxUsf5BP1KVb9QRP/YGRXs43dKA38qtISQqZHA1S\nQne9Tf0ti15ZSXT6ozIwMDAdBgNVHQ4EFgQUbQpFyJ/rdu8TGUsYq7hW7W00qoww\nDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgOBiwAwgYcCQgHyWwxM+HMAzbk0\n64P3EFQt3KuLWXdtL9E8iaX3GQ59CmuiFC24hG1Mof2FFGjr5HskJrVlLuE1uqNx\nOcxB7PJMdQJBC7Cf/PuOVDIJ9kZHvC2kpAc8nwADa/b0zSL+9VUDszQfHro8ZJTI\nCkJNJkK0omescePDRUccJTZM7CpfUp8b8Hk=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/timestamp/conf.go",
    "content": "package timestamp\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"math/big\"\n\t\"time\"\n)\n\n// Conf contains the configurable options for the TS api.\ntype Conf struct {\n\t// URL is the address of the service to which the TS requests are sent.\n\tURL string\n\n\t// Signers are the x509 certificates used by the server to sign the token.\n\tSigners []*x509.Certificate\n\n\t// DelayTime is the maximum time that GenTime and SignTime can differ.\n\t// Defaults to 1 second.\n\tDelayTime uint64\n\n\t// Retries is the number of times a TS request is retried in case of failure.\n\t// Defaults to 2 retries.\n\tRetries uint64\n}\n\ntype tsRequest struct {\n\tVersion        int `asn1:\"default:1\"`\n\tMessageImprint messageImprint\n\tNonce          *big.Int `asn1:\"optional\"`\n\tCertReq        bool\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.1\ntype messageImprint struct {\n\tHashAlgorithm pkix.AlgorithmIdentifier\n\tHashedMessage []byte\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\ntype tsResponse struct {\n\tPKIStatus      pkiStatusInfo\n\tTimeStampToken timestampToken `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\ntype pkiStatusInfo struct {\n\tStatus       int\n\tStatusString []string       `asn1:\"optional\"`\n\tFailInfo     asn1.BitString `asn1:\"optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#section-2.4.2\n// TimestampToken ::= ContentInfo\n//     -- contentType is id-signedData\n//     -- content is SignedData\ntype timestampToken struct {\n\tRaw         asn1.RawContent\n\tContentType asn1.ObjectIdentifier\n\tContent     signedData `asn1:\"explicit,tag:0\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.1\ntype signedData struct {\n\tVersion          int\n\tDigestAlgorithms []pkix.AlgorithmIdentifier `asn1:\"set\"`\n\tEncapContentInfo encapsulatedContentInfo\n\tCertificates     []certificateChoices   `asn1:\"set,tag:0,optional\"`\n\tCrls             []pkix.CertificateList `asn1:\"set,tag:1,optional\"`\n\tSignerInfos      []signerInfo           `asn1:\"set\"`\n}\n\n//https://tools.ietf.org/html/rfc5652#section-5.2\ntype encapsulatedContentInfo struct {\n\tRawContent   asn1.RawContent\n\tEContentType asn1.ObjectIdentifier\n\tEContent     []byte `asn1:\"explicit,tag:0,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#page-8\ntype tstInfo struct {\n\tVersion        int\n\tPolicy         asn1.ObjectIdentifier\n\tMessageImprint messageImprint\n\tSerialNumber   *big.Int\n\tGenTime        time.Time\n\tAccuracy       accuracy         `asn1:\"optional\"`\n\tOrdering       bool             `asn1:\"optional\"`\n\tNonce          *big.Int         `asn1:\"optional\"`\n\tExtensions     []pkix.Extension `asn1:\"tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc3161#page-10\ntype accuracy struct {\n\tSeconds int `asn1:\"optional\"`\n\tMillis  int `asn1:\"tag:0,optional\"`\n\tMicros  int `asn1:\"tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.2\ntype certificateChoices struct {\n\tRawContent asn1.RawContent\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype signerInfo struct {\n\tVersion int\n\t// Golang doesn't support ASN.1 CHOICE, so make 2 optional fields\n\tIssuerAndSerialNumber issuerAndSerialNumber `asn1:\"optional\"`\n\tSubjectKeyIdentifier  []byte                `asn1:\"tag:0,optional\"`\n\tDigestAlgorithm       pkix.AlgorithmIdentifier\n\tSignedAttrs           []attribute `asn1:\"set,tag:0,optional\"`\n\tSignatureAlgorithm    pkix.AlgorithmIdentifier\n\tSignature             []byte\n\tUnsignedAttrs         []attribute `asn1:\"set,tag:1,optional\"`\n}\n\n// https://tools.ietf.org/html/rfc5652#section-5.3\ntype attribute struct {\n\tAttrType  asn1.ObjectIdentifier\n\tAttrValue asn1.RawValue\n}\n\n// https://tools.ietf.org/html/rfc5652#section-10.2.4\ntype issuerAndSerialNumber struct {\n\tIssuer       pkix.RDNSequence\n\tSerialNumber *big.Int\n}\n"
  },
  {
    "path": "core/crypto/timestamp/testdata/DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEgzCCA2ugAwIBAgIQcGzJsYR4QLlft+S73s/WfTANBgkqhkiG9w0BAQsFADB9\nMQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1\nczEwMC4GA1UEAwwnVEVTVCBvZiBFRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBSb290\nIENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwHhcNMjAxMTMwMjEwMDAwWhcN\nMjUxMTMwMjEwMDAwWjB/MSwwKgYDVQQDDCNERU1PIFNLIFRJTUVTVEFNUElORyBB\nVVRIT1JJVFkgMjAyMDEXMBUGA1UEYQwOTlRSRUUtMTA3NDcwMTMxDDAKBgNVBAsM\nA1RTQTEbMBkGA1UECgwSU0sgSUQgU29sdXRpb25zIEFTMQswCQYDVQQGEwJFRTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMz8yTHQyp8gzyPnKt/CQg+0\n7c/ogDl4V1SmyFGPT+lQaYZvXIKNNZyJlzII+vNnsok6hIRvAX5ffDZs8dkeNdo8\nQOuQ81QbLn5JJT2VuSppvpnqpFCiL+uWY0/nnwNmyiDueMkUDDJavbSPCkWwmW+a\nQZCNGd+krSTL/zNHCfOt7cAVDQAL9C4Ue7olufIZoDCTqRA00S8bGbTQPyTS8uUM\nEuwWc4JYZqEu4c24bIGhbKoCOSR60WrD6cBoZXLlqwDbWdkX5SLjJ9dTCxGW+pLp\nnAWx+KqJY3HkDiSZCT46JXOaoVzmcFx3l7eqQfqWgkzRZs9TJvqQSLQ+vgSAOREC\nAwEAAaOB/DCB+TAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH\nAwgwHQYDVR0OBBYEFJ8v3/rNs6jK0l3BxyVSixDYEOJHMB8GA1UdIwQYMBaAFLU0\nCp2lLxDF5yEOvsSxZUcbA3b+MIGOBggrBgEFBQcBAQSBgTB/MCEGCCsGAQUFBzAB\nhhVodHRwOi8vZGVtby5zay5lZS9haWEwWgYIKwYBBQUHMAKGTmh0dHBzOi8vd3d3\nLnNrLmVlL3VwbG9hZC9maWxlcy9URVNUX29mX0VFX0NlcnRpZmljYXRpb25fQ2Vu\ndHJlX1Jvb3RfQ0EuZGVyLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAWWkQKAbEAT77\nn8L42gw5ql7BO1fdmUgRJRRwWL9Vo9l1c50lqieR8MUToF4wpF6D0PJUx9FDcKL0\nfbURFTRuETCgGekYmCjMbVQCiv6W38vMsIdJLBWjo2oT2AjtJ2VakwkrzzSxOSBr\nF5u0hPsAkP0VkBhmW1E0DHfm1Bti2xk5t9OsJMJqfTTl8v1HXktlnxi6WdUzLBcS\ndknFePDnSYoT3xOfOz1IlB3Ta729bgglAjVBEoWyrKX4kTjZPChxseMntXaW/pN+\nAgm3Xa9hniXdK4KamzX8d8LJ+qObxmc9TXmksbWZVup0ktfJYWIHCwZjmQukAed/\npIX8UV3N9w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/crypto/timestamp/timestamp.go",
    "content": "package timestamp\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"math\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"tivi.io/core/crypto/cms\"\n\t\"tivi.io/core/crypto/util\"\n)\n\nconst (\n\t// maxAge is the maximum amount of time the response GenTime\n\t// can differ from the current time when validating the response.\n\tmaxAge = 1 * time.Minute\n\n\t// maxSkew is the maximum amount of time the response GenTime\n\t// can be set in the future, correcting system clock inconsistencies.\n\t// One second for timestamps with only a second accuracy (i.e., that do\n\t// not contain fractions of seconds in Gentime) plus one second for\n\t// the actual clock skew.\n\tmaxSkew = 2 * time.Second\n\n\t// maxResponseSize is the maximum size allowed for\n\t// the timestamping server response (10 KiB).\n\tmaxResponseSize = 10240\n)\n\nconst idSigningTime = \"1.2.840.113549.1.9.5\"\n\n// RequestTimestamp sends a timestamp request for the given data to the specified TSA server url,\n// verifying the response and returning the timestamp token.\nfunc RequestTimestamp(ctx context.Context, data, nonce []byte, conf Conf) ([]byte, error) {\n\tif len(conf.URL) == 0 {\n\t\treturn nil, fmt.Errorf(\"no TSA URL specified\")\n\t}\n\tretries := util.DefaultUint64(conf.Retries, 2)\n\ttsToken, err := sendTimestampRequest(ctx, conf.URL, data, nonce, retries)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"send timestamp request: %w\", err)\n\t}\n\tinfo, err := verifyTokenInfo(tsToken, data, nonce)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify token info: %w\", err)\n\t}\n\terr = verifyGenTime(info.GenTime, info.Accuracy)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify generation time: %w\", err)\n\t}\n\tdelay := time.Duration(util.DefaultUint64(conf.DelayTime, 1)) * time.Second\n\totherSignedAttrs := map[string]cms.AttrToVerify{\n\t\tidSigningTime: {\n\t\t\tAttributeID: idSigningTime,\n\t\t\tVerifyCallback: func(b []byte) error {\n\t\t\t\treturn verifySigningTime(b, info.GenTime, delay)\n\t\t\t}}}\n\terr = cms.VerifySignedData(tsToken.Raw, conf.Signers, nil, otherSignedAttrs, map[string]cms.AttrToVerify{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify signed data: %w\", err)\n\t}\n\treturn tsToken.Raw, nil\n}\n\n// VerifyTimestamp verifies a DER encoded timestamp response against the original data and nonce,\n// returning the genTime, if the verification succeeds.\nfunc VerifyTimestamp(derTimestamp, data, nonce []byte, conf Conf) (time.Time, error) {\n\tvar tsToken timestampToken\n\trest, err := asn1.Unmarshal(derTimestamp, &tsToken)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal timestamp token: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal timestamp token excess bytes: %d\", len(rest))\n\t}\n\tinfo, err := verifyTokenInfo(tsToken, data, nonce)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"verify token info: %w\", err)\n\t}\n\tdelay := time.Duration(util.DefaultUint64(conf.DelayTime, 1)) * time.Second\n\totherSignedAttrs := map[string]cms.AttrToVerify{\n\t\tidSigningTime: {\n\t\t\tAttributeID: idSigningTime,\n\t\t\tVerifyCallback: func(b []byte) error {\n\t\t\t\treturn verifySigningTime(b, info.GenTime, delay)\n\t\t\t}}}\n\terr = cms.VerifySignedData(tsToken.Raw, conf.Signers, nil, otherSignedAttrs, map[string]cms.AttrToVerify{})\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"verify signed data: %w\", err)\n\t}\n\treturn info.GenTime, nil\n}\n\n// GetGenTime extracts and returns the genTime from a DER encoded timestamp response.\nfunc GetGenTime(derTimestamp []byte) (time.Time, error) {\n\tvar tsToken timestampToken\n\trest, err := asn1.Unmarshal(derTimestamp, &tsToken)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal timestamp token: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal timestamp token excess bytes: %d\", len(rest))\n\t}\n\t// VerifyAll EContentType\n\tencap := tsToken.Content.EncapContentInfo\n\tif !encap.EContentType.Equal(idCTTSTInfo) {\n\t\treturn time.Time{}, fmt.Errorf(\"bad econtent type: wanted '%s' got '%s'\", idCTTSTInfo, encap.EContentType)\n\t}\n\tvar info tstInfo\n\t// ASN1Unmarshal EContent\n\trest, err = asn1.Unmarshal(encap.EContent, &info)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal econtent: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal econtent excess bytes: %d\", len(rest))\n\t}\n\treturn info.GenTime, nil\n}\n\nconst timestampReply = \"application/timestamp-reply\"\n\nvar sha256Identifier = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}\n\nfunc sendTimestampRequest(ctx context.Context, url string, data, nonce []byte, retries uint64) (timestampToken, error) {\n\t// If nonce is not provided, generate a new one\n\tif nonce == nil {\n\t\tnonce = make([]byte, 20)\n\t\tif _, err := rand.Read(nonce); err != nil {\n\t\t\treturn timestampToken{}, fmt.Errorf(\"generate nonce: %w\", err)\n\t\t}\n\t}\n\t// Hash the data\n\thash := sha256.Sum256(data)\n\treq := tsRequest{\n\t\tVersion: 1,\n\t\tMessageImprint: messageImprint{\n\t\t\tHashAlgorithm: pkix.AlgorithmIdentifier{\n\t\t\t\tAlgorithm: sha256Identifier,\n\t\t\t},\n\t\t\tHashedMessage: hash[:],\n\t\t},\n\t\tNonce:   new(big.Int).SetBytes(nonce),\n\t\tCertReq: true,\n\t}\n\t// ASN11Marshal the request\n\treqBytes, err := asn1.Marshal(req)\n\tif err != nil {\n\t\treturn timestampToken{}, fmt.Errorf(\"marshal request: %w\", err)\n\t}\n\t// Create the HTTP POST request\n\thttpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn timestampToken{}, fmt.Errorf(\"new http request: %w\", err)\n\t}\n\thttpReq.Header.Set(\"Content-Type\", \"application/timestamp-query\")\n\tvar httpRespBody []byte\n\t// Send the HTTP request\n\t// In case of (server) failure, retry the number of times specified\n\t// with Exponential Backoff sleep time in between\n\tfor tries := uint64(0); ; tries++ {\n\t\tif tries > retries {\n\t\t\treturn timestampToken{}, err\n\t\t}\n\t\tsleep := time.Duration(math.Pow(2, float64(tries))) * time.Second\n\t\tvar httpResp *http.Response\n\t\thttpResp, err = http.DefaultClient.Do(httpReq)\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"send http request: %w\", err)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tdefer func() {\n\t\t\tif closeErr := httpResp.Body.Close(); closeErr != nil && err == nil {\n\t\t\t\terr = closeErr\n\t\t\t}\n\t\t}()\n\t\tif httpResp.StatusCode != http.StatusOK {\n\t\t\terr = fmt.Errorf(\"bad http status code: wanted '%d' got '%d'\", http.StatusOK, httpResp.StatusCode)\n\t\t\tif httpResp.StatusCode/100 != 5 {\n\t\t\t\treturn timestampToken{}, err\n\t\t\t}\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tif ct := httpResp.Header.Get(\"Content-Type\"); ct != timestampReply {\n\t\t\terr = fmt.Errorf(\"bad http response content type: wanted '%s' got '%s'\", timestampReply, ct)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\thttpRespBody, err = ioutil.ReadAll(io.LimitReader(httpResp.Body, maxResponseSize))\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"http response body: %w\", err)\n\t\t\ttime.Sleep(sleep)\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\tvar resp tsResponse\n\t// ASN1Unmarshal the response\n\trest, err := asn1.Unmarshal(httpRespBody, &resp)\n\tif err != nil {\n\t\treturn timestampToken{}, fmt.Errorf(\"unmarshal response: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn timestampToken{}, fmt.Errorf(\"unmarshal response excess bytes: %d\", len(rest))\n\t}\n\tif resp.PKIStatus.Status != 0 {\n\t\treturn timestampToken{}, fmt.Errorf(\"bad timestamp status code: wanted '%d' got '%d'\", 0, resp.PKIStatus.Status)\n\t}\n\treturn resp.TimeStampToken, nil\n}\n\nconst (\n\tidSHA256 = \"2.16.840.1.101.3.4.2.1\"\n\tidSHA384 = \"2.16.840.1.101.3.4.2.2\"\n\tidSHA512 = \"2.16.840.1.101.3.4.2.3\"\n)\n\nvar (\n\tidCTTSTInfo      = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}\n\tdigestAlgorithms = map[string]crypto.Hash{\n\t\tidSHA256: crypto.SHA256,\n\t\tidSHA384: crypto.SHA384,\n\t\tidSHA512: crypto.SHA512,\n\t}\n)\n\nfunc verifyTokenInfo(tsToken timestampToken, data []byte, nonce []byte) (tstInfo, error) {\n\t// VerifyAll EContentType\n\tencap := tsToken.Content.EncapContentInfo\n\tif !encap.EContentType.Equal(idCTTSTInfo) {\n\t\treturn tstInfo{}, fmt.Errorf(\"bad econtent type: wanted '%s' got '%s'\", idCTTSTInfo, encap.EContentType)\n\t}\n\tvar info tstInfo\n\t// ASN1Unmarshal EContent\n\trest, err := asn1.Unmarshal(encap.EContent, &info)\n\tif err != nil {\n\t\treturn tstInfo{}, fmt.Errorf(\"unmarshal econtent: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn tstInfo{}, fmt.Errorf(\"unmarshal econtent excess bytes: %d\", len(rest))\n\t}\n\tif info.Version != 1 {\n\t\treturn tstInfo{}, fmt.Errorf(\"bad timestamp token info version: wanted '%d' got '%d'\", 1, info.Version)\n\t}\n\t// VerifyAll hashed data with messageImprint algorithm\n\tcHash, ok := digestAlgorithms[info.MessageImprint.HashAlgorithm.Algorithm.String()]\n\tif !ok {\n\t\treturn tstInfo{}, fmt.Errorf(\"unsupported imprint algorithm: %s\", info.MessageImprint.HashAlgorithm.Algorithm)\n\t}\n\thash := cHash.New()\n\thash.Write(data)\n\tcalculated := hash.Sum(nil)\n\tif !bytes.Equal(info.MessageImprint.HashedMessage, calculated) {\n\t\treturn tstInfo{}, fmt.Errorf(\"different hashed messages with algorithm: %s\", info.MessageImprint.HashAlgorithm.Algorithm)\n\t}\n\t// VerifyAll nonce\n\tif nonce != nil {\n\t\tif info.Nonce == nil {\n\t\t\treturn tstInfo{}, fmt.Errorf(\"no nonce returned\")\n\t\t}\n\t\tn := new(big.Int).SetBytes(nonce)\n\t\tif info.Nonce.Cmp(n) != 0 {\n\t\t\treturn tstInfo{}, fmt.Errorf(\"bad nonce returned: wanted '%s' got '%s'\", n, info.Nonce)\n\t\t}\n\t}\n\treturn info, nil\n}\n\nfunc verifyGenTime(gen time.Time, acc accuracy) error {\n\tnow := time.Now()\n\taccuracy := time.Duration(acc.Seconds)*time.Second +\n\t\ttime.Duration(acc.Millis)*time.Millisecond +\n\t\ttime.Duration(acc.Micros)*time.Microsecond\n\t// VerifyAll GenTime age\n\tif age := now.Sub(gen) + accuracy; age > maxAge {\n\t\treturn fmt.Errorf(\"generation time '%s' too old: aged '%s'\", gen, age)\n\t}\n\t// VerifyAll GenTime skew\n\tskewed := now.Add(maxSkew - accuracy)\n\tif gen.After(skewed) {\n\t\treturn fmt.Errorf(\"generation time '%s' too advanced: skewed '%s'\", gen, skewed)\n\t}\n\treturn nil\n}\n\nfunc verifySigningTime(value []byte, gen time.Time, delay time.Duration) error {\n\tvar t time.Time\n\trest, err := asn1.Unmarshal(value, &t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unmarshal signing time: %w\", err)\n\t}\n\tif len(rest) > 0 {\n\t\treturn fmt.Errorf(\"unmarshal signing time excess bytes: %d\", len(rest))\n\t}\n\tdiff := t.Sub(gen)\n\tif diff < 0 || diff > delay {\n\t\treturn fmt.Errorf(\"signing time '%s' mismatches gen time '%s'\", t, gen)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/crypto/timestamp/timestamp_test.go",
    "content": "package timestamp\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"math/rand\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"tivi.io/core/crypto/util\"\n)\n\nvar conf Conf\n\nfunc init() {\n\tpem, err := ioutil.ReadFile(\"./testdata/DEMO_SK_TIMESTAMPING_AUTHORITY_2020.pem\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"read PEM file:\", err)\n\t\tos.Exit(1)\n\t}\n\tcert, err := util.GetPEMCertificate(pem)\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"get PEM certificate: %w\", err)\n\t\tos.Exit(1)\n\t}\n\tconf.URL = \"http://demo.sk.ee/tsa/\"\n\tconf.Signers = []*x509.Certificate{cert}\n}\n\nfunc TestRequestTimestamp(t *testing.T) {\n\tctx := context.Background()\n\tfor len := 15; len < 20; len++ {\n\t\tt.Run(fmt.Sprint(len), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tdata := make([]byte, len)\n\t\t\t_, err := rand.Read(data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif _, err = RequestTimestamp(ctx, data, nil, conf); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVerifyTimestamp(t *testing.T) {\n\ttype test struct {\n\t\tdescription string\n\t\tresponse    string\n\t\tdata        string\n\t\tnonce       string\n\t\tinvalid     bool\n\t}\n\ttests := []test{\n\t\t{\n\t\t\tdescription: \"valid timestamp #1\",\n\t\t\tresponse:    \"./testdata/response_1_test\",\n\t\t\tdata:        \"./testdata/test_file_1\",\n\t\t\tnonce:       \"123456789\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid timestamp #2\",\n\t\t\tresponse:    \"./testdata/response_2_test\",\n\t\t\tdata:        \"./testdata/test_file_2\",\n\t\t\tnonce:       \"11111111111\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid timestamp #3\",\n\t\t\tresponse:    \"./testdata/response_3_test\",\n\t\t\tdata:        \"./testdata/test_file_3\",\n\t\t\tnonce:       \"987654321\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid nonce\",\n\t\t\tresponse:    \"./testdata/response_3_test\",\n\t\t\tdata:        \"./testdata/test_file_3\",\n\t\t\tnonce:       \"000000000000000\",\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid timestamp response\",\n\t\t\tresponse:    \"./testdata/response_4_test\",\n\t\t\tdata:        \"./testdata/test_file_4\",\n\t\t\tnonce:       \"987654321\",\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid timestamp data\",\n\t\t\tresponse:    \"./testdata/response_5_test\",\n\t\t\tdata:        \"./testdata/test_file_5\",\n\t\t\tnonce:       \"987654321\",\n\t\t\tinvalid:     true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tbytes, err := ioutil.ReadFile(tc.response)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdata, err := ioutil.ReadFile(tc.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tnonce := []byte(tc.nonce)\n\t\t\t_, err = VerifyTimestamp(bytes, data, nonce, conf)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetGenTime(t *testing.T) {\n\ttype test struct {\n\t\tdescription string\n\t\tresponse    string\n\t\tgenTime     time.Time\n\t\tinvalid     bool\n\t}\n\ttests := []test{\n\t\t{\n\t\t\tdescription: \"valid gen time #1\",\n\t\t\tresponse:    \"./testdata/response_1_test\",\n\t\t\tgenTime:     time.Date(2022, 04, 28, 10, 55, 45, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid gen time #2\",\n\t\t\tresponse:    \"./testdata/response_2_test\",\n\t\t\tgenTime:     time.Date(2022, 04, 28, 10, 55, 52, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tdescription: \"valid gen time #3\",\n\t\t\tresponse:    \"./testdata/response_3_test\",\n\t\t\tgenTime:     time.Date(2022, 04, 28, 10, 56, 01, 0, time.UTC),\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid timestamp response\",\n\t\t\tresponse:    \"./testdata/response_4_test\",\n\t\t\tgenTime:     time.Date(2022, 04, 29, 10, 56, 01, 0, time.UTC),\n\t\t\tinvalid:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"invalid gen time\",\n\t\t\tresponse:    \"./testdata/response_5_test\",\n\t\t\tgenTime:     time.Date(2020, 04, 29, 10, 56, 01, 0, time.UTC),\n\t\t\tinvalid:     true,\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tbytes, err := ioutil.ReadFile(tc.response)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgenTime, err := GetGenTime(bytes)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.invalid {\n\t\t\t\t\tt.Fatal(\"unexpected failure:\", err)\n\t\t\t\t}\n\t\t\t\treturn // expected failure\n\t\t\t}\n\t\t\tif tc.invalid {\n\t\t\t\tif genTime.Equal(tc.genTime) {\n\t\t\t\t\tt.Fatal(\"unexpected success\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif !genTime.Equal(tc.genTime) {\n\t\t\t\t\tt.Fatalf(\"bad genTime: wanted '%s' got '%s'\", tc.genTime, genTime)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/crypto/util/util.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\n\tcrypto2 \"tivi.io/core/crypto\"\n\tasn_1 \"tivi.io/core/crypto/asn1\"\n\t\"tivi.io/core/math/group\"\n)\n\n// GetPEMCertificate decodes and returns a single certificate from the given PEM encoded data.\nfunc GetPEMCertificate(data []byte) (*x509.Certificate, error) {\n\tdecodedCert, err := decodePEMCertificate(data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decode PEM certificate: %w\", err)\n\t}\n\n\tvar cert *x509.Certificate\n\tcert, err = x509.ParseCertificate(decodedCert)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"x509 parse certificate: %w\", err)\n\t}\n\n\treturn cert, nil\n}\n\nfunc decodePEMCertificate(encodedCert []byte) ([]byte, error) {\n\tp, rest := pem.Decode(encodedCert)\n\tif p == nil {\n\t\treturn nil, fmt.Errorf(\"no PEM certificate\")\n\t}\n\n\tif len(rest) > 0 {\n\t\treturn nil, fmt.Errorf(\"PEM trailing error\")\n\t}\n\n\tif p.Type != \"CERTIFICATE\" {\n\t\treturn nil, fmt.Errorf(\"wrong PEM block type\")\n\t}\n\n\tif len(p.Headers) > 0 {\n\t\treturn nil, fmt.Errorf(\"PEM headers error\")\n\t}\n\n\treturn p.Bytes, nil\n}\n\nfunc DefaultUint64(value, def uint64) uint64 {\n\tif value == 0 {\n\t\treturn def\n\t}\n\n\treturn value\n}\n\nfunc AlgorithmIdentifierCmp(aid1, aid2 pkix.AlgorithmIdentifier) bool {\n\tif !aid1.Algorithm.Equal(aid2.Algorithm) {\n\t\treturn false\n\t}\n\n\taid1Params := aid1.Parameters.FullBytes\n\tif len(aid1Params) == 0 {\n\t\t// If the optional Group field is empty encode it as a NULL tag with length 0\n\t\taid1Params = []byte{5, 0}\n\t}\n\n\taid2Params := aid2.Parameters.FullBytes\n\tif len(aid2Params) == 0 {\n\t\t// If the optional Group field is empty encode it as a NULL tag with length 0\n\t\taid2Params = []byte{5, 0}\n\t}\n\n\treturn bytes.Equal(aid1Params, aid2Params)\n}\n\nfunc IsRDNSequenceEqual(seq1, seq2 pkix.RDNSequence) bool {\n\tif len(seq1) != len(seq2) {\n\t\treturn false\n\t}\n\n\tfor i, a := range seq1 {\n\t\tb := seq2[i]\n\t\tif len(a) != len(b) {\n\t\t\treturn false\n\t\t}\n\n\t\tc := make(pkix.RelativeDistinguishedNameSET, len(b))\n\t\tcopy(c, b)\n\t\t// If panic was caused, it means a and b are not equal\n\t\t// because of non-comparable attribute values.\n\t\tdefer func() {\n\t\t\trecover() //nolint:errcheck\n\t\t}()\n\t\tfor len(c) > 0 {\n\t\t\tfor _, aatv := range a {\n\t\t\t\tfound := false\n\t\t\t\t// Assuming an unordered sequence\n\t\t\t\tfor j, catv := range c {\n\t\t\t\t\tif aatv.Type.Equal(catv.Type) && aatv.Value == catv.Value {\n\t\t\t\t\t\t// Delete value matched and keep searching\n\t\t\t\t\t\tc[j] = c[len(c)-1]\n\t\t\t\t\t\tc = c[:len(c)-1]\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !found {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\n// EncryptAll uses encryption public key to encrypt plaintexts.\n//\n// Identifier is optional, but if not nil, then is used to identify a set of\n// plaintexts, normally you leave it nil and identifier is set to be a\n// public key algorithm, however, when you have a distributed encryption scheme\n// then public and private key algorithm differ. That is because public key is\n// of regular encryption scheme and private key, which is in form of private key\n// shares, is of distributed encryption scheme.\nfunc EncryptAll(rand *group.Scalar, pkey crypto2.Encrypter, identifier asn1.ObjectIdentifier, plaintexts ...[]byte) ([]byte, error) {\n\tvar err error\n\tciphertextsDer := make([][]byte, len(plaintexts))\n\n\t// Encrypt all plaintexts, we will then ASN.1 compact them together\n\tfor i, plaintext := range plaintexts {\n\t\t// Encrypt using pkey implementation\n\t\tciphertextsDer[i], err = pkey.Encrypt(rand, plaintext)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to encrypt a plaintext: %v\", err)\n\t\t}\n\t}\n\n\t// ASN.1 compact ciphertexts\n\tciphertextsDerCompacted, err := asn_1.Concat(ciphertextsDer...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compact ciphertexts into ASN.1: %v\", err)\n\t}\n\n\t// If nil, then use public key algorithm identifier, otherwise use provided one\n\tif identifier == nil {\n\t\tidentifier = pkey.Parameters().Algorithm()\n\t}\n\n\tidentified, err := asn_1.AddIdentifier(identifier, ciphertextsDerCompacted)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to add OID to compacted ciphertexts: %v\", err)\n\t}\n\n\treturn identified, nil\n}\n\n// DecryptAll uses encryption private key to decrypt a ciphertext. Returns ASN.1\n// unpacked ciphertexts (split ciphertext to many) and ASN.1 decrypted values.\n//\n// Identifier is optional, but if not nil, then is used to identify a set of plaintexts,\n// normally you leave it nil and identifier is set to be a private key algorithm,\n// however, when you have a distributed encryption scheme then public and\n// private key algorithm differ. That is because public key is of regular encryption\n// scheme and private key, which is in form of private key shares, is of distributed\n// encryption scheme.\nfunc DecryptAll(key crypto2.Decrypter, identifier asn1.ObjectIdentifier, ciphertext []byte, checkDecodable bool) ([][]byte, []crypto2.Decryption, error) {\n\t// ASN.1 split ciphertext to many\n\tciphertexts, err := ParseCiphertext(key, identifier, ciphertext)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to split ASN.1 ciphertext to many: %v\", err)\n\t}\n\n\tdecryptions := make([]crypto2.Decryption, len(ciphertexts))\n\n\t// Loop over all ciphertexts and decrypt then individually\n\tfor i, cipher := range ciphertexts {\n\t\tdecryptions[i], err = key.Decrypt(cipher, checkDecodable)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to decrypt a ciphertext: %v\", err)\n\t\t}\n\t}\n\n\treturn ciphertexts, decryptions, nil\n}\n\n// ProvableDecryptAll uses encryption private key to provably decrypt a ciphertext.\n// Returns ASN.1 unpacked ciphertexts (split ciphertext to many),\n// ASN.1 decrypted values and ASN.1 proofs.\n//\n// Identifier is optional, but if not nil, then is used to identify a set of plaintexts,\n// normally you leave it nil and identifier is set to be a private key algorithm,\n// however, when you have a distributed encryption scheme then public and\n// private key algorithm differ. That is because public key is of regular encryption\n// scheme and private key, which is in form of private key shares, is of distributed\n// encryption scheme.\nfunc ProvableDecryptAll(rand *group.Scalar, key crypto2.DecryptionKey, identifier asn1.ObjectIdentifier, ciphertext, salt []byte, checkDecodable bool) ([][]byte, []crypto2.Decryption, [][]byte, error) {\n\t// ASN.1 split ciphertext to many\n\tciphertexts, err := ParseCiphertext(key, identifier, ciphertext)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"failed to split ASN.1 ciphertext to many: %v\", err)\n\t}\n\n\tdecryptions := make([]crypto2.Decryption, len(ciphertexts))\n\tproofs := make([][]byte, len(ciphertexts))\n\n\t// Loop over all ciphertexts and provably decrypt then individually\n\tfor i, cipher := range ciphertexts {\n\t\tdecryptions[i], err = key.Decrypt(cipher, checkDecodable)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, fmt.Errorf(\"failed to decrypt a ciphertext: %v\", err)\n\t\t}\n\t\tproofs[i], err = key.Prove(rand, cipher, decryptions[i], salt)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, fmt.Errorf(\"failed to prove decrypt a ciphertext: %v\", err)\n\t\t}\n\t}\n\n\treturn ciphertexts, decryptions, proofs, nil\n}\n\n// SignAll uses signing private key to sign a set of data. The set of data is\n// ASN.1 compacted, then signed.\n// Signature is ASN.1 marshalled as well. Format of a signature depends on\n// key implementation.\nfunc SignAll(rand *group.Scalar, key crypto2.Signer, data ...[]byte) ([]byte, error) {\n\t// ASN.1 pack data\n\tpacked, err := asn_1.Pack(data...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 compact data: %v\", err)\n\t}\n\n\tsignature, err := key.Sign(rand, packed)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to sign ASN.1 compacted data: %v\", err)\n\t}\n\n\t// ASN.1 signature as {OID, signature}\n\tidentified, err := asn_1.AddIdentifier(key.Parameters().Algorithm(), signature)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to add ASN.1 identifier to signed data: %v\", err)\n\t}\n\n\treturn identified, nil\n}\n\n// VerifyAll uses signing public key to verify a signature.\n// Data is the data that was used during SignAll.\nfunc VerifyAll(rand *group.Scalar, pkey crypto2.SignatureVerifier, signature []byte, data ...[]byte) error {\n\t// Remove OID identifier from a signature\n\talgorithm, signatureNoIdentifier, err := asn_1.ParseIdentifier(signature)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse ASN.1 identifier from a signature: %v\", err)\n\t}\n\n\t// Algorithm of a public key doesn't match parsed algorithm identifier\n\t// from a signature\n\tif !pkey.Parameters().Algorithm().Equal(algorithm) {\n\t\treturn fmt.Errorf(\"public key algorithm %v mismatches signature's one %v\", pkey.Parameters().Algorithm(), algorithm)\n\t}\n\n\t// ASN.1 pack data (just the same way as in SignAll)\n\tpacked, err := asn_1.Pack(data...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to ASN.1 compact data: %v\", err)\n\t}\n\n\terr = pkey.Verify(rand, signatureNoIdentifier, packed)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to verify a signature: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// ParseCiphertext returns ASN.1 split of a ciphertext after ASN.1 unpacking.\nfunc ParseCiphertext(key crypto2.KeyInfo, identifier asn1.ObjectIdentifier, ciphertext []byte) ([][]byte, error) {\n\t// Get identifier from an ASN.1 compacted ciphertext and remove identifier\n\t// from ciphertext\n\talgorithm, ciphertextNoIdentifier, err := asn_1.ParseIdentifier(ciphertext)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse ASN.1 identifier from a ciphertext: %v\", err)\n\t}\n\n\t// If nil then use private key algorithm, otherwise provided one\n\tif identifier == nil {\n\t\tidentifier = key.Parameters().Algorithm()\n\t}\n\n\t// Identifiers of private key and ciphertext should match\n\tif !identifier.Equal(algorithm) {\n\t\treturn nil, fmt.Errorf(\"key algorithm %v mismatches signature's one %v\", key.Parameters().Algorithm(), algorithm)\n\t}\n\n\t// ASN.1 split ciphertext to many\n\tciphertexts, err := asn_1.Split(ciphertextNoIdentifier)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 split ciphertext: %v\", err)\n\t}\n\n\treturn ciphertexts, nil\n}\n"
  },
  {
    "path": "core/crypto/x509/encrypted_pkcs8.go",
    "content": "package x509\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"hash\"\n\t\"strconv\"\n\n\tcrypto2 \"tivi.io/core/crypto\"\n\tasn_1 \"tivi.io/core/crypto/asn1\"\n\t\"tivi.io/core/crypto/x509/internal\"\n)\n\nconst (\n\taes256KeySize = 32\n)\n\nvar (\n\t// https://tools.ietf.org/html/rfc4231#section-3.1\n\toidHMACWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 9}\n\t// https://tools.ietf.org/html/rfc3565#section-4.1\n\toidAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42}\n)\n\nfunc aes256CBCDecrypt(encrypted, key, iv []byte) (decrypted []byte) {\n\tblock, _ := aes.NewCipher(key) // We already checked key length, ignore err.\n\tmode := cipher.NewCBCDecrypter(block, iv)\n\tdecrypted = make([]byte, len(encrypted))\n\tmode.CryptBlocks(decrypted, encrypted)\n\treturn\n}\n\n// https://tools.ietf.org/html/rfc5208#section-6\ntype encryptedPrivateKeyInfo struct {\n\tEncryptionAlgorithm pkix.AlgorithmIdentifier\n\tEncryptedData       []byte\n}\n\n// https://tools.ietf.org/html/rfc2898#appendix-A.2\nvar oidPBKDF2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 12}\n\ntype pbkdf2Params struct {\n\tSalt           []byte\n\tIterationCount int\n\tKeyLength      int `asn1:\"optional\"`\n\tPRF            pkix.AlgorithmIdentifier\n}\n\n// https://tools.ietf.org/html/rfc2898#appendix-A.4\nvar oidPBES2 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}\n\ntype pbes2Params struct {\n\tKeyDerivationFunc pkix.AlgorithmIdentifier\n\tEncryptionScheme  pkix.AlgorithmIdentifier\n}\n\n// EncryptedPKCS8Unmarshal decrypts encrypted with password and then passes the\n// result to ParsePKCS8. The encrypted data can be encoded as either DER or PEM\n// with the block type ENCRYPTED PRIVATE KEY.\n//\n// Currently only keys encrypted using PBES2 with PBKDF2 as the key derivation\n// function and AES-256-CBC as the encryption scheme are supported (see RFC\n// 2898: PKCS #5 at https://tools.ietf.org/html/rfc2898).\nfunc (pkcs8 *UnmarshallerPKCS8[T]) EncryptedPKCS8Unmarshal(encrypted, password []byte) (params crypto2.AlgorithmIdentifierParameters, key T, err error) {\n\t// Unmarshal PEM to DER bytes\n\tder, err := internal.Pem2Der(encrypted, internal.EncryptedPKCS8Header)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"PEM to DER conversion failed: %v\", err)\n\t}\n\n\t// Unlock DER bytes using provided password\n\tvar encryptedKeyInfo encryptedPrivateKeyInfo\n\tvar rest []byte\n\tif rest, err = asn1.Unmarshal(der, &encryptedKeyInfo); err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to ASN.1 unmarshal encrypted private key\")\n\t} else if len(rest) > 0 {\n\t\treturn nil, key, fmt.Errorf(\"ASN.1 unmarshalled encrypted private key left trailing bytes\")\n\t}\n\n\t// Make sure the key is encrypted using PBES2 with PBKDF2 as the key\n\t// derivation function and AES-256-CBC as the encryption scheme.\n\tencryptionOID := encryptedKeyInfo.EncryptionAlgorithm.Algorithm\n\tif !encryptionOID.Equal(oidPBES2) {\n\t\treturn nil, key, fmt.Errorf(\"unsupported encrypted private key alogirthm: %s\", encryptionOID.String())\n\t}\n\n\tvar keyParams pbes2Params\n\tif rest, err = asn1.Unmarshal(encryptedKeyInfo.EncryptionAlgorithm.Parameters.FullBytes, &params); err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to ASN.1 unmarshal encrypted private key parameters\")\n\t} else if len(rest) > 0 {\n\t\treturn nil, key, fmt.Errorf(\"ASN.1 unmarshalled encrypted private key parameters left trailing bytes\")\n\t}\n\n\tkeyDerivationOID := keyParams.KeyDerivationFunc.Algorithm\n\tif !keyDerivationOID.Equal(oidPBKDF2) {\n\t\treturn nil, key, fmt.Errorf(\"unsupported encrytion private key derivation alogirthm: %s\", keyDerivationOID.String())\n\t}\n\n\tencryptionSchemeOID := keyParams.EncryptionScheme.Algorithm\n\tif !encryptionSchemeOID.Equal(oidAES256CBC) {\n\t\treturn nil, key, fmt.Errorf(\"unsupported encrytion private key scheme: %s\", keyDerivationOID.String())\n\t}\n\n\t// Parse PBKDF2 params and ensure IterationCount is positive, KeyLength\n\t// (if present) matches AES-256 key size, and the PRF is HMAC-SHA256.\n\tvar derivationParams pbkdf2Params\n\tif rest, err = asn1.Unmarshal(keyParams.KeyDerivationFunc.Parameters.FullBytes, &derivationParams); err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to ASN.1 unmarshal encrypted private key PBKDF2 parameters\")\n\t} else if len(rest) > 0 {\n\t\treturn nil, key, fmt.Errorf(\"ASN.1 unmarshalled encrypted private key PBKDF2 parameters left trailing bytes\")\n\t}\n\n\tif count := derivationParams.IterationCount; count <= 0 {\n\t\treturn nil, key, fmt.Errorf(\"encrypted private key derivation count <= 0\")\n\t}\n\n\tif keyLength := derivationParams.KeyLength; keyLength == 0 {\n\t\tderivationParams.KeyLength = aes256KeySize\n\t} else if keyLength != aes256KeySize {\n\t\treturn nil, key, fmt.Errorf(\"expected encrypted private key length %s, but got %s\", strconv.Itoa(aes256KeySize), strconv.Itoa(keyLength))\n\t}\n\n\thmacOID := derivationParams.PRF.Algorithm\n\tif !hmacOID.Equal(oidHMACWithSHA256) {\n\t\treturn nil, key, fmt.Errorf(\"unsupported encrytion private key PRF alogirthm: %s\", keyDerivationOID.String())\n\t}\n\th := sha256.New\n\n\t// Parse AES params and ensure IV length matches AES block size and\n\t// length of the encrypted data is a multiple of it.\n\tvar encryptionParams []byte // The initialization vector.\n\tif rest, err = asn1.Unmarshal(keyParams.EncryptionScheme.Parameters.FullBytes, &encryptionParams); err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to ASN.1 unmarshal encrypted private key scheme parameters\")\n\t} else if len(rest) > 0 {\n\t\treturn nil, key, fmt.Errorf(\"ASN.1 unmarshalled encrypted private key scheme parameters left trailing bytes\")\n\t}\n\tif len(encryptionParams) != aes.BlockSize {\n\t\treturn nil, key, fmt.Errorf(\"invalid encrypted private key scheme parameters bytes length %s\", strconv.Itoa(len(encryptionParams)))\n\t}\n\tif length := len(encryptedKeyInfo.EncryptedData); length%aes.BlockSize != 0 {\n\t\treturn nil, key, fmt.Errorf(\"invalid encrypted private key data bytes length %s\", strconv.Itoa(length))\n\t}\n\n\t// Derive the encryption key and decrypt the data.\n\taesKey := pbkdf2(h, password, derivationParams.Salt,\n\t\tderivationParams.IterationCount, derivationParams.KeyLength)\n\tkeyInfo := aes256CBCDecrypt(encryptedKeyInfo.EncryptedData, aesKey, encryptionParams)\n\n\t// Strip the PKCS #5/PKCS #7 padding from keyInfo\n\t// (https://tools.ietf.org/html/rfc2315#section-10.3) and parse it.\n\t// Don't worry about creating a padding oracle, because this is used on\n\t// local files anyway.\n\tpaddingSize := int(keyInfo[len(keyInfo)-1])\n\tif paddingSize <= 0 || paddingSize > aes.BlockSize {\n\t\treturn nil, key, fmt.Errorf(\"invalid encrypted private key data padding size: %s, while max. is %s\", strconv.Itoa(paddingSize), strconv.Itoa(aes.BlockSize))\n\t}\n\tpadding := keyInfo[len(keyInfo)-paddingSize:]\n\tif !bytes.Equal(padding, bytes.Repeat([]byte{byte(paddingSize)}, paddingSize)) {\n\t\treturn nil, key, fmt.Errorf(\"invalid encrypted private key data padding: %s\", asn_1.HexN(padding, aes.BlockSize))\n\t}\n\tkeyInfo = keyInfo[:len(keyInfo)-paddingSize]\n\n\t// Once DER bytes are unlocked, unmarshal it to crypto private key\n\tparams, key, err = pkcs8.Unmarshal(keyInfo)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to ASN.1 unmarshal unlocked private key: %v\", err)\n\t}\n\treturn\n}\n\n// https://tools.ietf.org/html/rfc2898#section-5.2\nfunc pbkdf2(h func() hash.Hash, password, salt []byte, iter, keylen int) (key []byte) {\n\tprf := hmac.New(h, password)\n\thlen := prf.Size()\n\tl := (keylen + hlen - 1) / hlen // Number of blocks in keylen.\n\n\tibytes := make([]byte, 4)\n\tfor i := 1; i <= l; i++ {\n\t\tprf.Reset()\n\t\tprf.Write(salt)\n\t\tbinary.BigEndian.PutUint32(ibytes, uint32(i))\n\t\tprf.Write(ibytes)\n\t\tblock := prf.Sum(nil) // U_1.\n\t\tkey = append(key, block...)\n\n\t\tfor c := 2; c <= iter; c++ { // Repeat the PRF iter times.\n\t\t\tprf.Reset()\n\t\t\tprf.Write(block)\n\t\t\tblock = prf.Sum(nil) // U_c.\n\t\t\tfor n, u := range block {\n\t\t\t\tkey[(i-1)*hlen+n] ^= u // XOR U_c into T_i.\n\t\t\t}\n\t\t}\n\t}\n\treturn key[:keylen]\n}\n"
  },
  {
    "path": "core/crypto/x509/internal/internal.go",
    "content": "// Package internal provides utility functions to operate on ElGamal package, e.g.\n// to provide internal factory functions etc.\n// It is a private package, caller couldn't call any of the code piece found\n// in this package.\npackage internal\n\nimport (\n\t\"encoding/asn1\"\n\t\"fmt\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\t\"tivi.io/core/math/group\"\n)\n\n// NewPublicKey is a factory function that returns new crypto public key by provided\n// key object identifier. Key object identifier must be supported by crypto library\n// otherwise error.\n//\n// Currently only ElGamal, Distributed ElGamal and ECDSA-with-SHAKE256 public keys are supported.\nfunc NewPublicKey(algorithm asn1.ObjectIdentifier) (func(params crypto.AlgorithmIdentifierParameters, pub group.Element) (crypto.EncryptionKey, error), error) {\n\tswitch algorithm.String() {\n\tcase crypto.ElGamalEncryptionOID().String():\n\t\treturn func(params crypto.AlgorithmIdentifierParameters, pub group.Element) (crypto.EncryptionKey, error) {\n\t\t\tparameters, ok := params.(*elgamal.Parameters)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot cast to ElGamal public key parameters type\")\n\t\t\t}\n\n\t\t\treturn elgamal.NewPublicKey(parameters, pub), nil\n\t\t}, nil\n\tcase crypto.DistributedElGamalEncryptionOID.String():\n\t\treturn func(params crypto.AlgorithmIdentifierParameters, pub group.Element) (crypto.EncryptionKey, error) {\n\t\t\tparameters, ok := params.(*distributed.Parameters)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot cast to distributed ElGamal public key parameters type\")\n\t\t\t}\n\n\t\t\treturn distributed.NewPublicKeyShare(parameters, pub), nil\n\t\t}, nil\n\tcase crypto.EcdsaWithSHAKE256.String():\n\t\t// TODO:\n\t\treturn nil, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown public key algorithm: %s\", algorithm.String())\n\t}\n}\n\n// NewPrivateKey is a factory function that returns new crypto private key by provided\n// key object identifier. Key object identifier must be supported by crypto library\n// otherwise error.\n//\n// Currently only ElGamal, Distributed ElGamal and ECDSA-with-SHAKE256 private keys are supported.\nfunc NewPrivateKey(algorithm asn1.ObjectIdentifier) (func(params crypto.AlgorithmIdentifierParameters, priv *group.Scalar) (any, error), error) {\n\tswitch algorithm.String() {\n\tcase crypto.ElGamalEncryptionOID().String():\n\t\treturn func(params crypto.AlgorithmIdentifierParameters, priv *group.Scalar) (any, error) {\n\t\t\tparameters, ok := params.(*elgamal.Parameters)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot cast to ElGamal private key parameters type\")\n\t\t\t}\n\n\t\t\tkey, err := elgamal.NewPrivateKey(parameters, priv)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create new ElGamal private key: %v\", err)\n\t\t\t}\n\n\t\t\treturn key, nil\n\t\t}, nil\n\tcase crypto.DistributedElGamalEncryptionOID.String():\n\t\treturn func(params crypto.AlgorithmIdentifierParameters, priv *group.Scalar) (any, error) {\n\t\t\tparameters, ok := params.(*distributed.Parameters)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot cast to distributed ElGamal private key parameters type\")\n\t\t\t}\n\n\t\t\tkey, err := distributed.NewPrivateKeyShareWithSecret(parameters, priv)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to create new distributed ElGamal private key: %v\", err)\n\t\t\t}\n\n\t\t\treturn key, nil\n\t\t}, nil\n\tcase crypto.EcdsaWithSHAKE256.String():\n\t\t// TODO:\n\t\treturn nil, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown private key algorithm: %s\", algorithm.String())\n\t}\n}\n"
  },
  {
    "path": "core/crypto/x509/internal/pem.go",
    "content": "package internal\n\nimport (\n\t\"bytes\"\n\tpem2 \"encoding/pem\"\n\t\"fmt\"\n)\n\nconst (\n\tX509Header           = \"PUBLIC KEY\"\n\tPKCS8Header          = \"PRIVATE KEY\"\n\tEncryptedPKCS8Header = \"ENCRYPTED PRIVATE KEY\"\n)\n\n// Der2Pem unmarshalls DER to PEM.\n//\n// header must be valid PEM header.\nfunc Der2Pem(der []byte, header string) ([]byte, error) {\n\tvar b bytes.Buffer\n\terr := pem2.Encode(&b, &pem2.Block{\n\t\tType:  header,\n\t\tBytes: der,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to convert DER to PEM: %v\", err)\n\t}\n\n\treturn b.Bytes(), err\n}\n\n// Pem2Der unmarshalls PEM to DER. If pem is DER bytes, nothing is done.\n// Also ensures that PEM header from pem matches provided header.\nfunc Pem2Der(pem []byte, header string) ([]byte, error) {\n\t// If block == nil, then input is DER\n\tblock, der := pem2.Decode(pem)\n\tif block != nil {\n\t\tif block.Type != header {\n\t\t\treturn nil, fmt.Errorf(\"expected PEM header %s, but got %s\", header, block.Type)\n\t\t}\n\n\t\t// No trailing bytes allowed\n\t\tif len(der) != 0 {\n\t\t\treturn nil, fmt.Errorf(\"trailing bytes left after PEM decoding\")\n\t\t}\n\n\t\tder = block.Bytes\n\t}\n\n\treturn der, nil\n}\n"
  },
  {
    "path": "core/crypto/x509/internal/pki.go",
    "content": "package internal\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n)\n\nconst (\n\tPkcs8Version = 0\n)\n\n// https://tools.ietf.org/html/rfc5208#section-5\ntype PrivateKeyInfo struct {\n\tVersion             int\n\tPrivateKeyAlgorithm pkix.AlgorithmIdentifier\n\tPrivateKey          []byte\n\t// Attributes are not used for this PrivateKeyInfo\n}\n\n// https://tools.ietf.org/html/rfc5280#section-4.1.2.7\ntype SubjectPublicKeyInfo struct {\n\tAlgorithm        pkix.AlgorithmIdentifier\n\tSubjectPublicKey asn1.BitString\n}\n\n// Pki is a generic public Key Infrastructure interface which wraps x509\n// (SubjectPublicKeyInfo) and PKCS8 (PrivateKeyInfo) standards.\ntype Pki interface {\n\tSubjectPublicKeyInfo | PrivateKeyInfo\n}\n\n// Der2Pki unmarshalls DER into either SubjectPublicKeyInfo or\n// PrivateKeyInfo depending on T type.\nfunc Der2Pki[T Pki](der []byte) (pki T, err error) {\n\t_, err = asn1.Unmarshal(der, &pki)\n\tif err != nil {\n\t\treturn pki, fmt.Errorf(\"failed to ASN.1 unmarshal PKI: %v\", err)\n\t}\n\n\treturn\n}\n\n// Pki2Der marshals either SubjectPublicKeyInfo or PrivateKeyInfo, depending on T type,\n// into DER.\nfunc Pki2Der[T Pki](pki T) (der []byte, err error) {\n\tder, err = asn1.Marshal(pki)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to ASN.1 marshal PKI: %v\", err)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/crypto/x509/marshal/marshal.go",
    "content": "// Package marshal contains marshalling and unmarshalling interfaces that client\n// should implement in order to be able to marshal/unmarshal custom x509/PKCS8\n// keys from/into crypto key object.\n//\n// These interfaces allow caller to have custom formatted x509/PKCS8 key's:\n//\n// a) parameters\n//\n// b) key material (element)\n//\n// Crypto key object is a central unit that allows caller to fully utilize crypto\n// library interfaces.\npackage marshal\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/math/group\"\n)\n\n// KeyMarshaller is an interface that allows a caller to marshal crypto key into\n// its own, custom formatted, x509/PKCS8 key.\ntype KeyMarshaller interface {\n\t// MarshalKeyParameters marshals crypto key parameters into PKIX parameters.\n\tMarshalKeyParameters(crypto.AlgorithmIdentifierParameters) (pkix.AlgorithmIdentifier, error)\n\n\t// MarshalKeyElement marshals crypto key element into a SubjectPublicKey (x509)\n\t// or PrivateKey (PKCS8).\n\tMarshalKeyElement(crypto.KeyInfo) (asn_1.DER, error)\n}\n\n// KeyUnmarshaller is an interface that allows a caller to unmarshal its own,\n// custom formatted, X509/PKCS8 key into a crypto key object.\n//\n// T is either group.Element for x509 or *group.Scalar for PKCS8, any other types\n// will result in error, as are not supported by crypto library.\ntype KeyUnmarshaller[T any] interface {\n\t// UnmarshalKeyParameters unmarshalls PKIX parameters into crypto key parameters.\n\tUnmarshalKeyParameters(pkix.AlgorithmIdentifier) (asn1.ObjectIdentifier, crypto.AlgorithmIdentifierParameters, error)\n\n\t// UnmarshalKeyElement unmarshalls SubjectPublicKey (x509) or PrivateKey (PKCS8)\n\t// into a crypto key element.\n\tUnmarshalKeyElement(group.Group, asn_1.DER) (T, error)\n}\n"
  },
  {
    "path": "core/crypto/x509/pkcs8.go",
    "content": "package x509\n\nimport (\n\t\"fmt\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/x509/internal\"\n\t\"tivi.io/core/crypto/x509/marshal\"\n\t\"tivi.io/core/math/group\"\n)\n\n// UnmarshallerPKCS8 is an PKCS8 key decoding crypto instance. Any PKCS8 decoding\n// from PKCS8 to crypto private key are done via this instance.\ntype UnmarshallerPKCS8[T any] struct {\n\t// unmarshaller is the implementation of marshal.KeyUnmarshaller interface,\n\t// i.e. custom PKCS8 parameters' and private key material parser.\n\tunmarshaller marshal.KeyUnmarshaller[*group.Scalar]\n}\n\n// NewUnmarshallerPKCS8 creates new PKCS8 key decoder to crypto private key instance.\nfunc NewUnmarshallerPKCS8[T any](unmarshaller marshal.KeyUnmarshaller[*group.Scalar]) *UnmarshallerPKCS8[T] {\n\treturn &UnmarshallerPKCS8[T]{unmarshaller: unmarshaller}\n}\n\n// Unmarshal unmarshalls PEM/DER to crypto private key.\nfunc (pkcs8 *UnmarshallerPKCS8[T]) Unmarshal(data []byte) (params crypto.AlgorithmIdentifierParameters, key T, err error) {\n\t// Unmarshal PEM or DER to DER\n\tder, err := internal.Pem2Der(data, internal.PKCS8Header)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"cannot PEM/DER decode PKCS8 key to DER: %v\", err)\n\t}\n\n\t// Unmarshal DER to PrivateKeyInfo\n\tpki, err := internal.Der2Pki[internal.PrivateKeyInfo](der)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"cannot decode DER to PKCS8 PrivateKeyInfo: %v\", err)\n\t}\n\n\t// Unmarshal PKCS8 AlgorithmIdentifier parameters\n\toid, params, err := pkcs8.unmarshaller.UnmarshalKeyParameters(pki.PrivateKeyAlgorithm)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"cannot unmarshal PrivateKeyAlgorithm parameters: %v\", err)\n\t}\n\n\t// Unmarshal PKCS8 PrivateKey, i.e. PKCS8 key material\n\tprivateKey, err := pkcs8.unmarshaller.UnmarshalKeyElement(params.Group(), pki.PrivateKey)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"cannot unmarshal PrivateKey material: %v\", err)\n\t}\n\n\t// Fetch crypto private key unmarshaller by OID returned by PKCS8 parameters parser\n\tprivateKeyUnmarshaller, err := internal.NewPrivateKey(oid)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to fetch crypto private key unmarshaller: %v\", err)\n\t}\n\n\tnewPrivateKey, err := privateKeyUnmarshaller(params, privateKey)\n\tif err != nil {\n\t\treturn nil, key, fmt.Errorf(\"failed to create new crypto private key: %v\", err)\n\t}\n\n\t// Check that created crypto private key can be cast to caller provided T type\n\tvar ok bool\n\tkey, ok = newPrivateKey.(T)\n\tif !ok {\n\t\treturn nil, key, fmt.Errorf(\"failed to cast crypto private key type to caller's provided T type\")\n\t}\n\n\treturn\n}\n\n// MarshallerPKCS8 is an PKCS8 key encoding crypto instance. Any PKCS8 encoding\n// to PKCS8 from crypto private key are done via this instance.\ntype MarshallerPKCS8 struct {\n\t// marshaller is the implementation of marshal.KeyMarshaller interface,\n\t// i.e. custom PKCS8 parameters' and private key material encoder.\n\tmarshaller marshal.KeyMarshaller\n}\n\n// NewMarshallerPKCS8 creates new PKCS8 key encoder from crypto private key instance\n// to PKCS8.\nfunc NewMarshallerPKCS8(marshaller marshal.KeyMarshaller) MarshallerPKCS8 {\n\treturn MarshallerPKCS8{marshaller: marshaller}\n}\n\n// Marshal marshals crypto private key to PEM.\nfunc (pkcs8 MarshallerPKCS8) Marshal(key crypto.KeyInfo) (pem PEM, err error) {\n\tder, err := pkcs8.MarshalDER(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal PKCS8 to DER: %v\", err)\n\t}\n\n\tpem, err = internal.Der2Pem(der, internal.PKCS8Header)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal PKCS8 DER key to PEM: %v\", err)\n\t}\n\n\treturn\n}\n\n// MarshalDER marshals crypto private key to DER.\nfunc (pkcs8 MarshallerPKCS8) MarshalDER(key crypto.KeyInfo) (der asn_1.DER, err error) {\n\t// Marshal crypto private key parameters\n\talgorithmIdentifier, err := pkcs8.marshaller.MarshalKeyParameters(key.Parameters())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal crypto private key parameters to PKCS8 parameters: %v\", err)\n\t}\n\n\t// Marshal crypto private key element\n\tpriv, err := pkcs8.marshaller.MarshalKeyElement(key) //keyInfo.Marshal()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal crypto private key material: %v\", err)\n\t}\n\n\t// Crypto private key to PrivateKeyInfo\n\tpki := internal.PrivateKeyInfo{\n\t\tVersion:             internal.Pkcs8Version,\n\t\tPrivateKeyAlgorithm: algorithmIdentifier,\n\t\tPrivateKey:          priv,\n\t}\n\n\tder, err = internal.Pki2Der[internal.PrivateKeyInfo](pki)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal PKCS8 to DER: %v\", err)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/crypto/x509/x509.go",
    "content": "// Package x509 implements x509 and PKCS8 parsing.\npackage x509\n\nimport (\n\t\"encoding/asn1\"\n\t\"fmt\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/x509/internal\"\n\t\"tivi.io/core/crypto/x509/marshal\"\n\t\"tivi.io/core/math/group\"\n)\n\n// PEM is a format of base-64 serialized x509/PKCS8 keys with included headers.\ntype PEM []byte\n\n// Unmarshaller is a x509 key decoding crypto instance. Any x509 decoding\n// from x509 to crypto public key are done via this instance.\ntype Unmarshaller[T any] struct {\n\t// unmarshaller is the implementation of marshal.KeyUnmarshaller interface,\n\t// i.e. custom x509 parameters' and public key material parser.\n\tunmarshaller marshal.KeyUnmarshaller[group.Element]\n}\n\n// NewUnmarshaller creates new x509 key decoder to crypto public key instance.\nfunc NewUnmarshaller[T any](unmarshaller marshal.KeyUnmarshaller[group.Element]) *Unmarshaller[T] {\n\treturn &Unmarshaller[T]{unmarshaller: unmarshaller}\n}\n\n// Unmarshal unmarshalls PEM/DER to crypto public key.\nfunc (x509 *Unmarshaller[T]) Unmarshal(data []byte) (params crypto.AlgorithmIdentifierParameters, pkey T, err error) {\n\t// Unmarshal PEM or DER to DER\n\tder, err := internal.Pem2Der(data, internal.X509Header)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"cannot PEM/DER decode x509 key to DER: %v\", err)\n\t}\n\n\t// Unmarshal DER to SubjectPublicKeyInfo\n\tspki, err := internal.Der2Pki[internal.SubjectPublicKeyInfo](der)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"cannot decode DER x509 to SubjectPublicKeyInfo format: %v\", err)\n\t}\n\n\t// Unmarshal x509 AlgorithmIdentifier parameters\n\toid, params, err := x509.unmarshaller.UnmarshalKeyParameters(spki.Algorithm)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"cannot unmarshal Algorithm parameters: %v\", err)\n\t}\n\n\t// Unmarshal x509 SubjectPublicKey, i.e. x509 key material\n\tpub, err := x509.unmarshaller.UnmarshalKeyElement(params.Group(), spki.SubjectPublicKey.Bytes)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"cannot unmarshal SubjectPublicKey material: %v\", err)\n\t}\n\n\t// Create new crypto public key\n\tpublicKeyUnmarshaller, err := internal.NewPublicKey(oid)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"failed to fetch crypto public key unmarshaller: %v\", err)\n\t}\n\n\tnewPublicKey, err := publicKeyUnmarshaller(params, pub)\n\tif err != nil {\n\t\treturn nil, pkey, fmt.Errorf(\"failed to create new crypto public key: %v\", err)\n\t}\n\n\t// Check that created crypto public key can be cast to caller provided T type\n\tvar ok bool\n\tpkey, ok = newPublicKey.(T)\n\tif !ok {\n\t\treturn nil, pkey, fmt.Errorf(\"failed to cast crypto public key type to caller's provided T type\")\n\t}\n\n\treturn\n}\n\n// Marshaller is a x509 key encoding crypto instance. Any x509 encoding\n// to x509 from crypto private key are done via this instance.\ntype Marshaller struct {\n\t// marshaller is the implementation of marshal.KeyMarshaller interface,\n\t// i.e. custom x509 parameters' and public key material encoder.\n\tmarshaller marshal.KeyMarshaller\n}\n\n// NewMarshaller creates new x509 key encoder from crypto public key instance\n// to x509.\nfunc NewMarshaller(marshaller marshal.KeyMarshaller) Marshaller {\n\treturn Marshaller{marshaller: marshaller}\n}\n\n// Marshal marshals crypto public key to PEM.\nfunc (x509 Marshaller) Marshal(keyInfo crypto.KeyInfo) (pem PEM, err error) {\n\tder, err := x509.MarshalDER(keyInfo)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal x509 to DER: %v\", err)\n\t}\n\n\tpem, err = internal.Der2Pem(der, internal.X509Header)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal x509 DER key to PEM: %v\", err)\n\t}\n\n\treturn\n}\n\n// MarshalDER marshals crypto public key to DER.\nfunc (x509 Marshaller) MarshalDER(key crypto.KeyInfo) (der asn_1.DER, err error) {\n\t// Marshal crypto public key parameters\n\talgorithmIdentifier, err := x509.marshaller.MarshalKeyParameters(key.Parameters())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal crypto public key parameters to PKCS8 parameters: %v\", err)\n\t}\n\n\t// Marshal crypto public key element\n\tpub, err := x509.marshaller.MarshalKeyElement(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal crypto public key element: %v\", err)\n\t}\n\n\t// Crypto public key to SubjectPublicKeyInfo\n\tspki := internal.SubjectPublicKeyInfo{\n\t\tAlgorithm: algorithmIdentifier,\n\t\tSubjectPublicKey: asn1.BitString{\n\t\t\tBytes:     pub,\n\t\t\tBitLength: len(pub) * 8,\n\t\t},\n\t}\n\n\tder, err = internal.Pki2Der[internal.SubjectPublicKeyInfo](spki)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal x509 to DER: %v\", err)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/crypto/x509/x509_test.go",
    "content": "package x509\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"tivi.io/core/crypto\"\n\t\"tivi.io/core/crypto/elgamal\"\n\t\"tivi.io/core/crypto/elgamal/distributed\"\n\tx509d \"tivi.io/core/crypto/elgamal/distributed/x509\"\n\t\"tivi.io/core/crypto/elgamal/homomorphic\"\n\tx509e \"tivi.io/core/crypto/elgamal/x509\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nvar (\n\tx509DefaultUnmarshaller = NewUnmarshaller[crypto.EncryptionKey](x509e.NewUnmarshaller())\n\tx509DefaultMarshaller   = NewMarshaller(x509e.NewMarshaller())\n\n\tpkcs8DefaultUnmarshaller = NewUnmarshallerPKCS8[crypto.DecryptionKey](x509e.NewUnmarshallerPKCS8())\n\tpkcs8DefaultMarshaller   = NewMarshallerPKCS8(x509e.NewMarshallerPKCS8())\n\n\tx509DistributedUnmarshaller = NewUnmarshaller[crypto.EncryptionKey](x509d.NewUnmarshaller())\n\tx509DistributedMarshaller   = NewMarshaller(x509e.NewMarshaller())\n\n\tpkcs8DistributedUnmarshaller = NewUnmarshallerPKCS8[crypto.DecryptionKey](x509d.NewUnmarshallerPKCS8())\n\tpkcs8DistributedMarshaller   = NewMarshallerPKCS8(x509e.NewMarshallerPKCS8())\n)\n\nfunc storePEMPKCS8PrivateKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn pkcs8DefaultMarshaller.Marshal(key)\n}\nfunc storePEMPKCS8PrivateKeyShare(key crypto.KeyInfo) ([]byte, error) {\n\treturn pkcs8DistributedMarshaller.Marshal(key)\n}\nfunc storeDERPKCS8PrivateKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn pkcs8DefaultMarshaller.MarshalDER(key)\n}\nfunc storeDERPKCS8PrivateKeyShare(key crypto.KeyInfo) ([]byte, error) {\n\treturn pkcs8DistributedMarshaller.MarshalDER(key)\n}\nfunc storePEMX509PublicKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509DefaultMarshaller.Marshal(key)\n}\n\nfunc storePEMX509PublicKeyShare(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509DistributedMarshaller.Marshal(key)\n}\n\nfunc storeDERX509PublicKey(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509DefaultMarshaller.MarshalDER(key)\n}\n\nfunc storeDERX509PublicKeyShare(key crypto.KeyInfo) ([]byte, error) {\n\treturn x509DistributedMarshaller.MarshalDER(key)\n}\n\n// encrypt a plaintext.\nfunc encrypt(encrypter crypto.Encrypter, plaintext []byte, rand *group.Scalar) ([]byte, error) {\n\treturn encrypter.Encrypt(rand, plaintext)\n}\n\n// decrypt a ciphertext.\nfunc decrypt(decrypter crypto.Decrypter, ciphertext []byte) (crypto.Decryption, error) {\n\treturn decrypter.Decrypt(ciphertext)\n}\n\nfunc sign(signer crypto.Signer, data []byte) ([]byte, error) {\n\trnd, err := group.RandomScalar(signer.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn signer.Sign(rnd, data)\n}\n\nfunc verifySignature(verifier crypto.SignatureVerifier, signature, data []byte) error {\n\trnd, err := group.RandomScalar(verifier.Parameters().Group().Order())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn verifier.Verify(rnd, signature, data)\n}\n\n// provableDecrypt a ciphertext with a proof of a correct decryption provided.\nfunc provableDecrypt(rand *group.Scalar, provableDecrypter crypto.ProvableDecrypter, ciphertext, extra []byte) (crypto.Decryption, []byte, error) {\n\treturn provableDecrypter.ProvableDecrypt(rand, ciphertext, extra)\n}\n\n// verify decryption proof.\nfunc verify(verifier crypto.EncryptionVerifier, proof, extra []byte) error {\n\treturn verifier.Verify(proof, extra)\n}\n\n// encryptHomomorphic ciphertext (marks) homomorphically.\nfunc encryptHomomorphic(encrypter crypto.HomomorphicEncrypter, pubkey crypto.EncryptionKey, marks []bool) ([]byte, error) {\n\treturn encrypter.Encrypt(pubkey, marks)\n}\n\n// aggregateHomomorphic sums up all choices into a single choice.\nfunc aggregateHomomorphic(aggregator crypto.HomomorphicAggregator, pk crypto.EncryptionKey, batchSize int, choices ...[]byte) ([]byte, error) {\n\t// Aggregate choices in parallel\n\twg := new(sync.WaitGroup)\n\tlock := new(sync.Mutex)\n\tvar aggregated []byte\n\n\t// Use homomorphic property of each choice to aggregate them together\n\tfor i := 0; i < len(choices); i += batchSize {\n\t\tend := i + batchSize\n\t\tif end > len(choices) {\n\t\t\tend = len(choices)\n\t\t}\n\t\tbatch := choices[i:end]\n\n\t\t// Each goroutine call will be recorded. Using wg.Wait() will be awaited\n\t\twg.Add(1)\n\n\t\t// Use goroutines to process each batch in parallel\n\t\tgo func() {\n\t\t\t// Aggregate batches (use as large batch as you can), when we aggregate\n\t\t\t// raw batch, we also wish to verify that each choice is correct\n\t\t\taggregatedBatch, err := aggregator.Aggregate(pk, true, batch...)\n\t\t\tif err != nil {\n\t\t\t\t// Log error and return\n\t\t\t\tpanic(err) // Don't ever panic in production!\n\t\t\t}\n\n\t\t\t// Aggregation of a final aggregated result should be done in a\n\t\t\t// synchronized manner, i.e. we, indeed, can produce independent\n\t\t\t// batches, but summing them all up into a single variable is a\n\t\t\t// synchronized process.\n\t\t\tgo func() {\n\t\t\t\tlock.Lock()\n\t\t\t\tdefer lock.Unlock()\n\n\t\t\t\t// Aggregate aggregated batches, withVerify is false, because\n\t\t\t\t// range proof verification therefore fails (we have already\n\t\t\t\t// aggregated choices here)\n\t\t\t\taggregated = append(aggregated, aggregatedBatch...)\n\t\t\t\taggregated, err = aggregator.Aggregate(pk, false, aggregated)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// Log error and return\n\t\t\t\t\tpanic(err) // Don't ever panic in production!\n\t\t\t\t}\n\n\t\t\t\t// Mark goroutine as finished (indicates that batch has been\n\t\t\t\t// aggregated and verified)\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t}()\n\t}\n\n\t// Block and wait for all unfinished goroutines (code didn't reach\n\t// wg.Done() statement)\n\twg.Wait()\n\n\treturn aggregated, nil\n}\n\n// tallyHomomorphic decrypts aggregated choice and presents voting results.\nfunc tallyHomomorphic(tallier crypto.HomomorphicTallier, privkey []crypto.DecryptionKey, pubkey []crypto.EncryptionKey, aggregated, extra []byte, maxCount int) ([]crypto.HomomorphicDecryption, error) {\n\treturn tallier.Tally(privkey, pubkey, aggregated, maxCount, extra)\n}\n\n// verifyHomomorphic verifies decrypted choice proof.\nfunc verifyHomomorphic(verifier crypto.HomomorphicVerifier, pubkey crypto.EncryptionKey, proof, extra []byte) error {\n\treturn verifier.Verify(pubkey, proof, extra)\n}\n\n// TestEncryptionPrivateKey simulates ElGamal private key encryption from a\n// perspective of a caller.\nfunc TestEncryptionPrivateKey(t *testing.T) {\n\tfor _, registeredGroup := range group.All() {\n\t\tt.Run(string(registeredGroup.Name()), func(t *testing.T) {\n\t\t\t// To generate encryption private key, user should provide:\n\t\t\t//\ta) Group name\n\t\t\t// With provided group name user can obtain a registered Group\n\t\t\t// implementation\n\t\t\tname := string(registeredGroup.Name())\n\t\t\tg, err := group.Get(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now user decides, which encryption scheme to use:\n\t\t\t//\ta) ElGamal\n\t\t\t//\tb) Paillier\n\t\t\t//\tc) RSA\n\t\t\t// Since TIVI Core only implements ElGamal - it will be our choice\n\t\t\tr, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tparams := elgamal.NewParameters(g)\n\t\t\tprivKey, err := elgamal.NewPrivateKey(params, r)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal keys have been generated, but before we pass it\n\t\t\t// around, we should cast them to the interface\n\t\t\tvar privKeyI crypto.DecryptionKey = privKey\n\t\t\tpubKeyI := privKeyI.EncryptionKey()\n\n\t\t\t// Simulate storing keys in some other place in I/O, this is\n\t\t\t// exactly what we do, when keys are generated\n\t\t\tprivKeyBytes, err := storePEMPKCS8PrivateKey(privKeyI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tpubKeyBytes, err := storePEMX509PublicKey(pubKeyI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now it is time to encrypt some data, for that, we need\n\t\t\t// public key, which is currently X509 bytes.\n\t\t\t//\n\t\t\t// Note, that X509 unmarshalling doesn't care about public key\n\t\t\t// implementation, even scheme... It is just X509 bytes unmarshalling\n\t\t\t_, pubKeyRestoredI, err := x509DefaultUnmarshaller.Unmarshal(pubKeyBytes) //(pubKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now, let's encrypt some data. Note that we pass around public\n\t\t\t// key interface, not the implementation\n\t\t\tplaintext := []byte(\"Hello World!\")\n\t\t\trand, err := group.RandomScalar(pubKeyRestoredI.Parameters().Group().Order())\n\t\t\tciphertext, err := encrypt(pubKeyRestoredI, plaintext, rand)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// We have our plaintext encrypted for now, next step is to decrypt\n\t\t\t// our ciphertext to prove that decryption matches the initial\n\t\t\t// plaintext\n\t\t\t//\n\t\t\t// But before we do it, obviously we need a private key, let's get it\n\t\t\t// from PKCS8, note again, that private key is an interface\n\t\t\t_, privKeyRestoredI, err := pkcs8DefaultUnmarshaller.Unmarshal(privKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Note, again, we pass interface only. Let's decrypt a ciphertext\n\t\t\tdecrypted, err := decrypt(privKeyRestoredI, ciphertext)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Let's check that initial plaintext matches decrypted one\n\t\t\tplaintext2, err := decrypted.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Compare plaintexts\n\t\t\tif !bytes.Equal(plaintext, plaintext2) {\n\t\t\t\tt.Fatal(\"Initial plaintext != decrypted plaintext\")\n\t\t\t}\n\n\t\t\t// Let's do the decryption, but now with proofs (to verify them\n\t\t\t// later using public key, i.e. auditing)\n\t\t\t//\n\t\t\t// We will use extra bytes, in order to add kind of identifier to the\n\t\t\t// decrypted proof (extra bytes are used in HASH creation for a proof),\n\t\t\t// we also call it \"to add a salt into a hash\".\n\t\t\t//\n\t\t\t// Note again, only private key interface is passed in\n\t\t\textra := []byte(\"Just some extra bytes\")\n\t\t\trand, err = group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdecrypted2, proof, err := provableDecrypt(rand, privKeyRestoredI, ciphertext, extra)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Let's do quick check that plaintexts match\n\t\t\tplaintext3, err := decrypted2.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Compare plaintexts again\n\t\t\tif !bytes.Equal(plaintext, plaintext3) {\n\t\t\t\tt.Fatal(\"Initial plaintext != decrypted with proof\")\n\t\t\t}\n\n\t\t\t// Note that in ProvableDecryptAll we used extra bytes - that means\n\t\t\t// that for the Verify, same extra bytes should be used as well\n\t\t\terr = verify(pubKeyRestoredI, proof, extra)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// / TestDistributedEncryptionPrivateKey simulates distributed ElGamal private key\n// encryption from a perspective of a caller.\nfunc TestDistributedEncryptionPrivateKey(t *testing.T) {\n\tfor _, registeredGroup := range group.All() {\n\t\tt.Run(string(registeredGroup.Name()), func(t *testing.T) {\n\t\t\t// To generate distributed encryption private key, user should provide:\n\t\t\t//\ta) Group name\n\t\t\t//\tb) threshold amount\n\t\t\t//\tc) Parties amount\n\t\t\tvar parties uint64 = 5\n\t\t\tvar threshold uint64 = 3\n\n\t\t\t// With provided group name user can obtain a registered Group\n\t\t\t// implementation\n\t\t\tname := string(registeredGroup.Name())\n\t\t\tg, err := group.Get(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now user decides, which distributed encryption scheme to use:\n\t\t\t//\ta) ElGamal\n\t\t\t//\tb) Paillier\n\t\t\t//\tc) RSA\n\t\t\t// Since TIVI Core only implements distributed ElGamal - it will be our choice\n\t\t\tprivKeyShares, pubKey, err := distributed.NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal key shares has been generated, but before we pass it\n\t\t\t// around, we should cast it to the interface\n\t\t\tprivKeySharesI := make([]crypto.DecryptionKey, len(privKeyShares))\n\t\t\tfor i, privKeyShare := range privKeyShares {\n\t\t\t\tprivKeySharesI[i] = privKeyShare\n\t\t\t}\n\n\t\t\t// Simulate storing key shares in some other place in I/O, this is\n\t\t\t// exactly what we do, when key shares are generated\n\t\t\tprivKeySharesBytes := make([][]byte, len(privKeySharesI))\n\t\t\tpubKeySharesBytes := make([][]byte, len(privKeySharesI))\n\t\t\tvar privKeyShareI crypto.DecryptionKey\n\t\t\tvar pubKeyShareI crypto.EncryptionKey\n\n\t\t\tfor i, privKeyShare := range privKeyShares {\n\t\t\t\t// Only interface is passed in\n\t\t\t\tprivKeyShareI = privKeyShare\n\t\t\t\tprivKeySharesBytes[i], err = storePEMPKCS8PrivateKeyShare(privKeyShareI)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\t// Only interface is passed in\n\t\t\t\tpubKeyShareI = privKeyShare.EncryptionKey()\n\t\t\t\tpubKeySharesBytes[i], err = storeDERX509PublicKeyShare(pubKeyShareI)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Interface\n\t\t\tvar pubKeyI crypto.EncryptionKey = pubKey\n\t\t\tpubKeyBytes, err := storePEMX509PublicKey(pubKeyI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now it is time to encrypt some data, for that, we need distributed\n\t\t\t// public key, which is currently X509 bytes.\n\t\t\t//\n\t\t\t// Note, that X509 unmarshalling doesn't care about distributed public\n\t\t\t// key implementation, even scheme... It is just X509 bytes unmarshalling\n\t\t\t_, pubKeyRestoredI, err := x509DefaultUnmarshaller.Unmarshal(pubKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now, let's encrypt some data\n\t\t\tplaintext := []byte(\"Hello World!\")\n\t\t\trand, err := group.RandomScalar(pubKeyRestoredI.Parameters().Group().Order())\n\t\t\tciphertext, err := encrypt(pubKeyRestoredI, plaintext, rand)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// We have our plaintext encrypted for now, next step is to decrypt\n\t\t\t// our ciphertext to prove that decryption matches the initial\n\t\t\t// plaintext\n\t\t\t//\n\t\t\t// But before we do it, obviously we need private key shares,\n\t\t\t// let's get them from PKCS8\n\t\t\tprivKeySharesRestoredI := make([]crypto.DecryptionKey, threshold)\n\t\t\tfor i, privKeyShareBytes := range privKeySharesBytes[:threshold] {\n\t\t\t\t_, privKeySharesRestoredI[i], err = pkcs8DistributedUnmarshaller.Unmarshal(privKeyShareBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// NB! Please note, that during decryption/verification period\n\t\t\t// we only use exactly threshold amount of key shares (we can use\n\t\t\t// more shares than a threshold, but not less)\n\n\t\t\t// Distributed encryption will decrypt ciphertext by parts (part == share),\n\t\t\t// each part is interface as well\n\t\t\tdecryptedI := make([]crypto.Decryption, len(privKeySharesRestoredI))\n\n\t\t\t// decryption parts of a ciphertext, before combined, should be\n\t\t\t// ASN1 marshalled, in order to allow many implementations (regular\n\t\t\t// or distributed) to format decrypted parts as they want\n\t\t\tdecryptedBytes := make([][]byte, len(privKeySharesRestoredI))\n\t\t\tfor i, privKeyShareRestoredI := range privKeySharesRestoredI {\n\t\t\t\tdecryptedI[i], err = decrypt(privKeyShareRestoredI, ciphertext)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecryptedBytes[i], err = decryptedI[i].Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Distributed ElGamal concrete type to create a decryption shares\n\t\t\t// combiner\n\t\t\tdecryptedCombiner := distributed.NewDecryptionShareCombiner(g, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t// Cast to interface\n\t\t\tvar decryptedCombinerI crypto.DecryptionSharesCombiner = decryptedCombiner\n\n\t\t\t// decryption parts are combined to present a meaningful decrypted value.\n\t\t\t// This value is of interface type as well\n\t\t\tdecryptedCombinedI, err := decryptedCombinerI.DecryptionSharesCombine(ciphertext, decryptedBytes...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Let's check that initial plaintext matches decrypted one\n\t\t\tplaintext2, err := decryptedCombinedI.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Compare plaintexts\n\t\t\tif !bytes.Equal(plaintext, plaintext2) {\n\t\t\t\tt.Fatal(\"Initial plaintext != decrypted plaintext\")\n\t\t\t}\n\n\t\t\t// Let's do the decryption, but now with proofs (to verify them later,\n\t\t\t// i.e. auditing)\n\t\t\t//\n\t\t\t// We will use extra bytes, in order to add kind of identifier to the\n\t\t\t// decrypted proof (extra bytes are used in HASH creation for a proof)\n\t\t\textra := []byte(\"Just some extra bytes\")\n\t\t\tproofsBytes := make([][]byte, len(privKeySharesRestoredI))\n\t\t\tdecrypted2I := make([]crypto.Decryption, len(privKeySharesRestoredI))\n\t\t\tdecrypted2Bytes := make([][]byte, len(privKeySharesRestoredI))\n\t\t\tfor i, privKeyShareRestoredI := range privKeySharesRestoredI {\n\t\t\t\trr, err := group.RandomScalar(g.Order())\n\t\t\t\tdecrypted2I[i], proofsBytes[i], err = provableDecrypt(rr, privKeyShareRestoredI, ciphertext, extra)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tdecrypted2Bytes[i], err = decrypted2I[i].Marshal()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdecrypted2CombinedI, err := decryptedCombinerI.DecryptionSharesCombine(ciphertext, decrypted2Bytes...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Let's do quick check that plaintexts matches\n\t\t\tplaintext3, err := decrypted2CombinedI.Plaintext()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Compare plaintexts again\n\t\t\tif !bytes.Equal(plaintext, plaintext3) {\n\t\t\t\tt.Fatal(\"Initial plaintext != decrypted with proof\")\n\t\t\t}\n\n\t\t\t// Now, since we don't want to verify proofs straight away, and\n\t\t\t// instead we store them in I/O for later auditing (that is how we\n\t\t\t// actually do in production, since auditors process info after\n\t\t\t// the decryption is done)\n\n\t\t\t// Simulating proof storage in I/O (here we don't do anything, since\n\t\t\t// proof is already in form of []byte, so we could just os.Write())\n\n\t\t\t// At this point, all voting results are known and auditor wish\n\t\t\t// to process proof, and for that reason he needs a public key shares,\n\t\t\t// to verify each decrypted part's proof\n\t\t\tpubKeySharesRestoredI := make([]crypto.EncryptionKey, threshold)\n\t\t\tfor i, pubKeyShareBytes := range pubKeySharesBytes[:threshold] {\n\t\t\t\t_, pubKeySharesRestoredI[i], err = x509DistributedUnmarshaller.Unmarshal(pubKeyShareBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Note that during ProvableDecryptAll we used extra bytes - that means\n\t\t\t// that for the VerifyAll, same extra bytes should be used as well\n\t\t\tfor i, proofBytes := range proofsBytes {\n\t\t\t\terr = verify(pubKeySharesRestoredI[i], proofBytes, extra)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// That's it! All main e2e workflow is done!\n\t\t})\n\t}\n}\n\n// TestHomomorphicEncryptionPrivateKey simulates homomorphic ElGamal private key\n// encryption from a perspective of a caller.\nfunc TestHomomorphicEncryptionPrivateKey(t *testing.T) {\n\tfor _, registeredGroup := range group.All() {\n\t\tt.Run(string(registeredGroup.Name()), func(t *testing.T) {\n\t\t\t// Even though we work with homomorphic encryption, first of all,\n\t\t\t// we still have to generate our encryption private key.\n\n\t\t\t// To generate encryption private key, user should provide:\n\t\t\t//\ta) Group name\n\t\t\t// With provided group name user can obtain a registered Group\n\t\t\t// implementation\n\t\t\tname := string(registeredGroup.Name())\n\t\t\tg, err := group.Get(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now user decides, which encryption scheme to use:\n\t\t\t//\ta) ElGamal\n\t\t\t//\tb) Paillier\n\t\t\t//\tc) RSA\n\t\t\t// Since TIVI Core only implements ElGamal - it will be our choice\n\t\t\tr, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tparams := elgamal.NewParameters(g)\n\t\t\tprivKey, err := elgamal.NewPrivateKey(params, r)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal private key has been generated, and before we pass it\n\t\t\t// around, we should cast it to the interface\n\t\t\tvar privKeyI crypto.DecryptionKey = privKey\n\t\t\tprivKeyBytes, err := storeDERPKCS8PrivateKey(privKeyI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tpubKeyBytes, err := storePEMX509PublicKey(privKeyI.EncryptionKey())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now it is time to encrypt some data, for that, we need ElGamal\n\t\t\t// public key, which is currently X509 bytes.\n\t\t\t//\n\t\t\t// Note, that X509 unmarshalling doesn't care about public key\n\t\t\t// implementation, even scheme... It is just X509 bytes unmarshalling\n\t\t\t_, pubKeyRestoredI, err := x509DefaultUnmarshaller.Unmarshal(pubKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now, let's encrypt some data, suppose we have PREDEFINED candidate\n\t\t\t// list as {John, Milana, Mike}, user has made his choice. NB! We are\n\t\t\t// only allowed to choose one candidate. So this time, we have chosen\n\t\t\t// Milana.\n\t\t\tvoterChoice := []bool{false, true, false}\n\n\t\t\t// Homomorphic ElGamal concrete implementation\n\t\t\thomomorphicEncryption := homomorphic.NewElGamalHomomorphicEncryption(g, 0)\n\t\t\t// Homomorphic encryption interface, which suits both for distributed\n\t\t\t// and regular encryption schemes\n\t\t\tvar homomorphicEncrypter crypto.HomomorphicEncrypter = homomorphicEncryption\n\n\t\t\t// Encrypt homomorphically. In terms of homomorphic encryption\n\t\t\t// ciphertext == choice\n\t\t\tchoice, err := encryptHomomorphic(homomorphicEncrypter, pubKeyRestoredI, voterChoice)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// We have our choice encrypted for now, next step is to decrypt\n\t\t\t// our choice to prove that after decryption matches the initial\n\t\t\t// choice\n\t\t\t//\n\t\t\t// But before we do it, obviously we need a private key, let's get it\n\t\t\t// from PKCS8\n\t\t\t_, privKeyRestoredI, err := pkcs8DefaultUnmarshaller.Unmarshal(privKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Homomorphic encryption has an amazing property - we can sum up\n\t\t\t// all choices made by all voters without decrypting each choice,\n\t\t\t// and therefore as a result of aggregation we will get a single\n\t\t\t// choice. But please note, that this choice is still encrypted,\n\t\t\t// BUT aggregated. To decrypt a final choice we use tally.\n\t\t\tvar homomorphicAggregator crypto.HomomorphicAggregator = homomorphicEncryption\n\t\t\taggregated, err := aggregateHomomorphic(homomorphicAggregator, pubKeyRestoredI, 1000, choice)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// All voter choices are summed up into a single choice, so in\n\t\t\t// out example that single choice will have exactly 3 candidates\n\t\t\t// ({John, Milana, Mike}) and the sum of all votes for them. To\n\t\t\t// obtain decrypted voting results we use tally\n\t\t\tvar homomorphicTallier crypto.HomomorphicTallier = homomorphicEncryption\n\t\t\ttallies, err := tallyHomomorphic(homomorphicTallier, []crypto.DecryptionKey{privKeyRestoredI}, []crypto.EncryptionKey{pubKeyRestoredI}, aggregated, nil, 3)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// marks = {false, true, false} --> {-, Milana, -}\n\t\t\tmarksInt := []uint64{0, 1, 0}\n\t\t\tfor i, count := range marksInt {\n\t\t\t\tif tallies[i].Count() != count {\n\t\t\t\t\tt.Fatalf(\"Mark nr.%d count expected %d, got %d\", i, count, tallies[i].Count())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// In our example, a choice has 3 marks exactly. So, when we\n\t\t\t// tallied results each choice will have 3 decryption proofs (one\n\t\t\t// per mark). And if we have distributed encryption, then a ciphertext\n\t\t\t// is partially decrypted by amount of private key shares, so each choice\n\t\t\t// will have (threshold * len(marks)).\n\t\t\tvar homomorphicVerifier crypto.HomomorphicVerifier = homomorphicEncryption\n\t\t\t// Each tally is a decrypted mark with a proof (regular encryption)\n\t\t\t// or proofs (distributed encryption, i.e. each private share decrypts\n\t\t\t// a mark independently)\n\t\t\t//\n\t\t\t// Note, that how we can make this verification all in parallel,\n\t\t\t// just the same way we did with aggregation\n\t\t\twg := new(sync.WaitGroup)\n\t\t\twg2 := new(sync.WaitGroup)\n\t\t\tfor _, tally := range tallies {\n\t\t\t\twg.Add(1)\n\n\t\t\t\tgo func(tally crypto.HomomorphicDecryption) {\n\t\t\t\t\ttallyProofs, err := tally.Proofs()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t\t// Log error and return from that particular goroutine\n\t\t\t\t\t\tpanic(err) // Don't ever panic in production like that\n\t\t\t\t\t}\n\n\t\t\t\t\t// For regular encryption there will only be a single proof, but\n\t\t\t\t\t// for distributed there will be threshold amount of proofs, so\n\t\t\t\t\t// we verify all of them\n\t\t\t\t\tfor _, proof := range tallyProofs {\n\t\t\t\t\t\twg2.Add(1)\n\t\t\t\t\t\tgo func(proof []byte) {\n\t\t\t\t\t\t\terr = verifyHomomorphic(homomorphicVerifier, pubKeyRestoredI, proof, nil)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\twg.Done()\n\t\t\t\t\t\t\t\twg2.Done()\n\t\t\t\t\t\t\t\t// Log error and return from that particular goroutine\n\t\t\t\t\t\t\t\tpanic(err) // Don't ever panic in production like that\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\twg2.Done()\n\t\t\t\t\t\t}(proof)\n\t\t\t\t\t}\n\t\t\t\t\twg.Done()\n\t\t\t\t}(tally)\n\t\t\t}\n\n\t\t\t// Wait all goroutines that verify proofs\n\t\t\twg2.Wait()\n\t\t\t// Wait all goroutines that spread proofs for verification to wg2\n\t\t\twg.Wait()\n\n\t\t\t// That's it! All main e2e workflow is done!\n\t\t})\n\t}\n}\n\n// TestDistributedHomomorphicEncryptionPrivateKey simulates homomorphic distributed\n// ElGamal private key encryption from a perspective of a caller.\nfunc TestDistributedHomomorphicEncryptionPrivateKey(t *testing.T) {\n\tfor _, registeredGroup := range group.All() {\n\t\tt.Run(string(registeredGroup.Name()), func(t *testing.T) {\n\t\t\t// Even though we work with homomorphic encryption, first of all,\n\t\t\t// we still have to generate our distributed encryption private key.\n\n\t\t\t// To generate distributed encryption private key, user should provide:\n\t\t\t//\ta) Group name\n\t\t\t//\tb) threshold amount\n\t\t\t//\tc) Parties amount\n\t\t\t// With provided group name user can obtain a registered Group\n\t\t\t// implementation\n\t\t\tname := string(registeredGroup.Name())\n\t\t\tg, err := group.Get(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tvar threshold uint64 = 3\n\t\t\tvar parties uint64 = 3\n\n\t\t\t// Now user decides, which distributed encryption scheme to use:\n\t\t\t//\ta) ElGamal\n\t\t\t//\tb) Paillier\n\t\t\t//\tc) RSA\n\t\t\t// Since TIVI Core only implements ElGamal - it will be our choice\n\t\t\tprivKeyShares, pubKey, err := distributed.NewPrivateKeyShares(g, parties, threshold)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// ElGamal key shares has been generated, but before we pass it\n\t\t\t// around, we should cast it to the interface\n\t\t\tprivKeySharesI := make([]crypto.DecryptionKey, len(privKeyShares))\n\t\t\tfor i, privKeyShare := range privKeyShares {\n\t\t\t\tprivKeySharesI[i] = privKeyShare\n\t\t\t}\n\n\t\t\t// Simulate storing private and public key shares in some other place\n\t\t\t// in a code, this is exactly what we do, when keys are generated\n\t\t\tprivKeySharesBytes := make([][]byte, len(privKeySharesI))\n\t\t\tpubKeySharesBytes := make([][]byte, len(privKeySharesI))\n\t\t\tvar privKeyShareI crypto.DecryptionKey\n\t\t\tvar pubKeyShareI crypto.EncryptionKey\n\n\t\t\tfor i, privKeyShare := range privKeyShares {\n\t\t\t\t// Only interface is passed in\n\t\t\t\tprivKeyShareI = privKeyShare\n\t\t\t\tprivKeySharesBytes[i], err = storePEMPKCS8PrivateKey(privKeyShareI)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\t// Only interface is passed in\n\t\t\t\tpubKeyShareI = privKeyShare.EncryptionKey()\n\t\t\t\tpubKeySharesBytes[i], err = storeDERX509PublicKey(pubKeyShareI)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Interface\n\t\t\tvar pubKeyI crypto.EncryptionKey = pubKey\n\t\t\tpubKeyBytes, err := x509DefaultMarshaller.Marshal(pubKeyI)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now it is time to encrypt some data, for that, we need ElGamal\n\t\t\t// public key, which is currently X509 bytes.\n\t\t\t//\n\t\t\t// Note, that X509 unmarshalling doesn't care about public key\n\t\t\t// implementation, even scheme... It is just X509 bytes unmarshalling\n\t\t\t_, pubKeyRestoredI, err := x509DefaultUnmarshaller.Unmarshal(pubKeyBytes)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Now, let's encrypt some data, suppose we have PREDEFINED candidate\n\t\t\t// list as {John, Milana, Mike}, user has made his choice. NB! We are\n\t\t\t// only allowed to choose one candidate. So this time, we have chosen\n\t\t\t// Milana.\n\t\t\tvoterChoice := []bool{false, true, false}\n\n\t\t\t// Homomorphic ElGamal concrete implementation\n\t\t\thomomorphicEncryption := homomorphic.NewElGamalHomomorphicEncryption(g, threshold)\n\t\t\t// Homomorphic encryption interface, which suits both for distributed\n\t\t\t// and regular encryption schemes\n\t\t\tvar homomorphicEncrypter crypto.HomomorphicEncrypter = homomorphicEncryption\n\n\t\t\t// Encrypt homomorphically. In terms of homomorphic encryption\n\t\t\t// ciphertext == choice\n\t\t\tchoice, err := encryptHomomorphic(homomorphicEncrypter, pubKeyRestoredI, voterChoice)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// Homomorphic encryption has an amazing property - we can sum up\n\t\t\t// all choices made by all voters without decrypting each choice,\n\t\t\t// and therefore as a result of aggregation we will get a single\n\t\t\t// choice. But please note, that this choice is still encrypted,\n\t\t\t// BUT aggregated. To decrypt a final choice we use tally.\n\t\t\tvar homomorphicAggregator crypto.HomomorphicAggregator = homomorphicEncryption\n\t\t\taggregated, err := aggregateHomomorphic(homomorphicAggregator, pubKeyRestoredI, 1000, choice)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// We have our choice encrypted for now, next step is to decrypt\n\t\t\t// our choice to prove that after decryption matches the initial\n\t\t\t// choice\n\t\t\t//\n\t\t\t// But before we do it, obviously we need distributed private key shares,\n\t\t\t// let's get them from PKCS8\n\t\t\tprivKeySharesRestoredI := make([]crypto.DecryptionKey, threshold)\n\t\t\tfor i, privKeyShareBytes := range privKeySharesBytes[:threshold] {\n\t\t\t\t_, privKeySharesRestoredI[i], err = pkcs8DistributedUnmarshaller.Unmarshal(privKeyShareBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tpubKeySharesRestoredI := make([]crypto.EncryptionKey, threshold)\n\t\t\tfor i, pubKeyShareBytes := range pubKeySharesBytes[:threshold] {\n\t\t\t\t_, pubKeySharesRestoredI[i], err = x509DistributedUnmarshaller.Unmarshal(pubKeyShareBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// All voter choices are summed up into a single choice, so in\n\t\t\t// out example that single choice will have exactly 3 candidates\n\t\t\t// ({John, Milana, Mike}) and the sum of all votes for them. To\n\t\t\t// obtain decrypted voting results we use tally\n\t\t\tvar homomorphicTallier crypto.HomomorphicTallier = homomorphicEncryption\n\t\t\ttallies, err := tallyHomomorphic(homomorphicTallier, privKeySharesRestoredI, pubKeySharesRestoredI, aggregated, nil, 3)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// marks = {false, true, false} --> {-, Milana, -}\n\t\t\tmarksInt := []uint64{0, 1, 0}\n\t\t\tfor i, count := range marksInt {\n\t\t\t\tif tallies[i].Count() != count {\n\t\t\t\t\tt.Fatalf(\"Mark nr.%d count expected %d, got %d\", i, count, tallies[i].Count())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// In our example, a choice has 3 marks exactly. So, when we\n\t\t\t// tallied results each choice will have 3 decryption proofs (one\n\t\t\t// per mark). And if we have distributed encryption, then a ciphertext\n\t\t\t// is partially decrypted by amount of private key shares, so each choice\n\t\t\t// will have (threshold * len(marks)).\n\t\t\tvar homomorphicVerifier crypto.HomomorphicVerifier = homomorphicEncryption\n\t\t\t// Each tally is a decrypted mark with a proof (regular encryption)\n\t\t\t// or proofs (distributed encryption, i.e. each private share decrypts\n\t\t\t// a mark independently)\n\t\t\t//\n\t\t\t// Note, that how we can make this verification all in parallel,\n\t\t\t// just the same way we did with aggregation\n\t\t\twg := new(sync.WaitGroup)\n\t\t\twg2 := new(sync.WaitGroup)\n\t\t\tfor _, tally := range tallies {\n\t\t\t\twg.Add(1)\n\n\t\t\t\tgo func(tally crypto.HomomorphicDecryption) {\n\t\t\t\t\ttallyProofs, err := tally.Proofs()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t\t// Log error and return from that particular goroutine\n\t\t\t\t\t\tpanic(err) // Don't ever panic in production like that\n\t\t\t\t\t}\n\n\t\t\t\t\t// For regular encryption there will only be a single proof, but\n\t\t\t\t\t// for distributed there will be threshold amount of proofs, so\n\t\t\t\t\t// we verify all of them\n\t\t\t\t\tfor i, pubKeyShareRestoredI := range pubKeySharesRestoredI {\n\t\t\t\t\t\twg2.Add(1)\n\t\t\t\t\t\tgo func(proof []byte, pubKeyShareRestoredI crypto.EncryptionKey) {\n\t\t\t\t\t\t\terr = verifyHomomorphic(homomorphicVerifier, pubKeyShareRestoredI, proof, nil)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\twg.Done()\n\t\t\t\t\t\t\t\twg2.Done()\n\t\t\t\t\t\t\t\t// Log error and return from that particular goroutine\n\t\t\t\t\t\t\t\tpanic(err) // Don't ever panic in production like that\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\twg2.Done()\n\t\t\t\t\t\t}(tallyProofs[i], pubKeyShareRestoredI)\n\t\t\t\t\t}\n\t\t\t\t\twg.Done()\n\t\t\t\t}(tally)\n\t\t\t}\n\n\t\t\t// Wait all goroutines that verify proofs\n\t\t\twg2.Wait()\n\t\t\t// Wait all goroutines that spread proofs for verification to wg2\n\t\t\twg.Wait()\n\n\t\t\t// That's it! All main e2e workflow is done!\n\t\t})\n\t}\n}\n\n//\tfunc TestSignAndVerify(t *testing.T) {\n//\t\tdata := []byte(\"Hello World!\")\n//\t\tfor _, g := range group.All() {\n//\t\t\t// Skip non-NIST groups\n//\t\t\tif !strings.Contains(string(g.Name()), \"NIST\") {\n//\t\t\t\tcontinue\n//\t\t\t}\n//\n//\t\t\tt.Run(string(g.Name()), func(t *testing.T) {\n//\t\t\t\t// Generate new ECDSA private key with only Group given, btw,\n//\t\t\t\t// we derive public key from a private\n//\t\t\t\tpriv, err := ecdsa.New(g)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\t\t\t\t// Cast to interface\n//\t\t\t\tvar privI crypto.SigningPrivateKey = priv\n//\t\t\t\tvar pubI = priv.EncryptionKey()\n//\n//\t\t\t\t// Since generated keys is likely to be marshalled after creation (and\n//\t\t\t\t// later reused, on unmarshal), we simulate storing in I/O\n//\t\t\t\tprivBytes, err := storePEMPKCS8PrivateKey(privI)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\t\t\t\tif strings.Contains(string(g.Name()), \"mod\") {\n//\t\t\t\t\tos.WriteFile(fmt.Sprintf(\"pub %v\", string(g.Name())), privBytes, 0777)\n//\t\t\t\t}\n//\t\t\t\tpubBytes, err := storePEMX509PublicKey(pubI)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\t\t\t\tif strings.Contains(string(g.Name()), \"mod\") {\n//\t\t\t\t\tos.WriteFile(string(g.Name()), privBytes, 0777)\n//\t\t\t\t}\n//\t\t\t\t// Simulate reading private key from I/O to concrete implementation\n//\t\t\t\t// first\n//\t\t\t\t_, privIRestored, err := DecodePKCS8PrivateKey[crypto.SigningPrivateKey](privBytes)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\n//\t\t\t\t// SignAll custom data\n//\t\t\t\tsigned, err := sign(privIRestored, data)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\n//\t\t\t\t// Simulate reading public key from I/O to concrete implementation\n//\t\t\t\t// first\n//\t\t\t\t_, pubIRestored, err := Unmarshal[crypto.SigningPublicKey](pubBytes)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\n//\t\t\t\t// VerifyAll signature\n//\t\t\t\terr = verify(pubIRestored, signed, data)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\t\t\t})\n//\t\t}\n//\t}\n//\n//\tfunc TestSignAndVerifyManyData(t *testing.T) {\n//\t\tdata1 := []byte(\"Hello World!\")\n//\t\tdata2 := []byte(\"I'm Alice\")\n//\t\tdata3 := []byte(\"Good weather\")\n//\t\tdata4 := []byte(\"Happy coding :)\")\n//\t\tdataSet := [][]byte{data1, data2, data3, data4}\n//\n//\t\tfor _, g := range group.All() {\n//\t\t\t// Skip non-NIST groups\n//\t\t\tif !strings.Contains(string(g.Name()), \"NIST\") {\n//\t\t\t\tcontinue\n//\t\t\t}\n//\t\t\tt.Run(string(g.Name()), func(t *testing.T) {\n//\t\t\t\t// Generate new ECDSA signing key\n//\t\t\t\tvar signingKeyI crypto.SigningPrivateKey\n//\t\t\t\tsigningKeyI, err := ecdsa.New(g)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\n//\t\t\t\t// SignAll many data\n//\t\t\t\tsignature, err := crypto.SignAll(signingKeyI, dataSet...)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\n//\t\t\t\t// VerifyAll many data\n//\t\t\t\terr = crypto.VerifyAll(signingKeyI.EncryptionKey(), signature, dataSet...)\n//\t\t\t\tif err != nil {\n//\t\t\t\t\tt.Fatal(err)\n//\t\t\t\t}\n//\t\t\t})\n//\t\t}\n//\t}\n//var base64ModP3072IVXVPublicKey = \"MIIDOTCCAagGCSsGAQQBl1UCATCCAZkCggGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaWbWcMNU5KvJgE8XRsCMoYIXwykF5GLjbOO+OedywYDoYDmyeDouwHoo+1xV3wb0xSyd4ry/aVWBcYOZVJfOqVauUV0iYYmPoFEBVyjlqKqsQtrTMXDQRQejOoVSGr3xy6ZOz7hQRY2+8KiupxV10GDH2zlw+FpuHkx6v1rozbCTPXHoyU4EolYZ3O49ImGtLua/Ev+gbZighk2HYCcz7IamRSHysYF3sgDLvhF1d6YV1sdwmIwLrZRuII4k+gdOWrMUPbW/zg/RCOS4LRIKk60sr//////////wIBAhsPVGVzdCBFbGVjdGlvbiAxA4IBiQAwggGEAoIBgEBFmDtb138D0DqHgzmmGJVgxKDr0IZUxWvURIbdcD/E2HlE19DwhY5HfvLco7c7S5atwDPKg900AKgzmoKi27YQmVSVzgdrvUZDUFUuB2exiy/HEXnrPEnB16STUeBPTCqnW3vWZuJePMBGDztnAkQPRIINu075bsAyQklXzkyyIpenn8FJRqNUl7GiwTkFYUwJdwngI/6nVMd3YnfmJxiofYiAy+vd8C8jR3e6to7/lGZ+VfvDnWgC8myF8OqwXgeMzqFWC+pESX9czYHI3b2gtwQFWfZXZ+MoYNwEoMg7nbU8MENGuPcW3ZeZ5jJDc2Dm6ycJXVpJgn/+tdZHp/TDa5RgQXEvokAIk+4aHz49MZA8vKx8oFxUlUOx7OWm+4vvPwvm7W249yhLOYmvT69qoTt2TxAT4fDr7Dl10HogbuXFELnlAt2gQMTebkrpMDUc74diG/mxnVEaW1LxrY/GF+1IrPmxfzo1ll3N6eZ0IhwU6XwGYSzfWiTHF3QDAQ==\"\n//var base64ModP3072IVXVPrivateKey = \"MIIDNwIBADCCAagGCSsGAQQBl1UCATCCAZkCggGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaWbWcMNU5KvJgE8XRsCMoYIXwykF5GLjbOO+OedywYDoYDmyeDouwHoo+1xV3wb0xSyd4ry/aVWBcYOZVJfOqVauUV0iYYmPoFEBVyjlqKqsQtrTMXDQRQejOoVSGr3xy6ZOz7hQRY2+8KiupxV10GDH2zlw+FpuHkx6v1rozbCTPXHoyU4EolYZ3O49ImGtLua/Ev+gbZighk2HYCcz7IamRSHysYF3sgDLvhF1d6YV1sdwmIwLrZRuII4k+gdOWrMUPbW/zg/RCOS4LRIKk60sr//////////wIBAhsPVGVzdCBFbGVjdGlvbiAxBIIBhASCAYBsSMVGGEoPLwC559hxFXZIuiHFpwFbEYm/0o6lZYuIS1e2hrPRcVJ0v0eUA41RB5giaMf7M/kBaq/c6QGJ6UVWWX7Xhnzk7xMwwBmfnDXzhfkKbMZzlrGASFsK6KcrroYEEFWkW6DYsG3NaWTV6hgXTAzisgC7WWvDtqAxF0ZTMg81MJj1yVgHklQ6rTPXfHBrFSi6XNKPSNNU0H66qICSnWcuwpUsILgq2vPHzjTcfjbr/PyRButIO0LrHlEInqK1uLaHyl1I6GWsvoJfFbm16rSLTFeRGTDTxjamwLI2PgEO5zN6KF2EVcmOoHEeTVguN4958VavDUDrK/sQalXwZrC6BoiIcbLgZF0xmm4EN9tzCk5uKBds6GSbDjkgCJsu8evnZBL6ces7VujW41E5q+OmZesWsJ99+ejuhT+vYbIz6B35q0/pUonYzNt/f4bBCS3AIhJCiCh/dnx5ekwsQCaAxUg7KYa5XI9a1mr9FpdmGCjqpZ48qvxLcUbW5D8=\"\n//\n//var base64NistP384IVXVPublicKey = \"MIGPMCUGCSsGAQQBho0fATAYGwVQLTM4NBsPVGVzdCBFbGVjdGlvbiAxA2YAMGMEYQR7W65Z9VYdHrA3zC5wuXhQnD2nfuvTNRNJrrp4kDJ2U0/+tzptXuN3J/4heawE8eLvoKnV6IfOf+qKCfnoV3qByCdd7FCmYrxo0EYvgHz006BGWN/UvSYNETdcmS8MFWI=\"\n//var base64NistP384IVXVPrivateKey = \"MGECAQAwJQYJKwYBBAGGjR8BMBgbBVAtMzg0Gw9UZXN0IEVsZWN0aW9uIDEENQQzAjEAtsRtj3M7c4hln67tQCVZnoOYebz1jge3VxCMbmE1cJAC3WXYAy54rELm/ltbqiXW\"\n//\n//type IVXVAlgorithmIdentifier struct {\n//\talgorithm  asn1.ObjectIdentifier\n//\tparameters ModPGroupParameters\n//}\n//\n//type ModPGroupParameters struct {\n//\tP          *big.Int\n//\tg          *big.Int\n//\tElectionID string\n//}\n//\n//type ModPGroupKey struct {\n//\tKey big.Int\n//}\n//\n//type IVXVPkcs8Unmarshaller[T group.Scalar] struct{}\n//\n//func NewIVXVPkcs8Unmarshaller[T group.Scalar]() IVXVPkcs8Unmarshaller[T] {\n//\treturn IVXVPkcs8Unmarshaller[T]{}\n//}\n//\n//func (x509 IVXVPkcs8Unmarshaller[T]) UnmarshalKeyParameters(algorithmIdentifier pkix.AlgorithmIdentifier) (parameters crypto.AlgorithmIdentifierParameters, err error) {\n//\tvar params ModPGroupParameters\n//\t_, err = asn1.Unmarshal(algorithmIdentifier.Group.FullBytes, &params)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\n//\tvar name string\n//\tif params.P.BitLen() == 3072 {\n//\t\tname = \"RFC3526ModPGroup3072\"\n//\t} else {\n//\t\tname = \"NIST-P384\"\n//\t}\n//\tg, err := group.Get(name)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\n//\treturn elgamal.Group{g: g}, nil\n//}\n//\n//func (x509 IVXVPkcs8Unmarshaller[T]) UnmarshalKeyElement(g group.g, bytes []byte) (element T, err error) {\n//\t//var key ModPGroupKey\n//\n//\tc := cryptobyte.String(bytes)\n//\tvar ss cryptobyte.String\n//\t//var sss cryptobyte.String\n//\tif !c.ReadASN1(&ss, asn_1.OCTET_STRING) {\n//\t\treturn element, fmt.Errorf(\"Unmarshal SEQUENCE\")\n//\t}\n//\t//\n//\t//if !ss.ReadAnyASN1(&sss, nil) {\n//\t//\treturn element, fmt.Errorf(\"Unmarshal SEQUENCE2\")\n//\t//}\n//\n//\tvar builder cryptobyte.Builder\n//\tbuilder.AddASN1(asn_1.INTEGER, func(c *cryptobyte.Builder) {\n//\t\tc.AddBytes(ss)\n//\t})\n//\n//\tb, err := builder.Bytes()\n//\tif err != nil {\n//\t\treturn element, fmt.Errorf(\"Unmarshal INTEGER\")\n//\t}\n//\n//\tel, err := elgamal.ASN1UnmarshalPrivateElement(g, b)\n//\tif err != nil {\n//\t\treturn element, err\n//\t}\n//\n//\treturn T(el), nil\n//}\n//\n//type IVXVX509Unmarshaller[T group.Element] struct{}\n//\n//func NewIVXVX509Unmarshaller[T group.Element]() IVXVX509Unmarshaller[T] {\n//\treturn IVXVX509Unmarshaller[T]{}\n//}\n//\n//func (x509 IVXVX509Unmarshaller[T]) UnmarshalKeyParameters(algorithmIdentifier pkix.AlgorithmIdentifier) (parameters crypto.AlgorithmIdentifierParameters, err error) {\n//\tvar params ModPGroupParameters\n//\t_, err = asn1.Unmarshal(algorithmIdentifier.Group.FullBytes, &params)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\n//\tvar name string\n//\tif params.P.BitLen() == 3072 {\n//\t\tname = \"RFC3526ModPGroup3072\"\n//\t} else {\n//\t\tname = \"NIST-P384\"\n//\t}\n//\tg, err := group.Get(name)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\n//\treturn elgamal.Group{g: g}, nil\n//}\n//\n//func (x509 IVXVX509Unmarshaller[T]) UnmarshalKeyElement(g group.g, bytes []byte) (element T, err error) {\n//\tc := cryptobyte.String(bytes)\n//\tvar ss cryptobyte.String\n//\tif !c.ReadASN1(&ss, asn_1.SEQUENCE) {\n//\t\treturn element, fmt.Errorf(\"Unmarshal SEQUENCE\")\n//\t}\n//\n//\tel, err := elgamal.ASN1UnmarshalPublicElement(g, ss)\n//\tif err != nil {\n//\t\treturn element, err\n//\t}\n//\n//\treturn el.(T), nil\n//}\n//\n//func TestCustomUnmarshaller(t *testing.T) {\n//\tpem, err := base64.StdEncoding.DecodeString(base64ModP3072IVXVPublicKey)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tivxvPublicKeyUnmarshaller := NewUnmarshaller[crypto.EncryptionKey, group.Element](NewIVXVX509Unmarshaller[group.Element]())\n//\t_, pkey, err := ivxvPublicKeyUnmarshaller.Unmarshal(pem)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tpem2, err := base64.StdEncoding.DecodeString(base64ModP3072IVXVPrivateKey)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tivxvPrivateKeyUnmarshaller := NewUnmarshallerPKCS8[crypto.DecryptionKey, group.Scalar](NewIVXVPkcs8Unmarshaller[group.Scalar]())\n//\t_, key, err := ivxvPrivateKeyUnmarshaller.Unmarshal(pem2)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tplaintext := []byte(\"Hello World! =)\")\n//\n//\tfmt.Println(pkey.Marshal())\n//\tfmt.Println(key.EncryptionKey().Marshal())\n//\n//\t// a part of ciphertext\n//\trand, err := group.RandomScalar(pkey.Group().Group().Order())\n//\tciphertext, err := encrypt(pkey, plaintext, rand)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tdecrypted, err := key.Decrypt(ciphertext)\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tplaintext2, err := decrypted.Plaintext()\n//\tif err != nil {\n//\t\tt.Fatal(err)\n//\t}\n//\n//\tif !bytes.Equal(plaintext, plaintext2) {\n//\t\tt.Fatal(\"Plaintext after decryption doesn't match the initial one\")\n//\t}\n//\n//\tfmt.Println(plaintext, plaintext2)\n//\n//}\n"
  },
  {
    "path": "core/encoding/asn1/asn1.go",
    "content": "// Package asn1 provides all ASN.1 related encoding types and constants.\npackage asn1\n\n// DER is the main serialization format of ASN.1\ntype DER []byte\n"
  },
  {
    "path": "core/go.mod",
    "content": "module tivi.io/core\n\ngo 1.23\n\nrequire (\n\tgithub.com/gtank/ristretto255 v0.1.2\n\tgithub.com/prologic/bitcask v0.3.10\n\tgo.etcd.io/bbolt v1.3.2\n\tgolang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d\n)\n\nrequire (\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/plar/go-adaptive-radix-tree v1.0.4 // indirect\n\tgolang.org/x/exp v0.0.0-20221011201855-a3968a42eed6 // indirect\n\tgolang.org/x/sys v0.10.0 // indirect\n)\n"
  },
  {
    "path": "core/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=\ngithub.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/plar/go-adaptive-radix-tree v1.0.4 h1:Ucd8R6RH2E7RW8ZtDKrsWyOD3paG2qqJO0I20WQ8oWQ=\ngithub.com/plar/go-adaptive-radix-tree v1.0.4/go.mod h1:Ot8d28EII3i7Lv4PSvBlF8ejiD/CtRYDuPsySJbSaK8=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/prologic/bitcask v0.3.10 h1:HXygU8zCvW5gLpZ8aQECPk5iV/YQ3hcqdg/zVeES6s0=\ngithub.com/prologic/bitcask v0.3.10/go.mod h1:8RKJdbHLE7HFGLYSGu9slnYXSV7DMIucwVkaIYOk9GY=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/tidwall/btree v0.2.2/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=\ngithub.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=\ngithub.com/tidwall/redcon v1.4.0/go.mod h1:IGzxyoKE3Ea5AWIXo/ZHP+hzY8sWXaMKr7KlFgcWSZU=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngo.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d h1:LiA25/KWKuXfIq5pMIBq1s5hz3HQxhJJSu/SUGlD+SM=\ngolang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=\ngolang.org/x/exp v0.0.0-20221011201855-a3968a42eed6 h1:+hSdOdB7nHAFs+EDQXTvkJj7kUMugNAcE2x+BwxlVt4=\ngolang.org/x/exp v0.0.0-20221011201855-a3968a42eed6/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.53.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\n"
  },
  {
    "path": "core/govar.mk",
    "content": "GO := /usr/lib/go-1.23/bin/go\n\n# Only use go version >= 1.23\nifeq ($(shell which $(GO)),)\n\tfallback := $(shell which go)\n\tifneq ($(fallback),)\n\t\tversion := $(shell $(fallback) version | cut -d' ' -f3)\n\t\tnewer := $(shell echo \"go1.23\\n$(version)\" | sort --version-sort \\\n\t\t| tail --lines=1)\n\t\tifeq ($(version),$(newer))\n\t\t\tGO := $(fallback)\n\t\tendif\n\tendif\nendif\n\nexport GO\n"
  },
  {
    "path": "core/math/group/all/all.go",
    "content": "package all\n\nimport (\n\t_ \"tivi.io/core/math/group/ecqp\"\n\t_ \"tivi.io/core/math/group/edwards25519\"\n\t_ \"tivi.io/core/math/group/modqp\"\n)\n"
  },
  {
    "path": "core/math/group/ecqp/ecqp.go",
    "content": "// Package ecqp implements prime-order subgroup of following NIST elliptic curves:\n//\n//\tNIST-P256\n//\tNIST-P384\n//\tNIST-P521\npackage ecqp\n\nimport (\n\t\"crypto/elliptic\"\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"tivi.io/core/math/group/internal\"\n)\n\n// init registers ECqP group in a group.registry.\nfunc init() {\n\tregisterWellKnownGroups()\n}\n\nconst (\n\t// logPrefix is a prefix for logging records.\n\tlogPrefix = \"group/ecqp\"\n)\n\n// shortWeierstrassFunc is a short Weierstrass equation that solves for Y^2, given X coordinate\n// and has a form of\n//\n//\ty^2 = x^3 + Ax + B\n//\n// where A and B are constants.\nfunc shortWeierstrassFunc(x *big.Int, curve elliptic.Curve) *big.Int {\n\tint3 := big.NewInt(3) //nolint:mnd\n\n\t// x^3\n\tx3 := new(big.Int).Exp(x, int3, curve.Params().P)\n\n\t// A = P - 3\n\tA := new(big.Int).Sub(curve.Params().P, int3)\n\n\t// Ax\n\tA.Mul(A, x)\n\n\t// x^3 + Ax\n\tx3.Add(x3, A)\n\n\t// (x^3 + Ax + B) mod P\n\tx3.Add(x3, curve.Params().B)\n\tx3.Mod(x3, curve.Params().P)\n\n\t// y^2\n\treturn x3\n}\n\n// unmarshal is a copy of elliptic.Unmarshal with an ability to distinguish\n// between invalid, uncompressed, infinity and IsOnCurve point errors.\n// Returns x, y, nil if point is valid (infinity point is also considered valid).\nfunc unmarshal(point []byte, curve elliptic.Curve) (x, y *big.Int, err error) { //nolint:nonamedreturns\n\tfieldByteLen := (curve.Params().BitSize + 7) / 8 //nolint:mnd\n\n\t// Uncompressed point slice is 50/50 <--> X/Y coordinates\n\tif len(point) != 1+2*fieldByteLen {\n\t\treturn nil, nil, fmt.Errorf(\"%s: invalid elliptic curve point format\", logPrefix)\n\t}\n\n\t// Is point in uncompressed form?\n\tif point[0] != 0x04 { //nolint:mnd\n\t\treturn nil, nil, fmt.Errorf(\"%s: not an uncompressed elliptic curve point\", logPrefix)\n\t}\n\n\tp := curve.Params().P\n\tx = new(big.Int).SetBytes(point[1 : 1+fieldByteLen])\n\ty = new(big.Int).SetBytes(point[1+fieldByteLen:])\n\n\t// Point outside a curve\n\tif x.Cmp(p) >= 0 || y.Cmp(p) >= 0 {\n\t\treturn nil, nil, fmt.Errorf(\"%s: point outside an elliptic curve\", logPrefix)\n\t}\n\n\tif err := isOnCurve(curve, x, y); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn x, y, nil\n}\n\n// marshal is a copy of elliptic.Marshal with an ability to distinguish\n// between invalid, uncompressed, infinity and IsOnCurve point errors.\n// Returns marshalled elliptic curve point.\nfunc marshal(curve elliptic.Curve, x, y *big.Int) ([]byte, error) {\n\tfieldByteLen := (curve.Params().BitSize + 7) / 8 //nolint:mnd\n\n\tif err := isOnCurve(curve, x, y); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 1 byte for uncompressed format byte and each coordinate has a size of field order\n\tpoint := make([]byte, 1+2*fieldByteLen)\n\tpoint[0] = 0x04 // uncompressed point\n\n\t// Fill slice 50/50 <--> X/Y coordinates\n\tx.FillBytes(point[1 : 1+fieldByteLen])\n\ty.FillBytes(point[1+fieldByteLen : 1+2*fieldByteLen])\n\n\treturn point, nil\n}\n\n// isOnCurve returns nil if (x,y) on an elliptic curve.\n// (0,0) is considered to be point at infinity.\nfunc isOnCurve(curve elliptic.Curve, x, y *big.Int) error {\n\t// Check point is at infinity (0, 0)\n\tif isAtInfinity(x, y) {\n\t\treturn nil\n\t}\n\n\t// Point at infinity is rejected (0,0)\n\tif !curve.IsOnCurve(x, y) {\n\t\treturn internal.NotOnCurve{Prefix: logPrefix}\n\t}\n\n\treturn nil\n}\n\n// isAtInfinity returns true if point is at infinity, i.e. (0,0).\nfunc isAtInfinity(x, y *big.Int) bool {\n\tif x.Cmp(new(big.Int)) == 0 && y.Cmp(new(big.Int)) == 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "core/math/group/ecqp/element.go",
    "content": "package ecqp\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\ntype ecqPElement struct {\n\tg *ecqPGroup\n\t// x is an x coordinate of an elliptic curve.\n\tx *big.Int\n\t// y is a y coordinate of an elliptic curve.\n\ty *big.Int\n}\n\n// Inverse returns E2 that satisfies\n//\n//\tE * E2 = 1\nfunc (E *ecqPElement) Inverse() group.Element {\n\tE2 := E.g.identity()\n\n\tE2.x.Set(E.x)\n\tE2.y.Neg(E.y)\n\tE2.y.Mod(E2.y, E2.g.Params().P)\n\n\treturn E2\n}\n\n// Scale returns\n//\n//\tE * s\nfunc (E *ecqPElement) Scale(s *group.Scalar) (group.Element, error) {\n\tE2 := E.g.identity()\n\n\tif s.Modulo().Cmp(E.g.Order()) != 0 {\n\t\treturn nil, internal.ScalarModCmpOrder{Prefix: logPrefix}\n\t}\n\n\t// ScalarMult requires s to be in Big-Endian form\n\tx, y := E2.g.ScalarMult(E.x, E.y, s.Value().Bytes())\n\n\tE2.x.Set(x)\n\tE2.y.Set(y)\n\n\treturn E2, nil\n}\n\n// Op returns\n//\n//\tE + E2\nfunc (E *ecqPElement) Op(E2 group.Element) (group.Element, error) {\n\tE3, err := internal.CastElementTo[*ecqPElement](E2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := E.g.Equal(E3.g); err != nil {\n\t\treturn nil, err\n\t}\n\n\tE4 := E.g.identity()\n\n\tx, y := E4.g.Add(E.x, E.y, E3.x, E3.y)\n\n\tE4.x.Set(x)\n\tE4.y.Set(y)\n\n\treturn E4, nil\n}\n\n// Equal return nil if E == E2.\nfunc (E *ecqPElement) Equal(E2 group.Element) error {\n\tE3, err := internal.CastElementTo[*ecqPElement](E2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := E.g.Equal(E3.g); err != nil {\n\t\treturn err\n\t}\n\n\tif E.x.Cmp(E3.x) != 0 {\n\t\treturn fmt.Errorf(\"%s: different elliptic curve point X coordinates\", logPrefix)\n\t}\n\n\tif E.y.Cmp(E3.y) != 0 {\n\t\treturn fmt.Errorf(\"%s: different elliptic curve point Y coordinates\", logPrefix)\n\t}\n\n\treturn nil\n}\n\n// Decode decodes a message from elliptic curve point X coordinate.\nfunc (E *ecqPElement) Decode() ([]byte, error) {\n\t// Ensure that this group element actually belongs to the correct group.\n\tif err := E.g.IsGroupElement(E); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Ensure that group element is padded up until elliptic curve field size.\n\t// -1 since padding head 0 bit is stripped by big.Int\n\tif E.x.BitLen() != E.g.FieldOrder().BitLen()-1 {\n\t\treturn nil, internal.PaddedBitLen{Prefix: logPrefix, MsgBitLen: E.x.BitLen(), ExpectedBitLen: E.g.FieldOrder().BitLen() - 1}\n\t}\n\n\t// Strip field encoding bits\n\treturn new(big.Int).Rsh(new(big.Int).Set(E.x), uint(E.g.fieldEncodingBits)).Bytes(), nil\n}\n\n// Marshal ASN.1 marshals group element as\n//\n//\tE ::= OCTET STRING\n//\n// where OCTET STRING is uncompressed elliptic curve point.\nfunc (E *ecqPElement) Marshal() (asn_1.DER, error) {\n\tpoint, err := marshal(E.g, E.x, E.y)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar builder cryptobyte.Builder\n\n\tbuilder.AddASN1OctetString(point)\n\n\treturn builder.Bytes() //nolint:wrapcheck\n}\n"
  },
  {
    "path": "core/math/group/ecqp/element_test.go",
    "content": "package ecqp\n\nimport (\n\t\"math/big\"\n\t\"reflect\"\n\t\"testing\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/edwards25519\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\nfunc TestElementScaleModulo(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\ts := group.NewScalar(new(big.Int).SetInt64(4), ecqPGroupP384.FieldOrder())\n\tE := ecqPGroupP384.Identity()\n\t_, err := E.Scale(s)\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.ScalarModCmpOrder{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.ScalarModCmpOrder{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestElementOpCast(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"Edwards25519\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE := ecqPGroupP384.Identity()\n\t_, err = E.Op(g.Generator())\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementOpDifferentGroup(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P256\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE := ecqPGroupP384.Identity()\n\t_, err = E.Op(g.Generator())\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementEqualCast(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"Edwards25519\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE := ecqPGroupP384.Identity()\n\terr = E.Equal(g.Generator())\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementEqualDifferentGroup(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P521\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE := ecqPGroupP384.Identity()\n\terr = E.Equal(g.Generator())\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementEqualDifferentX(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P384\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE := ecqPGroupP384.Identity()\n\terr = E.Equal(g.Generator())\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementEqualDifferentY(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P384\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tE1 := g.Generator()\n\tE2 := g.Generator()\n\tE2.(*ecqPElement).y.SetInt64(11112018)\n\n\terr = E1.Equal(E2)\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestElementDecode(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tE, err := ecqPGroupP384.Encode([]byte(\"Hello!\")) // Don't modify me!\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Add one bit to make size not equal to field order - 1\n\tE.(*ecqPElement).x.Add(E.(*ecqPElement).x, new(big.Int).SetInt64(1))\n\n\t// Luckily we have y2 perfect root here, but it is a pure luck because of combination of\n\t// curve and \"Hello!\" message\n\ty2 := shortWeierstrassFunc(E.(*ecqPElement).x, E.(*ecqPElement).g.Curve)\n\ty := new(big.Int).ModSqrt(y2, ecqPGroupP384.FieldOrder())\n\tE.(*ecqPElement).y.Set(y)\n\n\t_, err = E.Decode()\n\tif err == nil {\n\t\tt.Fatal(\"expected error got nil\")\n\t}\n}\n\nfunc TestElementMarshal(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tE, err := ecqPGroupP384.Encode([]byte(\"=]\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Add one bit to make size not equal to field order - 1\n\tE.(*ecqPElement).x.Sub(E.(*ecqPElement).x, new(big.Int).SetInt64(11112018))\n\n\t_, err = E.Marshal()\n\tif err == nil {\n\t\tt.Fatal(\"expected error got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NotOnCurve{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NotOnCurve{}, reflect.TypeOf(err))\n\t}\n}\n"
  },
  {
    "path": "core/math/group/ecqp/group.go",
    "content": "package ecqp\n\nimport (\n\t\"crypto/elliptic\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"math/bits\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\ntype ecqPGroup struct {\n\tname string\n\t// fieldEncodingBits is amount of bits reserved for encoding message into an\n\t// elliptic curve point. Should be 1 < fieldEncodingBits < 21.\n\tfieldEncodingBits uint8\n\t// Only use unnamed fields, like this one, if methods doesn't collide.\n\telliptic.Curve\n}\n\nfunc (G *ecqPGroup) Name() string {\n\treturn G.name\n}\n\n// Order returns elliptic curve N parameter.\nfunc (G *ecqPGroup) Order() *big.Int {\n\treturn new(big.Int).Set(G.Params().N)\n}\n\n// FieldOrder returns elliptic curve P parameter.\nfunc (G *ecqPGroup) FieldOrder() *big.Int {\n\treturn new(big.Int).Set(G.Params().P)\n}\n\n// Identity returns elliptic curve point at (0, 0).\nfunc (G *ecqPGroup) Identity() group.Element {\n\treturn G.identity()\n}\n\n// identity is a helper method for Identity, in order to\n// return implementation ecqPElement and not group.Element interface.\nfunc (G *ecqPGroup) identity() *ecqPElement {\n\treturn &ecqPElement{\n\t\tg: G,\n\t\tx: new(big.Int),\n\t\ty: new(big.Int),\n\t}\n}\n\n// Generator returns elliptic curve base point (Gx, Gy).\nfunc (G *ecqPGroup) Generator() group.Element {\n\treturn &ecqPElement{\n\t\tg: G,\n\t\tx: new(big.Int).Set(G.Params().Gx),\n\t\ty: new(big.Int).Set(G.Params().Gy),\n\t}\n}\n\n// Equal returns nil if generator and field orders are the same.\nfunc (G *ecqPGroup) Equal(G2 group.Group) error {\n\tif G.Order().Cmp(G2.Order()) != 0 {\n\t\treturn internal.DifferentOrder{Prefix: logPrefix}\n\t}\n\n\tif G.FieldOrder().Cmp(G2.FieldOrder()) != 0 {\n\t\treturn internal.DifferentFieldOrder{Prefix: logPrefix}\n\t}\n\n\treturn nil\n}\n\n// PadBytes pads a data into a byte-aligned slice that looks like:\n//\n//\t0b0111...0XX...X\n//\n// where 0b01 is a padding head, 11... are padding bits, ...0 is a padding end and XX...X are msg bits.\nfunc (G *ecqPGroup) PadBytes(msg []byte) ([]byte, error) {\n\tfieldBitLen := G.Params().BitSize\n\n\t// Max padded msg size, reserve padding head (2), padding end (1) and field encoding bits\n\tmaxMsgBitLen := fieldBitLen - 2 - 1 - int(G.fieldEncodingBits)\n\n\t// Msg is byte-aligned, i.e. 0b110110 will be represented as 0b00110110 (0 bits included)\n\tmsgBitLen := len(msg) * 8 //nolint:mnd\n\tif msgBitLen > maxMsgBitLen {\n\t\treturn nil, internal.MessageBitLenTooLarge{Prefix: logPrefix, MsgBitLen: msgBitLen, MaxMsgBitLen: maxMsgBitLen}\n\t}\n\n\t// Padding head and end bits are included in paddingBitLen.\n\t//\n\t// -1 here is due to left shift nature. Suppose you wish\n\t// to shift 8 bits to the left, so you would expect to get 0b10000000,\n\t// however when you do 1<<8 you get 0b00000001 0b00000000 and that\n\t// is because we start with initial 1 value, which is 0b00000001.\n\t// So -1 ensures that we actually shift only desired amount of bits to\n\t// the left.\n\tpaddingBitLen := fieldBitLen - 1 - msgBitLen - int(G.fieldEncodingBits)\n\n\tpadding := big.NewInt(1)\n\t// 0b100...000\n\tpadding.Lsh(padding, uint(paddingBitLen)) //nolint:gosec\n\t// Sets most-significant bit (msb) and least-significant bit (lsb) to 0,\n\t// this operation creates padding head and padding end bits 0b011...110\n\tpadding.Sub(padding, big.NewInt(2)) //nolint:mnd\n\t// Appends msg bytes to the end of padding (head+padding+end)\n\treturn append(padding.Bytes(), msg...), nil // 0b011...110 || msg\n}\n\n// PadBits uses the same padding schema as PadBytes, but in a more efficient way,\n// see group.PadBits for details.\nfunc (G *ecqPGroup) PadBits(msg []byte) ([]byte, error) {\n\t// We should leave space for elliptic curve field encoding bits\n\treturn group.PadBits(msg, uint64(G.FieldOrder().BitLen())-uint64(G.fieldEncodingBits)) //nolint:wrapcheck,gosec\n}\n\n// UnpadBytes strips padding bytes from padded and returns initial message.\nfunc (G *ecqPGroup) UnpadBytes(padded []byte) ([]byte, error) {\n\t// When 0xFE (padding end) is msb then convert 0b11111110 to 0b01111111\n\tmsb := padded[0] >> 1\n\n\t// Check that first byte can only contain sequence of 0 bits\n\t// followed by sequence of 1 bits (0b00001111), and combinations such as\n\t// 0b00101111 are not possible\n\tif bits.LeadingZeros8(msb) != 8-bits.OnesCount8(msb) {\n\t\treturn nil, internal.UnexpectedPaddingHeader{Prefix: logPrefix, Byte: msb}\n\t}\n\n\t// Will change first byte to 0xFF, unless first byte is 0xFE\n\tpadded[0] |= 0xFE\n\n\t// Loop exactly until 0xFE padding end byte found,\n\t// or until there are no bytes left to read from padded\n\tfor i, b := range padded {\n\t\tswitch b {\n\t\t// skip\n\t\tcase 0xFF: //nolint:mnd\n\t\t// padding end\n\t\tcase 0xFE: //nolint:mnd\n\t\t\t// +1, because current i position is at 0x00 padding end, so msg starts from next byte\n\t\t\treturn padded[i+1:], nil\n\t\tdefault:\n\t\t\treturn nil, internal.UnexpectedPaddingByte{Prefix: logPrefix, Byte: b, Index: i}\n\t\t}\n\t}\n\n\treturn nil, internal.NoEncodedMessage{Prefix: logPrefix}\n}\n\n// UnpadBits unpads a message that was padded in PadBits, see group.UnpadBits\n// for details.\nfunc (G *ecqPGroup) UnpadBits(padded []byte) ([]byte, error) {\n\treturn group.UnpadBits(padded) //nolint:wrapcheck\n}\n\n// IsGroupElement checks that E is on an elliptic curve.\nfunc (G *ecqPGroup) IsGroupElement(E group.Element) error {\n\tE1, err := internal.CastElementTo[*ecqPElement](E)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Groups equal\n\tif err := G.Equal(E1.g); err != nil {\n\t\treturn err\n\t}\n\n\tif err := isOnCurve(G, E1.x, E1.y); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Encode encodes a msg into an elliptic curve point using short Weierstrass equation.\nfunc (G *ecqPGroup) Encode(msg []byte) (group.Element, error) { //nolint:funlen\n\tx := new(big.Int).SetBytes(msg)\n\n\tmaxMsgBitLen := G.FieldOrder().BitLen() - int(G.fieldEncodingBits)\n\n\tif x.BitLen() > maxMsgBitLen {\n\t\treturn nil, internal.MessageBitLenTooLarge{Prefix: logPrefix, MsgBitLen: x.BitLen(), MaxMsgBitLen: maxMsgBitLen}\n\t}\n\n\t// We allocate bits for field encoding\n\tx = x.Lsh(x, uint(G.fieldEncodingBits)) // 0b...000\n\n\t// Limit for field encoding attempts, < 2^fieldEncodingBits\n\tlimit := 2 << (G.fieldEncodingBits - 1) //nolint:mnd\n\n\t// result of short Weierstrass equation is y^2\n\tvar result *big.Int\n\n\t// Loop until x is a quadratic residue or iterations limit is reached\n\tfor range limit {\n\t\tresult = shortWeierstrassFunc(x, G.Params())\n\n\t\t// Is result a perfect square?\n\t\tif big.Jacobi(result, G.Params().P) == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\t//nolint: godox\n\t\t// TODO: uncomment me to use random field encoding bits\n\t\t//// This mask will set to 1 all field encoding bits.\n\t\t// bitMask := new(big.Int).SetInt64(1 << fieldEncodingBits)\n\t\t// bitMask.Sub(bitMask, big.NewInt(1))\n\t\t//\n\t\t// random, err := rand.Int(rand.Reader, bitMask)\n\t\t// if err != nil {\n\t\t//\treturn nil, err\n\t\t// }\n\t\t//\n\t\t//// Will insert random value into allocated field encoding bits space\n\t\t// x.Or(x, random)\n\n\t\tx.Add(x, big.NewInt(1)) // 0b...0[+1]\n\t}\n\n\t// First solution of short Weierstrass equation is sqrt(result),\n\t// becomes (X,Y)\n\ty1 := new(big.Int).ModSqrt(result, G.Params().P)\n\n\t// This case should not happen, but rather be safe\n\tif y1 == nil {\n\t\treturn nil, fmt.Errorf(\"%s: short Weierstrass equation solved to 0\", logPrefix)\n\t}\n\n\t// Second solution of Weierstrass equation sqrt(y2),\n\t// becomes (X,-Y)\n\ty2 := new(big.Int).Neg(y1)\n\ty2.Mod(y2, G.FieldOrder())\n\n\t// We have to pick an Y value for an elliptic curve point (X,Y)\n\ty := new(big.Int)\n\n\t// Make decision which one to pick - take smallest Y\n\t// https://github.com/verificatum/verificatum-vcr/blob/97974cfc4ebbb323e49396222823e226cae2bebe/src/java/com/verificatum/arithm/ECqPGroup.magic#L485\n\tif y2.Cmp(y1) < 0 {\n\t\ty.Set(y2)\n\t} else {\n\t\ty.Set(y1)\n\t}\n\n\tE2 := G.identity()\n\tE2.x.Set(x)\n\tE2.y.Set(y)\n\n\treturn E2, nil\n}\n\n// ElementOf ASN.1 unmarshalls group element and ensures that it belongs to the group.\nfunc (G *ecqPGroup) ElementOf(der asn_1.DER) (group.Element, error) {\n\toctetString := cryptobyte.String(der)\n\n\tvar point cryptobyte.String\n\n\tif !octetString.ReadASN1(&point, asn1.OCTET_STRING) {\n\t\treturn nil, internal.NotASN1OctetString{Prefix: logPrefix}\n\t}\n\n\tif !octetString.Empty() {\n\t\treturn nil, internal.ASN1TrailingBytes{Prefix: logPrefix}\n\t}\n\n\t// Unmarshal elliptic curve point\n\tx, y, err := unmarshal(point, G)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tE := G.identity()\n\tE.x.Set(x)\n\tE.y.Set(y)\n\n\t// We must ensure that element actually belongs to this group\n\tif err := G.IsGroupElement(E); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn E, nil\n}\n"
  },
  {
    "path": "core/math/group/ecqp/group_encode_p384_7_test.go",
    "content": "package ecqp\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\nfunc TestGroupEncodeP384FieldEncodingBits7Positive(t *testing.T) {\n\tt.Log(\"Positive cases for encoding NIST-P384 group with field encoding bits 7\")\n\n\tfor _, message := range ecqPGroupP384MessagePositive {\n\t\tpadded, err := ecqPGroupP384.PadBytes(message)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\t_, err = ecqPGroupP384.Encode(padded)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestGroupEncodeP384FieldEncodingBits7Negative(t *testing.T) {\n\tt.Log(\"Negative cases for encoding NIST-P384 group with field encoding bits 7\")\n\n\tfor _, message := range ecqPGroupP384MessagePositive {\n\t\tpadded, err := ecqPGroupP384.PadBytes(message)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Make padded message 2 bits (376+2=378) bigger so it won't fit it max allowed 377 (384-7=377)\n\t\tpadded2 := make([]byte, 1)\n\t\tpadded2[0] = 0x02\n\t\tpadded2 = append(padded2, padded...)\n\n\t\t_, err = ecqPGroupP384.Encode(padded2)\n\t\tif err == nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif reflect.TypeOf(err) != reflect.TypeOf(internal.MessageBitLenTooLarge{}) {\n\t\t\tt.Fatalf(\"expected error %v, but got %v\", internal.MessageBitLenTooLarge{}, reflect.TypeOf(err))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/math/group/ecqp/group_pad_byte_p384_7_test.go",
    "content": "package ecqp\n\nimport (\n\t\"bytes\"\n\t\"math/big\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"tivi.io/core/math/group/internal\"\n)\n\nvar ecqPGroupP384MessagePositive = map[string][]byte{\n\t// Largest message is 368 bits, since PadBytes does byte-aligning on a message.\n\t// Actually maximum allowed message bits are 374 (384-2-1-7), so as you can see\n\t// 374-368=6 bits are left unused. That's why in a future we may switch to group.PadBits,\n\t// which doesn't align a message by bytes and instead uses raw bits.\n\t\"largest\": {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},\n\n\t// Empty slice\n\t\"empty slice\": {},\n\n\t// Empty message\n\t\"empty message\": []byte(\"\"),\n\n\t// Regular message 1\n\t\"regular message 1\": []byte(\"0000.101\"),\n\n\t// Regular message 2\n\t\"regular message 2\": []byte(\"3011.678\"),\n\n\t// Regular message 3\n\t\"regular message 3\": []byte(\"1111.2018\"),\n\n\t// Regular message 4\n\t\"regular message 4\": []byte(\"Candidate\\x1FParty\\x1FLogo\\x1FGünnar Metsäpuu\"),\n\n\t// All 368 bits of zero bytes message\n\t\"empty zero bytes\": {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\n\t// One zero byte message\n\t\"empty zero byte\": {0x00},\n}\n\nvar ecqPGroupP384MessageNegative = map[string][]byte{\n\t// Message is exactly 374 bits (384-2-1-7), but since it is byte-aligned (376 bits),\n\t// maximum allowed is 368 bits\n\t\"374bits\": {0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},\n\n\t// Message is exactly 373 bits (even smaller than (384-2-1-7)), but since it is\n\t// byte-aligned (376 bits), maximum allowed is 368 bits\n\t\"373bits\": {0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},\n\n\t// Message is exactly 376 bits, which is greater than even maximum allowed theoretical\n\t// 384-2-1-7=374 bits\n\t\"376bits\": {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},\n\n\t// 47 bytes of 0x00, while maximum allowed is 46 bytes (368/8)\n\t\"47bytes\": {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n}\n\nfunc TestGroupPadBytesP384FieldEncodingBits7Positive(t *testing.T) {\n\tt.Log(\"Positive cases for padding NIST-P384 group with field encoding bits 7\")\n\n\t// Field size is 384 bits, i.e. 48 bytes. Padding reserves 2 bits for padding head\n\t// and 1 bit for padding end. Also it reserves 7 bits for field encoding.\n\t// Which gives us total of 384-2-1-7=374 bits allowed for message encoding.\n\t// Since message is byte-aligned in PadBytes, there is no other option for\n\t// padding to be also byte-aligned. 374/8 we get 46.75 bytes. Padding should be\n\t// aligned to 47 bytes, i.e. 376 bits.\n\tresultByteLen := 47\n\tresultBitLen := 376\n\n\tfor _, message := range ecqPGroupP384MessagePositive {\n\t\tpadded, err := ecqPGroupP384.PadBytes(message)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(padded) != resultByteLen {\n\t\t\tt.Fatalf(\"expected bytelen %d, got %d\", resultByteLen, len(padded))\n\t\t}\n\n\t\tpaddedInt := new(big.Int).SetBytes(padded)\n\n\t\tif paddedInt.BitLen() != resultBitLen {\n\t\t\tt.Fatalf(\"expected bitlen %d, got %d\", resultBitLen, paddedInt.BitLen())\n\t\t}\n\t}\n}\n\nfunc TestGroupPadBytesP384FieldEncodingBits7Negative(t *testing.T) {\n\tt.Log(\"Negative cases for padding NIST-P384 group with field encoding bits 7\")\n\n\tfor _, message := range ecqPGroupP384MessageNegative {\n\t\t_, err := ecqPGroupP384.PadBytes(message)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, but got nil\")\n\t\t}\n\t\tif reflect.TypeOf(err) != reflect.TypeOf(internal.MessageBitLenTooLarge{}) {\n\t\t\tt.Fatalf(\"expected error %v, but got %v\", internal.MessageBitLenTooLarge{}, reflect.TypeOf(err))\n\t\t}\n\t}\n}\n\nfunc TestGroupUnpadBytesP384FieldEncodingBits7Positive(t *testing.T) {\n\tt.Log(\"Positive cases for unpadding NIST-P384 group with field encoding bits 7\")\n\n\tfor _, message := range ecqPGroupP384MessagePositive {\n\t\tpadded, err := ecqPGroupP384.PadBytes(message)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tunpadded, err := ecqPGroupP384.UnpadBytes(padded)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif !bytes.Equal(message, unpadded) {\n\t\t\tt.Fatal(\"expected initial and unpadded messages to be equal\")\n\t\t}\n\t}\n}\n\nfunc TestGroupUnpadBytesP384FieldEncodingBits7Negative(t *testing.T) {\n\tt.Log(\"Negative cases for unpadding NIST-P384 group with field encoding bits 7\")\n\n\tfor title, message := range ecqPGroupP384MessagePositive {\n\t\tpadded, err := ecqPGroupP384.PadBytes(message)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Backup for later use\n\t\tbackup := padded[0]\n\n\t\t// Set msb to 0b10111110, so when UnpadBytes checks header it will encounter\n\t\t// 0b01011111 (0xBE >> 1)\n\t\tpadded[0] = 0xBE\n\n\t\t_, err = ecqPGroupP384.UnpadBytes(padded)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, but got nil\")\n\t\t}\n\t\tif reflect.TypeOf(err) != reflect.TypeOf(internal.UnexpectedPaddingHeader{}) {\n\t\t\tt.Fatalf(\"expected error %v, but got %v\", internal.UnexpectedPaddingHeader{}, reflect.TypeOf(err))\n\t\t}\n\n\t\tpadded[0] = backup\n\n\t\tif title != \"largest\" && title != \"empty zero bytes\" {\n\t\t\t// Set second byte to 0b01111010, so when UnpadBytes checks padding bytes it will report\n\t\t\t// that second byte is invalid\n\t\t\tbackup = padded[1]\n\t\t\tpadded[1] = 0x7A\n\n\t\t\t_, err = ecqPGroupP384.UnpadBytes(padded)\n\t\t\tif err == nil {\n\t\t\t\tt.Fatal(\"expected error, but got nil\")\n\t\t\t}\n\n\t\t\tif reflect.TypeOf(err) != reflect.TypeOf(internal.UnexpectedPaddingByte{}) {\n\t\t\t\tt.Fatalf(\"expected error %v, but got %v\", internal.UnexpectedPaddingByte{}, reflect.TypeOf(err))\n\t\t\t}\n\n\t\t\tpadded[1] = backup\n\t\t\tfor i := range padded[1:] {\n\t\t\t\tpadded[i+1] = 0xFF\n\t\t\t}\n\n\t\t\tif _, err = ecqPGroupP384.UnpadBytes(padded); err == nil {\n\t\t\t\tt.Fatal(\"expected error, but got nil\")\n\t\t\t}\n\n\t\t\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NoEncodedMessage{}) {\n\t\t\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NoEncodedMessage{}, reflect.TypeOf(err))\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/math/group/ecqp/group_test.go",
    "content": "package ecqp\n\nimport (\n\t\"crypto/elliptic\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/edwards25519\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\nfunc init() {\n\tname := \"NIST-P384\"\n\tvar err error\n\tecqPGroupP384, err = group.Get(name)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tecqPGroupP384.(*ecqPGroup).fieldEncodingBits = 7\n}\n\nvar ecqPGroupP384 group.Group\n\nvar ecqPGroupP224 = &ecqPGroup{name: \"NIST-P224\", Curve: elliptic.P224()}\n\nfunc TestGroupEqualDifferentOrder(t *testing.T) {\n\tt.Log(\"Equality checks should fail for different groups, but succeed for the same ones\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(fmt.Sprintf(\"Negative cases for %s group\", g.Name()), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif err := g.Equal(ecqPGroupP224); err == nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Positive cases\n\tfor _, g := range group.All() {\n\t\tt.Run(fmt.Sprintf(\"Positive cases for %s group\", g.Name()), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif err := g.Equal(g); err != nil {\n\t\t\t\tt.Fatalf(\"expected nil error, but got %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupIsGroupElementCastError(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"Edwards25519\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err = ecqPGroupP384.IsGroupElement(g.Generator()); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestGroupIsGroupElementDifferentGroups(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P521\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err = ecqPGroupP384.IsGroupElement(g.Generator()); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestGroupIsGroupElementNotOnCurve(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\tg, err := group.Get(\"NIST-P384\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tE := g.Generator()\n\tE.(*ecqPElement).x.SetInt64(11112018)\n\n\tif err = ecqPGroupP384.IsGroupElement(E); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NotOnCurve{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NotOnCurve{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupElementOfASN1OctetString(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 BitString hex encoded\n\tE := \"03620004aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NotASN1OctetString{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NotASN1OctetString{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupElementOfASN1TrailingBytes(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// with 0xAA, 0xBB, 0xCC trailing bytes (appended at the end)\n\tE := \"046104aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5faabbcc\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.ASN1TrailingBytes{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.ASN1TrailingBytes{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupElementOfNotUncompressedPoint(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// with 0xAA, 0xBB, 0xCC trailing bytes (appended at the end)\n\tE := \"046100aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestGroupElementOfCoordinatesOverflow(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// X and Y coordinates are of exactly elliptic curve P parameter value,\n\t// which makes it to error at unmarshalling\n\tE := \"046104fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestGroupElementOfNotUOnCurve(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// X coordinate has 0xAA as fifth byte, which makes it not compatible with\n\t// Y coordinate and therefore outside the curve\n\tE := \"046104aa87ca22aa8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NotOnCurve{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NotOnCurve{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupElementOfPointInvalidFormat(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// X coordinate has 0xAA as fifth byte, which makes it not compatible with\n\t// Y coordinate and therefore outside the curve\n\tE := \"046204aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab73617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5fff\"\n\n\tder, err := hex.DecodeString(E)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n}\n\nfunc TestGroupElementOfPointAtInfinity(t *testing.T) {\n\tt.Log(\"TODO:\")\n\n\t// Generator element of P384 group as ASN.1 OctetString hex encoded\n\t// X coordinate has 0xAA as fifth byte, which makes it not compatible with\n\t// Y coordinate and therefore outside the curve\n\tE := ecqPGroupP384.Identity()\n\tder, err := E.Marshal()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err = ecqPGroupP384.ElementOf(der); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "core/math/group/ecqp/wellknown.go",
    "content": "package ecqp\n\nimport (\n\t\"crypto/elliptic\"\n\n\t\"tivi.io/core/math/group\"\n)\n\nconst fieldEncodingBits = 10\n\n// registerWellKnownGroups registers popular NIST curve implementations in a group.registry.\n// Names of NIST curve implementations are defined by TIVI Core library, in order to keep\n// backwards compatibility with elliptic library.\nfunc registerWellKnownGroups() {\n\tcurves := map[string]elliptic.Curve{\n\t\t\"NIST-P256\": elliptic.P256(),\n\t\t\"NIST-P384\": elliptic.P384(),\n\t\t\"NIST-P521\": elliptic.P521(),\n\t}\n\n\t// Register curves in group.registry\n\tfor name, curve := range curves {\n\t\tgroup.Register(&ecqPGroup{name: name, Curve: curve, fieldEncodingBits: fieldEncodingBits})\n\t}\n}\n"
  },
  {
    "path": "core/math/group/edwards25519/edwards25519.go",
    "content": "// Package edwards25519 implements prime-order subgroup of Edwards25519 elliptic curve,\n// using Ristretto255 g.\npackage edwards25519\n\nimport (\n\t\"fmt\"\n\t\"github.com/gtank/ristretto255\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"math/big\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\t\"tivi.io/core/math/group/internal\"\n\n\t\"tivi.io/core/math/group\"\n)\n\nconst (\n\t// Maximum encoded message bytes size for Edwards25519 curve\n\tmaxEncodedMsgBytesLen = 32\n\t// Maximum message bytes size for Edwards25519 curve\n\tmaxMsgBytesLen = 29\n)\n\n// init registers Edwards25519 g in a group.registry.\nfunc init() {\n\tgroup.Register(new(edwards25519Group))\n}\n\nvar (\n\t// l is an order of an Edwards25519 curve base point (b), i.e. generator order\n\t// Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1\n\tl = []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xde, 0xf9, 0xde, 0xa2, 0xf7, 0x9c, 0xd6, 0x58, 0x12, 0x63, 0x1a, 0x5c, 0xf5, 0xd3, 0xed}\n\t// p is an order of an Edwards25519 curve field, i.e. g field order\n\t// Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1\n\tp = []byte{0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xed}\n)\n\ntype edwards25519Group struct{} //nolint:revive\n\nfunc (G *edwards25519Group) Name() string {\n\treturn \"Edwards25519\"\n}\n\n// Order returns Ristretto255 group L parameter.\nfunc (G *edwards25519Group) Order() *big.Int {\n\treturn new(big.Int).SetBytes(l)\n}\n\n// FieldOrder returns Ristretto255 group P parameter.\nfunc (G *edwards25519Group) FieldOrder() *big.Int {\n\treturn new(big.Int).SetBytes(p)\n}\n\n// Identity returns Edwards25519 curve point at (X=0, Y=1, Z=1, T=0).\nfunc (G *edwards25519Group) Identity() group.Element {\n\treturn G.identity()\n}\n\n// identity returns Edwards25519 curve point at (X=0, Y=1, Z=1, T=0).\n//\n// identity is a helper method for Identity, in order to\n// return concrete implementation edwards25519Element and not group.Element.\nfunc (G *edwards25519Group) identity() *edwards25519Element {\n\treturn &edwards25519Element{\n\t\tg: G,\n\t\tp: ristretto255.NewElement(),\n\t}\n}\n\n// Generator returns Edwards25519 curve b parameter, i.e. base point.\nfunc (G *edwards25519Group) Generator() group.Element {\n\treturn &edwards25519Element{\n\t\tg: G,\n\t\tp: ristretto255.NewElement().Base(),\n\t}\n}\n\nfunc (G *edwards25519Group) Equal(G2 group.Group) error {\n\tif G.Order().Cmp(G2.Order()) != 0 {\n\t\treturn fmt.Errorf(\"group/edwards25519: %s\", internal.ErrDifferentGenOrder)\n\t}\n\n\tif G.FieldOrder().Cmp(G2.FieldOrder()) != 0 {\n\t\treturn fmt.Errorf(\"group/edwards25519: %s\", internal.ErrDifferentFieldOrder)\n\t}\n\n\treturn nil\n}\n\n// Pad is unimplemented for Edwards25519 group. Padding is done in EncodeMessage.\nfunc (G *edwards25519Group) PadBytes(msg []byte) ([]byte, error) {\n\treturn msg, nil\n}\n\n// PadBits is unimplemented for Edwards25519 group. Padding is done in EncodeMessage.\nfunc (G *edwards25519Group) PadBits(msg []byte) ([]byte, error) {\n\treturn msg, nil\n}\n\n// Unpad is unimplemented for Edwards25519 group. Unpadding is done in Decode.\nfunc (G *edwards25519Group) UnpadBytes(padded []byte) (msg []byte, err error) {\n\treturn padded, nil\n}\n\n// UnpadBits is unimplemented for Edwards25519 group. Unpadding is done in Decode.\nfunc (G *edwards25519Group) UnpadBits(padded []byte) (msg []byte, err error) {\n\treturn padded, nil\n}\n\n// IsGroupElement is unimplemented for Edwards25519 group and always returns nil.\nfunc (G *edwards25519Group) IsGroupElement(_ group.Element) error {\n\treturn nil\n}\n\n// EncodeMessage encodes a msg, whose byte length is <= 29, into Edwards25519 curve point.\nfunc (G *edwards25519Group) Encode(msg []byte) (group.Element, error) {\n\tE := G.identity()\n\n\t// 32 - 29 = 3, these 3 bytes are reserved and must always present in encoded message\n\tif len(msg) > maxMsgBytesLen {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: %s: %d\", internal.ErrLargeMessageBytes, maxMsgBytesLen)\n\t}\n\n\t// initialized with 0, i.e. []byte{0, 0, 0, ...}\n\tvar buf [maxEncodedMsgBytesLen]byte\n\n\t// []byte{0, 0, len(msg), 0, 0, ...}\n\tbuf[2] = byte(len(msg))\n\n\t// if len(msg) == 29, then\n\t// []byte{0, 0, len(msg), M, E, S, S, a, g, E}\n\t//\n\t// if len(msg) < 29, then\n\t// []byte{0, 0, len(msg), M, E, S, S, a, g, E, 0, 0, ..., 0}\n\tcopy(buf[3:], msg)\n\n\t// Loop until buf[1] != 255 or msg is successfully encoded (err == nil)\n\tvar err error\n\n\t// TODO: possibly infinite loop\n\tfor ; buf[1] != 0xFF; buf[1]++ {\n\t\terr = E.p.Decode(buf[:])\n\t\tif err == nil {\n\t\t\treturn E, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"group/edwards25519: cannot encode a message into Edwards25519 curve point: %v\", err)\n}\n\n// ElementOfASN1 ASN.1 unmarshalls group element and ensures that it belongs to the group.\nfunc (G *edwards25519Group) ElementOf(der asn_1.DER) (group.Element, error) {\n\toctetString := cryptobyte.String(der)\n\tvar ristrettoElement cryptobyte.String\n\n\tif !octetString.ReadASN1(&ristrettoElement, asn1.OCTET_STRING) {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: %s\", internal.ErrNotASN1OctetString)\n\t}\n\n\tif !octetString.Empty() {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: %s\", internal.ErrTrailingBytesASN1)\n\t}\n\n\tE := G.identity()\n\n\tif err := E.p.Decode(ristrettoElement); err != nil {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: cannot decode Ristretto255 element into Edwards25519 curve point: %v\", err)\n\t}\n\n\t// Always nil\n\tif err := G.IsGroupElement(E); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn E, nil\n}\n\ntype edwards25519Element struct { //nolint:revive\n\tg *edwards25519Group\n\t// p is an underlying Ristretto255 g element.\n\tp *ristretto255.Element\n}\n\n// Inverse returns E2 that satisfies\n//\n//\tE * E2 = 1\nfunc (E *edwards25519Element) Inverse() group.Element { //nolint:revive\n\tE2 := E.g.identity()\n\n\tE2.p.Negate(E.p)\n\n\treturn E2\n}\n\n// Scale returns\n//\n//\tE * s.\nfunc (E *edwards25519Element) Scale(s *group.Scalar) (group.Element, error) { //nolint:revive\n\tE2 := E.g.identity()\n\n\tif s.Modulo().Cmp(E.g.Order()) != 0 {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: %s\", internal.ErrScalarModCmpGenOrder)\n\t}\n\n\ts2, err := toRistrettoScalar(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tE2.p.ScalarMult(s2, E.p)\n\n\treturn E2, nil\n}\n\n// Op returns\n//\n//\tE1 + E2.\nfunc (E *edwards25519Element) Op(E2 group.Element) (group.Element, error) {\n\tE3, err := internal.CastElementTo[*edwards25519Element](E2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = E.g.Equal(E3.g)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tE4 := E.g.identity()\n\n\tE4.p.Add(E.p, E3.p)\n\n\treturn E4, nil\n}\n\n// Equal return nil if E == E2.\nfunc (E *edwards25519Element) Equal(E2 group.Element) error {\n\tE3, err := internal.CastElementTo[*edwards25519Element](E2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = E.g.Equal(E3.g); err != nil {\n\t\treturn err\n\t}\n\n\tif E.p.Equal(E3.p) != 1 {\n\t\treturn fmt.Errorf(\"group/edwards25519: different Ristretto255 elements\")\n\t}\n\n\treturn nil\n}\n\n// DecodeMessage decodes a message from Edwards25519 curve point.\nfunc (E *edwards25519Element) Decode() ([]byte, error) { //nolint:revive\n\t// Ensure that this group element actually belongs to the correct group.\n\tif err := E.g.IsGroupElement(E); err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuf := E.p.Encode(nil)\n\tif buf[0] != 0x00 {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: first byte of decoded message should be 0x00\")\n\t}\n\n\t// Ensure that decoded message bytelen isn't > 29\n\tif buf[2] > 0x1D {\n\t\treturn nil, fmt.Errorf(\"group/edwards25519: %s: %d\", internal.ErrLargeMessageBytes, maxMsgBytesLen)\n\t}\n\n\t// +3 because we set slice end index, buf[2] returns msg bytelen, but we\n\t// start slicing from index=3, therefore our end index becomes buf[2]+3\n\treturn buf[3 : buf[2]+3], nil\n}\n\n// Marshal ASN.1 marshals group element as\n//\n//\tE ::= OCTET STRING\n//\n// where OCTET STRING is Edwards25519 curve point.\nfunc (E *edwards25519Element) Marshal() (der asn_1.DER, err error) { //nolint:revive\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1OctetString(E.p.Encode(nil))\n\n\tder, err = builder.Bytes()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn\n}\n\n// toRistrettoScalar converts from big-endian Edwards25519 scalar\n// to little-endian Ristretto255 scalar.\nfunc toRistrettoScalar(s *group.Scalar) (rs *ristretto255.Scalar, err error) {\n\tvar bigEndian [32]byte\n\ts.Value().FillBytes(bigEndian[:])\n\n\tvar littleEndian [32]byte\n\tfor i := 0; i < 32; i++ {\n\t\tlittleEndian[31-i] = bigEndian[i]\n\t}\n\n\trs = ristretto255.NewScalar()\n\n\terr = rs.Decode(littleEndian[:])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/math/group/group.go",
    "content": "// Package group defines an interface for finite cyclic abstract groups.\n// An abstract group is a set that has the following properties:\n//\n//\ta) has operation either * or +\n//\tb) applying operation on any of the group elements should always return a group element\n//\tc) each group element has an inverse element in the group\n//\td) group has an identity element\n//\te) all operations are associative\npackage group\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n)\n\n// Group is a finite cyclic abstract group. You use Group interface to\n// create new Element.\ntype Group interface {\n\t// Name returns name of this group.\n\tName() string\n\n\t// Order returns order of this group generator and is used as a Scalar modulo.\n\tOrder() *big.Int\n\n\t// FieldOrder returns order of this group field and is used as Element modulo.\n\tFieldOrder() *big.Int\n\n\t// Identity returns this group identity element.\n\tIdentity() Element\n\n\t// Generator returns this group generator element.\n\tGenerator() Element\n\n\t// Equal returns nil if this group equals to g.\n\tEqual(G Group) error\n\n\t// PadBytes pads a msg up to the maximum allowed byte length.\n\t// Maximum allowed byte length decision is up to the implementation.\n\tPadBytes(msg []byte) (padded []byte, err error)\n\n\t// PadBits pads a msg up to the maximum allowed bit length.\n\t// Maximum allowed bit length decision is up to the implementation.\n\tPadBits(msg []byte) (padded []byte, err error)\n\n\t// UnpadBytes unpads a msg from padded byte slice.\n\tUnpadBytes(padded []byte) (msg []byte, err error)\n\n\t// UnpadBits unpads a msg from padded bit slice.\n\tUnpadBits(padded []byte) (msg []byte, err error)\n\n\t// IsGroupElement return nil if E belongs to this group.\n\tIsGroupElement(E Element) error\n\n\t// Encode encodes a message into this group element.\n\tEncode(msg []byte) (Element, error)\n\n\t// ElementOf ASN.1 unmarshalls group element into Element.\n\tElementOf(der asn_1.DER) (Element, error)\n}\n\n// Element represents a Group element.\ntype Element interface {\n\t// Inverse returns E, which is an inverse of this group element such that condition is met\n\t//\tthis * E = identity\n\t// for multiplicative group\n\t//\tthis + E = identity\n\t// for additive group.\n\tInverse() (E Element)\n\n\t// Scale returns group element whose value is\n\t//\tthis ^ s\n\t// for multiplicative group\n\t//\tthis * s\n\t// for additive group.\n\tScale(s *Scalar) (Element, error)\n\t// Op returns a result of group operation whose value is\n\t//\tthis * E\n\t// for multiplicative group\n\t//\tthis + E\n\t// for additive group.\n\tOp(E Element) (Element, error)\n\n\t// Equal returns nil if this group element equals to E.\n\tEqual(E Element) error\n\n\t// Decode decodes a message from this group element.\n\tDecode() ([]byte, error)\n\n\t// Marshal returns this group element ASN.1 marshalled.\n\tMarshal() (asn_1.DER, error)\n}\n\n// RandomElement returns random element from a group.\nfunc RandomElement(G Group) (Element, error) {\n\ts, err := RandomScalar(G.Order())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn G.Generator().Scale(s)\n}\n\n// Marshal ASN.1 marshals group as\n//\n//\tg ::= PrintableString\n//\n// where PrintableString is a group name.\nfunc Marshal(G Group) (asn_1.DER, error) {\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.PrintableString, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddBytes([]byte(G.Name()))\n\t})\n\n\treturn builder.Bytes()\n}\n\n// Unmarshal ASN.1 unmarshalls group.\nfunc Unmarshal(der asn_1.DER) (Group, error) {\n\tprintableString := cryptobyte.String(der)\n\tvar name []byte\n\tif !printableString.ReadASN1Bytes(&name, asn1.PrintableString) {\n\t\treturn nil, fmt.Errorf(\"math/group: group is not an ASN.1 PrintableString\")\n\t}\n\n\tif !printableString.Empty() {\n\t\treturn nil, fmt.Errorf(\"math/group: incomplete ASN.1 group\")\n\t}\n\n\treturn Get(string(name))\n}\n"
  },
  {
    "path": "core/math/group/group_test.go",
    "content": "package group_test\n\nimport (\n\t\"bytes\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\t\"testing\"\n\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc TestGroupName(t *testing.T) {\n\tt.Log(\"Ensure that group.Name works as expected for backwards compatibility\")\n\n\tsupported := []string{\n\t\t\"RFC3526ModPGroup2048\",\n\t\t\"RFC3526ModPGroup3072\",\n\t\t\"RFC3526ModPGroup4096\",\n\t\t\"RFC3526ModPGroup8192\",\n\t\t\"NIST-P256\",\n\t\t\"NIST-P384\",\n\t\t\"NIST-P521\",\n\t\t\"Edwards25519\",\n\t}\n\n\tif len(supported) != len(group.All()) {\n\t\tt.Fatal(\"Supported list of groups in test is incomplete\")\n\t}\n\n\tfor _, name := range supported {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tg, err := group.Get(name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.Name() != name {\n\t\t\t\tt.Fatalf(\"expected %s name, got %s\", name, g.Name())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupOrderInstance(t *testing.T) {\n\tt.Log(\"Ensure that group.Order always returns new instance (check immutability property)\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.Order() == g.Order() {\n\t\t\t\tt.Fatal(\"expected order to be unique pointer\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupFieldOrderInstance(t *testing.T) {\n\tt.Log(\"Ensure that group.FieldOrder always returns new instance (check immutability property)\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.FieldOrder() == g.FieldOrder() {\n\t\t\t\tt.Fatal(\"expected field order to be unique pointer\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupIdentityInstance(t *testing.T) {\n\tt.Log(\"Ensure that group.Identity always returns new instance (check immutability property)\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.Identity() == g.Identity() {\n\t\t\t\tt.Fatal(\"expected identity to be unique pointer\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupIdentity(t *testing.T) {\n\tt.Log(\"Ensure that any element operation on group.Identity always returns that element\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE1 := g.Generator()\n\t\t\tE2, err := E1.Op(g.Identity())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif err := E1.Equal(E2); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupGeneratorInstance(t *testing.T) {\n\tt.Log(\"Ensure that group.Generator always returns new instance (check immutability property)\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tg, err := group.Get(g.Name())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.Generator() == g.Generator() {\n\t\t\t\tt.Fatal(\"expected generator to be unique pointer\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupGetUnknownGroup(t *testing.T) {\n\tunknown := \"UNKNOWN\"\n\n\t_, err := group.Get(unknown)\n\tif err == nil {\n\t\tt.Fatal(\"expected err, got nil\")\n\t}\n}\n\nfunc TestASN1UnmarshalReadASN1Error(t *testing.T) {\n\tnotAGroup := []byte(\"Not an ASN.1 group\")\n\n\t_, err := group.Unmarshal(notAGroup)\n\tif err == nil {\n\t\tt.Fatal(\"expected err, got nil\")\n\t}\n}\n\nfunc TestASN1UnmarshalNameMissingError(t *testing.T) {\n\tvar builder cryptobyte.Builder\n\n\tbuilder.AddASN1(asn1.SEQUENCE, func(_ *cryptobyte.Builder) {})\n\n\tder, err := builder.Bytes()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = group.Unmarshal(der)\n\tif err == nil {\n\t\tt.Fatal(\"expected err, got nil\")\n\t}\n}\n\nfunc TestASN1UnmarshalTrailingBytesError(t *testing.T) {\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1(asn1.SEQUENCE, func(builder *cryptobyte.Builder) {\n\t\tbuilder.AddASN1(asn1.PrintableString, func(_ *cryptobyte.Builder) {})\n\t})\n\n\t// Add just any bytes to the already structured DER, and they will be\n\t// considered as trailing bytes\n\ttrailingBytes := []byte{0x00}\n\tbuilder.AddBytes(trailingBytes)\n\n\tder, err := builder.Bytes()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = group.Unmarshal(der)\n\tif err == nil {\n\t\tt.Fatal(\"expected err, got nil\")\n\t}\n}\n\nfunc TestEncodeAndDecodeMessage(t *testing.T) {\n\tmsg := []byte(\"foobar\")\n\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\t// So how do we encode a plaintext:\n\n\t\t\t// 1. We pad a plaintext, returns padded plaintext\n\t\t\tmsg2, err := g.PadBytes(msg)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// 2. We encode padded plaintext into a group element\n\t\t\tE, err := g.Encode(msg2)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// 3. Once we obtained a group element from plaintext,\n\t\t\t// we can do any cryptographic operations on it.\n\t\t\t// At some time, there may be a need to check whether a plaintext\n\t\t\t// was encoded into a group element or not? That's what we do here\n\t\t\tif _, err = E.Decode(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// 4. Once we ensured that group element can be decoded to a plaintext,\n\t\t\t// we can decode it, returns padded plaintext.\n\t\t\t// NB! This isDecodable() check is optional, but allows to fail fast, if\n\t\t\t// group element doesn't contain a plaintext\n\t\t\tmsg3, err := E.Decode()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t// 5. Now we unpad a padded plaintext, returns initial plaintext\n\t\t\tmsg4, err := g.UnpadBytes(msg3)\n\t\t\tif !bytes.Equal(msg, msg4) {\n\t\t\t\tt.Fatalf(\"Unequal messages m1=%v and m2=%v\\n\", msg, msg2)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupMarshalAndUnmarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tder, err := group.Marshal(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tg2, err := group.Unmarshal(der)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif g.Name() != g2.Name() {\n\t\t\t\tt.Fatalf(\"Unequal groups g1=%v and g2=%v\\n\", g, g2)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestElementMarshalAndUnmarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tE, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tder, err := E.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE2, err := g.ElementOf(der)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\terr = E2.Equal(E)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMath(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\tE1, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tE2, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tE3, err := E1.Op(E2)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tE4 := E2.Inverse()\n\n\t\t\tE5, err := E3.Op(E4)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\terr = E5.Equal(E1)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestScalarMath(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\ts, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\ts2, err := s.Add(s)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE, err := group.RandomElement(g)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE2, err := E.Scale(s2)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE, err = E.Scale(s)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tE, err = E.Op(E)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\terr = E.Equal(E2)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/math/group/internal/errors.go",
    "content": "package internal\n\nimport \"fmt\"\n\nconst (\n\tErrDifferentGenOrder    = \"different generator orders\"\n\tErrDifferentFieldOrder  = \"different field orders\"\n\tErrNotASN1OctetString   = \"element is not an ASN.1 OCTET STRING\"\n\tErrTrailingBytesASN1    = \"incomplete ASN.1 element\"\n\tErrScalarModCmpGenOrder = \"scalar modulo != generator order\"\n\tErrLargeMessageBytes    = \"message byte length is larger than allowed\"\n)\n\ntype UnexpectedPaddingByte struct {\n\tPrefix string\n\tIndex  int\n\tByte   byte\n}\n\nfunc (e UnexpectedPaddingByte) Error() string {\n\treturn fmt.Sprintf(\"%s: unexpected padding byte %x at slice[%d]\", e.Prefix, e.Byte, e.Index)\n}\n\ntype UnexpectedPaddingHeader struct {\n\tPrefix string\n\tByte   byte\n}\n\nfunc (e UnexpectedPaddingHeader) Error() string {\n\treturn fmt.Sprintf(\"%s: unexpected padding header %x\", e.Prefix, e.Byte)\n}\n\ntype NoEncodedMessage struct {\n\tPrefix string\n}\n\nfunc (e NoEncodedMessage) Error() string {\n\treturn fmt.Sprintf(\"%s: message is padded to the full length, no space for data left\", e.Prefix)\n}\n\ntype MessageBitLenTooLarge struct {\n\tPrefix       string\n\tMsgBitLen    int\n\tMaxMsgBitLen int\n}\n\nfunc (e MessageBitLenTooLarge) Error() string {\n\treturn fmt.Sprintf(\"%s: message bit length %d is too large for maximum %d allowed\", e.Prefix, e.MsgBitLen, e.MaxMsgBitLen)\n}\n\ntype MessageByteLenTooLarge struct {\n\tPrefix        string\n\tMsgByteLen    int\n\tMaxMsgByteLen int\n}\n\nfunc (e MessageByteLenTooLarge) Error() string {\n\treturn fmt.Sprintf(\"%s: message byte length %d is too large for maximum %d allowed\", e.Prefix, e.MsgByteLen, e.MaxMsgByteLen)\n}\n\ntype NotASN1OctetString struct {\n\tPrefix string\n}\n\nfunc (e NotASN1OctetString) Error() string {\n\treturn fmt.Sprintf(\"%s: element is not an ASN.1 OCTET STRING\", e.Prefix)\n}\n\ntype NotASN1Integer struct {\n\tPrefix string\n}\n\nfunc (e NotASN1Integer) Error() string {\n\treturn fmt.Sprintf(\"%s: element is not an ASN.1 INTEGER\", e.Prefix)\n}\n\ntype ASN1TrailingBytes struct {\n\tPrefix string\n}\n\nfunc (e ASN1TrailingBytes) Error() string {\n\treturn fmt.Sprintf(\"%s: trailing bytes while parsing ASN.1 element\", e.Prefix)\n}\n\ntype DifferentOrder struct {\n\tPrefix string\n}\n\nfunc (e DifferentOrder) Error() string {\n\treturn fmt.Sprintf(\"%s: groups have different generator order\", e.Prefix)\n}\n\ntype DifferentFieldOrder struct {\n\tPrefix string\n}\n\nfunc (e DifferentFieldOrder) Error() string {\n\treturn fmt.Sprintf(\"%s: groups have different field order\", e.Prefix)\n}\n\ntype NotOnCurve struct {\n\tPrefix string\n}\n\nfunc (e NotOnCurve) Error() string {\n\treturn fmt.Sprintf(\"%s: point is not on a curve\", e.Prefix)\n}\n\ntype ScalarModCmpOrder struct {\n\tPrefix string\n}\n\nfunc (e ScalarModCmpOrder) Error() string {\n\treturn fmt.Sprintf(\"%s: scalar modulo not equal to generator order\", e.Prefix)\n}\n\ntype PaddedByteLen struct {\n\tPrefix          string\n\tMsgByteLen      int\n\tExpectedByteLen int\n}\n\nfunc (e PaddedByteLen) Error() string {\n\treturn fmt.Sprintf(\"%s: padded byte length %d is not equal to expected %d\", e.Prefix, e.MsgByteLen, e.ExpectedByteLen)\n}\n\ntype PaddedBitLen struct {\n\tPrefix         string\n\tMsgBitLen      int\n\tExpectedBitLen int\n}\n\nfunc (e PaddedBitLen) Error() string {\n\treturn fmt.Sprintf(\"%s: padded bit length %d is not equal to expected %d\", e.Prefix, e.MsgBitLen, e.ExpectedBitLen)\n}\n\ntype ElementLessThanOne struct {\n\tPrefix string\n}\n\nfunc (e ElementLessThanOne) Error() string {\n\treturn fmt.Sprintf(\"%s: element value is less than 1\", e.Prefix)\n}\n"
  },
  {
    "path": "core/math/group/internal/internal.go",
    "content": "// Package internal provides group internal low-level functionalities.\npackage internal\n\nimport (\n\t\"errors\"\n\n\t\"tivi.io/core/math/group\"\n)\n\n// CastGroupTo casts group.Group to T.\nfunc CastGroupTo[T any](G group.Group) (T, error) {\n\tvar g T\n\tvar ok bool\n\tif g, ok = G.(T); !ok {\n\t\treturn g, errors.New(\"group/internal: cannot cast group.Group to T type\")\n\t}\n\treturn g, nil\n}\n\n// CastElementTo casts group.Element to T.\nfunc CastElementTo[T any](E group.Element) (T, error) {\n\tvar e T\n\tvar ok bool\n\tif e, ok = E.(T); !ok {\n\t\treturn e, errors.New(\"group/internal: cannot cast group.Element to T type\")\n\t}\n\treturn e, nil\n}\n"
  },
  {
    "path": "core/math/group/modqp/element.go",
    "content": "package modqp\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\ntype modqPElement struct {\n\tg     *modqPGroup\n\tvalue *big.Int\n}\n\n// Inverse returns E2 that satisfies\n//\n//\tE * E2 = 1\nfunc (E *modqPElement) Inverse() group.Element { //nolint:revive\n\tE2 := E.g.identity()\n\n\tE2.value.ModInverse(E.value, E.g.fieldOrder)\n\n\treturn E2\n}\n\n// Scale returns\n//\n//\tE ^ s\nfunc (E *modqPElement) Scale(s *group.Scalar) (group.Element, error) { //nolint:revive\n\tE2 := E.g.identity()\n\n\tif s.Modulo().Cmp(E.g.groupOrder) != 0 {\n\t\treturn nil, internal.ScalarModCmpOrder{Prefix: logPrefix}\n\t}\n\n\tE2.value.Exp(E.value, s.Value(), E.g.fieldOrder)\n\n\treturn E2, nil\n}\n\n// Op returns\n//\n//\tE * E2\nfunc (E *modqPElement) Op(E2 group.Element) (group.Element, error) {\n\tE3, err := internal.CastElementTo[*modqPElement](E2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = E.g.Equal(E3.g)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tE4 := E.g.identity()\n\tE4.value.Mul(E.value, E3.value)\n\tE4.value.Mod(E4.value, E4.g.fieldOrder)\n\n\treturn E4, nil\n}\n\n// Equal returns nil if E == E2.\nfunc (E *modqPElement) Equal(E2 group.Element) error {\n\tE3, err := internal.CastElementTo[*modqPElement](E2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = E.g.Equal(E3.g); err != nil {\n\t\treturn err\n\t}\n\n\tif E.value.Cmp(E3.value) != 0 {\n\t\treturn fmt.Errorf(\"%s: different element values\", logPrefix)\n\t}\n\n\treturn nil\n}\n\n// Decode decodes a message from group element.\nfunc (E *modqPElement) Decode() (padded []byte, err error) { //nolint:revive\n\t// Ensure that this group element actually belongs to the correct group.\n\tif err := E.g.IsGroupElement(E); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// All group elements whose value > ModqP group generator order are considered to be quadratic non-residues\n\tdecoded := new(big.Int).Set(E.value)\n\tif E.value.Cmp(E.g.Order()) > 0 {\n\t\t// Make a value to be a quadratic residue\n\t\tdecoded.Sub(E.g.FieldOrder(), E.value)\n\t}\n\n\tmsgByteLen := (decoded.BitLen() + 7) / 8\n\tmaxByteLen := E.g.maxPaddedMsgByteLen() - 1 //((E.g.Order().BitLen() + 7) / 8) - 1\n\t// Ensure that group element is padded up until Sophie-Germain q value bytelen\n\t// -1 since padding header 0x00 byte is stripped by big.Int\n\tif msgByteLen != maxByteLen {\n\t\treturn nil, internal.PaddedByteLen{Prefix: logPrefix, MsgByteLen: msgByteLen, ExpectedByteLen: maxByteLen}\n\t}\n\n\tpadded = make([]byte, E.g.maxPaddedMsgByteLen())\n\tdecoded.FillBytes(padded)\n\treturn\n}\n\n// Marshal ASN.1 marshals group element as\n//\n//\tE ::= INTEGER\n//\n// where INTEGER is group element value\nfunc (E *modqPElement) Marshal() (der asn_1.DER, err error) { //nolint:revive\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1BigInt(E.value)\n\n\treturn builder.Bytes()\n}\n"
  },
  {
    "path": "core/math/group/modqp/group.go",
    "content": "package modqp\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\ntype modqPGroup struct {\n\tname      string\n\tgenerator *big.Int\n\t// groupOrder is Scalar modulo.\n\tgroupOrder *big.Int\n\t// fieldOrder is Element modulo.\n\tfieldOrder *big.Int\n}\n\nfunc (G *modqPGroup) Name() string {\n\treturn G.name\n}\n\n// Order returns Sophie-Germain prime value q.\nfunc (G *modqPGroup) Order() *big.Int {\n\treturn new(big.Int).Set(G.groupOrder)\n}\n\n// FieldOrder returns safe prime value p.\nfunc (G *modqPGroup) FieldOrder() *big.Int {\n\treturn new(big.Int).Set(G.fieldOrder)\n}\n\n// Identity returns 1.\nfunc (G *modqPGroup) Identity() group.Element {\n\treturn G.identity()\n}\n\n// identity returns 1.\n//\n// identity is a helper method for Identity, in order to\n// return concrete implementation modqPElement and not group.Element interface.\nfunc (G *modqPGroup) identity() *modqPElement {\n\treturn &modqPElement{\n\t\tg:     G,\n\t\tvalue: big.NewInt(1),\n\t}\n}\n\n// Generator returns group generator value g, most likely 2.\nfunc (G *modqPGroup) Generator() group.Element {\n\treturn &modqPElement{\n\t\tg:     G,\n\t\tvalue: new(big.Int).Set(G.generator),\n\t}\n}\n\nfunc (G *modqPGroup) Equal(G2 group.Group) error {\n\tif G.Order().Cmp(G2.Order()) != 0 {\n\t\treturn internal.DifferentOrder{Prefix: logPrefix}\n\t}\n\n\tif G.FieldOrder().Cmp(G2.FieldOrder()) != 0 {\n\t\treturn internal.DifferentFieldOrder{Prefix: logPrefix}\n\t}\n\n\tG3, err := internal.CastGroupTo[*modqPGroup](G2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif G.generator.Cmp(G3.generator) != 0 {\n\t\treturn fmt.Errorf(\"%s: groups have different generators\", logPrefix)\n\t}\n\n\treturn nil\n}\n\n// PadBytes pads a data into a byte slice that looks like:\n//\n//\t{0x00 0x01 0xFF 0xFF ... 0xFF 0x00 X X ... X}\n//\n// where 0x00, 0x01 are required and 0xFF are optional bytes.\n// X X ... X denotes msg bytes, which are filled up until padded msg end.\nfunc (G *modqPGroup) PadBytes(msg []byte) ([]byte, error) {\n\t// Max msg bytelen that can be padded\n\tmaxMsgByteLen := G.maxPaddedMsgByteLen()\n\n\t// 3 bytes are reserved for padding head 0x00 0x01 and end 0x00 bytes\n\tif len(msg) > maxMsgByteLen-2-1 {\n\t\treturn nil, internal.MessageByteLenTooLarge{Prefix: logPrefix, MsgByteLen: len(msg), MaxMsgByteLen: maxMsgByteLen - 2 - 1}\n\t}\n\n\tpadded := make([]byte, maxMsgByteLen)\n\n\t// Padding head 0x00 and 0x01 bytes\n\tpadded[0] = 0x00\n\tpadded[1] = 0x01\n\t// Calculate how many 0xFF bytes will be inserted between padding head and end bytes\n\tpaddingEnd := maxMsgByteLen - 1 - len(msg)\n\t// Padding end 0x00 byte\n\tpadded[paddingEnd] = 0x00\n\n\t// Fill slice with 0xFF padding bytes, right after padding head and up until padding end bytes\n\tfor i := range padded[2:paddingEnd] {\n\t\tpadded[2+i] = 0xFF\n\t}\n\n\t// Copy msg bytes right after padding end 0x00 byte\n\tcopy(padded[maxMsgByteLen-len(msg):], msg)\n\n\treturn padded, nil\n}\n\n// PadBits uses bit padding schema, instead of byte padding as done in Pad.\n// Bit padding is more efficient, see group.PadBits for details.\nfunc (G *modqPGroup) PadBits(msg []byte) (padded []byte, err error) {\n\treturn group.PadBits(msg, uint64(G.groupOrder.BitLen()))\n}\n\n// UnpadBytes strips padding bytes from padded and returns initial message.\nfunc (G *modqPGroup) UnpadBytes(padded []byte) ([]byte, error) {\n\tif len(padded) != G.maxPaddedMsgByteLen() {\n\t\treturn nil, internal.PaddedByteLen{Prefix: logPrefix, MsgByteLen: len(padded), ExpectedByteLen: G.maxPaddedMsgByteLen()}\n\t}\n\n\tif padded[0] != 0x00 {\n\t\treturn nil, fmt.Errorf(\"%s: first byte %x is not equal to expected 0x00\", logPrefix, padded[0])\n\t}\n\n\tif padded[1] != 0x01 {\n\t\treturn nil, fmt.Errorf(\"%s: second byte %x is not equal to expected 0x01\", logPrefix, padded[1])\n\t}\n\n\t// Loop exactly until 0x00 padding end byte found,\n\t// or until there are no bytes left to read from padded\n\tfor i, p := range padded[2:] {\n\t\tswitch p {\n\t\tcase 0xFF: // skip\n\t\tcase 0x00: // end of padding\n\t\t\treturn padded[i+2+1:], nil\n\t\tdefault:\n\t\t\treturn nil, internal.UnexpectedPaddingByte{Prefix: logPrefix, Byte: p, Index: i}\n\t\t}\n\t}\n\n\treturn nil, internal.NoEncodedMessage{Prefix: logPrefix}\n}\n\n// UnpadBits unpads a message that was padded in PadBits, see group.UnpadBits\n// for details.\nfunc (G *modqPGroup) UnpadBits(padded []byte) (msg []byte, err error) {\n\treturn group.UnpadBits(padded)\n}\n\nfunc (G *modqPGroup) IsGroupElement(E group.Element) error {\n\tE1, err := internal.CastElementTo[*modqPElement](E)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = G.Equal(E1.g); err != nil {\n\t\treturn err\n\t}\n\n\tif E1.value.Cmp(big.NewInt(0)) < 1 {\n\t\treturn internal.ElementLessThanOne{Prefix: logPrefix}\n\t}\n\n\tif E1.value.Cmp(G.fieldOrder) > 0 {\n\t\treturn fmt.Errorf(\"%s: element > safe prime p value\", logPrefix)\n\t}\n\n\tif big.Jacobi(E1.value, G.fieldOrder) != 1 {\n\t\treturn fmt.Errorf(\"%s: element  is not a quadratic residue\", logPrefix)\n\t}\n\n\treturn nil\n}\n\n// Encode encodes msg into group element. Msg can be any value from a [1, Order()] range.\n// After encoding, all group elements from a (Order(), FieldOrder()) range are quadratic\n// non-residues, and group elements from a [1, Order()] range are quadratic residues.\nfunc (G *modqPGroup) Encode(msg []byte) (group.Element, error) {\n\tE := new(big.Int).SetBytes(msg)\n\n\tif E.Cmp(big.NewInt(0)) < 1 {\n\t\treturn nil, internal.ElementLessThanOne{Prefix: logPrefix}\n\t}\n\n\tif E.Cmp(G.groupOrder) > 0 {\n\t\treturn nil, fmt.Errorf(\"%s: padded message value cannot be greater than generator order\", logPrefix)\n\t}\n\n\t// Check if E is a quadratic residue\n\tswitch big.Jacobi(E, G.fieldOrder) {\n\tcase 0:\n\t\t// Should never happen, since we check on 0\n\t\treturn nil, fmt.Errorf(\"%s: padded message value is 0\", logPrefix)\n\tcase -1:\n\t\t// E is not a quadratic residue.\n\t\t// Subtracting also means that now E becomes > group generator order,\n\t\t// which therefore means that all group element which are > group generator\n\t\t// order are quadratic non-residues, and those which value is <= group\n\t\t// generator order are quadratic residues\n\t\tE.Sub(G.fieldOrder, E)\n\t}\n\n\t// Guaranteed to be quadratic residue\n\tE2 := G.identity()\n\tE2.value.Set(E)\n\n\treturn E2, nil\n}\n\n// ElementOf ASN.1 unmarshalls group element and ensures that it belongs to the group.\nfunc (G *modqPGroup) ElementOf(der asn_1.DER) (group.Element, error) {\n\tinteger := cryptobyte.String(der)\n\tE := new(big.Int)\n\n\tif !integer.ReadASN1Integer(E) {\n\t\treturn nil, internal.NotASN1Integer{Prefix: logPrefix}\n\t}\n\n\tif !integer.Empty() {\n\t\treturn nil, internal.ASN1TrailingBytes{Prefix: logPrefix}\n\t}\n\n\tE2 := G.identity()\n\tE2.value.Set(E)\n\tE2.value.Mod(E2.value, E2.g.fieldOrder)\n\n\t// We must ensure that element actually belongs to this group\n\tif err := G.IsGroupElement(E2); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn E2, nil\n}\n\n// maxPaddedMsgByteLen returns non-inclusive upper limit of a maximum bytes\n// length that padded message may have to be encoded into a ModqP group element.\nfunc (G *modqPGroup) maxPaddedMsgByteLen() int {\n\treturn (G.groupOrder.BitLen() + 7) / 8\n}\n"
  },
  {
    "path": "core/math/group/modqp/group_test.go",
    "content": "package modqp\n\nimport (\n\t\"bytes\"\n\t\"math/big\"\n\t\"reflect\"\n\t\"testing\"\n\t\"tivi.io/core/math/group\"\n\t\"tivi.io/core/math/group/internal\"\n)\n\nfunc init() {\n\tname := \"RFC3526ModPGroup3072\"\n\tvar err error\n\tmodqPGroup3072, err = group.Get(name)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nvar modqPGroup3072 group.Group\n\nvar modqPGroup23 = &modqPGroup{name: \"RFC3526ModPGroup23\", groupOrder: big.NewInt(11), fieldOrder: big.NewInt(23), generator: big.NewInt(2)}\n\nvar positive3072Msgs = [][]byte{\n\tgenerateBytes(384-2-1, 0xFF),\n\tgenerateBytes(384-2-1, 0x00),\n\t{},\n\t[]byte(\"\"),\n\t[]byte(\"0000.101\"),\n\t[]byte(\"Candidate\\x1FParty\\x1FLogo\\x1FGünnar Metsäpuu\"),\n\t{0x00},\n\t[]byte(\"1111.2018\"),\n}\n\nfunc generateBytes(size int, b byte) []byte {\n\tpadding := make([]byte, size)\n\t// Fill the array with some values (e.g., index values)\n\tfor i := range size {\n\t\tpadding[i] = b\n\t}\n\treturn padding\n}\n\nfunc TestGroupEqualDifferentOrder(t *testing.T) {\n\tt.Log(\"Equality checks should fail for different groups, but succeed for the same ones\")\n\n\tvar err error\n\n\tif err = modqPGroup3072.Equal(modqPGroup23); err == nil {\n\t\tt.Fatal(err)\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.DifferentOrder{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.DifferentOrder{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupEqualDifferentFieldOrder(t *testing.T) {\n\tt.Log(\"Equality checks should fail for different groups, but succeed for the same ones\")\n\n\tvar err error\n\n\tmodqPGroup23.groupOrder = modqPGroup3072.Order()\n\n\tif err = modqPGroup3072.Equal(modqPGroup23); err == nil {\n\t\tt.Fatal(err)\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.DifferentFieldOrder{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.DifferentFieldOrder{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupEqualDifferentGenerator(t *testing.T) {\n\tt.Log(\"Equality checks should fail for different groups, but succeed for the same ones\")\n\n\tvar err error\n\n\tmodqPGroup23.groupOrder = modqPGroup3072.Order()\n\tmodqPGroup23.fieldOrder = modqPGroup3072.FieldOrder()\n\tmodqPGroup23.generator = big.NewInt(1)\n\tif err = modqPGroup3072.Equal(modqPGroup23); err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestGroupPadBytes3072(t *testing.T) {\n\tfor _, padded := range positive3072Msgs {\n\t\tpadded2, err := modqPGroup3072.PadBytes(padded)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(padded2) != 384 {\n\t\t\tt.Fatalf(\"expected %d bytelen, got %d\", 384, len(padded2))\n\t\t}\n\t}\n\n\tpadded := generateBytes(382, 0xFF)\n\n\t_, err := modqPGroup3072.PadBytes(padded)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\") // too large\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.MessageByteLenTooLarge{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.MessageByteLenTooLarge{}, reflect.TypeOf(err))\n\t}\n}\n\nfunc TestGroupUnpadBytes3072(t *testing.T) {\n\tfor _, msg := range positive3072Msgs {\n\t\tpadded2, err := modqPGroup3072.PadBytes(msg)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(padded2) != 384 {\n\t\t\tt.Fatalf(\"expected %d bytelen, got %d\", 384, len(padded2))\n\t\t}\n\t\tunpadded, err := modqPGroup3072.UnpadBytes(padded2)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif !bytes.Equal(unpadded, msg) {\n\t\t\tt.Fatal(\"initial message not equal to unpadded\")\n\t\t}\n\n\t}\n\t// Grow by 1 byte\n\tpadded2 := generateBytes(385, 0xFF)\n\n\t_, err := modqPGroup3072.UnpadBytes(padded2)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.PaddedByteLen{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.PaddedByteLen{}, reflect.TypeOf(err))\n\t}\n\n\tpadded2 = padded2[:len(padded2)-1]\n\tpadded2[0] = 0x01\n\n\t_, err = modqPGroup3072.UnpadBytes(padded2)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\n\tpadded2[0] = 0x00\n\tpadded2[1] = 0x00\n\n\t_, err = modqPGroup3072.UnpadBytes(padded2)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\n\tpadded2[1] = 0x01\n\tpadded2[2] = 0xFF\n\tpadded2[24] = 0x24\n\t_, err = modqPGroup3072.UnpadBytes(padded2)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.UnexpectedPaddingByte{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.UnexpectedPaddingByte{}, reflect.TypeOf(err))\n\t}\n\n\tpadded2[24] = 0xFF\n\t_, err = modqPGroup3072.UnpadBytes(padded2)\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\tif reflect.TypeOf(err) != reflect.TypeOf(internal.NoEncodedMessage{}) {\n\t\tt.Fatalf(\"expected error %v, but got %v\", internal.NoEncodedMessage{}, reflect.TypeOf(err))\n\t}\n}\n"
  },
  {
    "path": "core/math/group/modqp/modqp.go",
    "content": "// Package modqp represents multiplicative subgroup of quadratic residues of following RFC3526 groups:\n//\n//\t2048-bit\n//\t3072-bit\n//\t4096-bit\n//\t8192-bit\n//\n// Each subgroup contains only quadratic residue elements.\n//\n// For each subgroup, exponents (scalars) are in a range of [1, q), whereas elements are in [1, p) range.\npackage modqp\n\nconst (\n\t// logPrefix is a prefix for logging records.\n\tlogPrefix = \"group/modqp\"\n)\n\n// init registers ModqP groups in a group.registry.\nfunc init() {\n\tregisterWellKnownGroups()\n}\n"
  },
  {
    "path": "core/math/group/modqp/wellknown.go",
    "content": "package modqp\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n\t\"strings\"\n\n\t\"tivi.io/core/math\"\n\t\"tivi.io/core/math/group\"\n)\n\nvar wellknownGroups = map[string]map[string]string{\n\t\"RFC3526ModPGroup2048\": {\n\t\t`FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1\n\t\t29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD\n\t\tEF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245\n\t\tE485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED\n\t\tEE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D\n\t\tC2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F\n\t\t83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D\n\t\t670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B\n\t\tE39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9\n\t\tDE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510\n\t\t15728E5A 8AACAA68 FFFFFFFF FFFFFFFF`: \"2\",\n\t},\n\t\"RFC3526ModPGroup3072\": {\n\t\t`FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1\n\t\t29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD\n\t\tEF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245\n\t\tE485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED\n\t\tEE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D\n\t\tC2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F\n\t\t83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D\n\t\t670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B\n\t\tE39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9\n\t\tDE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510\n\t\t15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64\n\t\tECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7\n\t\tABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B\n\t\tF12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C\n\t\tBBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31\n\t\t43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF\n\t\t`: \"2\",\n\t},\n\t\"RFC3526ModPGroup4096\": {\n\t\t`FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1\n\t\t\t29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD\n\t\t\tEF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245\n\t\t\tE485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED\n\t\t\tEE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D\n\t\t\tC2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F\n\t\t\t83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D\n\t\t\t670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B\n\t\t\tE39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9\n\t\t\tDE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510\n\t\t\t15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64\n\t\t\tECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7\n\t\t\tABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B\n\t\t\tF12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C\n\t\t\tBBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31\n\t\t\t43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7\n\t\t\t88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA\n\t\t\t2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6\n\t\t\t287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED\n\t\t\t1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9\n\t\t\t93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199\n\t\t\tFFFFFFFF FFFFFFFF`: \"2\",\n\t},\n\t\"RFC3526ModPGroup8192\": {\n\t\t`FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1\n\t\t\t29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD\n\t\t\tEF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245\n\t\t\tE485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED\n\t\t\tEE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D\n\t\t\tC2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F\n\t\t\t83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D\n\t\t\t670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B\n\t\t\tE39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9\n\t\t\tDE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510\n\t\t\t15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64\n\t\t\tECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7\n\t\t\tABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B\n\t\t\tF12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C\n\t\t\tBBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31\n\t\t\t43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7\n\t\t\t88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA\n\t\t\t2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6\n\t\t\t287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED\n\t\t\t1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9\n\t\t\t93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492\n\t\t\t36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD\n\t\t\tF8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831\n\t\t\t179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B\n\t\t\tDB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF\n\t\t\t5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6\n\t\t\tD55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3\n\t\t\t23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA\n\t\t\tCC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328\n\t\t\t06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C\n\t\t\tDA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE\n\t\t\t12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4\n\t\t\t38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300\n\t\t\t741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568\n\t\t\t3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9\n\t\t\t22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B\n\t\t\t4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A\n\t\t\t062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36\n\t\t\t4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1\n\t\t\tB1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92\n\t\t\t4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47\n\t\t\t9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71\n\t\t\t60C980DD 98EDD3DF FFFFFFFF FFFFFFFF`: \"2\",\n\t},\n}\n\n// initWellKnownGroups registers wellknown ModqP groups in a registry.\n// Feel free to panic, if any error occurs as it refers to programming error.\nfunc registerWellKnownGroups() {\n\tfor name, wellknownGroup := range wellknownGroups {\n\t\tfor modulo, generator := range wellknownGroup {\n\t\t\trepr := strings.Join(strings.Fields(modulo), \"\")\n\t\t\tmod, ok := new(big.Int).SetString(repr, 16)\n\t\t\tif !ok {\n\t\t\t\tpanic(fmt.Sprintf(\"invalid base-16 group modulo: %v\", repr))\n\t\t\t}\n\n\t\t\tgen, ok := new(big.Int).SetString(generator, 16)\n\t\t\tif !ok {\n\t\t\t\tpanic(fmt.Sprintf(\"invalid base-16 group generator: %v\", generator))\n\t\t\t}\n\n\t\t\t// mod is a safe prime, but for ModqP group we need Sophie-Germain prime\n\t\t\tq := math.SophieGermainPrime(mod)\n\n\t\t\tg := &modqPGroup{\n\t\t\t\tname:       name,\n\t\t\t\tgenerator:  gen,\n\t\t\t\tgroupOrder: q,\n\t\t\t\tfieldOrder: mod,\n\t\t\t}\n\n\t\t\tgroup.Register(g)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "core/math/group/padding.go",
    "content": "package group\n\nimport (\n\t\"fmt\"\n\t\"math/big\"\n)\n\n// toBytes converts *big.Int msg to raw bytes, preserving all leading zero bits.\nfunc toBytes(msg *big.Int) (raw []byte, err error) {\n\t// Empty message == empty byte slice\n\tif msg.BitLen() == 0 {\n\t\traw = []byte{}\n\t} else {\n\t\traw = msg.Bytes()\n\t}\n\n\t// From bits to bytes\n\tmsgByteLen := (msg.BitLen() + 7) / 8\n\tresult := make([]byte, msgByteLen)\n\n\tif len(raw) > 0 {\n\t\tcopy(result[msgByteLen-len(raw):], raw)\n\t}\n\n\treturn result, nil\n}\n\n// PadBits pads msg up until (group field bitlen - 2 - 1).\n//\n// Padded msg looks like:\n//\n//\t0b0111... 0b110XX...XXX\n//\n// where first 0 and 1 bits are padding header, following by padding 1 bits,\n// and after padding 1 bits there is a 0 padding end bit. X bits\n// denote msg bits which are filled up until the end.\n//\n// NB! Currently only supported by ModqP and ECqP groups.\n//\n// The main idea behind PadBits is that message is padded by bits\n// which therefore leaves no empty space in padded message as it would\n// be with traditional byte padding, where message is byte-aligned.\nfunc PadBits(msg []byte, fieldBitLen uint64) (padded []byte, err error) {\n\t// -2 to reserve padding header bits\n\t// -1 to reserve padding end bit\n\tmsgMaxBitLen := fieldBitLen - 2 - 1\n\n\tmsgInt := new(big.Int).SetBytes(msg)\n\n\t// 3 bits are reserved, while other bits can be occupied by msg\n\tif uint64(msgInt.BitLen()) > msgMaxBitLen {\n\t\treturn nil, fmt.Errorf(\"math/group: msg bit length %v is larger than allowed %v\", msgInt.BitLen(), msgMaxBitLen)\n\t}\n\n\t// Padding (header and end bits included) bits occupy the rest space that msg bits doesn't\n\tpaddingBitLen := msgMaxBitLen - uint64(msgInt.BitLen())\n\n\tpaddedInt := big.NewInt(1)\n\n\t// +2 to allocate padding header 0 and 1 bits, but\n\t// currently it looks like 0b1000..., so you can\n\t// see that first padding header bit is not 0\n\tpaddedInt.Lsh(paddedInt, uint(paddingBitLen)+2)\n\n\t// This clever trick does 3 things:\n\t// a) sets padding header bits to 0b01\n\t// b) sets padding bits, i.e. 0b011111...\n\t// c) sets padding end bit to 0, i.e. 0b011111...0\n\tpaddedInt.Sub(paddedInt, big.NewInt(2))\n\n\t// Allocates exactly msg bitlen bits\n\tpaddedInt.Lsh(paddedInt, uint(msgInt.BitLen()))\n\n\t// Sets all msg bits to the allocated space\n\tpaddedInt.Or(paddedInt, msgInt)\n\n\t// Returns byte slice with preserved leading zero bits\n\treturn toBytes(paddedInt)\n}\n\n// UnpadBits strips bit padding from padded message.\n//\n// NB! Currently only supported by ModqP and ECqP groups.\nfunc UnpadBits(padded []byte) (msg []byte, err error) {\n\tunpadded := new(big.Int).SetBytes(padded)\n\n\t// Most-significant (padding header 1 bit) bit should be 1, since padding header 0 bit is stripped by big.Int\n\tisMsbSet := unpadded.Bit(unpadded.BitLen() - 1)\n\n\tif isMsbSet != 1 {\n\t\treturn nil, fmt.Errorf(\"math/group: padding header is not 0b01\")\n\t}\n\n\t// Mask 0b0111...0, not to override existing bits in padding message\n\tmask := big.NewInt(1)\n\tmask.Lsh(mask, uint(unpadded.BitLen()))\n\tmask.Sub(mask, big.NewInt(1))\n\n\t// If we apply boolean NOT on a padded message, it will\n\t// mirror padding bits to be 0b1000...1, instead of initial 0b0111...0\n\tunpadded.Not(unpadded)\n\n\t// Now we apply 0b0111...0 mask, that will clear all header and padding bits,\n\t// only padding end bit will survive\n\tunpadded.And(unpadded, mask)\n\n\t// Now we check that padding end bit, inverted, is indeed 1\n\tisMsbSet = unpadded.Bit(unpadded.BitLen() - 1)\n\n\tif isMsbSet != 1 {\n\t\treturn nil, fmt.Errorf(\"math/group: padding end bit is not 1\")\n\t}\n\n\t// Now we create another mask to clear all 0 bits from a padded message,\n\t// which is currently has a form of 0b1XXX...X, where 1 is a padding end bit\n\t// and X are inverted message bits\n\tmask = big.NewInt(1)\n\tmask.Lsh(mask, uint(unpadded.BitLen()))\n\tmask.Sub(mask, big.NewInt(1))\n\n\t// Boolean NOT will invert padding end bit back to the original 0 value,\n\t// as well as message bits\n\tunpadded.Not(unpadded)\n\tunpadded.And(unpadded, mask)\n\n\t// Padding end bit is stripped by big.Int and only message bits are left behind\n\treturn toBytes(unpadded)\n}\n"
  },
  {
    "path": "core/math/group/registry.go",
    "content": "package group\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n)\n\n// registry is a map with sync.RWMutex. And its purpose is to hold all group\n// implementations.\n//\n// sync.Map itself is optimized for get operations. It is assumed that\n// Register will be infrequent, otherwise no advantage of using sync.Map.\nvar registry = new(sync.Map)\n\n// Register registers a group implementation in a registry.\nfunc Register(G Group) {\n\tregistry.Store(G.Name(), G)\n}\n\n// Get gets a group implementation by its name.\n// NB! Caller should have 'all' package imported as:\n//\n//\t_ import \"tivi.io/core/math/group/all\nfunc Get(name string) (Group, error) {\n\tG, ok := registry.Load(name)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"math/group: unknown group: %s\", name)\n\t}\n\n\t// Safe to cast, g is checked at compile time\n\treturn G.(Group), nil\n}\n\n// All returns all registered group implementations.\nfunc All() []Group {\n\tgroups := make([]Group, 0)\n\n\tregistry.Range(func(_, G any) bool {\n\t\tgroups = append(groups, G.(Group))\n\t\t// If you `return false`, then Range will stop and exit\n\t\treturn true\n\t})\n\n\treturn groups\n}\n"
  },
  {
    "path": "core/math/group/scalar.go",
    "content": "package group\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n)\n\n// Scalar is an immutable scalar of an underlying group, all scalar operations\n// are performed modulo group generator order.\ntype Scalar struct {\n\tvalue *big.Int\n\t// modulo is a group generator order.\n\tmodulo *big.Int\n}\n\n// NewScalar returns new Scalar of val.\n// NB! If mod is <= 0, then behaviour of Scalar is undefined.\nfunc NewScalar(val, mod *big.Int) *Scalar {\n\treturn &Scalar{\n\t\tvalue:  new(big.Int).Set(val),\n\t\tmodulo: new(big.Int).Set(mod),\n\t}\n}\n\n// ZeroScalar returns new group scalar with value=0.\nfunc ZeroScalar(mod *big.Int) *Scalar {\n\treturn NewScalar(new(big.Int), mod)\n}\n\n// OneScalar returns new group scalar with value=1.\nfunc OneScalar(mod *big.Int) *Scalar {\n\treturn NewScalar(big.NewInt(1), mod)\n}\n\n// RandomScalar returns new random group scalar with value=[1,mod).\n// Randomness is obtained via rand.Reader.\n// See ScalarValueOfReader if you prefer custom randomness source.\nfunc RandomScalar(mod *big.Int) (*Scalar, error) {\n\treturn ScalarValueOfReader(rand.Reader, mod)\n}\n\n// Value returns value of a group scalar.\nfunc (s *Scalar) Value() *big.Int {\n\treturn new(big.Int).Set(s.value)\n}\n\n// Modulo returns modulo of a group scalar.\nfunc (s *Scalar) Modulo() *big.Int {\n\treturn new(big.Int).Set(s.modulo)\n}\n\n// Add returns\n//\n//\tthis + s2\nfunc (s *Scalar) Add(s2 *Scalar) (*Scalar, error) {\n\terr := s.equalMod(s2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts3 := NewScalar(s.value, s.modulo)\n\ts3.value.Add(s3.value, s2.value)\n\ts3.value.Mod(s3.value, s3.modulo)\n\n\treturn s3, nil\n}\n\n// Sub returns\n//\n//\tthis - s2\nfunc (s *Scalar) Sub(s2 *Scalar) (*Scalar, error) {\n\terr := s.equalMod(s2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts3 := NewScalar(s.value, s.modulo)\n\ts3.value.Sub(s3.value, s2.value)\n\ts3.value.Mod(s3.value, s3.modulo)\n\n\treturn s3, nil\n}\n\n// Mul returns\n//\n//\tthis * s2\nfunc (s *Scalar) Mul(s2 *Scalar) (*Scalar, error) {\n\terr := s.equalMod(s2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts3 := NewScalar(s.value, s.modulo)\n\ts3.value.Mul(s3.value, s2.value)\n\ts3.value.Mod(s3.value, s3.modulo)\n\n\treturn s3, nil\n}\n\n// Exp returns\n//\n//\tthis ^ e\nfunc (s *Scalar) Exp(e *big.Int) *Scalar { //nolint:revive\n\ts2 := NewScalar(s.value, s.modulo)\n\ts2.value.Exp(s2.value, e, s2.modulo)\n\n\treturn s2\n}\n\n// Negate returns\n//\n//\t-this\nfunc (s *Scalar) Negate() *Scalar { //nolint:revive\n\ts2 := NewScalar(s.value, s.modulo)\n\ts2.value.Sub(s2.modulo, s2.value)\n\ts2.value.Mod(s2.value, s2.modulo)\n\n\treturn s2\n}\n\n// Inverse returns s2 such that condition is met\n//\n//\tthis * s2 = 1\nfunc (s *Scalar) Inverse() *Scalar { //nolint:revive\n\ts2 := NewScalar(s.value, s.modulo)\n\ts2.value.ModInverse(s2.value, s2.modulo)\n\n\treturn s2\n}\n\n// Equal return nil if this == s2.\nfunc (s *Scalar) Equal(s2 *Scalar) error {\n\terr := s.equalMod(s2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.value.Cmp(s2.value) != 0 {\n\t\treturn fmt.Errorf(\"math/group: different scalar values\")\n\t}\n\n\treturn nil\n}\n\n// Returns nil if this.modulo == s2.modulo.\nfunc (s *Scalar) equalMod(s2 *Scalar) error {\n\tif s.modulo.Cmp(s2.modulo) != 0 {\n\t\treturn fmt.Errorf(\"math/group: different scalar modulos\")\n\t}\n\n\treturn nil\n}\n\n// ScalarValueOfReader reads a value from r, then returns a new group scalar with\n// value=[1, mod). The value is deterministic unless r is randomness source.\nfunc ScalarValueOfReader(r io.Reader, mod *big.Int) (*Scalar, error) {\n\tif mod.Cmp(new(big.Int).SetInt64(0)) != 1 {\n\t\treturn nil, fmt.Errorf(\"math/group: scalar mod <= 0\")\n\t}\n\n\t// If mod <= 0, then rand.Int will panic.\n\t// val=[0,mod)\n\tval, err := rand.Int(r, mod)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Don't allow val=0\n\tif val.Cmp(new(big.Int).SetInt64(0)) == 0 {\n\t\tval = new(big.Int).SetInt64(1)\n\t}\n\n\treturn NewScalar(val, mod), nil\n}\n\n// Marshal ASN.1 marshals group scalar as\n//\n//\ts ::= INTEGER\n//\n// where INTEGER is a group scalar value.\nfunc (s *Scalar) Marshal() (der asn_1.DER, err error) { //nolint:revive\n\tvar builder cryptobyte.Builder\n\tbuilder.AddASN1BigInt(s.value)\n\n\treturn builder.Bytes()\n}\n\n// UnmarshalScalar ASN.1 unmarshalls group scalar.\nfunc UnmarshalScalar(der asn_1.DER, mod *big.Int) (*Scalar, error) {\n\tinteger := cryptobyte.String(der)\n\tval := new(big.Int)\n\n\tif !integer.ReadASN1Integer(val) {\n\t\treturn nil, fmt.Errorf(\"math/group: scalar is not an ASN.1 INTEGER\")\n\t}\n\n\tif !integer.Empty() {\n\t\treturn nil, fmt.Errorf(\"math/group: incomplete ASN.1 scalar\")\n\t}\n\n\tif val.Cmp(mod) >= 0 {\n\t\treturn nil, fmt.Errorf(\"math/group: scalar >= mod\")\n\t}\n\n\treturn NewScalar(val, mod), nil\n}\n"
  },
  {
    "path": "core/math/group/scalar_test.go",
    "content": "package group\n\nimport (\n\t\"bytes\"\n\t\"math/big\"\n\t\"testing\"\n)\n\nfunc TestAddScalar(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(3)\n\tmod2 := new(big.Int).SetInt64(3)\n\n\ts1 := OneScalar(mod1)\n\ts2 := OneScalar(mod2)\n\n\t// 1 + 1 = 2\n\ts3, err := s1.Add(s2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpected := new(big.Int).SetInt64(2)\n\n\tif s3.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s3=%d, got=%d\\n\", expected, s3.value)\n\t}\n}\n\nfunc TestSubScalar(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(3)\n\tmod2 := new(big.Int).SetInt64(3)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(3), mod1)\n\ts2 := NewScalar(new(big.Int).SetInt64(2), mod2)\n\n\t// 3 - 2 = 1\n\ts3, err := s1.Sub(s2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpected := new(big.Int).SetInt64(1)\n\n\tif s3.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s3=%d, got=%d\\n\", expected, s3.value)\n\t}\n}\n\nfunc TestMulScalar(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\tmod2 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\ts2 := NewScalar(new(big.Int).SetInt64(3), mod2)\n\n\t// 3 * 2 = 6\n\ts3, err := s1.Mul(s2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpected := new(big.Int).SetInt64(6)\n\n\tif s3.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s3=%d, got=%d\\n\", expected, s3.value)\n\t}\n}\n\nfunc TestExp(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\n\texp := new(big.Int).SetInt64(3)\n\n\t// 2^3 = 8\n\t// 8 mod 7 = 1\n\ts3 := s1.Exp(exp)\n\n\texpected := new(big.Int).SetInt64(1)\n\n\tif s3.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s3=%d, got=%d\\n\", expected, s3.value)\n\t}\n}\n\nfunc TestNegate(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\n\t// -2 modulo 7 = 5, since 7 - 2 = 5\n\ts2 := s1.Negate()\n\n\texpected := new(big.Int).SetInt64(5)\n\n\tif s2.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s2=%d, got=%d\\n\", expected, s2.value)\n\t}\n}\n\nfunc TestInverse(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\n\ts2 := s1.Inverse()\n\n\t// 2 modinv 7 = 4, since 2*4 modulo 7 = 1\n\texpected := new(big.Int).SetInt64(4)\n\n\tif s2.value.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected s2=%d, got=%d\\n\", expected, s2.value)\n\t}\n}\n\nfunc TestEqual(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\tmod2 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\ts2 := NewScalar(new(big.Int).SetInt64(2), mod2)\n\n\terr := s1.Equal(s2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestEqualMod(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(1)\n\tmod2 := new(big.Int).SetInt64(1)\n\n\ts1 := ZeroScalar(mod1)\n\ts2 := ZeroScalar(mod2)\n\n\tif err := s1.equalMod(s2); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestEqualModDifferentMods(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(1)\n\tmod2 := new(big.Int).SetInt64(2)\n\n\ts1 := ZeroScalar(mod1)\n\ts2 := ZeroScalar(mod2)\n\n\tif err := s1.equalMod(s2); err == nil {\n\t\tt.Fatal(\"expected error, but got nil\")\n\t}\n}\n\nfunc TestScalarValueOfReader(t *testing.T) {\n\tmod1 := new(big.Int).SetInt64(7)\n\n\ts1 := NewScalar(new(big.Int).SetInt64(2), mod1)\n\n\t// Always returns 2\n\treader := bytes.NewReader(s1.value.Bytes())\n\n\ts3, err := ScalarValueOfReader(reader, s1.modulo)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif s3.value.Cmp(s1.value) != 0 {\n\t\tt.Fatalf(\"expected s1=%d to be equal to s3=%d\\n\", s1.value, s3.value)\n\t}\n}\n\nfunc TestScalarMarshalAndUnmarshalASN1(t *testing.T) {\n\tfor _, g := range All() {\n\t\tt.Run(g.Name(), func(t *testing.T) {\n\t\t\ts1, err := RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tder, err := s1.Marshal()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\ts2, err := UnmarshalScalar(der, g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\terr = s1.Equal(s2)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/math/math.go",
    "content": "package math\n\nimport (\n\t\"math/big\"\n)\n\n// SophieGermainPrime computes a value of\n//\n//\tq = (p - 1) / 2\nfunc SophieGermainPrime(p *big.Int) *big.Int {\n\tq := new(big.Int)\n\tq.Sub(p, new(big.Int).SetInt64(1))\n\tq.Div(q, new(big.Int).SetInt64(2))\n\treturn q\n}\n"
  },
  {
    "path": "core/math/math_test.go",
    "content": "package math\n\nimport (\n\t\"math/big\"\n\t\"testing\"\n)\n\nfunc TestSophieGermainPrime(t *testing.T) {\n\tp := new(big.Int).SetInt64(23)\n\tq := SophieGermainPrime(p) // (23 - 1) / 2\n\texpected := new(big.Int).SetInt64(11)\n\tif q.Cmp(expected) != 0 {\n\t\tt.Fatalf(\"expected Sophie-Germain prime to be=%d, got=%d\\n\", expected, q)\n\t}\n}\n"
  },
  {
    "path": "core/math/polynomial/polynomial.go",
    "content": "/*\nPackage polynomial provides an implementation of univariate algebraic\npolynomials. An univariate polynomial is a function in the form\n\n\tp(x) = a_0 + a_1 x + a_2 x^2 + ... + a_n x^n\n\nIn this case, we say that the degree of the polynomial p is n.\n\nBy evaluating the polynomial at a point x, we obtain the value\n\n\ty <- p(x)\n\nUsing Lagrange interpolation, it is possible to recover the polynomial if n+1\nvalues are known.\n*/\npackage polynomial\n\nimport (\n\t\"math/big\"\n\tasn_1 \"tivi.io/core/encoding/asn1\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/cryptobyte/asn1\"\n\n\t\"tivi.io/core/math/group\"\n)\n\n// Point is the polynomial point (x,y) == (x, p(x)).\ntype Point struct {\n\t// x\n\tX *group.Scalar\n\t// p(x)\n\tY *group.Scalar\n}\n\n// polynomialPoints is a helper type which you can use to transform from []Point\n// to []group.Scalar, and therefore being able to use xCoords() method to\n// obtain only Point.X coordinates of a polynomial.\ntype polynomialPoints []*Point\n\nfunc (points polynomialPoints) xCoords() []*group.Scalar {\n\txCoords := make([]*group.Scalar, len(points))\n\tfor i := range points {\n\t\txCoords[i] = points[i].X\n\t}\n\treturn xCoords\n}\n\n// ASN1Marshal marshals a polynomial point\n//\n//\te ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tINTEGER\n//\t}\n//\n// where first INTEGER is an x-coordinate and second is y-coordinate.\nfunc (e *Point) MarshalASN1() ([]byte, error) {\n\txcoord, err := e.X.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPointASN1MarshalXError{Err: err}\n\t}\n\n\tycoord, err := e.Y.Marshal()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPointASN1MarshalYError{Err: err}\n\t}\n\n\tvar c cryptobyte.Builder\n\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tc.AddBytes(xcoord)\n\t\tc.AddBytes(ycoord)\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPointError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// UnmarshalPoint unmarshalls polynomial point.\nfunc UnmarshalPoint(g group.Group, data []byte) (*Point, error) {\n\tc := cryptobyte.String(data)\n\tvar inner cryptobyte.String\n\tvar xB cryptobyte.String\n\tvar yB cryptobyte.String\n\n\tif !c.ReadASN1(&inner, asn1.SEQUENCE) {\n\t\treturn nil, UnmarshalPointReadASN1Error{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&xB, nil) {\n\t\treturn nil, UnmarshalPointReadAnyASN1ElementXError{}\n\t}\n\n\tif !inner.ReadAnyASN1Element(&yB, nil) {\n\t\treturn nil, UnmarshalPointReadAnyASN1ElementYError{}\n\t}\n\n\tx, err := group.UnmarshalScalar(asn_1.DER(xB), g.Order())\n\tif err != nil {\n\t\treturn nil, UnmarshalPointASN1UnmarshalScalarXError{Err: err}\n\t}\n\n\ty, err := group.UnmarshalScalar(asn_1.DER(yB), g.Order())\n\tif err != nil {\n\t\treturn nil, UnmarshalPointASN1UnmarshalScalarYError{Err: err}\n\t}\n\n\tif !c.Empty() || !inner.Empty() {\n\t\treturn nil, UnmarshalPointTrailingBytesError{Err: err}\n\t}\n\n\treturn &Point{\n\t\tX: x,\n\t\tY: y,\n\t}, nil\n}\n\ntype Polynomial struct {\n\tGroup group.Group\n\t//\t5x^3 + 6x^2 + 8x + 1\n\t//\n\t// where Coefficients = {5, 6, 8}\n\tCoefficients []*group.Scalar\n}\n\n// NewRandom generates a polynomial, that has exactly degree+1 points.\n// All polynomial coefficients are chosen randomly from a set of [0, p),\n// where p is the group order.\nfunc NewRandom(g group.Group, degree uint64) (*Polynomial, error) {\n\tcoeffs := make([]*group.Scalar, degree+1)\n\n\tfor i := range coeffs {\n\t\tcoeff, err := group.RandomScalar(g.Order())\n\t\tif err != nil {\n\t\t\treturn nil, NewRandomRandomScalarError{Err: err}\n\t\t}\n\t\tcoeffs[i] = coeff\n\t}\n\n\treturn &Polynomial{\n\t\tGroup:        g,\n\t\tCoefficients: coeffs,\n\t}, nil\n}\n\n// NewRandomWithConstant generates a new polynomial where coefficient with degree=0\n// is given by constant, e.g.\n//\n//\tp(x) = Ax^3 + Bx^2 + Cx + constant\n//\n// which in turn means that if x=0, then p(x) = constant.\n//\n// Rest of coefficients are chosen randomly from a set of [0, p), where p is\n// the group order.\nfunc NewRandomWithConstant(g group.Group, degree uint64, constant *group.Scalar) (*Polynomial, error) {\n\tcoeffs := make([]*group.Scalar, degree+1)\n\n\tcoeff := group.NewScalar(constant.Value(), constant.Modulo())\n\tcoeffs[0] = coeff\n\n\tfor i := 1; i < len(coeffs); i++ {\n\t\tcoeff, err := group.RandomScalar(g.Order())\n\t\tif err != nil {\n\t\t\treturn nil, NewRandomWithFreeRandomScalarError{Err: err}\n\t\t}\n\t\tcoeffs[i] = coeff\n\t}\n\n\treturn &Polynomial{\n\t\tGroup:        g,\n\t\tCoefficients: coeffs,\n\t}, nil\n}\n\n// Degree returns the degree of the polynomial.\nfunc (p *Polynomial) Degree() uint64 {\n\tif len(p.Coefficients) == 0 {\n\t\t// if no Coefficients, then assume that constant coefficient is zero\n\t\treturn 0\n\t}\n\treturn uint64(len(p.Coefficients) - 1)\n}\n\n// Coefficient returns the value of the i-th coefficient.\nfunc (p *Polynomial) Coefficient(i uint64) *group.Scalar {\n\tif i > p.Degree() {\n\t\treturn group.ZeroScalar(p.Group.Order())\n\t}\n\treturn p.Coefficients[i]\n}\n\n// Evaluate evaluates the polynomial\n//\n//\tp(x) = a_0 + a_1 * x^1 + a_2 * x^2 + ... + a_i * x^i\n//\n// Note that if x=0, then p(x) = a_0\nfunc (p *Polynomial) Evaluate(x *group.Scalar) (*Point, error) {\n\tif len(p.Coefficients) == 0 {\n\t\t// (x, a_0)\n\t\treturn &Point{X: x, Y: group.ZeroScalar(p.Group.Order())}, nil\n\t}\n\n\tvar err error\n\n\t// Initially it is x_i = x^0\n\tvar x_i *group.Scalar //nolint:revive\n\t// a_i * x_i\n\tvar mul_ai_xi *group.Scalar //nolint:revive\n\t// p(x) = a_0\n\tpX := group.NewScalar(p.Coefficients[0].Value(), p.Coefficients[0].Modulo())\n\n\t// p(x) = a_0 + a_1 * x^1 + a_2 * x^2 + ... + a_i * x^i\n\tfor i := uint64(1); i <= p.Degree(); i++ {\n\t\t// x^i\n\t\tx_i = x.Exp(new(big.Int).SetUint64(i))\n\n\t\t// a_i * x^i\n\t\tmul_ai_xi, err = x_i.Mul(p.Coefficients[i])\n\t\tif err != nil {\n\t\t\treturn nil, EvaluatePolynomialMulError{Err: err}\n\t\t}\n\n\t\t// p(x) = p(x) + (a_i * x^i)\n\t\tpX, err = pX.Add(mul_ai_xi)\n\t\tif err != nil {\n\t\t\treturn nil, EvaluatePolynomialAddError{Err: err}\n\t\t}\n\t}\n\n\treturn &Point{X: x, Y: pX}, nil\n}\n\n// ASN1Marshal marshals polynomial\n//\n//\tp ::= SEQUENCE {\n//\t\tINTEGER\n//\t\tINTEGER\n//\t\t...\n//\t}\n//\n// where INTEGER is a polynomial coefficient and amount of INTEGERs depending on\n// a polynomial degree.\nfunc (p *Polynomial) MarshalASN1() ([]byte, error) {\n\tcoeffs := make([][]byte, 0, len(p.Coefficients))\n\n\tfor _, coeff := range p.Coefficients {\n\t\tb, err := coeff.Marshal()\n\t\tif err != nil {\n\t\t\treturn nil, ASN1MarshalPolynomialASN1MarshalCoefficientError{Err: err}\n\t\t}\n\t\tcoeffs = append(coeffs, b)\n\t}\n\n\tvar c cryptobyte.Builder\n\n\tc.AddASN1(asn1.SEQUENCE, func(c *cryptobyte.Builder) {\n\t\tfor _, coeff := range coeffs {\n\t\t\tc.AddBytes(coeff)\n\t\t}\n\t})\n\n\tb, err := c.Bytes()\n\tif err != nil {\n\t\treturn nil, ASN1MarshalPolynomialError{Err: err}\n\t}\n\n\treturn b, nil\n}\n\n// UnmarshalPolynomial unmarshalls a polynomial.\nfunc UnmarshalPolynomial(g group.Group, data []byte) (*Polynomial, error) {\n\tvar coeffs []*group.Scalar\n\tc := cryptobyte.String(data)\n\tvar inner cryptobyte.String\n\n\tif !c.ReadASN1(&inner, asn1.SEQUENCE) {\n\t\treturn nil, UnmarshalPolynomialReadASN1Error{}\n\t}\n\tfor !inner.Empty() {\n\t\tvar coefdata cryptobyte.String\n\n\t\tif !inner.ReadAnyASN1Element(&coefdata, nil) {\n\t\t\treturn nil, UnmarshalPolynomialReadAnyASN1ElementError{}\n\t\t}\n\n\t\tcoeff, err := group.UnmarshalScalar(asn_1.DER(coefdata), g.Order())\n\t\tif err != nil {\n\t\t\treturn nil, UnmarshalPolynomialUnmarshalScalarError{Err: err}\n\t\t}\n\n\t\tcoeffs = append(coeffs, coeff)\n\t}\n\n\tif !c.Empty() {\n\t\treturn nil, UnmarshalPolynomialTrailingBytesError{}\n\t}\n\n\treturn &Polynomial{\n\t\tGroup:        g,\n\t\tCoefficients: coeffs,\n\t}, nil\n}\n\n// LagrangeBasisPolynomial checks whether polynomial passes x-coordinate by using\n// following Lagrange basis polynomial equation\n//\n//\tl_i(x) = (x - j_0) / (i - j_0) * (x - j_1) / (i - j_1) * ... * (x - j_n) / (i - j_n)\n//\n// where x is an x-coordinate of a point, for which we try to evaluate Lagrange\n// basis polynomial, i.e. to understand, whether that polynomial passes this\n// particular x-coordinate or not.\n//\n// i is an x-coordinate of a polynomial point that we try to evaluate Lagrange\n// basis polynomial for. Recall, we evaluate Lagrange basis polynomial for each\n// i, which means that we call this function exactly j_n times in order to\n// evaluate all i.\n//\n// j_0, j_1, ..., j_n are all x-coordinates where polynomial passes points through\nfunc LagrangeBasisPolynomial(g group.Group, x *group.Scalar, points []*group.Scalar, i *group.Scalar) (*group.Scalar, error) {\n\tvar err error\n\tvar l_i *group.Scalar //nolint:revive\n\t// At start, (x - j_0) == (x - j_n)\n\tnom := group.OneScalar(g.Order())\n\t// At start, (i - j_0) == (i - j_n)\n\tdenom := group.OneScalar(g.Order())\n\n\t// Loop over all polynomial points, starting with n=0\n\tfor _, j_n := range points { //nolint:revive\n\n\t\t// i == j_n will result in (x - j_n) / (i - j_n), where (i - j_n) == 0,\n\t\t// division by zero is prohibited!\n\t\terr = i.Equal(j_n)\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// (x - j_n+1)\n\t\tl_i, err = x.Sub(j_n)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeBasisPolynomialSubNomError{Err: err}\n\t\t}\n\n\t\t// (x - j_n) * (x - j_n+1)\n\t\tnom, err = nom.Mul(l_i)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeBasisPolynomialMulNomError{Err: err}\n\t\t}\n\n\t\t// (i - j_n+1)\n\t\tl_i, err = i.Sub(j_n)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeBasisPolynomialSubDenomError{Err: err}\n\t\t}\n\n\t\t// (i - j_n) * (i - j_n+1)\n\t\tdenom, err = denom.Mul(l_i)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeBasisPolynomialMulDenomError{Err: err}\n\t\t}\n\t}\n\n\t// denom^-1 == 1/denom\n\tdenom = denom.Inverse()\n\n\t// nom * denom\n\tl_i, err = nom.Mul(denom)\n\tif err != nil {\n\t\treturn nil, LagrangeBasisPolynomialMulNomAndDenomError{Err: err}\n\t}\n\n\treturn l_i, nil\n}\n\n// LagrangeInterpolate computes the value of a polynomial at x-coordinate, using\n// Lagrange interpolation\n//\n//\tL(x) = f(i_0) * l_i_0(x) + f(i_1) * l_i_1(x) + ... + f(i_n) * l_i_n(x)\n//\n// where (i, f(i_n)) are (x,y) coordinates of a polynomial point\n//\n// This function will loop until all polynomial points are interpolated as shown\n// in the equation above. If less than degree+1 evaluations are given, then the\n// interpolated value is uniformly random.\nfunc LagrangeInterpolate(g group.Group, points []*Point, x *group.Scalar) (*Point, error) {\n\t// x is found, it is already located at polynomial\n\tfor _, i := range points {\n\t\terr := i.X.Equal(x)\n\t\tif err == nil {\n\t\t\treturn i, nil\n\t\t}\n\t}\n\n\t// At start, L_x = 0\n\tL_x := group.ZeroScalar(g.Order()) //nolint:revive\n\n\t// Loop over all polynomial points\n\tfor _, i := range points {\n\t\tl_i, err := LagrangeBasisPolynomial(g, x, polynomialPoints(points).xCoords(), i.X) //nolint:revive\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeInterpolateLagrangeBasisPolynomial{Err: err}\n\t\t}\n\n\t\t// (l_i * i)\n\t\tl_i, err = l_i.Mul(i.Y)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeInterpolateMulError{Err: err}\n\t\t}\n\n\t\t// L_x += (l_i * i)\n\t\tL_x, err = L_x.Add(l_i)\n\t\tif err != nil {\n\t\t\treturn nil, LagrangeInterpolateAddPolynomial{Err: err}\n\t\t}\n\t}\n\n\treturn &Point{X: x, Y: L_x}, nil\n}\n"
  },
  {
    "path": "core/math/polynomial/polynomial_test.go",
    "content": "package polynomial\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n)\n\nfunc TestNewRandom(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tfor i := uint64(1); i < 11; i++ {\n\t\t\tt.Run(fmt.Sprintf(\"%s-degree-%d\", g.Name(), i), func(t *testing.T) {\n\t\t\t\tpoly, err := NewRandom(g, i)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\tif poly.Degree() != i {\n\t\t\t\t\tt.Errorf(\"Expected degree=%v, got=%v\\n\", poly.Degree(), i)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestEvaluate(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tfor i := uint64(1); i < 11; i++ {\n\t\t\tt.Run(fmt.Sprintf(\"%s-degree-%d\", g.Name(), i), func(t *testing.T) {\n\t\t\t\tpoly1, err := NewRandom(g, i)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\tpoly2, err := NewRandom(g, i)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\ts, err := group.RandomScalar(g.Order())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\tev1, err := poly1.Evaluate(s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\tev2, err := poly2.Evaluate(s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// Randomness of polynomial coefficient is broken\n\t\t\t\t// and there is a chance that same polynomial will be given\n\t\t\t\tif err = ev1.Y.Equal(ev2.Y); err == nil {\n\t\t\t\t\tt.Error(\"Same polynomial, despite randomness was used\")\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestRandomWithConstantAndEvaluate(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tfor i := uint64(1); i < 11; i++ {\n\t\t\tt.Run(fmt.Sprintf(\"%s-degree-%d\", g.Name(), i), func(t *testing.T) {\n\t\t\t\tconstant, err := group.RandomScalar(g.Order())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// p(x) = Ax^n + Bx^n-1 + Cx^n-2 + ... + constant\n\t\t\t\tpoly, err := NewRandomWithConstant(g, i, constant)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\ts := group.ZeroScalar(g.Order())\n\n\t\t\t\t// p(x) = constant + 0 + 0 + ... = constant\n\t\t\t\tev, err := poly.Evaluate(s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// p(x) == constant\n\t\t\t\terr = ev.Y.Equal(constant)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestInterpolate(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tfor i := uint64(1); i < 11; i++ {\n\t\t\tt.Run(fmt.Sprintf(\"%s-degree-%d\", g.Name(), i), func(t *testing.T) {\n\t\t\t\tconstant, err := group.RandomScalar(g.Order())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// Polynomial with coefficients\n\t\t\t\tpoly, err := NewRandomWithConstant(g, i, constant)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// NB! Note that len(points) == i, and polynomial degree\n\t\t\t\t// is also == i, but for polynomial interpolation\n\t\t\t\t// len(points) should be i+1, i.e. degree+1\n\t\t\t\tpoints := make([]*Point, i)\n\n\t\t\t\t// Construct a polynomial using coefficients and coordinates\n\t\t\t\tfor j := 0; j < len(points); j++ {\n\t\t\t\t\t// x-coordinate\n\t\t\t\t\tpoint, err := group.RandomScalar(g.Order())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Error(err)\n\t\t\t\t\t}\n\n\t\t\t\t\t// y-coordinate\n\t\t\t\t\tpoints[j], err = poly.Evaluate(point)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Error(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// x=0\n\t\t\t\ts := group.ZeroScalar(g.Order())\n\n\t\t\t\t// interpolation is not correctly done, since degree of poly\n\t\t\t\t// is the same as len(points), there should always be at least\n\t\t\t\t// len(points) == degree+1\n\t\t\t\tinterpolated, err := LagrangeInterpolate(g, points, s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// Poly is interpolated incorrectly, since not enough points given\n\t\t\t\tif interpolated.Y.Equal(constant) == nil {\n\t\t\t\t\tt.Error(\"Interpolation hasn't failed, but should\")\n\t\t\t\t}\n\n\t\t\t\t// Another x-coordinate\n\t\t\t\tpoint, err := group.RandomScalar(g.Order())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\t// Another y-coordinate\n\t\t\t\tev, err := poly.Evaluate(point)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\tpoints = append(points, ev)\n\n\t\t\t\t// Now, it should be correctly interpolated, since\n\t\t\t\t// len(points) == poly degree+1\n\t\t\t\tinterpolated, err = LagrangeInterpolate(g, points, s)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\n\t\t\t\terr = interpolated.Y.Equal(constant)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestMarshal(t *testing.T) {\n\tfor _, g := range group.All() {\n\t\tt.Run(string(g.Name()), func(t *testing.T) {\n\t\t\tpoly, err := NewRandom(g, 3)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tdata, err := poly.MarshalASN1()\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tpoly2, err := UnmarshalPolynomial(g, data)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tif len(poly2.Coefficients) != len(poly.Coefficients) {\n\t\t\t\tt.Errorf(\"Unequal polynomial lengths p1=%v, p2=%v\\n\", len(poly2.Coefficients), len(poly.Coefficients))\n\t\t\t}\n\n\t\t\tfor i := range poly.Coefficients {\n\t\t\t\terr = poly.Coefficients[i].Equal(poly2.Coefficients[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsc, err := group.RandomScalar(g.Order())\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tev, err := poly.Evaluate(sc)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tdata, err = ev.MarshalASN1()\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tev2, err := UnmarshalPoint(g, data)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\terr = ev.X.Equal(ev2.X)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\terr = ev.Y.Equal(ev2.Y)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/test/fixtures/fixtures.go",
    "content": "package fixtures\n\n// KeyPairProof proves to auditor that public key used in vote encryption is a pair of\n// private key used in vote decryption.\ntype KeyPairProof struct {\n\tProof []byte\n}\n\n// CiphertextProof proves to auditor that encrypted vote (ciphertext) produced by public key,\n// private key used in vote decryption.\ntype CiphertextProof struct {\n\tCiphertext []byte\n\tDecryption []byte\n\tProof      []byte\n}\n"
  },
  {
    "path": "core/version.go",
    "content": "package core\n\n// Version is the current TIVI Core Go library version.\nconst Version = \"v1.0.0\"\n"
  },
  {
    "path": "core/voterlist/persistence/bitcask.go",
    "content": "package persistence\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/prologic/bitcask\"\n\t\"tivi.io/core/common/files\"\n\t\"tivi.io/core/voterlist\"\n)\n\nconst (\n\tMaxKeySize      = 32\n\tMaxDatafileSize = 512 * 1024 * 1024\n)\n\ntype BitcaskVoterList struct {\n\tdb   *bitcask.Bitcask\n\tpath string\n}\n\nfunc NewBitcaskVoterList(path string) (*BitcaskVoterList, error) {\n\n\tif files.IsDir(path) != nil {\n\t\treturn nil, fmt.Errorf(\"voter list at %s does not exist\", path)\n\t}\n\n\t// actual db must be opened by a explicit call to Open\n\treturn &BitcaskVoterList{\n\t\tdb:   nil,\n\t\tpath: path,\n\t}, nil\n}\n\nfunc (v *BitcaskVoterList) Open() (err error) {\n\n\tif v.db == nil {\n\t\topts := []bitcask.Option{\n\t\t\tbitcask.WithMaxKeySize(MaxKeySize),\n\t\t\tbitcask.WithMaxDatafileSize(MaxDatafileSize),\n\t\t}\n\n\t\tdb, err := bitcask.Open(v.path, opts...)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"OpenDB %s %v\", v.path, err)\n\t\t}\n\n\t\tv.db = db\n\t}\n\n\treturn\n}\n\nfunc (v *BitcaskVoterList) Path() string {\n\treturn v.path\n}\n\n// Get return the value mapped to key in the persistent value. ok is false if\n// no such key was found.\nfunc (v *BitcaskVoterList) Voter(key string) (voter *voterlist.Voter, err error) {\n\n\tif v.db == nil {\n\t\treturn nil, fmt.Errorf(\"Voterlist DB is not open\")\n\t}\n\n\tvar rawkey []byte\n\tvar rawvalue []byte\n\n\trawkey, err = voterlist.GetRawKey(key)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif !v.db.Has(rawkey) {\n\t\treturn nil, nil\n\t}\n\n\trawvalue, err = v.db.Get(rawkey)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn voterlist.NewVoterFromKeyValue(rawkey, rawvalue)\n}\n\nfunc (v *BitcaskVoterList) Count() (uint, error) {\n\n\tif v.db == nil {\n\t\treturn 0, fmt.Errorf(\"Voterlist DB is not open\")\n\t}\n\n\treturn uint(v.db.Len()), nil\n}\n\nfunc (v *BitcaskVoterList) Close() {\n\tif v.db != nil {\n\t\tv.db.Close()\n\t\tv.db = nil\n\t}\n}\n"
  },
  {
    "path": "core/voterlist/persistence/bolt.go",
    "content": "package persistence\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"go.etcd.io/bbolt\"\n\n\t\"tivi.io/core/common/files\"\n\t\"tivi.io/core/voterlist\"\n)\n\nconst (\n\tBucketName = \"voters\"\n)\n\ntype BoltVoterList struct {\n\tdb   *bbolt.DB\n\tpath string\n}\n\nfunc NewBoltVoterList(path string) (*BoltVoterList, error) {\n\n\tdbpath := path\n\n\tif files.IsFile(dbpath) != nil {\n\t\tif files.IsDir(dbpath) == nil {\n\t\t\tdbpath = filepath.Join(dbpath, \"dreinv-database.db\")\n\t\t\tif files.IsFile(dbpath) != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"voter list at %s does not exist\", dbpath)\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"voter list at %s does not exist\", dbpath)\n\t\t}\n\t}\n\n\treturn &BoltVoterList{\n\t\tdb:   nil,\n\t\tpath: dbpath,\n\t}, nil\n}\n\nfunc (v *BoltVoterList) Open() (err error) {\n\n\tif v.db == nil {\n\t\tdb, err := bbolt.Open(v.path, 0600,\n\t\t\t&bbolt.Options{\n\t\t\t\tTimeout:  5 * time.Second,\n\t\t\t\tReadOnly: true,\n\t\t\t})\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"OpenDB %s %v\", v.path, err)\n\t\t}\n\n\t\tv.db = db\n\t}\n\n\treturn\n}\n\nfunc (v *BoltVoterList) Path() string {\n\treturn v.path\n}\n\nfunc (v *BoltVoterList) Voter(key string) (voter *voterlist.Voter, err error) {\n\n\tif v.db == nil {\n\t\treturn nil, fmt.Errorf(\"Voterlist DB is not open\")\n\t}\n\n\tvar rawkey []byte\n\n\trawkey, err = voterlist.GetRawKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv.db.View(func(tx *bbolt.Tx) error { // nolint:errcheck\n\t\tb := tx.Bucket([]byte(BucketName))\n\t\trawvalue := b.Get(rawkey)\n\t\tif rawvalue == nil {\n\t\t\treturn nil\n\t\t}\n\t\tvoter, err = voterlist.NewVoterFromKeyValue(rawkey, rawvalue)\n\t\treturn nil\n\t})\n\n\treturn\n}\n\nfunc (v *BoltVoterList) Count() (count uint, err error) {\n\n\tif v.db == nil {\n\t\treturn 0, fmt.Errorf(\"Voterlist DB is not open\")\n\t}\n\n\terr = v.db.View(func(tx *bbolt.Tx) error {\n\t\tb := tx.Bucket([]byte(BucketName))\n\t\tcount = uint(b.Stats().KeyN)\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn count, nil\n}\n\nfunc (v *BoltVoterList) Close() {\n\tif v.db != nil {\n\t\tv.db.Close()\n\t\tv.db = nil\n\t}\n}\n"
  },
  {
    "path": "core/voterlist/persistence/factory.go",
    "content": "package persistence\n\nimport (\n\t\"fmt\"\n\n\t\"tivi.io/core/voterlist\"\n)\n\nfunc GetVoterListImpl(name string, path string) (vl voterlist.VoterList, err error) {\n\n\tswitch name {\n\tcase \"bolt\":\n\t\tvl, err = NewBoltVoterList(path)\n\tcase \"bitcask\":\n\t\tvl, err = NewBitcaskVoterList(path)\n\tdefault:\n\t\terr = fmt.Errorf(\n\t\t\t\"provided voterlist backend option is not supported: %s\", name)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "core/voterlist/voterlist.go",
    "content": "package voterlist\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n)\n\nconst (\n\tVoterNumberBinLength = 32\n\tVoterNumberHexLength = 64\n)\n\nvar (\n\tErrFormat = fmt.Errorf(\"Invalid voter format\")\n)\n\ntype VoterList interface {\n\tOpen() error\n\tPath() string\n\tVoter(string) (*Voter, error)\n\tCount() (uint, error)\n\tClose()\n}\n\ntype Voter struct {\n\twkz   string\n\thashA string\n\thashB string\n}\n\nfunc GetRawKey(key string) (rawkey []byte, err error) {\n\treturn hex.DecodeString(key)\n}\n\nfunc NewVoterFromString(wkz, n2, n3 string) (v *Voter, err error) {\n\n\tif len(wkz) != VoterNumberHexLength {\n\t\terr = ErrFormat\n\t\treturn\n\t}\n\tif len(n2) != VoterNumberHexLength {\n\t\terr = ErrFormat\n\t\treturn\n\t}\n\tif len(n3) != VoterNumberHexLength {\n\t\terr = ErrFormat\n\t\treturn\n\t}\n\n\tv = &Voter{\n\t\twkz:   wkz,\n\t\thashA: n2,\n\t\thashB: n3,\n\t}\n\treturn\n}\n\nfunc NewVoterFromKeyValue(key, value []byte) (v *Voter, err error) {\n\n\tif len(key) != VoterNumberBinLength {\n\t\terr = ErrFormat\n\t\treturn\n\t}\n\n\tif len(value) != 2*VoterNumberBinLength {\n\t\terr = ErrFormat\n\t\treturn\n\t}\n\n\twkz := hex.EncodeToString(key)\n\tn2 := hex.EncodeToString(value[:32])\n\tn3 := hex.EncodeToString(value[32:])\n\n\tv = &Voter{\n\t\twkz:   wkz,\n\t\thashA: n2,\n\t\thashB: n3,\n\t}\n\treturn\n}\n\nfunc (v *Voter) KeyValue() (key []byte, value []byte, err error) {\n\n\tif key, err = hex.DecodeString(v.wkz); err != nil {\n\t\treturn\n\t}\n\n\tif value, err = hex.DecodeString(v.hashA + v.hashB); err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (v *Voter) GetDOBHash() string {\n\treturn v.hashA\n}\n\nfunc (v *Voter) HashB() string {\n\treturn v.hashB\n}\n"
  },
  {
    "path": "debian/.gitignore",
    "content": "*-stamp\n*.debhelper\n*.log\n*.substvars\nfiles\n\n# package dirs\ntmp\npython3-ivxv-common\nivxv-admin\nivxv-backup\nivxv-choices\nivxv-common\nivxv-log\nivxv-mid\nivxv-smartid\nivxv-webeid\nivxv-proxy\nivxv-storage\nivxv-verification\nivxv-voting\nivxv-votesorder\nivxv-sessionstatus\n"
  },
  {
    "path": "debian/changelog",
    "content": "ivxv (1.10.3) jammy; urgency=medium\n\n  * Documentation release - review, translation, spelling\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 05 Sep 2025 12:13:28 +0000\n\nivxv (1.10.2) jammy; urgency=medium\n\n  * Change log rotation frequency to hourly basis\n  * Update javascript dependencies\n  * Add support for voters as young as 14 years\n  * Update Smart-ID flows to support smoother user experience\n  * Improve testing and releasing tools\n  * Update ZIP handling to strict format validations\n  * Update voterlist download strategy from VIS to avoid bottlenecks\n  * Resolve memory-leak in the administrative interface\n  * Various minor bugfixes\n\n -- IVXV Developer <info@ivotingcentre.ee>  Sun, 24 Aug 2025 09:48:28 +0000\n\nivxv (1.10.1) jammy; urgency=medium\n\n  * Hotfix to SmartID service to accomodate for the changed cert profile\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 03 Apr 2025 18:18:13 +0000\n\nivxv (1.10.0) jammy; urgency=medium\n\n  * Add backend log documentation\n  * Update OCSP URL handling\n  * Add support for ECC\n  * Update disaster recovery documentation\n  * Update protocol documentation\n  * Add invalid ciphertext detection to collection service\n  * Update dependencies\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 17 Dec 2024 07:20:16 +0000\n\nivxv (1.9.10) jammy; urgency=medium\n\n  * Hotfix to admin web, don't export detailstats to VIS when downloading\n  * Hotfix to ivxv-storage install/uninstall .deb scripts\n  * Hotfix to admin web, removed old CAs to validate ID-card certificate\n  * Hotfix to admin web, check client certificate with OCSP\n  * Hotfix to disable Apache web server directories indexing\n  * Hotfix to admin web, disable weak ciphers, only allow TLSv1.3\n  * Hotfix to admin web, remove sensitive info from server response\n  * Hotfix to ivxv-admin-sudo install-pkg, extract filename only from path\n  * ivxv-storageorder runs under cron\n  * Processor should check a ballot size to prevent zip-bomb attack\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 14 May 2024 05:37:36 +0000\n\nivxv (1.9.9) jammy; urgency=medium\n\n  * Add script to prepare csv for ivxv-storageorder\n  * Hotfix to ivxv-storageorder SeqNo == successful vote per voterid check\n  * Hotfix to xroad-service HTTP errors handling\n  * Hotfix to IVXV logs directory creation\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 30 Apr 2024 10:45:58 +0000\n\nivxv (1.9.8) jammy; urgency=medium\n\n  * Add backend error logs monitoring docs\n  * Check qualification times, that set in \"qualification:\" (election.yml) as\n    [a.Time <= b.Time <= ...]\n  * Refactor election.yml period processing\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 19 Apr 2024 14:27:30 +0000\n\nivxv (1.9.7) jammy; urgency=medium\n\n  * Ensure that votesorder data is not lost if etcd txn has \"Succeeded: false\"\n  * Proper logging of votesorder errors\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 11 Apr 2024 06:37:53 +0000\n\nivxv (1.9.6) jammy; urgency=medium\n\n  * Hotfix to votesorder error handling\n  * Set systemd startup timeout for ivxv-storage service\n  * All IVXV logs are stored in /var/log/ivxv/\n  * Hotfix to ivxv-sessionstatus Documentation\n  * Hotfix to sessionstatus etcd timeouts usage\n  * Store vote as follows: vote, then tspreg, then ocsp\n  * Hotfix to xtee service docs\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 09 Apr 2024 12:06:53 +0000\n\nivxv (1.9.4) jammy; urgency=medium\n\n  * Hotfix to sessionstatus service to return errors properly\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 01 Apr 2024 15:50:02 +0000\n\nivxv (1.9.3) jammy; urgency=medium\n\n  *  Hotfix to processor export votes extension\n  *  Hotfix to RPC.Verify when ignorevoterlist is set\n  *  Hotfix to ivxv-storage.postrm\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 22 Mar 2024 11:50:57 +0000\n\nivxv (1.9.2) jammy; urgency=medium\n\n  * Hotfix to VIS URL\n  * Hotfix to day-order of detailed statistics\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 12 Mar 2024 17:07:07 +0000\n\nivxv (1.9.1) jammy; urgency=medium\n\n  * Java applications\n    - implement tools for processor integrity verification\n  * Collector\n    - improve transactions for storing votes\n  * Documentation\n    - Adjust documentation for easier translation\n\n -- IVXV Developer <info@ivotingcentre.ee>  Sat, 02 Mar 2024 15:10:28 +0200\n\nivxv (1.9.0+hotfix.3) jammy; urgency=medium\n\n  * Hotfix to properly handle DER encoding in PKCS15 cards\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 30 Jan 2024 11:14:38 +0000\n\nivxv (1.9.0+hotfix.2) jammy; urgency=medium\n\n  * Hotfix version to parse webeid origin\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 18 Jan 2024 13:02:23 +0000\n\nivxv (1.9.0+hotfix.1) jammy; urgency=medium\n\n  * Hotfix version to include etcd dependency\n\n -- IVXV Developer <info@ivotingcentre.ee>  Wed, 17 Jan 2024 10:52:51 +0000\n\nivxv (1.9.0) jammy; urgency=low\n\n  * Operating system\n    - upgrade from Ubuntu 20.04 LTS to Ubuntu 22.04 LTS\n    - upgrade debhelper from 11 to 13\n    - python dependencies for management interface are shipped in separate .deb\n    - `dpkg-buildpackage` on Ubuntu 22.04 LTS uses .zst compression instead of .xz\n  * Management interface\n    - upgrade PyPi dependencies to the latest possible\n  * Online voting components\n    - upgrade from Go 1.14 to Go 1.21\n    - switch from GOPATH layout to go.mod\n    - upgrade from Python 3.8 to 3.10\n  * Offline voting component\n    - upgrade Java from 1.11 to 1.17\n  * Tests\n    - `docker-compose` is deprecated, use `docker compose` instead\n    - newer version of systemd support in docker\n  * Web eID\n    - support Web eID authentication method for IVXV backend\n  * Session status\n    - new microservice to report SessionID status to IVXV services.\n      This could help IVXV services to detect whether SessionID\n      has been tampered or not.\n  * BDOC\n    - remove TM profiles support for backend as it is not anymore\n      supported by digidoc-tool\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 23 Jan 2023 18:11:51 +0000\n\nivxv (1.8.2) focal; urgency=medium\n\n  * General\n    - Simplify voterlist format for VIS compatibility\n\n  * x-Road Service\n    - CI\n    - Documentation\n\n  * Management interface\n    - Export unique reports\n\n  * Processor application\n    - Add revokeAndAnonymize utility\n\n  * Collector service\n    - Reject SessionID with invalid structure\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 23 Jan 2023 18:11:51 +0000\n\nivxv (1.8.1) focal; urgency=medium\n\n  * Collector service:\n    - SmartID support\n    - X-road service for VIS communication\n    - Ordering of the votes\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 16 Dec 2022 11:53:15 +0000\n\nivxv (1.8.0) focal; urgency=medium\n\n  * Collector service:\n    - Introduction of votes order interface for VIS\n    - Configurable SNI\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 01 Dec 2022 21:48:08 +0000\n\nivxv (1.7.8) focal; urgency=medium\n\n  * Collector service:\n    - Fix rebuilding storage index to not corrupt the latest vote index.\n\n  * Key application\n    - Attempt to retry in case of errors\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 28 Dec 2021 00:11:09 +0000\n\nivxv (1.7.7) focal; urgency=medium\n\n  * Documentation\n    - Explain mixnet release\n\n -- IVXV Developer <info@ivotingcentre.ee>  Sat, 02 Oct 2021 19:12:39 +0000\n\nivxv (1.7.6) focal; urgency=medium\n\n  * Documentation\n    - Finalised documentation\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 27 Sep 2021 03:37:31 +0000\n\nivxv (1.7.5) focal; urgency=medium\n\n  * Collector service:\n    - Added verification.latestonly election configuration option, which if\n      enabled limits verification to the latest vote of a voter only.\n  * Management service:\n    - Grouped download forms in management UI\n    - Added downloading of anonymized voting sessions from Log Monitor\n    - Reduced verbosity of cron tasks if collector is not configured\n    - Fixed config files classification\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 21 Sep 2021 12:06:39 +0000\n\nivxv (1.7.4) focal; urgency=medium\n\n  * Collector service:\n    - Require that the OCSP response of a timestamped BDOC signature is\n      produced after the timestamp, up to a configurable delay.\n  * Management service:\n    - Implemented ivxv-voterstats to forward stats to VIS3\n    - Added utility ivxv-generate-processor-input\n\n -- IVXV Developer <info@ivotingcentre.ee>  Wed, 01 Sep 2021 12:21:34 +0000\n\nivxv (1.7.3) focal; urgency=medium\n\n  * Changed voter list update management. List updates are now\n    automatically downloaded from the election information system VIS3\n  * Collector service:\n    - Added new election config parameters to define access to VIS3\n    - Increased maximum count of voter lists to 10000\n    - Added new command type to skip invalid voter list\n  * Management service:\n    - Added utility ivxv-voter-list-download\n    - Removed utility ivxv-cmd-remove\n\n -- IVXV Developer <info@ivotingcentre.ee>  Wed, 18 Aug 2021 06:48:20 +0000\n\nivxv (1.7.2) focal; urgency=medium\n\n  * Base system changed to Ubuntu 20.04 LTS (Focal Fossa)\n  * Management service:\n    - Do not install etcd packages from Debian buster anymore\n      * Require version 3.2.26 from Ubuntu instead, which is built using a\n        newer golang-google-grpc, so the Debian version is no longer needed\n  * Collector service:\n    - Added election ID consistency validation to ivxv-config-validate\n    - Require district list to be loaded before voters list\n    - Allow explicit SignatureTimeStamp CanonicalizationMmethod attribute in\n      bdoc/asice container signatures\n      * The only allowed value is still \"http://www.w3.org/2006/12/xml-c14n11\"\n    - Import voter lists from unsigned ZIP in addition to trusted containers\n    - Updated etcd client library version from 3.2.17 to 3.2.26\n    - Use district lists for assigning voters to districts instead of using\n      their administrative unit code directly.\n    - Added VoterForeignEHAK election configuration option used for assigning\n      districts to foreign voters during non-KOV elections.\n    - Removed VoterDistrictEHAK election configuration option.\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 21 Jun 2021 23:34:46 +0000\n\nivxv (1.7.1) bionic; urgency=medium\n\n  * Collector service:\n    - Fixed voters list consistency check\n    - Added voterdistrictehak validation pattern for election config\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 31 Dec 2020 12:30:49 +0000\n\nivxv (1.7.0) bionic; urgency=medium\n\n  * Documentation:\n    - Fixed national identity number pattern in revocation list schema\n    - Updated voters list format to version 2\n  * Collector service:\n    - Updated voters list format to version 2\n    - Added VoterDistrictEHAK election configuration option necessary for\n      version 2 voter lists during non-KOV elections.\n    - Removed stations from districts list\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 18 Dec 2020 11:42:16 +0200\n\nivxv (1.6.0) bionic; urgency=medium\n\n  * Collector service:\n    - Fixed log collector default port\n    - Utility ivxv-copy-log-to-logmon:\n      * Raise error if some rsync process fails\n      * Added process locking\n      * Added logging level\n    - Changed list of supported time-stamping protocol signature algorithms:\n      * Removed \"rsaEncryption\" and \"ecdsa-with-Recommended\".\n      * Added \"sha256WithRSAEncryption\", \"sha384WithRSAEncryption\",\n        \"sha512WithRSAEncryption\", \"ecdsa-with-SHA256\", \"ecdsa-with-SHA384\",\n        and \"ecdsa-with-SHA512\".\n      * Old signatures storing timestamps of the now unsupported protocols\n        will no longer validate.\n    - New package ivxv-mid deprecates ivxv-dds\n      * The DigiDocService-based Mobile-ID support service \"dds\" has been\n        replaced with the SK MID REST interface-based service \"mid\".\n  * Management service:\n    - Use Bootstrap, DataTables, Font Awesome and jQuery javascript libraries\n      from Ubuntu repository\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 01 Jun 2020 11:39:19 +0300\n\nivxv (1.5.1) bionic; urgency=medium\n\n  * Management service:\n    - Don't fail ivxv-backup if log files grow during backup process\n    - Improve whitespace handling in voters list validator\n    - Improve ivxv-service error message if election configuration is missing\n    - Fixed authentication with user names containing unicode characters\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 07 May 2019 13:04:29 +0300\n\nivxv (1.5.0) bionic; urgency=medium\n\n  * Management service:\n    * Added jsonschema support for ivxv-config-validate to check choices and\n      districts lists schema.\n    * Added alternative log transport channel (rsync) to feed Log Monitor\n    * Renamed utility ivxv-consolidate-votes to ivxv-export-votes\n    * Consolidation is now optional in ballot box export\n    * Increased maximum size of syslog messages in configured services to 32k\n    * Install etcd packages from Debian buster, where they are built using a\n      newer version of golang-google-grpc\n    * Changed ballot box downloading in UI to two step operation\n  * Collector service:\n    * Added \"option tcplog\" to HAProxy template to monitor connections better\n    * Removed \"option dontlognull\" from HAProxy template to log everything\n    * Updated etcd client library version from 3.1.0 to 3.2.17\n    * Added request size limiting support to codec filter\n    * Added raw request logging to codec filter\n      - Request log will be stored at /var/log/ivxv-request-YYYY-MM-DD.log\n    * Write log files to /var/log/ivxv-YYYY-MM-DD.log\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 22 Apr 2019 16:22:39 +0300\n\nivxv (1.4.4) bionic; urgency=medium\n\n  * Java application-only release\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 28 Feb 2019 14:37:56 +0200\n\nivxv (1.4.3) bionic; urgency=medium\n\n  * Management service:\n      - Use hardcoded extension .bdoc for config file names in /etc/ivxv\n      - Fixed setting timezone offset in UI data files\n  * Collector service:\n      - Send full log to external log collectors\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 11 Feb 2019 12:23:56 +0200\n\nivxv (1.4.2) bionic; urgency=medium\n\n  * Management service:\n    * Enforce station number uniqueness only within a district\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 01 Feb 2019 15:13:05 +0200\n\nivxv (1.4.1) bionic; urgency=medium\n\n  * Collector service:\n    * Set the modification time in Zip local file headers of exported votes\n    * Recommended ca-certificates for ivxv-dds and ivxv-voting\n    * Added container type ASiCE as an alias for BDOC\n      - Election configuration and voting must still use the canonical BDOC\n        name, ASiCE is only allowed as a configuration container extension\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 25 Jan 2019 15:46:05 +0200\n\nivxv (1.4.0) bionic; urgency=medium\n\n  * Management service:\n    * Allow empty voters list\n    * Fixed replacing invalid election config in microservices\n    * Fixed using multiple external log servers\n    * Fixed package version upgrade\n    * Improved service initialization:\n      - Always initialize service if the first tech config is applied\n      - Fixed backup service initialization\n    * Added missing stats field to admin UI (age group 18-14)\n    * Added choice ID uniqueness check to config validator\n  * Collector service:\n    * Added support for UTF-8 byte order mark at the beginning of YAML files\n    * Added support for BDOC TS profile\n    * Added option for automatically retrying OCSP requests\n    * Added option for automatically retrying TSP requests\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 18 Jan 2019 16:31:14 +0200\n\nivxv (1.3.0) bionic; urgency=medium\n\n  * Third minor release for 1.x\n  * Base system changed to Ubuntu 18.04 LTS (Bionic Beaver)\n  * Added service crash recovery procedures\n  * Management service:\n    * Raise error if reloading current config file\n    * Better stats error indication\n    * IVXV service status hints converted to background information messages\n    * Added utility to start and stop microservices\n  * Collector service:\n    * Added automatic pruning of deconfigured etcd cluster members on restart\n      of existing instances\n    * Unwrapped etcd logs from ivxv-storage JSON logs: they are now forwarded\n      to syslog unmodified and with the program name \"etcd\"\n    * Upgrade minimal Go version to 1.9.\n    * Fixed logging to multiple log servers\n    * Added \"pnoee\" identity type\n  * Improved service state indication in Management Interface.\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 09 Jul 2018 11:39:58 +0300\n\nivxv (1.2.0) xenial; urgency=medium\n\n  * Second minor release for 1.x\n  * Updated Log Monitor version to 1.1\n  * Management service:\n    * Added backup service\n    * Added support to create ballot box which contains\n      the union of votes from storage and backup services\n    * Removed obsolete technical config parameters \"stats.*\"\n    * Improved command file loading and fixed validation issues\n    * Fixed permissions config file creation error while uploading user\n      permission command\n    * Fixed Agent Daemon issues while loading stats data\n    * Added Management Service event logging with log browsing support\n    * Simplified ivxv-status utility CLI arguments\n    * Added state files for some config files to track config applying process\n    * Added config validator utility ivxv-config-validate\n    * Use ED25519 algorithm instead of RSA to generate SSH keypair for\n      ivxv-admin user\n  * Management service user interface:\n    * Added configuration hints to service page\n    * Added indication page to watch config applying status and log\n    * Config upload form moved to config applying status page\n  * Collector service:\n    * Added support for CR and CR LF line endings in YAML files\n    * Rewrote BDOC XML parsing and canonicalization to be more strict\n    * Added size limits to DDS request fields\n    * Log transport protocol to Log Monitor changed to RELP (port 20514)\n    * Added support for prefixed filenames (e.g., prefix.trust.yaml instead of\n      trust.yaml) in configuration containers\n    * Moved configuration fields specified by the election organizer (the\n      voterlist, auth, identity, age, vote, dds and qualification sections)\n      from technical to election configuration\n    * Added automatic update of etcd cluster when adding or replacing members\n      * As a prerequisite, added bootstrap field to etcd storage configuration\n  * Documentation:\n    * Update BDOC XML templates in protocol specification to exclude empty\n      attributes and include an Id for the SignedProperties Reference\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 06 Jul 2018 13:55:49 +0300\n\nivxv (1.1.0) xenial; urgency=medium\n\n  * First minor release for 1.x\n  * Updated Log Monitor version to 1.0.2j\n  * Switch etcd source from Ubuntu 17.04 to 17.10\n  * Management service:\n    * Added utility to remove pending list of voters\n    * Added support to load districts list\n    * Added support for stats filtering by district\n    * Fixed issues while loading non-UTF8 encoded voters list\n  * Collector service:\n    * Added support for configuring etcd backend timeouts through environment\n      variables\n    * Increased etcd storage client tolerance of leader failures\n    * Made TLS cipher suites configurable\n    * Replaced BDOC checktimemark option with profile\n    * Added support for requiring ID-code and/or phone number for DDS\n      authentication\n    * Added vote submission rate limiting\n\n -- IVXV Developer <info@ivotingcentre.ee>  Fri, 27 Apr 2018 16:13:32 +0300\n\nivxv (1.0.2) xenial; urgency=medium\n\n  * Second hotfix release for 1.0\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 28 Sep 2017 16:12:57 +0300\n\nivxv (1.0.1) xenial; urgency=medium\n\n  * First hotfix release for 1.0\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 26 Sep 2017 10:11:19 +0300\n\nivxv (1.0) xenial; urgency=medium\n\n  * First official release\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 19 Sep 2017 14:32:27 +0300\n\nivxv (0.9) xenial; urgency=medium\n\n  * Release candidate for first official release\n\n -- IVXV Developer <info@ivotingcentre.ee>  Thu, 31 Aug 2017 15:56:06 +0300\n\nivxv (0.5) xenial; urgency=medium\n\n  * Test release for RIA public pilot\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 01 Aug 2017 18:22:06 +0300\n\nivxv (0.4) xenial; urgency=medium\n\n  * Test release for RIA private pilot\n\n -- IVXV Developer <info@ivotingcentre.ee>  Sat, 24 Jun 2017 20:44:32 +0300\n\nivxv (0.3-ria) xenial; urgency=medium\n\n  * Test release for RIA to test high availability installation\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 08 May 2017 18:39:34 +0300\n\nivxv (0.2-ria) xenial; urgency=medium\n\n  * Test release for RIA to test single instance installation\n\n -- IVXV Developer <info@ivotingcentre.ee>  Mon, 27 Mar 2017 08:04:40 +0300\n\nivxv (0.1-1) xenial; urgency=medium\n\n  * Initial release\n\n -- IVXV Developer <info@ivotingcentre.ee>  Tue, 22 Nov 2016 09:24:25 +0200\n"
  },
  {
    "path": "debian/control",
    "content": "Source: ivxv\nSection: misc\nPriority: optional\nMaintainer: IVXV Developer <info@ivotingcentre.ee>\n# Dependencies for admin-ui: dh-python, python3-all, python3-setuptools\n# Dependencies for services: dh-exec\nBuild-Depends: debhelper-compat (= 13), dh-exec, dh-python, python3-all, python3-debian, python3-setuptools\nStandards-Version: 3.9.8\n\nPackage: ivxv-common\nArchitecture: all\nDepends: ${misc:Depends}, adduser, openssh-server, openssl, rsync, rsyslog, rsyslog-relp, sudo, tzdata\nRecommends: locales | locales-all\nDescription: IVXV alamteenuste ühispakk\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse alamteenuste ühisosa\n\nPackage: python3-ivxv-common\nArchitecture: all\nDepends: ${misc:Depends}, python3, python3-pip\nDescription: IVXV admin välised Python sõltuvused\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab hääletamise admin consooli välised Python sõltuvused.\n\nPackage: ivxv-admin\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, apache2, cron, ivxv-common, python3-ivxv-common, language-pack-et, libapache2-mod-wsgi-py3, python3-gdbm, ssl-cert\nDescription: IVXV kogumisteenuse haldusteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse haldusteenust\n\nPackage: ivxv-proxy\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, haproxy, ivxv-common, libpam-systemd\nDescription: IVXV vahendusteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse vahendusteenust\n\nPackage: ivxv-choices\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ivxv-common, libpam-systemd\nDescription: IVXV nimekirjateenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse nimekirjateenust\n\nPackage: ivxv-verification\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ivxv-common, libpam-systemd\nDescription: IVXV kontrolliteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse kontrolliteenust\n\nPackage: ivxv-voting\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ca-certificates, ivxv-common, libpam-systemd\nDescription: IVXV hääletamisteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse hääletamisteenust\n\nPackage: ivxv-storage\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ivxv-common, libpam-systemd\nDescription: IVXV talletamisteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse talletamisteenust\n\nPackage: ivxv-mid\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ca-certificates, ivxv-common, libpam-systemd\nDescription: IVXV mobiil-ID tugiteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse mobiil-ID tugiteenust\n\nPackage: ivxv-smartid\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ca-certificates, ivxv-common, libpam-systemd\nDescription: IVXV Smart-ID tugiteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse Smart-ID tugiteenust\n\nPackage: ivxv-webeid\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ca-certificates, ivxv-common, libpam-systemd\nDescription: IVXV Web eID tugiteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse Web eID tugiteenust\n\nPackage: ivxv-log\nArchitecture: all\nDepends: ${misc:Depends}, ivxv-common, rsync\nConflicts: ivxv-admin, ivxv-backup, ivxv-choices, ivxv-mid, ivxv-proxy, ivxv-storage, ivxv-verification, ivxv-voting\nDescription: IVXV logikogumisteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse logikogumisteenust\n\nPackage: ivxv-backup\nArchitecture: amd64\nDepends: ${misc:Depends}, ivxv-common, rsync\nConflicts: ivxv-admin, ivxv-choices, ivxv-log, ivxv-mid, ivxv-proxy, ivxv-storage, ivxv-verification, ivxv-voting\nDescription: IVXV varundusteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab kogumisteenuse varundusteenust\n\nPackage: ivxv-votesorder\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ivxv-common, libpam-systemd\nDescription: IVXV häälte kirjete edastusteenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab häälte nimekirja edastusteenust\n\nPackage: ivxv-sessionstatus\nArchitecture: amd64\nDepends: ${shlibs:Depends}, ${misc:Depends}, ivxv-common, libpam-systemd\nDescription: IVXV SessionID staatuse raporteeriv teenus\n Elektroonilise hääletamise infosüsteem IVXV\n .\n Käesolev pakk sisaldab staatuse reporteermisteenust\n"
  },
  {
    "path": "debian/copyright",
    "content": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\n\nFiles: *\nCopyright: Vabariigi Valimisteenistus (Estonian State Electoral Office), www.valimised.ee\nComment: Created in 2016-2017 by Cybernetica AS, www.cyber.ee\nLicense: CC-BY-NC-ND-3.0\n Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported\n .\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL\n SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT\n RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN \"AS-IS\" BASIS.\n CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND\n DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE.\n .\n License\n .\n THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE\n COMMONS PUBLIC LICENSE (\"CCPL\" OR \"LICENSE\"). THE WORK IS PROTECTED BY\n COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS\n AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.\n .\n BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE\n BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE\n CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE\n IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.\n .\n 1. Definitions\n .\n  a. \"Adaptation\" means a work based upon the Work, or upon the Work and other\n     pre-existing works, such as a translation, adaptation, derivative work,\n     arrangement of music or other alterations of a literary or artistic work,\n     or phonogram or performance and includes cinematographic adaptations or any\n     other form in which the Work may be recast, transformed, or adapted\n     including in any form recognizably derived from the original, except that a\n     work that constitutes a Collection will not be considered an Adaptation for\n     the purpose of this License. For the avoidance of doubt, where the Work is\n     a musical work, performance or phonogram, the synchronization of the Work\n     in timed-relation with a moving image (\"synching\") will be considered an\n     Adaptation for the purpose of this License.\n .\n  b. \"Collection\" means a collection of literary or artistic works, such as\n     encyclopedias and anthologies, or performances, phonograms or broadcasts,\n     or other works or subject matter other than works listed in Section 1(f)\n     below, which, by reason of the selection and arrangement of their contents,\n     constitute intellectual creations, in which the Work is included in its\n     entirety in unmodified form along with one or more other contributions,\n     each constituting separate and independent works in themselves, which\n     together are assembled into a collective whole. A work that constitutes a\n     Collection will not be considered an Adaptation (as defined above) for the\n     purposes of this License.\n .\n  c. \"Distribute\" means to make available to the public the original and copies\n     of the Work through sale or other transfer of ownership.\n .\n  d. \"Licensor\" means the individual, individuals, entity or entities that\n     offer(s) the Work under the terms of this License.\n .\n  e. \"Original Author\" means, in the case of a literary or artistic work, the\n     individual, individuals, entity or entities who created the Work or if no\n     individual or entity can be identified, the publisher; and in addition (i)\n     in the case of a performance the actors, singers, musicians, dancers, and\n     other persons who act, sing, deliver, declaim, play in, interpret or\n     otherwise perform literary or artistic works or expressions of folklore;\n     (ii) in the case of a phonogram the producer being the person or legal\n     entity who first fixes the sounds of a performance or other sounds; and,\n     (iii) in the case of broadcasts, the organization that transmits the\n     broadcast.\n .\n  f. \"Work\" means the literary and/or artistic work offered under the terms of\n     this License including without limitation any production in the literary,\n     scientific and artistic domain, whatever may be the mode or form of its\n     expression including digital form, such as a book, pamphlet and other\n     writing; a lecture, address, sermon or other work of the same nature; a\n     dramatic or dramatico-musical work; a choreographic work or entertainment\n     in dumb show; a musical composition with or without words; a\n     cinematographic work to which are assimilated works expressed by a process\n     analogous to cinematography; a work of drawing, painting, architecture,\n     sculpture, engraving or lithography; a photographic work to which are\n     assimilated works expressed by a process analogous to photography; a work\n     of applied art; an illustration, map, plan, sketch or three-dimensional\n     work relative to geography, topography, architecture or science; a\n     performance; a broadcast; a phonogram; a compilation of data to the extent\n     it is protected as a copyrightable work; or a work performed by a variety\n     or circus performer to the extent it is not otherwise considered a literary\n     or artistic work.\n .\n  g. \"You\" means an individual or entity exercising rights under this License\n     who has not previously violated the terms of this License with respect to\n     the Work, or who has received express permission from the Licensor to\n     exercise rights under this License despite a previous violation.\n .\n  h. \"Publicly Perform\" means to perform public recitations of the Work and to\n     communicate to the public those public recitations, by any means or\n     process, including by wire or wireless means or public digital\n     performances; to make available to the public Works in such a way that\n     members of the public may access these Works from a place and at a place\n     individually chosen by them; to perform the Work to the public by any means\n     or process and the communication to the public of the performances of the\n     Work, including by public digital performance; to broadcast and rebroadcast\n     the Work by any means including signs, sounds or images.\n .\n  i. \"Reproduce\" means to make copies of the Work by any means including without\n     limitation by sound or visual recordings and the right of fixation and\n     reproducing fixations of the Work, including storage of a protected\n     performance or phonogram in digital form or other electronic medium.\n .\n 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit,\n or restrict any uses free from copyright or rights arising from limitations or\n exceptions that are provided for in connection with the copyright protection\n under copyright law or other applicable laws.\n .\n 3. License Grant. Subject to the terms and conditions of this License, Licensor\n hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the\n duration of the applicable copyright) license to exercise the rights in the\n Work as stated below:\n .\n  a. to Reproduce the Work, to incorporate the Work into one or more\n    Collections, and to Reproduce the Work as incorporated in the Collections;\n    and,\n .\n  b. to Distribute and Publicly Perform the Work including as incorporated in\n     Collections.\n .\n The above rights may be exercised in all media and formats whether now known or\n hereafter devised. The above rights include the right to make such\n modifications as are technically necessary to exercise the rights in other\n media and formats, but otherwise you have no rights to make Adaptations.\n Subject to 8(f), all rights not expressly granted by Licensor are hereby\n reserved, including but not limited to the rights set forth in Section 4(d).\n .\n 4. Restrictions. The license granted in Section 3 above is expressly made\n subject to and limited by the following restrictions:\n .\n  a. You may Distribute or Publicly Perform the Work only under the terms of\n     this License. You must include a copy of, or the Uniform Resource\n     Identifier (URI) for, this License with every copy of the Work You\n     Distribute or Publicly Perform. You may not offer or impose any terms on\n     the Work that restrict the terms of this License or the ability of the\n     recipient of the Work to exercise the rights granted to that recipient\n     under the terms of the License. You may not sublicense the Work. You must\n     keep intact all notices that refer to this License and to the disclaimer of\n     warranties with every copy of the Work You Distribute or Publicly Perform.\n     When You Distribute or Publicly Perform the Work, You may not impose any\n     effective technological measures on the Work that restrict the ability of a\n     recipient of the Work from You to exercise the rights granted to that\n     recipient under the terms of the License. This Section 4(a) applies to the\n     Work as incorporated in a Collection, but this does not require the\n     Collection apart from the Work itself to be made subject to the terms of\n     this License. If You create a Collection, upon notice from any Licensor You\n     must, to the extent practicable, remove from the Collection any credit as\n     required by Section 4(c), as requested.\n .\n  b. You may not exercise any of the rights granted to You in Section 3 above in\n     any manner that is primarily intended for or directed toward commercial\n     advantage or private monetary compensation. The exchange of the Work for\n     other copyrighted works by means of digital file-sharing or otherwise shall\n     not be considered to be intended for or directed toward commercial\n     advantage or private monetary compensation, provided there is no payment of\n     any monetary compensation in connection with the exchange of copyrighted\n     works.\n .\n  c. If You Distribute, or Publicly Perform the Work or Collections, You must,\n     unless a request has been made pursuant to Section 4(a), keep intact all\n     copyright notices for the Work and provide, reasonable to the medium or\n     means You are utilizing: (i) the name of the Original Author (or pseudonym,\n     if applicable) if supplied, and/or if the Original Author and/or Licensor\n     designate another party or parties (e.g., a sponsor institute, publishing\n     entity, journal) for attribution (\"Attribution Parties\") in Licensor's\n     copyright notice, terms of service or by other reasonable means, the name\n     of such party or parties; (ii) the title of the Work if supplied; (iii) to\n     the extent reasonably practicable, the URI, if any, that Licensor specifies\n     to be associated with the Work, unless such URI does not refer to the\n     copyright notice or licensing information for the Work. The credit required\n     by this Section 4(c) may be implemented in any reasonable manner; provided,\n     however, that in the case of a Collection, at a minimum such credit will\n     appear, if a credit for all contributing authors of Collection appears,\n     then as part of these credits and in a manner at least as prominent as the\n     credits for the other contributing authors.  For the avoidance of doubt,\n     You may only use the credit required by this Section for the purpose of\n     attribution in the manner set out above and, by exercising Your rights\n     under this License, You may not implicitly or explicitly assert or imply\n     any connection with, sponsorship or endorsement by the Original Author,\n     Licensor and/or Attribution Parties, as appropriate, of You or Your use of\n     the Work, without the separate, express prior written permission of the\n     Original Author, Licensor and/or Attribution Parties.\n .\n  d. For the avoidance of doubt:\n .\n     i. Non-waivable Compulsory License Schemes. In those jurisdictions in which\n\tthe right to collect royalties through any statutory or compulsory\n\tlicensing scheme cannot be waived, the Licensor reserves the exclusive\n\tright to collect such royalties for any exercise by You of the rights\n\tgranted under this License;\n .\n    ii. Waivable Compulsory License Schemes. In those jurisdictions in which the\n\tright to collect royalties through any statutory or compulsory licensing\n\tscheme can be waived, the Licensor reserves the exclusive right to\n\tcollect such royalties for any exercise by You of the rights granted\n\tunder this License if Your exercise of such rights is for a purpose or\n\tuse which is otherwise than noncommercial as permitted under Section\n\t4(b) and otherwise waives the right to collect royalties through any\n\tstatutory or compulsory licensing scheme; and,\n .\n   iii. Voluntary License Schemes. The Licensor reserves the right to collect\n\troyalties, whether individually or, in the event that the Licensor is a\n\tmember of a collecting society that administers voluntary licensing\n\tschemes, via that society, from any exercise by You of the rights\n\tgranted under this License that is for a purpose or use which is\n\totherwise than noncommercial as permitted under Section 4(b).\n .\n  e. Except as otherwise agreed in writing by the Licensor or as may be\n     otherwise permitted by applicable law, if You Reproduce, Distribute or\n     Publicly Perform the Work either by itself or as part of any Collections,\n     You must not distort, mutilate, modify or take other derogatory action in\n     relation to the Work which would be prejudicial to the Original Author's\n     honor or reputation.\n .\n 5. Representations, Warranties and Disclaimer\n .\n UNLESS OTHERWISE MUTUALLY AGREED BY THE PARTIES IN WRITING, LICENSOR OFFERS THE\n WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING\n THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT\n LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR\n PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,\n OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME\n JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH\n EXCLUSION MAY NOT APPLY TO YOU.\n .\n 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN\n NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL,\n INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS\n LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE\n POSSIBILITY OF SUCH DAMAGES.\n .\n 7. Termination\n .\n  a. This License and the rights granted hereunder will terminate automatically\n     upon any breach by You of the terms of this License. Individuals or\n     entities who have received Collections from You under this License,\n     however, will not have their licenses terminated provided such individuals\n     or entities remain in full compliance with those licenses. Sections 1, 2,\n     5, 6, 7, and 8 will survive any termination of this License.\n .\n  b. Subject to the above terms and conditions, the license granted here is\n     perpetual (for the duration of the applicable copyright in the Work).\n     Notwithstanding the above, Licensor reserves the right to release the Work\n     under different license terms or to stop distributing the Work at any time;\n     provided, however that any such election will not serve to withdraw this\n     License (or any other license that has been, or is required to be, granted\n     under the terms of this License), and this License will continue in full\n     force and effect unless terminated as stated above.\n .\n 8. Miscellaneous\n .\n  a. Each time You Distribute or Publicly Perform the Work or a Collection, the\n     Licensor offers to the recipient a license to the Work on the same terms\n     and conditions as the license granted to You under this License.\n .\n  b. If any provision of this License is invalid or unenforceable under\n     applicable law, it shall not affect the validity or enforceability of the\n     remainder of the terms of this License, and without further action by the\n     parties to this agreement, such provision shall be reformed to the minimum\n     extent necessary to make such provision valid and enforceable.\n .\n  c. No term or provision of this License shall be deemed waived and no breach\n     consented to unless such waiver or consent shall be in writing and signed\n     by the party to be charged with such waiver or consent.\n .\n  d. This License constitutes the entire agreement between the parties with\n     respect to the Work licensed here. There are no understandings, agreements\n     or representations with respect to the Work not specified here. Licensor\n     shall not be bound by any additional provisions that may appear in any\n     communication from You. This License may not be modified without the mutual\n     written agreement of the Licensor and You.\n .\n  e. The rights granted under, and the subject matter referenced, in this\n     License were drafted utilizing the terminology of the Berne Convention for\n     the Protection of Literary and Artistic Works (as amended on September 28,\n     1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the\n     WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright\n     Convention (as revised on July 24, 1971). These rights and subject matter\n     take effect in the relevant jurisdiction in which the License terms are\n     sought to be enforced according to the corresponding provisions of the\n     implementation of those treaty provisions in the applicable national law.\n     If the standard suite of rights granted under applicable copyright law\n     includes additional rights not granted under this License, such additional\n     rights are deemed to be included in the License; this License is not\n     intended to restrict the license of any rights under applicable law.\n .\n Creative Commons Notice\n .\n Creative Commons is not a party to this License, and makes no warranty\n whatsoever in connection with the Work. Creative Commons will not be liable to\n You or any party on any legal theory for any damages whatsoever, including\n without limitation any general, special, incidental or consequential damages\n arising in connection to this license. Notwithstanding the foregoing two (2)\n sentences, if Creative Commons has expressly identified itself as the Licensor\n hereunder, it shall have all rights and obligations of Licensor.\n .\n Except for the limited purpose of indicating to the public that the Work is\n licensed under the CCPL, Creative Commons does not authorize the use by either\n party of the trademark \"Creative Commons\" or any related trademark or logo of\n Creative Commons without the prior written consent of Creative Commons. Any\n permitted use will be in compliance with Creative Commons' then-current\n trademark usage guidelines, as may be published on its website or otherwise\n made available upon request from time to time. For the avoidance of doubt, this\n trademark restriction does not form part of this License.\n .\n Creative Commons may be contacted at http://creativecommons.org/.\n"
  },
  {
    "path": "debian/ivxv-admin.cron.d",
    "content": "# IVXV Internet voting framework\n\n# Crontab for Management Service\n# /etc/cron.d/ivxv-admin\n\n# Copy service log files to Log Monitor with 15 min interval\n10,25,40,55 * * * *   ivxv-admin      if [ -x /usr/bin/ivxv-copy-log-to-logmon ]; then /usr/bin/ivxv-copy-log-to-logmon --log-level=WARNING; fi\n\n"
  },
  {
    "path": "debian/ivxv-admin.install",
    "content": "#!/usr/bin/dh-exec\n\n# config file for admin utils\ncollector-admin/ivxv-collector-admin.conf etc/ivxv/\n\n# IVXV admin interface\ncollector-admin/site/index.html      var/www/collector-admin/\ncollector-admin/site/favicon.ico     var/www/collector-admin/\ncollector-admin/site/ivxv            var/www/collector-admin/\n# WSGI application\ncollector-admin/site/cgi             var/www/\n# IVXV admin JavaScript\ncollector-admin/site/js              var/www/collector-admin/\n\n# Apache config file\ncollector-admin/config/ivxv-admin-ui.conf etc/apache2/sites-available/\n# Rsyslog config file\ncollector-admin/config/rsyslog.conf => etc/rsyslog.d/90-ivxv-admin.conf\n\n# \"SB Admin 2\" theme\ncommon/external/js/startbootstrap-sb-admin-2/dist   var/www/collector-admin/\ncommon/external/js/startbootstrap-sb-admin-2/js     var/www/collector-admin/\ncommon/external/js/startbootstrap-sb-admin-2/vendor var/www/collector-admin/\n\n# Bootstrap\ncommon/external/js/bootstrap var/www/collector-admin/vendor/\n\n# DataTables\ncommon/external/js/datatables var/www/collector-admin/vendor/\n\n# Font Awesome\ncommon/external/js/font-awesome var/www/collector-admin/vendor/\n\n# JQuery\ncommon/external/js/jquery/dist/jquery.min.js var/www/collector-admin/vendor/jquery/\n\n# BDOC verifier binary\nusr/bin/verifier  => usr/bin/ivxv-verify-container\n"
  },
  {
    "path": "debian/ivxv-admin.ivxv-admin-agent.service",
    "content": "[Unit]\nDescription=IVXV Management Service Agent Daemon\n\n[Service]\nExecStart=/usr/bin/ivxv-agent-daemon\nUser=ivxv-admin\nGroup=www-data\nRestart=always\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "debian/ivxv-admin.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-admin: hardening-no-relro usr/bin/ivxv-verify-container\nivxv-admin: hardening-no-pie usr/bin/ivxv-verify-container\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-admin: binary-without-manpage\n\n# The package configures /var/www/collector-admin to be the web server document\n# root itself, so we do not care that it is not specified in the FHS.\nivxv-admin: dir-or-file-in-var-www var/www/collector-admin/*\n"
  },
  {
    "path": "debian/ivxv-admin.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-admin\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # generate ssh key\n        SSH_KEY_FILE=~ivxv-admin/.ssh/id_ed25519\n        SSH_PUBKEY_FILE=${SSH_KEY_FILE}.pub\n        if [ ! -e ${SSH_KEY_FILE} ]; then\n            echo \"# Generating SSH key for user 'ivxv-admin'\"\n            rm -f \"${SSH_PUBKEY_FILE}\"\n            su -c \"ssh-keygen -t ed25519 -N '' -f ${SSH_KEY_FILE}\" ivxv-admin\n        fi\n        # mark ssh key with comment \"ivxv-admin-account\"\n        if ! grep --quiet ivxv-admin-account \"${SSH_PUBKEY_FILE}\"; then\n            sed --in-place --regexp-extended \\\n                --expression='s/ [^ ]+$/ ivxv-admin-account/' \\\n                \"${SSH_PUBKEY_FILE}\"\n        fi\n\n        # generate SSH config for ivxv-admin account\n        SSH_CONFIG_FILE=~ivxv-admin/.ssh/config\n        if [ ! -e \"${SSH_CONFIG_FILE}\" ]; then\n            echo \"# Creating SSH config for user 'ivxv-admin'\"\n            echo \"# SSH config file for \" > \"${SSH_CONFIG_FILE}\"\n            echo \"AddKeysToAgent yes\" >> \"${SSH_CONFIG_FILE}\"\n        fi\n        chown --changes ivxv-admin:ivxv \"${SSH_CONFIG_FILE}\"\n        chmod --changes 600 \"${SSH_CONFIG_FILE}\"\n\n        # CREATE DATA DIRECTORIES\n        ivxv-create-data-dirs\n\n        DIR='/var/lib/ivxv/admin-ui-permissions'\n        chown --changes ivxv-admin:www-data ${DIR}\n        chmod --changes 2770 ${DIR}\n\n        DIR='/var/lib/ivxv/upload'\n        chown --changes www-data:ivxv ${DIR}\n        chmod --changes g+s ${DIR}\n        chmod --changes 2770 ${DIR}\n\n        DIR='/var/lib/ivxv/ballot-box'\n        chown --changes www-data:ivxv ${DIR}\n        chmod --changes 2770 ${DIR}\n\n        for DIR in '/var/lib/ivxv/db' '/var/lib/ivxv/commands'\n        do\n            chown --changes ivxv-admin:ivxv ${DIR}\n            chmod --changes 2770 ${DIR}\n        done\n\n        DIR='/var/lib/ivxv/admin-ui-data'\n        chown --changes ivxv-admin:www-data ${DIR}\n        chmod --changes g+s ${DIR}\n\n        DIR='/var/lib/ivxv/db'\n        if [ ! -e ${DIR}/ivxv-management.db ]; then\n            echo 'Management service database is not initialized'\n            su -c 'env LC_ALL=et_EE.UTF-8 ivxv-db-reset --force' ivxv-admin\n        fi\n\n        DIR='/var/lib/ivxv/vis'\n        chown --changes ivxv-admin:ivxv ${DIR}\n        chmod --changes 2770 ${DIR}\n\n        # provide access to ivxv data for www-data account\n        usermod --groups ivxv www-data\n\n        # provide access to /etc/ssl/private for ivxv-admin account\n        # to read client certificate key\n        usermod --groups ssl-cert ivxv-admin\n\n        # set management service config file permissions\n        chmod --changes 755 /etc/ivxv/ivxv-collector-admin.conf\n\n        # Default client certificate for IVXV management service\n        CRT_SUBJ=\"/CN=ivxv-admin-client/O=IVXV kogumisteenuse haldusteenus/L=Somewhere/C=EE\"\n        CRT_FILE=\"/etc/ssl/certs/ivxv-admin-client.crt\"\n        RSA_KEY_FILE=\"/etc/ssl/private/ivxv-admin-client.key\"\n        if [ ! -f ${RSA_KEY_FILE} ]; then\n            echo \"# Generating RSA-key for Management service client certificate\"\n            openssl genrsa -out ${RSA_KEY_FILE} 2048\n            rm --force ${CRT_FILE}\n        fi\n        chmod 0640 ${RSA_KEY_FILE}\n        chown root:ssl-cert ${RSA_KEY_FILE}\n        if [ ! -f ${CRT_FILE} ]; then\n            echo \"# Generating client certificate for Management service\"\n            openssl req -x509 -days 365 -subj \"${CRT_SUBJ}\" -key \"${RSA_KEY_FILE}\" -out \"${CRT_FILE}\"\n        fi\n\n        # CONFIGURE APACHE\n        APACHE_RESTART=\"reload\"\n\n        # enable system default locale for Apache\n        if grep --quiet '^#\\. /etc/default/locale' /etc/apache2/envvars; then\n            echo \"# Enabling system default locale for Apache\"\n            sed --regexp-extended --in-place \\\n                --expression 's/^#(\\. .etc.default.locale)/\\1/' \\\n                /etc/apache2/envvars\n            APACHE_RESTART=\"restart\"\n        fi\n\n        # remove Apache default site config\n        if [ -L /etc/apache2/sites-enabled/000-default.conf ]; then\n            echo \"# Disabling Apache default site\"\n            a2dissite --quiet 000-default\n            APACHE_RESTART=\"restart\"\n        fi\n\n        # install IVXV collector admin UI site config\n        if [ ! -L /etc/apache2/sites-enabled/ivxv-admin-ui.conf ]; then\n            echo \"# Enabling Apache site for IVXV collector management service UI\"\n            a2ensite --quiet ivxv-admin-ui\n            APACHE_RESTART=\"restart\"\n        fi\n\n        # enable required modules for Apache\n        APACHE_MODULES_TO_ENABLE=\"expires rewrite ssl wsgi\"\n        for MODULE in ${APACHE_MODULES_TO_ENABLE}; do\n            if [ ! -L /etc/apache2/mods-enabled/${MODULE}.load ]; then\n                a2enmod ${MODULE}\n                APACHE_RESTART=\"restart\"\n            fi\n        done\n\n        # Default SSL certificate for Apache web server\n        CRT_SUBJ=\"/CN=ivxv-admin-ui/O=IVXV kogumisteenuse haldusteenus/L=Somewhere/C=EE\"\n        CRT_FILE=\"/etc/ssl/certs/ivxv-admin-default.crt\"\n        RSA_KEY_FILE=\"/etc/ssl/private/ivxv-admin-default.key\"\n        if [ ! -f ${RSA_KEY_FILE} ]; then\n            echo \"# Generating RSA-key for Apache web server default certificate\"\n            openssl genrsa -out ${RSA_KEY_FILE} 2048\n            rm -f ${CRT_FILE}\n        fi\n        chmod 0640 ${RSA_KEY_FILE}\n        chown root:root ${RSA_KEY_FILE}\n        if [ ! -f ${CRT_FILE} ]; then\n            echo \"# Generating certificate for Apache web server\"\n            openssl req -x509 -subj \"${CRT_SUBJ}\" -days 365 -key ${RSA_KEY_FILE} -out ${CRT_FILE}\n        fi\n        DH_PARAMS_FILE=\"/etc/ssl/dhparams.pem\"\n        if [ ! -f \"${DH_PARAMS_FILE}\" ]; then\n            echo \"# Generating strong Diffie-Hellman group file\"\n            openssl dhparam -out ${DH_PARAMS_FILE}.tmp 2048\n            mv ${DH_PARAMS_FILE}.tmp ${DH_PARAMS_FILE}\n        fi\n\n        # (re)starting Apache server\n        echo \"# Starting Apache web server\"\n        deb-systemd-invoke status apache2 > /dev/null || APACHE_RESTART=\"start\"\n        deb-systemd-invoke ${APACHE_RESTART} apache2\n\n        # CONFIGURE RSYSLOG\n        echo \"# Restarting rsyslog log server\"\n        deb-systemd-invoke restart rsyslog\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-admin.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-admin\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # CONFIGURE APACHE\n        if which a2ensite > /dev/null; then\n            # install Apache default site config\n            if [ ! -L /etc/apache2/sites-enabled/000-default.conf ] &&\n                [ -e /etc/apache2/sites-available/000-default.conf ]; then\n                echo \"# Enabling Apache default site\"\n                a2ensite --quiet 000-default\n            fi\n\n            # remove IVXV collector admin UI site config\n            if [ -L /etc/apache2/sites-enabled/ivxv-admin-ui.conf ]; then\n                echo \"# Disabling Apache site for IVXV collector management service UI\"\n                a2dissite --quiet ivxv-admin-ui\n            fi\n\n            deb-systemd-invoke restart apache2\n        fi\n\n        # remove ivxv-admin crontab\n        if crontab -l -u ivxv-admin > /dev/null; then\n            echo \"# Removing crontab for user ivxv-admin\"\n            crontab -r -u ivxv-admin\n        fi\n    ;;\n\n    purge)\n        # DATA DIRECTORIES\n        for DIR in /var/lib/ivxv/admin-ui-data \\\n                   /var/lib/ivxv/admin-ui-permissions \\\n                   /var/lib/ivxv/commands \\\n                   /var/lib/ivxv/db \\\n                   /var/lib/ivxv/upload \\\n                   /var/lib/ivxv/ballot-box\n        do\n            if [ -d \"${DIR}\" ]; then\n                echo \"# Removing data directory ${DIR}\"\n                rm -rf \"${DIR}\"\n            fi\n        done\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-admin.service",
    "content": "[Unit]\nDescription=IVXV Management Service Daemon\n\n[Service]\nExecStart=/usr/bin/ivxv-admin-httpd\nUser=ivxv-admin\nGroup=ivxv\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "debian/ivxv-backup.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/voteunion => usr/bin/ivxv-voteunion\n"
  },
  {
    "path": "debian/ivxv-backup.lintian-overrides",
    "content": "# Go produces static binaries, this is a feature not a bug.\nivxv-backup: statically-linked-binary usr/bin/ivxv-voteunion\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-backup: binary-without-manpage\n"
  },
  {
    "path": "debian/ivxv-backup.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-backup\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # create backup directory\n        mkdir --parents \\\n            /var/backups/ivxv/management-conf \\\n            /var/backups/ivxv/log \\\n            /var/backups/ivxv/ballot-box\n    chmod 700 \\\n            /var/backups/ivxv/management-conf \\\n            /var/backups/ivxv/log \\\n            /var/backups/ivxv/ballot-box\n    chown ivxv-admin:ivxv \\\n            /var/backups/ivxv/management-conf \\\n            /var/backups/ivxv/log \\\n            /var/backups/ivxv/ballot-box\n    mkdir --parents /var/log/ivxv\n    chown --changes syslog:syslog /var/log/ivxv\n    chmod --changes 755 /var/log/ivxv\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-backup.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-backup\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    purge)\n        rm -rf /var/backups/ivxv\n        rm -rf /var/log/ivxv\n    ;;\n\n    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-choices.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/choices     => usr/bin/ivxv-choices\nusr/bin/choiceimp   => usr/bin/ivxv-choiceimp\nusr/bin/voterimp    => usr/bin/ivxv-voterimp\nusr/bin/districtimp => usr/bin/ivxv-districtimp\n\nusr/lib/systemd/user/ivxv-choices@.service\n"
  },
  {
    "path": "debian/ivxv-choices.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-choices: hardening-no-relro usr/bin/ivxv-choices\nivxv-choices: hardening-no-pie usr/bin/ivxv-choices\n\nivxv-choices: hardening-no-relro usr/bin/ivxv-choiceimp\nivxv-choices: hardening-no-pie usr/bin/ivxv-choiceimp\n\nivxv-choices: hardening-no-relro usr/bin/ivxv-voterimp\nivxv-choices: hardening-no-pie usr/bin/ivxv-voterimp\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-choices: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-choices: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-choices.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-choices\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-choices USER\n        # add user account\n        if ! getent passwd ivxv-choices > /dev/null; then\n            echo \"# Adding user 'ivxv-choices'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-choices \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-choices\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-choices/.ssh ||\n        mkdir --parents ~ivxv-choices/.ssh\n        chmod 700 ~ivxv-choices/.ssh\n        chown ivxv-choices:ivxv ~ivxv-choices/.ssh\n        test -e ~ivxv-choices/.ssh/authorized_keys ||\n            touch ~ivxv-choices/.ssh/authorized_keys\n        chmod 600 ~ivxv-choices/.ssh/authorized_keys\n        chown ivxv-choices:ivxv ~ivxv-choices/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-choices\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-choices.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-choices\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-choices service\n        deb-systemd-invoke stop \"ivxv-choices@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-choices\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-common.install",
    "content": "#!/usr/bin/dh-exec\netc/ivxv/rsyslog-ivxv-templates.conf => etc/rsyslog.d/10-ivxv-templates.conf\netc/ivxv/sudoers => etc/sudoers.d/ivxv\n\nusr/bin/ivxv-admin-sudo.sh => usr/bin/ivxv-admin-sudo\nusr/bin/ivxv-admin-helper.sh => usr/bin/ivxv-admin-helper\n"
  },
  {
    "path": "debian/ivxv-common.lintian-overrides",
    "content": "# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-common: binary-without-manpage\n"
  },
  {
    "path": "debian/ivxv-common.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-common\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-admin USER\n        # add user group\n        if ! getent group ivxv > /dev/null; then\n            echo \"# Adding user group 'ivxv'\"\n            addgroup --system ivxv\n        fi\n        # add user account\n        if ! getent passwd ivxv-admin > /dev/null; then\n            echo \"# Adding user 'ivxv-admin'\"\n            adduser --home /var/lib/ivxv/user/ivxv-admin --shell /bin/bash \\\n                --system --ingroup ivxv ivxv-admin\n        fi\n\n        # prepare ivxv-admin SSH directory\n        test -d ~ivxv-admin/.ssh ||\n            mkdir ~ivxv-admin/.ssh\n        chown --changes ivxv-admin:ivxv ~ivxv-admin/.ssh\n        chmod --changes 700 ~ivxv-admin/.ssh\n        test -f ~ivxv-admin/.ssh/authorized_keys ||\n            touch ~ivxv-admin/.ssh/authorized_keys\n        chown --changes ivxv-admin:ivxv ~ivxv-admin/.ssh/authorized_keys\n        chmod --changes 600 ~ivxv-admin/.ssh/authorized_keys\n\n        # set /etc/ivxv permissions\n        test -d /etc/ivxv/debs ||\n            mkdir --parents /etc/ivxv/debs\n        chown ivxv-admin:ivxv /etc/ivxv /etc/ivxv/debs\n        chmod 770 /etc/ivxv /etc/ivxv/debs\n\n        # create /var/lib/ivxv/*\n        if [ ! -d /var/lib/ivxv/service ]; then\n            echo \"# Creating data directory /var/lib/ivxv/service\"\n            mkdir --parents /var/lib/ivxv/service\n        fi\n        chmod 750 /var/lib/ivxv\n        chown ivxv-admin:ivxv /var/lib/ivxv\n        chmod 770 /var/lib/ivxv/service\n        chown ivxv-admin:ivxv /var/lib/ivxv/service\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-common.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-common\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    purge)\n        # delete data directory\n        if [ -d /var/lib/ivxv ]; then\n            echo \"# Removing data directory /var/lib/ivxv\"\n            rm -rf /var/lib/ivxv\n        fi\n\n        # REMOVE ivxv-admin USER\n        if getent passwd ivxv-admin > /dev/null; then\n            deluser --system ivxv-admin\n        fi\n        if getent group ivxv > /dev/null; then\n            delgroup --system ivxv\n        fi\n    ;;\n\n    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-log.install",
    "content": "#!/usr/bin/dh-exec\netc/ivxv/rsyslog-logcollector.conf => etc/rsyslog.d/ivxv-logcollector.conf\n"
  },
  {
    "path": "debian/ivxv-log.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-log\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        echo \"# Restarting rsyslog log server\"\n        deb-systemd-invoke restart rsyslog\n    mkdir --parents /var/log/ivxv\n    chown --changes syslog:syslog /var/log/ivxv\n    chmod --changes 755 /var/log/ivxv\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-log.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-log\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    purge)\n        echo \"# Restarting rsyslog log server\"\n        deb-systemd-invoke restart rsyslog\n\n        echo \"# Deleting /var/log/ivxv/* files\"\n        rm -rf /var/log/ivxv\n    ;;\n\n    remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-mid.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/mid => usr/bin/ivxv-mid\n\nusr/lib/systemd/user/ivxv-mid@.service\n"
  },
  {
    "path": "debian/ivxv-mid.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-mid: hardening-no-relro usr/bin/ivxv-mid\nivxv-mid: hardening-no-pie usr/bin/ivxv-mid\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-mid: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-mid: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-mid.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-mid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-mid USER\n        # add user account\n        if ! getent passwd ivxv-mid > /dev/null; then\n            echo \"# Adding user 'ivxv-mid'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-mid \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-mid\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-mid/.ssh ||\n        mkdir --parents ~ivxv-mid/.ssh\n        chmod 700 ~ivxv-mid/.ssh\n        chown ivxv-mid:ivxv ~ivxv-mid/.ssh\n        test -e ~ivxv-mid/.ssh/authorized_keys ||\n            touch ~ivxv-mid/.ssh/authorized_keys\n        chmod 600 ~ivxv-mid/.ssh/authorized_keys\n        chown ivxv-mid:ivxv ~ivxv-mid/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-mid\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-mid.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-mid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-mid service\n        deb-systemd-invoke stop \"ivxv-mid@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-mid\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-proxy.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/proxy => usr/bin/ivxv-proxy\n\nusr/share/ivxv/haproxy.cfg.tmpl\nusr/share/ivxv/haproxy-rsyslog.conf\nusr/lib/systemd/user/ivxv-proxy@.service\n"
  },
  {
    "path": "debian/ivxv-proxy.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-proxy: hardening-no-relro usr/bin/ivxv-proxy\nivxv-proxy: hardening-no-pie usr/bin/ivxv-proxy\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-proxy: binary-without-manpage\n"
  },
  {
    "path": "debian/ivxv-proxy.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-proxy\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\nensure_dir() {\n        test -d \"$1\" || mkdir --parents \"$1\"\n        chmod $2 \"$1\"\n        chown $3 \"$1\"\n}\n\nensure_file() {\n        test -e \"$1\" || echo \"$4\" > \"$1\"\n        chmod $2 \"$1\"\n        chown $3 \"$1\"\n}\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE haproxy USER created by haproxy package\n        # check if the user exists\n        if ! getent passwd haproxy > /dev/null; then\n            echo \"no such user: \\`haproxy'\" >&2\n            exit 2\n        fi\n\n        # prepare ssh directory for user account\n        ensure_dir  ~haproxy/.ssh                 700 haproxy:ivxv\n        ensure_file ~haproxy/.ssh/authorized_keys 600 haproxy:ivxv\n\n        # prepare config directory for enabling systemd user services\n        ensure_dir ~haproxy/.config 700 haproxy:ivxv\n\n        # Set login shell and ivxv group for haproxy user\n        usermod --shell /bin/bash --groups ivxv haproxy\n\n        # systemd user services can not depend on system services directly, so\n        # require haproxy to be up before the systemd user instance for haproxy\n        # is started\n        userd=/lib/systemd/system/user@$(id --user haproxy).service.d\n        ensure_dir  $userd              755 root:root\n        ensure_file $userd/haproxy.conf 644 root:root \"[Unit]\\nWants=haproxy.service\\nAfter=haproxy.service\"\n\n        # enable user to automatically start service\n        loginctl enable-linger haproxy\n\n        # change haproxy.cfg owner to haproxy:ivxv\n        chown haproxy:ivxv /etc/haproxy/haproxy.cfg\n\n        # replace haproxy logging config\n        HAPROXY_CONF=\"/etc/rsyslog.d/49-haproxy.conf\"\n        if [ ! -L \"${HAPROXY_CONF}\" ]; then\n            echo \"# Overriding haproxy logging config file\"\n            dpkg-divert --add \"${HAPROXY_CONF}\"\n            ln --symbolic --force /usr/share/ivxv/haproxy-rsyslog.conf \"${HAPROXY_CONF}\"\n            echo \"# Restarting rsyslog\"\n            deb-systemd-invoke restart rsyslog\n        fi\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-proxy.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-proxy\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-proxy service\n        deb-systemd-invoke stop \"ivxv-proxy@*service\"\n    ;;\n\n    purge)\n        # remove haproxy dependency\n        if getent passwd haproxy > /dev/null\n        then\n            userd=\"/lib/systemd/system/user@$(id --user haproxy).service.d\"\n            rm -f \"$userd/haproxy.conf\"\n            test -n \"$(ls -A \"$userd\")\" || rm -rf \"$userd\"\n        fi\n\n        # remove diversion of haproxy logging config\n        dpkg-divert --remove /etc/rsyslog.d/49-haproxy.conf\n        rm -f /etc/rsyslog.d/49-haproxy.conf\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-sessionstatus.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/sessionstatus => usr/bin/ivxv-sessionstatus\n\nusr/lib/systemd/user/ivxv-sessionstatus@.service\n"
  },
  {
    "path": "debian/ivxv-sessionstatus.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-sessionstatus: hardening-no-relro usr/bin/ivxv-sessionstatus\nivxv-sessionstatus: hardening-no-pie usr/bin/ivxv-sessionstatus\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-sessionstatus: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-sessionstatus: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-sessionstatus.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-sessionstatus\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-sessionstatus USER\n        # add user account\n        if ! getent passwd ivxv-sessionstatus > /dev/null; then\n            echo \"# Adding user 'ivxv-sessionstatus'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-sessionstatus \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-sessionstatus\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-sessionstatus/.ssh ||\n        mkdir --parents ~ivxv-sessionstatus/.ssh\n        chmod 700 ~ivxv-sessionstatus/.ssh\n        chown ivxv-sessionstatus:ivxv ~ivxv-sessionstatus/.ssh\n        test -e ~ivxv-sessionstatus/.ssh/authorized_keys ||\n            touch ~ivxv-sessionstatus/.ssh/authorized_keys\n        chmod 600 ~ivxv-sessionstatus/.ssh/authorized_keys\n        chown ivxv-sessionstatus:ivxv ~ivxv-sessionstatus/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-sessionstatus\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-sessionstatus.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-sessionstatus\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-sessionstatus service\n        deb-systemd-invoke stop \"ivxv-sessionstatus@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-sessionstatus\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-smartid.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/smartid => usr/bin/ivxv-smartid\n\nusr/lib/systemd/user/ivxv-smartid@.service\n"
  },
  {
    "path": "debian/ivxv-smartid.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-smartid: hardening-no-relro usr/bin/ivxv-smartid\nivxv-smartid: hardening-no-pie usr/bin/ivxv-smartid\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-smartid: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-smartid: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-smartid.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-smartid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-smartid USER\n        # add user account\n        if ! getent passwd ivxv-smartid > /dev/null; then\n            echo \"# Adding user 'ivxv-smartid'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-smartid \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-smartid\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-smartid/.ssh ||\n        mkdir --parents ~ivxv-smartid/.ssh\n        chmod 700 ~ivxv-smartid/.ssh\n        chown ivxv-smartid:ivxv ~ivxv-smartid/.ssh\n        test -e ~ivxv-smartid/.ssh/authorized_keys ||\n            touch ~ivxv-smartid/.ssh/authorized_keys\n        chmod 600 ~ivxv-smartid/.ssh/authorized_keys\n        chown ivxv-smartid:ivxv ~ivxv-smartid/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-smartid\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-smartid.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-smartid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-smartid service\n        deb-systemd-invoke stop \"ivxv-smartid@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-smartid\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-storage.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/storage    => usr/bin/ivxv-storage\nusr/bin/storageidx => usr/bin/ivxv-storageidx\nusr/bin/storageorder => usr/bin/ivxv-storageorder\n\nusr/lib/systemd/user/ivxv-storage@.service\n\nusr/bin/ivxv-storage_db_install.sh\nusr/bin/ivxv-storage_db_uninstall.sh\nusr/bin/ivxv-storage_db.tar.gz => usr/ivxv-storage_db.tar.gz\n"
  },
  {
    "path": "debian/ivxv-storage.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-storage: hardening-no-relro usr/bin/ivxv-storage\nivxv-storage: hardening-no-pie usr/bin/ivxv-storage\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-storage: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-storage: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-storage.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-storage\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # install db for ivxv-storage\n        if [ -f \"/usr/bin/ivxv-storage_db_install.sh\" ]; then\n            /usr/bin/ivxv-storage_db_install.sh\n        fi\n\n        # CONFIGURE ivxv-storage USER\n        # add user account\n        if ! getent passwd ivxv-storage > /dev/null; then\n            echo \"# Adding user 'ivxv-storage'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-storage \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-storage\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-storage/.ssh ||\n        mkdir --parents ~ivxv-storage/.ssh\n        chmod 700 ~ivxv-storage/.ssh\n        chown ivxv-storage:ivxv ~ivxv-storage/.ssh\n        test -e ~ivxv-storage/.ssh/authorized_keys ||\n            touch ~ivxv-storage/.ssh/authorized_keys\n        chmod 600 ~ivxv-storage/.ssh/authorized_keys\n        chown ivxv-storage:ivxv ~ivxv-storage/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-storage\n\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-storage.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-storage\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-storage service\n        deb-systemd-invoke stop \"ivxv-storage@*service\"\n\n        # uninstall db for ivxv-storage\n        if [ -f \"/usr/bin/ivxv-storage_db_uninstall.sh\" ]; then\n            /usr/bin/ivxv-storage_db_uninstall.sh\n        fi\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-storage\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            pkill --uid \"${USER_ACCOUNT}\" || true && sleep 1\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n\n        fi\n\n        # uninstall db for ivxv-storage\n        if [ -f \"/usr/bin/ivxv-storage_db_uninstall.sh\" ]; then\n            /usr/bin/ivxv-storage_db_uninstall.sh\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n        # uninstall db for ivxv-storage\n        if [ -f \"/usr/bin/ivxv-storage_db_uninstall.sh\" ]; then\n            /usr/bin/ivxv-storage_db_uninstall.sh\n        fi\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-verification.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/verification => usr/bin/ivxv-verification\n\nusr/lib/systemd/user/ivxv-verification@.service\n"
  },
  {
    "path": "debian/ivxv-verification.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-verification: hardening-no-relro usr/bin/ivxv-verification\nivxv-verification: hardening-no-pie usr/bin/ivxv-verification\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-verification: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-verification: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-verification.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-verification\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-verification USER\n        # add user account\n        if ! getent passwd ivxv-verification > /dev/null; then\n            echo \"# Adding user 'ivxv-verification'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-verification \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-verification\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-verification/.ssh ||\n        mkdir --parents ~ivxv-verification/.ssh\n        chmod 700 ~ivxv-verification/.ssh\n        chown ivxv-verification:ivxv ~ivxv-verification/.ssh\n        test -e ~ivxv-verification/.ssh/authorized_keys ||\n            touch ~ivxv-verification/.ssh/authorized_keys\n        chmod 600 ~ivxv-verification/.ssh/authorized_keys\n        chown ivxv-verification:ivxv ~ivxv-verification/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-verification\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-verification.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-verification\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-verification service\n        deb-systemd-invoke stop \"ivxv-verification@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-verification\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-votesorder.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/votesorder     => usr/bin/ivxv-votesorder\n\nusr/lib/systemd/user/ivxv-votesorder@.service\n"
  },
  {
    "path": "debian/ivxv-votesorder.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-votesorder: hardening-no-relro usr/bin/ivxv-votesorder\nivxv-votesorder: hardening-no-pie usr/bin/ivxv-votesorder\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-votesorder: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-votesorder: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-votesorder.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-votesorder\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-votesorder USER\n        # add user account\n        if ! getent passwd ivxv-votesorder > /dev/null; then\n            echo \"# Adding user 'ivxv-xroad'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-votesorder \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-votesorder\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-votesorder/.ssh ||\n        mkdir --parents ~ivxv-votesorder/.ssh\n        chmod 700 ~ivxv-votesorder/.ssh\n        chown ivxv-votesorder:ivxv ~ivxv-votesorder/.ssh\n        test -e ~ivxv-votesorder/.ssh/authorized_keys ||\n            touch ~ivxv-votesorder/.ssh/authorized_keys\n        chmod 600 ~ivxv-votesorder/.ssh/authorized_keys\n        chown ivxv-votesorder:ivxv ~ivxv-votesorder/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-votesorder\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-votesorder.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-votesorder\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-votesorder service\n        deb-systemd-invoke stop \"ivxv-votesorder@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-votesorder\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-voting.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/voting     => usr/bin/ivxv-voting\nusr/bin/voteexp    => usr/bin/ivxv-voteexp\nusr/bin/voterstats => usr/bin/ivxv-voterstats\n\nusr/lib/systemd/user/ivxv-voting@.service\n"
  },
  {
    "path": "debian/ivxv-voting.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-voting: hardening-no-relro usr/bin/ivxv-voting\nivxv-voting: hardening-no-pie usr/bin/ivxv-voting\n\nivxv-voting: hardening-no-relro usr/bin/ivxv-voteexp\nivxv-voting: hardening-no-pie usr/bin/ivxv-voteexp\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-voting: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-voting: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-voting.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-voting\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-voting USER\n        # add user account\n        if ! getent passwd ivxv-voting > /dev/null; then\n            echo \"# Adding user 'ivxv-voting'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-voting \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-voting\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-voting/.ssh ||\n        mkdir --parents ~ivxv-voting/.ssh\n        chmod 700 ~ivxv-voting/.ssh\n        chown ivxv-voting:ivxv ~ivxv-voting/.ssh\n        test -e ~ivxv-voting/.ssh/authorized_keys ||\n            touch ~ivxv-voting/.ssh/authorized_keys\n        chmod 600 ~ivxv-voting/.ssh/authorized_keys\n        chown ivxv-voting:ivxv ~ivxv-voting/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-voting\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-voting.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-voting\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-voting service\n        deb-systemd-invoke stop \"ivxv-voting@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-voting\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-webeid.install",
    "content": "#!/usr/bin/dh-exec\nusr/bin/webeid => usr/bin/ivxv-webeid\n\nusr/lib/systemd/user/ivxv-webeid@.service\n"
  },
  {
    "path": "debian/ivxv-webeid.lintian-overrides",
    "content": "# Hardening the binaries with relro and pie is not necessary since memory\n# errors should not occur in Go binaries. Although we could use -buildmode=pie,\n# we have not tested the effect this will have, so leave it off for now.\nivxv-webeid: hardening-no-relro usr/bin/ivxv-webeid\nivxv-webeid: hardening-no-pie usr/bin/ivxv-webeid\n\n# We do not provide manpages, since these packages are not meant for\n# distribution.\nivxv-webeid: binary-without-manpage\n\n# The package depends on ivxv-common, which depends on adduser.\nivxv-webeid: maintainer-script-needs-depends-on-adduser postinst\n"
  },
  {
    "path": "debian/ivxv-webeid.postinst",
    "content": "#!/bin/sh\n# postinst script for ivxv-webeid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> `abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    configure)\n        # CONFIGURE ivxv-webeid USER\n        # add user account\n        if ! getent passwd ivxv-webeid > /dev/null; then\n            echo \"# Adding user 'ivxv-webeid'\"\n            adduser --quiet --home /var/lib/ivxv/user/ivxv-webeid \\\n                --shell /bin/bash --system --ingroup ivxv ivxv-webeid\n        fi\n\n        # prepare ssh directory for user account\n        test -d ~ivxv-webeid/.ssh ||\n        mkdir --parents ~ivxv-webeid/.ssh\n        chmod 700 ~ivxv-webeid/.ssh\n        chown ivxv-webeid:ivxv ~ivxv-webeid/.ssh\n        test -e ~ivxv-webeid/.ssh/authorized_keys ||\n            touch ~ivxv-webeid/.ssh/authorized_keys\n        chmod 600 ~ivxv-webeid/.ssh/authorized_keys\n        chown ivxv-webeid:ivxv ~ivxv-webeid/.ssh/authorized_keys\n\n        mkdir --parents /var/log/ivxv\n        chown --changes syslog:syslog /var/log/ivxv\n        chmod --changes 755 /var/log/ivxv\n\n        # enable user to automatically start service\n        loginctl enable-linger ivxv-webeid\n    ;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/ivxv-webeid.postrm",
    "content": "#!/bin/sh\n# postrm script for ivxv-webeid\n#\n# see: dh_installdeb(1)\n\nset -e\n\n# summary of how this script can be called:\n#        * <postrm> `remove'\n#        * <postrm> `purge'\n#        * <old-postrm> `upgrade' <new-version>\n#        * <new-postrm> `failed-upgrade' <old-version>\n#        * <new-postrm> `abort-install'\n#        * <new-postrm> `abort-install' <old-version>\n#        * <new-postrm> `abort-upgrade' <old-version>\n#        * <disappearer's-postrm> `disappear' <overwriter>\n#          <overwriter-version>\n# for details, see https://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\n\ncase \"$1\" in\n    remove)\n        # stop ivxv-webeid service\n        deb-systemd-invoke stop \"ivxv-webeid@*service\"\n    ;;\n\n    purge)\n        # Remove user account\n        USER_ACCOUNT=\"ivxv-webeid\"\n        if getent passwd \"${USER_ACCOUNT}\" > /dev/null; then\n            # terminate user sessions\n            loginctl terminate-user \"${USER_ACCOUNT}\"\n\n            # kill user processes\n            if pgrep --count --uid \"${USER_ACCOUNT}\" > /dev/null ; then\n                pkill --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n                pkill --signal KILL --uid \"${USER_ACCOUNT}\" || true\n                sleep 1\n            fi\n\n            USER_HOME_DIR=\"$(getent passwd ${USER_ACCOUNT} | cut -d: -f6)\"\n            # remove user home directory using local hack\n            # to avoid dependency of perl-modules\n            # that is required for use deluser --remove-home option.\n            deluser --system \"${USER_ACCOUNT}\"\n            rm -rf ${USER_HOME_DIR}\n        fi\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n\n# reload the systemd manager configuration\nif [ -d /run/systemd/users ]; then\n    systemctl daemon-reload >/dev/null || true\nfi\n\n# dh_installdeb will replace this with shell code automatically\n# generated by other debhelper scripts.\n\n#DEBHELPER#\n\nexit 0\n"
  },
  {
    "path": "debian/python3-ivxv-common.install",
    "content": "#!/usr/bin/dh-exec\n\ncommon/external/python/wheels /usr/lib/python3.10/.cache/pip/\ncommon/external/python/requirements.txt /usr/lib/python3.10/.cache/pip/\n"
  },
  {
    "path": "debian/python3-ivxv-common.postinst",
    "content": "#!/bin/sh\n\nset -e\n\nPYTHON_VERSION=$(python3 --version | cut -d ' ' -f 2 | cut -d '.' -f 1,2)\n\npip3 install --no-index --find-links /usr/lib/python3.10/.cache/pip/wheels/ --requirement /usr/lib/python3.10/.cache/pip/requirements.txt --require-hash --target /usr/local/lib/python$PYTHON_VERSION/dist-packages/\n\nexit 0\n"
  },
  {
    "path": "debian/python3-ivxv-common.prerm",
    "content": "#!/bin/sh\n\nset -e\n\npip3 uninstall --requirement /usr/lib/python3.10/.cache/pip/requirements.txt --yes\n\nexit 0\n"
  },
  {
    "path": "debian/rules",
    "content": "#!/usr/bin/make -f\n\n# See debhelper(7) (uncomment to enable)\n# output every command that modifies files on the build system.\n#export DH_VERBOSE = 1\n\n# disable testing of python module (module does not have unit tests)\nexport PYBUILD_DISABLE = test\n\n%:\n\tdh $@ --with python3\n\noverride_dh_auto_clean:\n\t# Do not clean Java applications nor documentation.\n\t$(MAKE) clean-go clean-systemd\n\n\t# Clean ivxv-admin.\n\t# FIXME: The python build files should not live in the root directory,\n\t#        but be hidden behind the Makefile facade like Go, Java, and\n\t#        documentation are.\n\tdh_auto_clean --package ivxv-admin --buildsystem pybuild\n\noverride_dh_auto_build:\n\t# Update version strings in subcomponents\n\t$(MAKE) version\n\n\t# Do not build Java applications nor documentation for Debian.\n\t$(MAKE) go\n\n\t# Build ivxv-admin.\n\t# FIXME: The python build files should not live in the root directory,\n\t#        but be hidden behind the Makefile facade like Go, Java, and\n\t#        documentation are.\n\tdh_auto_build --package ivxv-admin --buildsystem pybuild\n\noverride_dh_auto_test:\n\t# This target contains workarounds for DEB_BUILD_OPTIONS=nocheck bug\n\t# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=568897\n\n\t# Do not test Java applications.\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\t$(MAKE) test-go GOTESTFLAGS=-short\nendif\n\n\t# Test ivxv-admin.\n\t# FIXME: The python build files should not live in the root directory,\n\t#        but be hidden behind the Makefile facade like Go, Java, and\n\t#        documentation are.\nifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))\n\tdh_auto_test --package ivxv-admin --buildsystem pybuild\nendif\n\noverride_dh_auto_install:\n\t# install ivxv-storage db\n\t$(MAKE) -C storage ivxv_storage_db\n\n\tdh_installsystemd\n\tdh_auto_install --no-package ivxv-admin\n\n\t# Install ivxv-admin.\n\t# FIXME: The python build files should not live in the root directory,\n\t#        but be hidden behind the Makefile facade like Go, Java, and\n\t#        documentation are.\n\tdh_auto_install --package ivxv-admin --buildsystem pybuild --destdir debian/ivxv-admin/\n\tinstall -d debian/ivxv-admin/etc/ssl/certs\n\tcat collector-admin/sk-certs/*.crt > debian/ivxv-admin/etc/ssl/certs/sk-juur.crt\n\n\t# 1. `cp ivxv-admin-agent.service /lib/systemd/system/`\n\t# 2. `systemctl enable ivxv-admin-agent.service`\n\tdh_installsystemd --package ivxv-admin --name ivxv-admin-agent ivxv-admin-agent.service\n\n# Without that target you will get:\n#     Normalizing XXX using File::StripNondeterminism::handlers::zip\n# `dh_strip_nondeterminism` itself removes a timestamp from an archive\n# which means that each wheel's hash, after that, will differ as well,\n# this will lead to `pip3 install --require-hashes` failure\noverride_dh_strip_nondeterminism:\n\tdh_strip_nondeterminism --no-package=python3-ivxv-common\n\n# Silently skip dh_installinit to avoid redundant code in postinst/postrm\n# scripts as packages do not use obsolete init system.\noverride_dh_installinit:\n\n# Go >= 1.19 alread compresses ELF section in binaries that are produced during `go build/install`\n# dwz: ./ivxv-XXX/usr/bin/ivxv-XXX: Found compressed .debug_abbrev section, not attempting dwz compression\noverride_dh_dwz:\n"
  },
  {
    "path": "key/.gitignore",
    "content": ".classpath\n.gradle/\n.idea/\n.project\nbuild/\n/log/\nbin/\n.settings/\ndummy_card_filesystems\n"
  },
  {
    "path": "key/Makefile",
    "content": "include ../common/java/common.mk\n"
  },
  {
    "path": "key/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n\n-----------------\n Key application\n-----------------\n\nKey application is a command line application for generating key parameters,\ngenerating private keys and decrypting ballots.\n\nThe application functionality is provided by the tools described below:\n\n* *util* - utility functions such as listing smart card readers and inserted\n  smart cards or testing correct functionality of key shares.\n* *groupgen* - generate group parameters suitable for generating a key pair.\n* *init* - generate the decryption and signing key in a distributed manner on\n  smart cards, construct the corresponding public keys and output the public\n  keys and certificate corresponding to the encryption key.\n* *testkey* - encrypt a random message using the given public key and perform\n  the decryption of this message using the given key share. Can be used to test\n  the usability of the key shares.\n* *decrypt* - take the anonymized ballot box from the *processor*, construct the\n  decryption key from the shares on the smart cards, decrypt the encrypted\n  ballots and provide proofs of correct decryption.\n\nBuilding\n--------\n\nIVXV java applications have 2 levels of build systems:\n\n* *make* - the build system facade. Must be installed on the user's machine.\n* *gradle* - the implementation of the build system. Gradle is located under\n  ``common/external/gradle-8.11``, with the executable ``bin/gradle(.bat)``.\n\nBuilding:\n\n* ``make`` or\n* ``make all`` or\n* ``gradle build installDist`` - build and test the application.\n* ``make clean`` or\n* ``gradle clean`` - clean build resources, i.e. the directory ``build``.\n\nApplication executable is ``build/install/key/bin/key(.bat)``.\n\nDistributable application packages are provided under ``build/distributions/``.\n\nRandomness sources\n------------------\n\nDuring key parameter generation and key initialization, random bytes are\nrequired. The IVXV framework provides several interfaces for obtaining the\nrandom bytes:\n\n* *file* - randomness source is a file with finite length.\n* *stream* - randomness source is a stream with infinite length. For example, on\n  Linux, `/dev/urandom` is one such stream.\n* *DPRNG* - deterministic pseudo-random number generator uses the seed and a\n  hash function to stretch the seed into stream of bytes. A digest of input file\n  is computed and the digest is used as seed.\n* *system* - system's native random source is used. On Windows, this is provided\n  by CryptoAPI and on Linux, this is provided by getrandom().\n* *user* - external entropy source is used. A program is executed which should\n  listen on the port provided as a command line argument. It is sent big-endian\n  integer denoting the amount of bytes to be read and it should reply with\n  exactly that amount of entropy. In practice, this application should read user\n  source (mouse movements, keyboard interaction). The path should point to\n  application location which should be run.\n\nFurthermore, the entropy source inputs are conditioned using SHAKE-256\nextendable digest function. We refer to documentation of entropy mixing for\nfurther details.\n\nThe tools can take several randomness sources and in this case, the order of\ninputs matters. There can be several inputs of same type.\n\nFor input types *file*, *stream* and *DPRNG*, a required argument *path* must be\ndefined.\n\nKey parameters\n--------------\n\nThe group generation tool can generate the group parameters for integer groups\nand elliptic groups and the initialization tool can use the corresponding\ngroups. The following types of groups are implemented:\n\n* group of integers modulo a prime. The group generation tool generates a safe\n  prime, i.e. a prime p such that p = 2*q+1, where q is also a prime, and a\n  generator g. For such a choice of parameters, the multiplicative subgroup with\n  generator g has order q. The group generation tool takes the value `mod` for\n  argument `--paramtype`. The argument `--length` defines the bit-length of the\n  value p.\n\n  In this case, the subargument `--mod` with the corresponding values for the\n  argument `--paramtype` has to be set in the initialization tool.\n* elliptic curve group. The group generation tool takes an elliptic curve from a\n  list of predefined curves. The group generation tool takes the value `ec` for\n  argument `--paramtype`. The argument `--length` defines the bit-length of the\n  underlying field.\n\n  In this case, the subargument `--ec` with the corresponding elliptic curve\n  group name has to be defined in the initialization tool.\n\nKey initialization and decryption protocols\n-------------------------------------------\n\nKey could be initialized using different protocols. Currently, only a protocol\ndue to Desmedt has been implemented.\n\nFurthermore, decryption could be performed using different protocol. Currently,\nonly a protocol which reconstructs a key in memory has been implemented.\n\nA description of protocol interfaces, protocol linking and descriptions of\nimplemented protocols is given in documentation.\n\nSample executions\n-----------------\n\nAs the command-line arguments have to be defined precisely for correct\noperation, we strongly recommend using a parameters file. Refer to the\nconfiguration preparation documentation for the example configurations.\n\n* Display the card readers and inserted cards::\n\n    key util --conf app-conf.bdoc --listreaders\n\n  The application should display::\n\n    ID | NIMI                                       | KAARDIGA\n    0  | Gemalto Ezio Shield (S1370135510111) 00 00 | Ei\n\n* Generate group parameters for integers modulo a prime for prime with\n  bit-length 3072 bits::\n\n    key groupgen --conf app-conf.bdoc --params key-app-conf-3-parties.bdoc\n\n  The application writes the group parameters into a file suitable for using in\n  initialization configuration.\n\n  When generating parameters, the recommended approach is to use either\n  determinstic random sources so that the generation could be verified. Another\n  approach is to use the group parameters from known standards, e.g. RFC3526_.\n\n* Initialize key with an group of integers modulo a prime, with the key shared\n  between three shareholders with a threshold of two shareholders required for\n  reconstruction::\n\n    key init --conf app-conf.bdoc --params key-app-conf-3-parties.bdoc\n\n  The key initialization tool is interactive. Two scenarios are supported:\n\n  1. **lazy**-initialization - the number of smart card readers is less than the\n     number of shareholders (smart cards).\n  2. **fast**-initialization - the number of smart card readers is at least as\n     the number of shareholders (smart cards).\n\n  The application asks the user which smart card will be inserted into which\n  smart card reader. Then, the application asks the user to insert the\n  corresponding smart card to the corresponding smart card reader. If PIN-code\n  for storing and accessing the blobs on smart card is required, then it is\n  asked from the user.\n\n  In case of lazy-initialization, if card change is needed in the reader, the\n  application instructs the user to remove the smart card from the smart card\n  reader and insert another card. The PIN-codes are not stored between\n  insertions, so the user has to insert PIN-code for every smart card insertion.\n\n  After initializing the key, a test decryption is performed to verify the\n  correct operation of smart cards. This test can be omitted by setting\n  `skiptest` argument during initialization and running utility tool later.\n\n  Four files are output into the output folder (in the example parameters case,\n  `initoutmod/` folder): ElGamal encryption key in DER encoding, ElGamal\n  encryption key in PEM encoding, self-signed certificate for RSA verification\n  key and certificate for the ElGamal encryption key signed by the corresponding\n  RSA signing key.\n\n* Test the keyshares by encrypting a randomly generated ciphertext and decoding\n  it using a keyshares::\n\n    key testkey --conf app-conf.bdoc --params key-app-conf-3-parties.bdoc\n\n  Similarly to the initialization tool, the utility tool asks interactively,\n  which smart card to insert to which smart card reader.\n\n* Decrypt the anonymized ballot box::\n\n    key decrypt --conf app-conf.bdoc --params key-app-conf-3-parties.bdoc\n\n  Similarly to the initialization tool, the decryption tool asks interactively,\n  which smart card is inserted to which smart card reader.\n\n  The tool creates four files in the output directory (`decout/` in our case):\n  tally, signature of the tally (verifiable using the verification key output\n  in previous step), proof of correct decryption and list of invalid ballots.\n\nSample configuration\n--------------------\n\n.. code-block:: yaml\n\n  util:\n    listreaders: true\n\n  groupgen:\n    paramtype: mod\n    length: 3072\n    init_template: key.init.template.yaml\n    random_source:\n    - random_source_type: file\n      random_source_path: key-app-conf-3-parties.bdoc\n    - random_source_type: system\n    - random_source_type: DPRNG\n      random_source_path: key-app-conf-3-parties.bdoc\n\n  init:\n    identifier: TEST ELECTION\n    paramtype:\n      mod:\n        p: 5809605995369958062791915965639201402176612226902900533702900882779736177890990861472094774477339581147373410185646378328043729800750470098210924487866935059164371588168047540943981644516632755067501626434556398193186628990071248660819361205119793693985433297036118232914410171876807536457391277857011849897410207519105333355801121109356897459426271845471397952675959440793493071628394122780510124618488232602464649876850458861245784240929258426287699705312584509625419513463605155428017165714465363094021609290561084025893662561222573202082865797821865270991145082200656978177192827024538990239969175546190770645685893438011714430426409338676314743571154537142031573004276428701433036381801705308659830751190352946025482059931306571004727362479688415574702596946457770284148435989129632853918392117997472632693078113129886487399347796982772784615865232621289656944284216824611318709764535152507354116344703769998514148343807\n        g: 2\n    out: initout\n    skiptest: true\n    fastmode: true\n    signaturekeylen: 3072\n    signcn: SIGNATURE\n    signsn: 1\n    enccn: ENCRYPTION\n    encsn: 2\n    required_randomness: 128\n    genprotocol:\n      desmedt:\n        threshold: 2\n        parties: 3\n    random_source:\n      - random_source_type: file\n        random_source_path: key-app-conf-3-parties.bdoc\n      - random_source_type: system\n      - random_source_type: DPRNG\n        random_source_path: key-app-conf-3-parties.bdoc\n\n  testkey:\n    identifier: TEST ELECTION\n    out: initout\n    threshold: 2\n    parties: 3\n    fastmode: true\n\n  decrypt:\n    identifier: TEST ELECTION\n    protocol:\n      recover:\n        threshold: 2\n        parties: 3\n    anonballotbox: bb.json\n    anonballotbox_checksum: bb.json.sha256sum.bdoc\n    candidates: choices.bdoc\n    districts: districts.bdoc\n    provable: true\n    check_decodable: false\n    out: decout\n\nNotes\n-----\n\nJava smartcard library\n~~~~~~~~~~~~~~~~~~~~~~\n\nRunning this application might require setting a JVM parameter\n`sun.security.smartcardio.library` for proper smart card terminal functionality.\nNotably required when using Oracle JDK. Run the JVM with argument::\n\n    -Dsun.security.smartcardio.library=/path/to/libpcsclite\n\nOn Ubuntu 18.04 LTS (Bionic Beaver) the path for the libpcslite is\n`/usr/lib/x86_64-linux-gnu/libpcsclite.so.1`\n\nSmart card support\n~~~~~~~~~~~~~~~~~~\n\nThe key application has been tested with Aventra ActiveSecurity MyEID (ATR 3B F5\n18 00 00 81 31 FE 45 4D 79 45 49 44 9A). The smart card can be obtained from\n`Aventra <http://www.aventra.fi/webshop/?route=product/product&product_id=53>`.\n\nSmart card initialization\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe key application depends on the existence of fresh PKCS#15 file system on\nsmart card. An example tool `erase_and_install.sh` is provided in the\n`tools/pkcs15_initialization/` directory for erasing the smart card and\ninstalling the file system.\n\nInterfering programs\n~~~~~~~~~~~~~~~~~~~~\n\nAs the tested smart card supports only a single session, then other programs may\ninterfere with smart card communication. Check and verify that no other program\nis running and using the smart card.\n\nLazy initialization\n~~~~~~~~~~~~~~~~~~~\n\nEven though lazy initialization is supported, different unrecoverable errors can\nbe encountered. In this case, the initialization tool has to be run with freshly\nformatted cards.\n\nSome examples of reasons which can cause unrecoverable errors:\n\n* Windows Smart Card Service card autodetect card probing\n* incomplete insertion of smart card\n* insertion of wrong smart card into terminal\n\nDevelopment mode\n~~~~~~~~~~~~~~~~\n\nWhen building the application in development mode (by setting the environment\nvalue `DEVELOPMENT`), then in-memory smart card implementations are used during\nrunning the application. For persistence, the cards filesystems are written into\na JSON-encoded file. By default the location for the filesystems is\n`dummy_card_filesystems`, but the location can be overridden by setting the\nenvironment variable `DUMMY_CARDS_PATH` with a suitable path.\n\nPlatform support\n~~~~~~~~~~~~~~~~\n\nThe key application has been tested on Linux and Windows.\n\n* On Linux, `opensc` is required.\n* On Windows, follow the installation procedure\n\n.. todo:: add the installation procedure\n\nSmart card reader support\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.. todo:: add list of supported smart card readers\n\n.. _RFC3526: https://tools.ietf.org/html/rfc3526\n"
  },
  {
    "path": "key/build.gradle",
    "content": "buildscript {\n    ext.base = '../'\n    apply from: \"${base}/common/java/common-buildscript.gradle\", to: buildscript\n}\n\napply from: \"${base}/common/java/common-build.gradle\"\napply plugin: 'application'\n\ndependencies {\n    implementation project(\":common\")\n\n    testImplementation testFixtures(project(\":common\"))\n}\n\napplication {\n\tmainClass = \"ee.ivxv.key.Key\"\n}\n"
  },
  {
    "path": "key/settings.gradle",
    "content": "include \"common\"\nproject(\":common\").projectDir = file(\"../common/java\")\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/Key.java",
    "content": "package ee.ivxv.key;\n\nimport ee.ivxv.common.cli.AppRunner;\n\npublic class Key {\n\n    public static void main(String[] args) {\n        KeyApp app = new KeyApp();\n        AppRunner<KeyContext> runner = new AppRunner<>(app);\n\n        if (!runner.run(args)) {\n            System.exit(1);\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/KeyApp.java",
    "content": "package ee.ivxv.key;\n\nimport ee.ivxv.common.cli.App;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.key.tool.DecryptTool;\nimport ee.ivxv.key.tool.DecryptTool.DecryptArgs;\nimport ee.ivxv.key.tool.GroupGenTool;\nimport ee.ivxv.key.tool.GroupGenTool.GroupGenArgs;\nimport ee.ivxv.key.tool.InitTool;\nimport ee.ivxv.key.tool.InitTool.InitArgs;\nimport ee.ivxv.key.tool.UtilTool;\nimport ee.ivxv.key.tool.UtilTool.UtilArgs;\nimport ee.ivxv.key.tool.TestKeyTool;\nimport ee.ivxv.key.tool.TestKeyTool.TestKeyArgs;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class KeyApp extends App<KeyContext> {\n\n    KeyApp() {\n        super(Msg.app_key, createTools());\n    }\n\n    private static List<Tool<KeyContext, ?>> createTools() {\n        return Arrays.asList( //\n                new Tool<>(Msg.tool_decrypt, DecryptArgs::new, DecryptTool::new),\n                new Tool<>(Msg.tool_groupgen, GroupGenArgs::new, GroupGenTool::new),\n                new Tool<>(Msg.tool_init, InitArgs::new, InitTool::new),\n                new Tool<>(Msg.tool_util, UtilArgs::new, UtilTool::new),\n                new Tool<>(Msg.tool_testkey, TestKeyArgs::new, TestKeyTool::new));\n    }\n\n    @Override\n    public KeyContext createContext(InitialContext ctx, Conf conf, CommonArgs args) {\n        return new KeyContext(ctx, conf, args);\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/KeyContext.java",
    "content": "package ee.ivxv.key;\n\nimport ee.ivxv.common.cli.AppContext;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.conf.Conf;\n\npublic class KeyContext extends AppContext<Conf> {\n\n    public KeyContext(InitialContext i, Conf conf, CommonArgs args) {\n        super(i, conf, args);\n    }\n\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/Msg.java",
    "content": "package ee.ivxv.key;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.util.NameHolder;\n\n@BaseName(\"i18n.key-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg implements NameHolder {\n    /*-\n     * The part of the enum name until the first '_' (including) is excluded from the getName().\n     * This is a means to provide multiple translations for the same tool or argument name.\n     */\n\n    // App\n    app_key,\n\n    // Tools\n    tool_decrypt, tool_groupgen, tool_init, tool_util, tool_testkey,\n\n    // Common tool arguments\n    arg_identifier(\"i\"), arg_parties(\"n\"), arg_threshold(\"m\"), arg_out(\"o\"), //\n    arg_mod, arg_ec, arg_paramtype, arg_random_source(\"r\"), arg_random_source_type, //\n    arg_random_source_path, arg_fastmode,\n\n    // 'decrypt' tool arguments\n    d_anonballotbox, d_anonballotbox_checksum, //\n    d_questioncount, d_candidates, d_districts, d_recover, d_protocol, //\n    d_provable, d_check_decodable, d_prove_invalid, //\n\n    // 'groupgen' tool arguments\n    g_length(\"l\"), g_init_template,\n\n    // 'init' tool arguments\n    i_p, i_g, i_name, //\n    i_desmedt(\"d\"), i_genprotocol, i_signaturekeylen(\"s\"), //\n    i_signcn, i_signsn, i_enccn, i_encsn, i_skiptest, //\n    i_required_randomness,\n\n    // 'util' tool arguments\n    u_listreaders, u_testkey,\n\n    // error\n    e_testencryption_fail, e_quorum_test_fail, e_no_cardterminals_found, //\n    e_abb_invalid_question_count, e_illegal_vote_district, e_illegal_vote_parish,\n\n    // messages\n    m_id, m_name, m_with_card, m_yes, m_no, m_quorum_test_ok, m_gen_group_params, //\n    m_certificates_generated, m_generate_decryption_key, m_test_decryption_key, //\n    m_generate_signature_key, m_test_signature_key, m_votecount, //\n    m_abb_dist_verifying, m_abb_dist_ok, m_protocol_init, m_protocol_init_ok, //\n    m_dec_start, m_dec_done, m_out_tally, m_out_plainbb, m_out_proof, m_out_proof_invalid, m_out_invalid, m_out_logs, //\n    m_keys_saved, m_collecting_required_randomness, m_with_proof, m_without_proof, m_card_id, //\n    m_fastmode_disabled, m_fastmode_enabled, m_storing_shares, m_generating_certificate;\n\n\n    private final String shortName;\n\n    Msg() {\n        this(null);\n    }\n\n    Msg(String shortName) {\n        this.shortName = shortName;\n    }\n\n    @Override\n    public String getShortName() {\n        return shortName;\n    }\n\n    @Override\n    public String getName() {\n        return extractName(name());\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/RandomSourceArg.java",
    "content": "package ee.ivxv.key;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Arg.TreeList;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.crypto.rnd.CombineRnd;\nimport ee.ivxv.common.crypto.rnd.DPRNG;\nimport ee.ivxv.common.crypto.rnd.FileRnd;\nimport ee.ivxv.common.crypto.rnd.NativeRnd;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.crypto.rnd.UserRnd;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.function.Function;\n\n/**\n * Helper class for parsing random source arguments.\n */\npublic class RandomSourceArg extends Args {\n    /**\n     * Randomness source type.\n     */\n    static enum RndType {\n        /**\n         * Stream random source reads infinite random source.\n         * <p>\n         * Stream file source corresponds to {@link ee.ivxv.common.crypto.rnd.FileRnd}, initializing\n         * it with finite argument true.\n         * <p>\n         * The command line takes a single argument denoting the location of the stream source.\n         */\n        stream(p -> newFileRnd(p, false)),\n        /**\n         * File random source reads finite random source.\n         * <p>\n         * Stream file source corresponds to {@link ee.ivxv.common.crypto.rnd.FileRnd}, initializing\n         * it with finite argument false.\n         * <p>\n         * The command line takes a single argument denoting the location of the file.\n         */\n        file(p -> newFileRnd(p, true)),\n        /**\n         * DPRNG random source reads a seed from the file and initializes a deterministic pseudo\n         * random number generator.\n         * <p>\n         * It uses {@link ee.ivxv.common.crypto.rnd.DPRNG}.\n         * <p>\n         * The command line takes a single argument denoting the location of the seed file.\n         */\n        DPRNG(RndType::newDPRNG),\n        /**\n         * System random source uses the system random source.\n         * <p>\n         * Internally, it uses {@link ee.ivxv.common.crypto.rnd.NativeRnd}.\n         * <p>\n         * Does not process given arguments.\n         */\n        system(p -> new NativeRnd()),\n        /**\n         * User random source uses external program to obtain randomness.\n         * <p>\n         * This method uses {@link ee.ivxv.common.crypto.rnd.UserRnd}.\n         * <p>\n         * Takes as a location the external program which is run for obtaining entropy.\n         */\n        user(p -> newUserRnd(p));\n\n        private final Function<Path, Rnd> supplier;\n\n        RndType(Function<Path, Rnd> supplier) {\n            this.supplier = supplier;\n        }\n\n        static Rnd newFileRnd(Path path, boolean finite) {\n            try {\n                requirePath(path);\n                return new FileRnd(path, finite);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        static Rnd newDPRNG(Path path) {\n            try {\n                requirePath(path);\n                return new DPRNG(path);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        static Rnd newUserRnd(Path path) {\n            try {\n                requirePath(path);\n                // port 22062 is chosen randomly from high range\n                // the current implementation is continuous\n                return new UserRnd(path, true, 22062);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        static void requirePath(Path path) {\n            if (path == null) {\n                throw new MessageException(ee.ivxv.common.cli.Msg.e_invalid_path_not_exists, path);\n            }\n        }\n\n        public Rnd getRnd(Path path) {\n            return supplier.apply(path);\n        }\n    }\n\n    /**\n     * Single random source argument.\n     */\n    public static class RndListEntry extends Args {\n        Arg<RndType> type = Arg.aChoice(Msg.arg_random_source_type, RndType.values());\n        // Must be optional, because of NATIVE random type that does not require path\n        Arg<Path> path = Arg.aPath(Msg.arg_random_source_path, true, false).setOptional();\n\n        RndListEntry() {\n            args.add(type);\n            args.add(path);\n        }\n    }\n\n    /**\n     * Get argument for setting multiple random source values.\n     * \n     * @return\n     */\n    public static Arg<List<RndListEntry>> getArgument() {\n        return new TreeList<>(Msg.arg_random_source, RndListEntry::new);\n    }\n\n    /**\n     * Construct a random source combining given argument values.\n     * <p>\n     * If the argument is not set, then returns null. Otherwise, initialize all random sources and\n     * combine it using {@link ee.ivxv.common.crypto.rnd.CombineRnd} and return the instance.\n     * \n     * @param argument Argument values\n     * @return Combined random source or null if arguments not given.\n     * @throws IOException When exception occurs during initialization of single random source.\n     */\n    public static CombineRnd combineFromArgument(Arg<List<RndListEntry>> argument)\n            throws IOException {\n        if (!argument.isSet()) {\n            return null;\n        }\n        CombineRnd cr = new CombineRnd();\n        for (RndListEntry rle : argument.value()) {\n            cr.addSource(rle.type.value().getRnd(rle.path.value()));\n        }\n        return cr;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/model/Invalid.java",
    "content": "package ee.ivxv.key.model;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * JSON serializable list of invalid votes.\n */\npublic class Invalid {\n    private final String election;\n    private final List<Vote> invalid = new ArrayList<>();\n\n    /**\n     * Initialize using election identifier.\n     * \n     * @param election\n     */\n    public Invalid(String election) {\n        this.election = election;\n    }\n\n    /**\n     * Get the election identifier.\n     * \n     * @return\n     */\n    public String getElection() {\n        return election;\n    }\n\n    /**\n     * Get the list of invalid votes.\n     * \n     * @return\n     */\n    public List<Vote> getInvalid() {\n        return invalid;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/model/PlainBallotBox.java",
    "content": "package ee.ivxv.key.model;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\n\nimport java.util.LinkedHashMap;\nimport java.util.HashMap;\nimport java.util.ArrayList;\nimport java.util.Map;\nimport java.util.List;\n\n/**\n * JSON serializable structure for holding the decrypted ballot box.\n */\npublic class PlainBallotBox {\n    private final String election;\n    private final Map<String, Map<String, List<String>>> byParish = new HashMap<>();\n\n    /**\n     * Initialize using values.\n     *\n     * @param election   Election identifier.\n     * @param candidates List of candidates.\n     * @param districts  List of districts.\n     */\n    public PlainBallotBox(String election, CandidateList candidates, DistrictList districts) {\n        this.election = election;\n        init(candidates, districts);\n    }\n\n    private void init(CandidateList candidates, DistrictList districts) {\n        // No candidate validity check: we assume the check was done during ballot validation.\n        districts.getDistricts().forEach((dId, d) -> {\n            Map<String, List<String>> plaintexts = new LinkedHashMap<>();\n            byParish.put(dId, plaintexts);\n            d.getParish().forEach(p -> {\n                List<String> pts = new ArrayList<>();\n                plaintexts.put(p, pts);\n            });\n        });\n    }\n\n    public String getElection() {\n        return election;\n    }\n\n    @JsonProperty(\"byparish\")\n    public Map<String, Map<String, List<String>>> getByParish() {\n        return byParish;\n    }\n\n    @JsonProperty(\"bydistrict\")\n    public Map<String, List<String>> getByDistrict() {\n        Map<String, List<String>> res = new LinkedHashMap<>();\n        getByParish().forEach((d, sMap) -> {\n            List<String> pList = res.computeIfAbsent(d, tmp -> new ArrayList<>());\n            sMap.forEach((s, plains) -> pList.addAll(plains));\n        });\n        return res;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/model/Tally.java",
    "content": "package ee.ivxv.key.model;\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * JSON serializable structure for holding the tally of the votes.\n */\npublic class Tally {\n    public static final String INVALID_VOTE_ID = \"invalid\";\n    private final String election;\n    private final Map<String, Map<String, Map<String, Integer>>> byParish = new HashMap<>();\n\n    /**\n     * Initialize using values.\n     *\n     * @param election Election identifier.\n     * @param candidates List of candidates.\n     * @param districts List of districts.\n     */\n    public Tally(String election, CandidateList candidates, DistrictList districts) {\n        this.election = election;\n        init(candidates, districts);\n    }\n\n    private void init(CandidateList candidates, DistrictList districts) {\n        districts.getDistricts().forEach((dId, d) -> {\n            Map<String, Map<String, String>> dCands = candidates.getCandidates().get(dId);\n            Map<String, Map<String, Integer>> dTally = new LinkedHashMap<>();\n            byParish.put(dId, dTally);\n            d.getParish().forEach(p -> {\n                Map<String, Integer> pTally = new LinkedHashMap<>();\n                dTally.put(p, pTally);\n                if (dCands != null) {\n                    dCands.forEach((pName, pCandMap) -> pCandMap.forEach((cId, cName) -> {\n                        pTally.put(cId, 0);\n                    }));\n                }\n                pTally.put(INVALID_VOTE_ID, 0);\n            });\n        });\n    }\n\n    /**\n     * Get the election identifier.\n     *\n     * @return\n     */\n    public String getElection() {\n        return election;\n    }\n\n    /**\n     * @return Returns a map from district id to a map from station id to a map from candidate id to\n     *         number of received votes.\n     */\n    @JsonProperty(\"byparish\")\n    public Map<String, Map<String, Map<String, Integer>>> getByParish() {\n        return byParish;\n    }\n\n    /**\n     * @return Returns a map from district id to a map from candidate id to number of received\n     *         votes.\n     */\n    @JsonProperty(\"bydistrict\")\n    public Map<String, Map<String, Integer>> getByDistrict() {\n        Map<String, Map<String, Integer>> res = new LinkedHashMap<>();\n        getByParish().forEach((d, sMap) -> {\n            Map<String, Integer> ccMap = res.computeIfAbsent(d, tmp -> new LinkedHashMap<>());\n            sMap.forEach((s, cMap) -> cMap.forEach((c, count) -> {\n                ccMap.compute(c, (cc, ccount) -> ccount == null ? count : ccount + count);\n            }));\n\n        });\n        return res;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/model/Vote.java",
    "content": "package ee.ivxv.key.model;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\n\n/**\n * JSON serializable structure for holding the encrypted vote using metadata and decryption result.\n */\npublic class Vote {\n    private final String district;\n    private final String station;\n    private final String question;\n    private final byte[] vote;\n    @JsonIgnore\n    private ElGamalDecryptionProof proof;\n\n    /**\n     * Initialize using values\n     * \n     * @param district District identifier\n     * @param station Station identifier\n     * @param question Question identifier\n     * @param vote Decrypted vote\n     */\n    public Vote(String district, String station, String question, byte[] vote) {\n        this.district = district;\n        this.station = station;\n        this.question = question;\n        this.vote = vote;\n    }\n\n    /**\n     * Get the district identifier of the vote.\n     * \n     * @return\n     */\n    public String getDistrict() {\n        return district;\n    }\n\n    /**\n     * Get the station identifier of the vote.\n     * \n     * @return\n     */\n    public String getStation() {\n        return station;\n    }\n\n    /**\n     * Get the question identifier of the vote.\n     * \n     * @return\n     */\n    public String getQuestion() {\n        return question;\n    }\n\n    /**\n     * Get the vote.\n     * \n     * @return\n     */\n    public byte[] getVote() {\n        return vote;\n    }\n\n    /**\n     * Get the decryption proof.\n     * <p>\n     * Depending on the decryption protocol, the proof of correct decryption value may be null. The\n     * values for ciphertext, decryption and public key must be set.\n     * \n     * @return\n     */\n    public ElGamalDecryptionProof getProof() {\n        return proof;\n    }\n\n    /**\n     * Set the decryption proof.\n     * \n     * @param proof\n     */\n    public void setProof(ElGamalDecryptionProof proof) {\n        this.proof = proof;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/DecryptionProtocol.java",
    "content": "package ee.ivxv.key.protocol;\n\nimport ee.ivxv.common.crypto.CorrectnessUtil;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport java.io.IOException;\n\n/**\n * DecryptionProtocol defines interface which the protocols for decrypting ciphertext must\n * implement.\n */\npublic interface DecryptionProtocol {\n    /**\n     * Take in a ciphertext as bytes and output a proof of correct decryption.\n     * <p>\n     * The returned {@link ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof} does not need to be\n     * complete. If the protocol implementation does not allow for proving the correctness, then the\n     * corresponding fields should be left null. Also, if the protocol can be initialized for not\n     * providing proofs.\n     * <p>\n     * The fields for message, ciphertext and public key must be set.\n     * \n     * @param msg\n     * @return\n     * @throws ProtocolException\n     * @throws IOException\n     */\n    ElGamalDecryptionProof decryptMessage(byte[] msg) throws ProtocolException, IOException;\n\n    /**\n     * Check if the ciphertext could be decrypted using the protocol.\n     * <p>\n     * Check if the input ciphertext is correctly serialized and part of the ciphertext space. If\n     * the result is valid, then a call to {@link #checkCorrectness(byte[])} decrypts correctly the\n     * ciphertext.\n     * <p>\n     * This check does not actually decrypt the ciphertext. Thus, the decrypted message may not be\n     * correctly padded or contain valuable information.\n     * \n     * @param msg\n     * @return\n     * @throws ProtocolException\n     */\n    CorrectnessUtil.CiphertextCorrectness checkCorrectness(byte[] msg) throws ProtocolException;\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/GenerationProtocol.java",
    "content": "package ee.ivxv.key.protocol;\n\nimport java.io.IOException;\n\n/**\n * Interface for constructing a key pair usable for signing or encryption.\n */\npublic interface GenerationProtocol {\n    /**\n     * Generate the key and output serialized public key.\n     * <p>\n     * The serialized public key depends on the protocol and underlying crypto system.\n     * \n     * @return\n     * @throws ProtocolException\n     * @throws IOException\n     */\n    byte[] generateKey() throws ProtocolException, IOException;\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/ProtocolException.java",
    "content": "package ee.ivxv.key.protocol;\n\n@SuppressWarnings(\"serial\")\npublic class ProtocolException extends Exception {\n    public ProtocolException(String msg) {\n        super(msg);\n    }\n\n    public ProtocolException(String msg, Exception e) {\n        super(msg, e);\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/ProtocolUtil.java",
    "content": "package ee.ivxv.key.protocol;\n\nimport ee.ivxv.common.math.Polynomial;\nimport java.math.BigInteger;\n\n/**\n * Utility functions for threshold protocols.\n */\npublic class ProtocolUtil {\n    /**\n     * Evaluate the polynomial at points to generate shares for Lagrange interpolation.\n     * \n     * @param pol Polynomial to secret share\n     * @param amount The number of evaluation points to generate.\n     * @return List of polynomial evaluations.\n     */\n    public static BigInteger[] generateShares(Polynomial pol, int amount) {\n        BigInteger[] shares = new BigInteger[amount];\n        for (int i = 0; i < amount; i++) {\n            shares[i] = pol.evaluate(i + 1);\n        }\n        return shares;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/SigningProtocol.java",
    "content": "package ee.ivxv.key.protocol;\n\nimport org.bouncycastle.operator.ContentSigner;\n\n/**\n * Interface for signing messages.\n */\npublic interface SigningProtocol extends ContentSigner {\n    /**\n     * Sign the message using the signing protocol and return serialized signature.\n     * \n     * @param msg Message to be signed.\n     * @return Serialized signature.\n     * @throws ProtocolException When exception occurs during protocol run.\n     */\n    byte[] sign(byte[] msg) throws ProtocolException;\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/ThresholdParameters.java",
    "content": "package ee.ivxv.key.protocol;\n\n/**\n * Parameters for operations with threshold.\n */\npublic class ThresholdParameters {\n    private final int parties;\n    private final int threshold;\n\n    /**\n     * Initialize the threshold parameters.\n     * \n     * @param parties The number of parties of the protocol.\n     * @param threshold The number of parties required for reconstruction.\n     * @throws IllegalArgumentException If parties < 2*threshold-1\n     */\n    public ThresholdParameters(int parties, int threshold) throws IllegalArgumentException {\n        if (parties <= 0) {\n            throw new IllegalArgumentException(\"Number of parties must be positive\");\n        }\n        if (parties < (2 * threshold - 1)) {\n            throw new IllegalArgumentException(\n                    \"Number of parties must be higher than 2*threshold - 1\");\n        }\n        this.parties = parties;\n        this.threshold = threshold;\n    }\n\n    /**\n     * Get the number of parties.\n     * \n     * @return\n     */\n    public int getParties() {\n        return this.parties;\n    }\n\n    /**\n     * Get the threshold for reconstructing the output.\n     * \n     * @return\n     */\n    public int getThreshold() {\n        return this.threshold;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/decryption/desmedt/DesmedtDecryption.java",
    "content": "package ee.ivxv.key.protocol.decryption.desmedt;\n\nimport ee.ivxv.common.crypto.CorrectnessUtil.CiphertextCorrectness;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.key.protocol.DecryptionProtocol;\n\n/**\n * DesmedtDecryption combines the decryption shares into a decrypted message without recovering the\n * key. Currently this protocol is not implemented.\n */\npublic class DesmedtDecryption implements DecryptionProtocol {\n    /**\n     * Get the corresponding decryption share.\n     * <p>\n     * Currently, this method is not implemented and returns null.\n     * \n     * @param party\n     * @param msg\n     * @return\n     */\n    public byte[] getDecryptionShare(int party, byte[] msg) {\n        return null;\n    }\n\n    /**\n     * Get the decrypted message.\n     * <p>\n     * Decryption using DesmedDecryption is not implemented and the method returns null.\n     * \n     * @param msg\n     */\n    @Override\n    public ElGamalDecryptionProof decryptMessage(byte[] msg) {\n        return null;\n    }\n\n    /**\n     * Check the correctness of the message.\n     * <p>\n     * Checking correctness using DesmedtDecryption is not implemented and the method returns null.\n     * \n     * @param msg\n     */\n    @Override\n    public CiphertextCorrectness checkCorrectness(byte[] msg) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/decryption/recover/RecoverDecryption.java",
    "content": "package ee.ivxv.key.protocol.decryption.recover;\n\nimport ee.ivxv.common.crypto.CorrectnessUtil;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.common.crypto.elgamal.ElGamalParameters;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPrivateKey;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.LagrangeInterpolation;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.key.protocol.DecryptionProtocol;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.Set;\n\n/**\n * RecoverDecryption is a protocol for decrypting the ciphertext by reconstructing the private key\n * from private key shares.\n */\npublic class RecoverDecryption implements DecryptionProtocol {\n    private final Set<IndexedBlob> blobs;\n    private final ThresholdParameters tparams;\n    private final boolean withProof;\n    private ElGamalPrivateKey sk;\n    private int modByteLen;\n\n    /**\n     * Initialize the protocol from values.\n     * \n     * @param blobs The set of blobs that contain the private key shares.\n     * @param tparams Parameters for threshold decryption.\n     * @throws ProtocolException When the number of cards is less than is required for decryption.\n     */\n    public RecoverDecryption(Set<IndexedBlob> blobs, ThresholdParameters tparams)\n            throws ProtocolException {\n        this(blobs, tparams, true);\n    }\n\n    /**\n     * Initialize the protocol from values.\n     * \n     * @param blobs The set of blobs that contain the private key shares.\n     * @param tparams Parameters for threshold decryption.\n     * @param withProof Boolean indicating if decrypting without providing proofs of correct\n     *        decryption.\n     * @throws ProtocolException When the number of cards is less than is required for decryption.\n     */\n    public RecoverDecryption(Set<IndexedBlob> blobs, ThresholdParameters tparams, boolean withProof)\n            throws ProtocolException {\n        if (blobs.size() < tparams.getThreshold()) {\n            throw new ProtocolException(\"Fewer cards available than threshold\");\n        }\n        this.blobs = blobs;\n        this.tparams = tparams;\n        this.withProof = withProof;\n        recoverKey();\n    }\n\n    private void recoverKey() throws ProtocolException {\n        if (this.sk == null) {\n            this.sk = forceKeyRecover();\n            this.modByteLen = (this.sk.getParameters().getOrder().bitLength() + 7) / 8;\n        }\n    }\n\n    private ElGamalPrivateKey forceKeyRecover() throws ProtocolException {\n        ElGamalPrivateKey[] parsedKeys = parseAllBlobs(blobs);\n        if (!filterKeys(parsedKeys)) {\n            throw new ProtocolException(\"Key share parameters mismatch\");\n        }\n        ElGamalPrivateKey secretKey = combineKeys(parsedKeys);\n        return secretKey;\n    }\n\n    private ElGamalPrivateKey[] parseAllBlobs(Set<IndexedBlob> blobs) throws ProtocolException {\n        ElGamalPrivateKey[] parsed = new ElGamalPrivateKey[tparams.getParties()];\n        for (IndexedBlob blob : blobs) {\n\n            int i = blob.getIndex();\n            try {\n                parsed[i - 1] = new ElGamalPrivateKey(blob.getBlob());\n            } catch (IllegalArgumentException e) {\n                throw new ProtocolException(\n                        \"Exception while parsing secret share: \" + e.toString());\n            }\n\n        }\n        return parsed;\n    }\n\n    private boolean filterKeys(ElGamalPrivateKey[] keys) throws ProtocolException {\n        // this method checks that the parameters for all the keys are the\n        // same. If not, then we do not perform any recovery.\n        if (keys.length == 0) {\n            throw new ProtocolException(\"No key shares ready for filtering\");\n        }\n        ElGamalParameters params = null;\n        for (ElGamalPrivateKey key : keys) {\n            if (key == null) {\n                continue;\n            }\n            if (params == null) {\n                params = key.getParameters();\n            } else if (!params.equals(key.getParameters())) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private ElGamalPrivateKey combineKeys(ElGamalPrivateKey[] keys) throws ProtocolException {\n        if (keys.length == 0) {\n            throw new ProtocolException(\"No key shares ready for filtering\");\n        }\n        if (keys.length < tparams.getThreshold()) {\n            throw new ProtocolException(\"Fewer keys available than is the threshold\");\n        }\n        ElGamalParameters params = null;\n        BigInteger k = BigInteger.ZERO;\n        for (int i = 0; i < keys.length; i++) {\n            if (keys[i] == null) {\n                continue;\n            }\n            params = keys[i].getParameters();\n            BigInteger p = keys[i].getSecretPart();\n            p = p.multiply(LagrangeInterpolation.basisPolynomial(params.getGeneratorOrder(), keys,\n                    BigInteger.valueOf(i + 1)));\n            k = k.add(p).mod(params.getGeneratorOrder());\n        }\n        return new ElGamalPrivateKey(params, k);\n    }\n\n    /**\n     * Decrypt the message using private key reconstructed in memory.\n     * <p>\n     * If the protocol was initialized to decrypt without proofs, then the corresponding values in\n     * the returned value are null.\n     * \n     * @param msg Message to be decrypted. Must be serialized instance of\n     *        {@link ee.ivxv.common.crypto.elgamal.ElGamalCiphertext}\n     * @return Decrypted message\n     * @throws ProtocolException If the secret key has not bee decrypted or computation exception\n     *         occurs.\n     * @throws IllegalArgumentException If invalid input.\n     */\n    @Override\n    public ElGamalDecryptionProof decryptMessage(byte[] msg)\n            throws ProtocolException, IllegalArgumentException, IOException {\n        if (this.sk == null) {\n            throw new ProtocolException(\"Secret key not reconstructed\");\n        }\n        ElGamalCiphertext ct = new ElGamalCiphertext(this.sk.getParameters(), msg);\n        ElGamalDecryptionProof dp;\n        try {\n            if (withProof) {\n                dp = this.sk.provableDecrypt(ct);\n            } else {\n                GroupElement decrypted = this.sk.decrypt(ct, true);\n                dp = new ElGamalDecryptionProof(ct, decrypted, this.sk.getPublicKey());\n            }\n        } catch (MathException e) {\n            throw new ProtocolException(\"Arithmetic error: \" + e.toString());\n        }\n\n        return dp;\n    }\n\n    /**\n     * Check the ciphertext correctness.\n     * <p>\n     * Calls {@link ee.ivxv.common.crypto.CorrectnessUtil#isValidCiphertext(ElGamalPublicKey, byte[])}.\n     * \n     * @see ee.ivxv.common.crypto.CorrectnessUtil\n     * \n     * @param msg\n     * @throws ProtocolException When the protocol is not fully initialized\n     */\n    @Override\n    public CorrectnessUtil.CiphertextCorrectness checkCorrectness(byte[] msg)\n            throws ProtocolException {\n        if (this.sk == null) {\n            throw new ProtocolException(\"Secret key not reconstructed\");\n        }\n        ElGamalPublicKey pk = sk.getPublicKey();\n        return CorrectnessUtil.isValidCiphertext(pk, msg);\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/generation/desmedt/DesmedtGeneration.java",
    "content": "package ee.ivxv.key.protocol.generation.desmedt;\n\nimport ee.ivxv.common.crypto.elgamal.ElGamalParameters;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPrivateKey;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.Polynomial;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.key.protocol.GenerationProtocol;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.ProtocolUtil;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport java.io.IOException;\nimport java.math.BigInteger;\n\n/**\n * DesmedtGeneration is a key generation for ElGamal crypto system.\n * <p>\n * In this protocol, the key is shared using Shamir secret sharing as private key shares. The shares\n * are stored on card tokens.\n */\npublic class DesmedtGeneration implements GenerationProtocol {\n    private final ElGamalParameters params;\n    private final ThresholdParameters tparams;\n    private final Rnd rnd;\n    private byte[][] sharestorage;\n\n    /**\n     * Initialize the protocol using values.\n     * \n     * @param cards List of cards to store the private key shares.\n     * @param params ElGamal crypto system parameters to use for key generation.\n     * @param tparams Threshold parameters defining the number of share and threshold for\n     *        decryption.\n     * @param rnd Random source.\n     * @param cardShareAID Authentication identifier to apply for private key shares.\n     * @param cardShareName Identifier of the private key shares on card tokens.\n     * @throws IOException When the number of available cards is less than the number of shares.\n     */\n    public DesmedtGeneration(Cards cards, ElGamalParameters params, ThresholdParameters tparams,\n            Rnd rnd, byte[] cardShareAID, byte[] cardShareName, byte[][] sharestorage)\n            throws IOException {\n        if (cards.count() < tparams.getParties()) {\n            throw new IOException(\"Fewer cards available than requested number of shareholders\");\n        }\n        this.params = params;\n        this.tparams = tparams;\n        this.rnd = rnd;\n        this.sharestorage = sharestorage;\n    }\n\n    /**\n     * Generate the key and store the shares on card tokens.\n     * <p>\n     * The algorithm for generating the key is shortly as follows: {@code\n     *  1. generate polynomial of degree t-1 (i.e. f(x) = a0 + a1 x + a2 x^2 + ... a(t-1) x^(t-1).\n     *   2. compute shares si = f(i) for every node 1 <= i <= n\n     *  3. compute modified share Ki = L(si, i), where L is the Lagrange interpolation coeficcient\n     *   4. encode the modified shares as ASN1 DER\n     *   5. store the modified shares at cards\n     *   6. compute the public key y = g^f(0)\n     *   7. encode the public key\n     *   8. finally, return the public key\n     *   }\n     * \n     * @return Serialized {@link ee.ivxv.common.crypto.elgamal.ElGamalPublicKey} instance.\n     * @throws ProtocolException When exception occurs during protocol run\n     * @throws IOException When exception occurs during card token communication.\n     * \n     */\n    @Override\n    public byte[] generateKey() throws ProtocolException, IOException {\n        // 1. generate polynomial of degree t-1 (i.e. f(x) = a0 + a1 x + a2 x^2 + ... a(t-1)\n        // x^(t-1).\n        // 2. compute shares si = f(i) for every node 1 <= i <= n\n        // 3. compute modified share Ki = L(si, i), where L is the Lagrange\n        // interpolation coeficcient\n        // 4. encode the modified shares as ASN1 DER\n        // 5. store the modified shares at cards\n        // 6. compute the public key y = g^f(0)\n        // 7. encode the public key\n        // 8. finally, return the public key\n\n        Polynomial pol = generatePolynomial();\n        BigInteger[] shares = ProtocolUtil.generateShares(pol, tparams.getParties());\n        ElGamalPrivateKey[] packedShares = packShares(shares);\n        storeShares(packedShares);\n        ElGamalPublicKey pubKey = generatePublicKey(pol);\n        byte[] ret;\n        ret = pubKey.getBytes();\n        return ret;\n    }\n\n    Polynomial generatePolynomial() throws IOException {\n        return new Polynomial(tparams.getThreshold() - 1, params.getGeneratorOrder(), rnd);\n    }\n\n    ElGamalPrivateKey[] packShares(BigInteger[] shares) {\n        ElGamalPrivateKey[] packedShares = new ElGamalPrivateKey[shares.length];\n        for (int i = 0; i < shares.length; i++) {\n            packedShares[i] = new ElGamalPrivateKey(params, shares[i]);\n        }\n        return packedShares;\n    }\n\n    void storeShares(ElGamalPrivateKey[] packedShares) {\n        for (int i = 0; i < packedShares.length; i++) {\n            sharestorage[i] = packedShares[i].getBytes();\n        }\n    }\n\n    ElGamalPublicKey generatePublicKey(Polynomial pol) {\n        return new ElGamalPublicKey(params, params.getGenerator().scale(pol.evaluate(0)));\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/generation/pedersen/NOTIMPLEMENTED",
    "content": ""
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/generation/shoup/ShoupGeneration.java",
    "content": "package ee.ivxv.key.protocol.generation.shoup;\n\nimport ee.ivxv.common.asn1.RSAParams;\nimport ee.ivxv.common.crypto.SignatureUtil;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.IntegerConstructor;\nimport ee.ivxv.common.math.MathUtil;\nimport ee.ivxv.common.math.Polynomial;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.key.protocol.GenerationProtocol;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.ProtocolUtil;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.security.interfaces.RSAPublicKey;\n\n/**\n * Generate the key for using with RSA algorithm.\n * <p>\n * The private key shares are stored on card tokens.\n */\npublic class ShoupGeneration implements GenerationProtocol {\n    private final int modLen;\n    private final ThresholdParameters tparams;\n    private final Rnd rnd;\n    private byte[][] sharestorage;\n\n    /**\n     * Initialize the protocol using values.\n     * \n     * @param cards List of cards to store the private key shares on.\n     * @param modLen Length of modulus in bits.\n     * @param tparams Threshold scheme parameters indicating the number of shares and the threshold\n     *        for signing.\n     * @param rnd Random source.\n     * @param cardShareAID Authentication identifier to be applied for private key shares.\n     * @param cardShareName Private key share name on card tokens.\n     * @throws IOException When the number of cards is less than the number of expected shares.\n     */\n    public ShoupGeneration(Cards cards, int modLen, ThresholdParameters tparams, Rnd rnd,\n            byte[] cardShareAID, byte[] cardShareName, byte[][] sharestorage) throws IOException {\n        if (cards.count() < tparams.getParties()) {\n            throw new IOException(\"Fewer cards available than requested number of shareholders\");\n        }\n        this.tparams = tparams;\n        this.rnd = rnd;\n        this.modLen = modLen;\n        this.sharestorage = sharestorage;\n    }\n\n    /**\n     * Generate RSA private key shares and output serialized public key.\n     * <p>\n     * The algorithm for constructing the key is shortly as follows: {@code\n     *  1. generate primes p, q of size nLen\n     *  2. generate public exponent 0 < e < (p-1)(q-1)\n     *  3. find private exponent d = e^-1 mod (p-1)(q-1)\n     *  4. generate polynomial with free entry d\n     *  5. compute shares si = f(i) for every node 1 <= i <= n\n     *  6. encode the shares (si, n) as ASN1 DER\n     *  7. store the ASN1-encoded shares on card i\n     *  8. encode the public key (n, e) as ASN1 DER\n     *  9. output encoded public key\n     * }\n     * \n     * @return Serialized {@link java.security.interfaces.RSAPublicKey} instance.\n     * @throws ProtocolException When exception occurs during protocol run\n     * @throws IOException When exception occurs during card token communication.\n     */\n    @Override\n    public byte[] generateKey() throws ProtocolException, IOException {\n        // 1. generate primes p, q of size nLen\n        // 2. generate public exponent 0 < e < (p-1)(q-1)\n        // 3. find private exponent d = e^-1 mod (p-1)(q-1)\n        // 4. generate polynomial with free entry d\n        // 5. compute shares si = f(i) for every node 1 <= i <= n\n        // 6. encode the shares (si, n) as ASN1 DER\n        // 7. store the ASN1-encoded shares on card i\n        // 8. encode the public key (n, e) as ASN1 DER\n        // 9. output encoded public key\n        BigInteger p, q, n, e, d;\n        Polynomial pol;\n        do {\n            // we want e such that GCD(e, phi(p*q))=1. as e is actually fixed\n            // below, then we need to search for suitable p and q\n            p = generateCofactor();\n            q = generateCofactor();\n            n = computeModulus(p, q);\n            e = generatePublicExponent(p, q);\n        } while (!verifyCofactors(p, q, e));\n        d = computePrivateExponent(p, q, e);\n        pol = generatePolynomial(p, q, d);\n        BigInteger[] shares = ProtocolUtil.generateShares(pol, tparams.getParties());\n        RSAParams[] packedShares = packShares(e, shares, n);\n        storeShares(packedShares);\n        RSAPublicKey pk = SignatureUtil.RSA.paramsToRSAPublicKey(e, n);\n        return pk.getEncoded();\n    }\n\n    BigInteger generateCofactor() throws IOException {\n        BigInteger limit = BigInteger.ONE.shiftLeft(modLen / 2).subtract(BigInteger.ONE);\n        return IntegerConstructor.constructPrime(rnd, limit);\n    }\n\n    BigInteger computeModulus(BigInteger p, BigInteger q) {\n        return p.multiply(q);\n    }\n\n    BigInteger generatePublicExponent(BigInteger p, BigInteger q) {\n        // we use a fixed public exponent for fast verification and signature\n        // share combination\n        return new BigInteger(\"65537\");\n    }\n\n    boolean verifyCofactors(BigInteger p, BigInteger q, BigInteger e) {\n        return e.gcd(MathUtil.phiSemiprime(p, q)).compareTo(BigInteger.ONE) == 0;\n    }\n\n    BigInteger computePrivateExponent(BigInteger p, BigInteger q, BigInteger e) {\n        return e.modInverse(MathUtil.phiSemiprime(p, q));\n    }\n\n    Polynomial generatePolynomial(BigInteger p, BigInteger q, BigInteger d) throws IOException {\n        return new Polynomial(tparams.getThreshold() - 1, MathUtil.phiSemiprime(p, q), d, rnd);\n    }\n\n    RSAParams[] packShares(BigInteger e, BigInteger[] shares, BigInteger n) {\n        RSAParams[] packedShares = new RSAParams[shares.length];\n        for (int i = 0; i < packedShares.length; i++) {\n            packedShares[i] = SignatureUtil.RSA.paramsToRSAParams(e, shares[i], n);\n        }\n        return packedShares;\n    }\n\n    void storeShares(RSAParams[] shares) {\n        for (int i = 0; i < shares.length; i++) {\n            sharestorage[i] = shares[i].encode();\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/protocol/signing/shoup/ShoupSigning.java",
    "content": "package ee.ivxv.key.protocol.signing.shoup;\n\nimport ee.ivxv.common.asn1.RSAParams;\nimport ee.ivxv.common.crypto.SignatureUtil;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.LagrangeInterpolation;\nimport ee.ivxv.common.math.MathUtil;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.SigningProtocol;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.math.BigInteger;\nimport java.security.SignatureException;\nimport java.security.spec.InvalidKeySpecException;\nimport java.util.Set;\nimport org.bouncycastle.asn1.ASN1Primitive;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x509.AlgorithmIdentifier;\n\n/**\n * Protocol for signing a message using threshold number of card tokens with RSA signing scheme.\n */\npublic class ShoupSigning implements SigningProtocol {\n    private final Set<IndexedBlob> blobs;\n    private final ThresholdParameters tparams;\n    private final Rnd rnd;\n    private final ByteArrayOutputStream out = new ByteArrayOutputStream();\n\n    /**\n     * Initialize the protocol using values.\n     * \n     * @param blobs The set of blobs that contain the private key shares.\n     * @param tparams Threshold parameters.\n     * @param rnd Random source for signing.\n     * @throws ProtocolException When the number of available cards is less than the threshold.\n     */\n    public ShoupSigning(Set<IndexedBlob> blobs, ThresholdParameters tparams, Rnd rnd)\n            throws ProtocolException {\n        if (blobs.size() < tparams.getThreshold()) {\n            throw new ProtocolException(\"Fewer cards available than threshold\");\n        }\n        this.blobs = blobs;\n        this.tparams = tparams;\n        this.rnd = rnd;\n    }\n\n    /**\n     * Sign the message using RSA-PSS signing scheme.\n     * <p>\n     * The algorithm for constructing the signature is as follows: {@code\n     *  1. get available blobs\n     *  2. parse RSAPrivateKey from every blob\n     *  2b. check that no keys have different modulus\n     *  3. use RSA-PSS signing using every RSAPrivateKEy\n     *  4. compute Lagrange coeficcient for every share\n     *  5. exponentiate modulo n every signature share\n     *  6. multiply the shares\n     *  7. verify the signature\n     * }\n     * \n     * @return RSA-PSS signature\n     * @throws ProtocolException When exception occurs during card token communication or signature\n     *         share generation.\n     */\n    @Override\n    public byte[] sign(byte[] msg) throws ProtocolException {\n\n        byte[] salt = new byte[SignatureUtil.RSA.RSA_PSS.HASH_LENGTH];\n        byte[] signature;\n        byte[][] sigShares;\n        RSAParams[] parsedKeys;\n        try {\n            parsedKeys = unpackAllBlobs(blobs);\n        } catch (InvalidKeySpecException ex) {\n            throw new ProtocolException(\"Invalid key blob: \" + ex.toString());\n        }\n        if (!filterKeys(parsedKeys)) {\n            throw new ProtocolException(\"Key share parameters mismatch\");\n        }\n        BigInteger n = getModulus(parsedKeys);\n        BigInteger e = getPublicExponent(parsedKeys);\n        try {\n            rnd.read(salt, 0, salt.length);\n        } catch (IOException ex) {\n            throw new ProtocolException(\"Reading from random source failed\", ex);\n        }\n        try {\n            sigShares = generateSignatureShares(msg, parsedKeys, salt);\n        } catch (SignatureException ex) {\n            throw new ProtocolException(\"Signature share generation failed: \" + ex.toString());\n        }\n        try {\n            signature = combineSignatureShares(sigShares, msg, n, e, salt);\n        } catch (SignatureException ex) {\n            throw new ProtocolException(\"Signature combining failed: \" + ex.toString());\n        }\n        signature = SignatureUtil.stripSignature(signature);\n        return signature;\n    }\n\n    private RSAParams[] unpackAllBlobs(Set<IndexedBlob> blobs) throws InvalidKeySpecException {\n        RSAParams[] parsed = new RSAParams[tparams.getParties()];\n        for (IndexedBlob blob : blobs) {\n            parsed[blob.getIndex() - 1] = SignatureUtil.RSA.bytesToRSAParams(blob.getBlob());\n        }\n        return parsed;\n    }\n\n    private boolean filterKeys(RSAParams[] keys) throws ProtocolException {\n        if (keys.length == 0) {\n            throw new ProtocolException(\"No key shares parsed\");\n        }\n        BigInteger mod = null;\n        BigInteger pubexp = null;\n        for (RSAParams sk : keys) {\n            if (sk == null) {\n                continue;\n            }\n            if (mod == null) {\n                mod = sk.getModulus();\n            }\n            if (pubexp == null) {\n                pubexp = sk.getPublicExponent();\n            }\n            if (sk.getModulus().compareTo(mod) != 0) {\n                return false;\n            }\n            if (sk.getPublicExponent().compareTo(pubexp) != 0) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    private byte[][] generateSignatureShares(byte[] msg, RSAParams[] keys, byte[] salt)\n            throws SignatureException {\n        byte[][] shares = new byte[keys.length][];\n        for (int i = 0; i < shares.length; i++) {\n            if (keys[i] == null) {\n                continue;\n            }\n            shares[i] = SignatureUtil.RSA.RSA_PSS.generateSignature(msg, keys[i], salt);\n        }\n        return shares;\n    }\n\n    private byte[] combineSignatureShares(byte[][] shares, byte[] msg, BigInteger n, BigInteger e,\n            byte[] salt) throws SignatureException {\n        BigInteger c;\n        BigInteger sigShare = BigInteger.ONE;\n        BigInteger exp;\n        byte[] encShare;\n        for (int i = 0; i < shares.length; i++) {\n            if (shares[i] == null) {\n                continue;\n            }\n            c = new BigInteger(1, shares[i]);\n            exp = LagrangeInterpolation.basisInverselessPolynomial(shares,\n                    BigInteger.valueOf(i + 1));\n            c = c.modPow(exp, n);\n            sigShare = sigShare.multiply(c).mod(n);\n        }\n        BigInteger[] bezout = MathUtil.extendedEuclidean(e,\n                MathUtil.factorial(BigInteger.valueOf(tparams.getParties())));\n        sigShare = sigShare.modPow(bezout[1], n);\n        encShare = SignatureUtil.RSA.RSA_PSS.encode(msg, n, salt);\n        c = new BigInteger(1, encShare);\n        c = c.modPow(bezout[0], n);\n        sigShare = sigShare.multiply(c).mod(n);\n        return sigShare.toByteArray();\n    }\n\n    private BigInteger getModulus(RSAParams[] keys) {\n        for (RSAParams sk : keys) {\n            if (sk == null) {\n                continue;\n            }\n            return sk.getModulus();\n        }\n        return null;\n    }\n\n    private BigInteger getPublicExponent(RSAParams[] keys) {\n        for (RSAParams sk : keys) {\n            if (sk == null) {\n                continue;\n            }\n            return sk.getPublicExponent();\n        }\n        return null;\n    }\n\n    /**\n     * Get the algorithm identifier for the signing scheme.\n     * \n     * @return SHA256-with-RSA-Encryption\n     */\n    @Override\n    public AlgorithmIdentifier getAlgorithmIdentifier() {\n        ASN1Primitive params = SignatureUtil.RSA.RSA_PSS.getDefaultAlgorithmIdentifier();\n        return new AlgorithmIdentifier(PKCSObjectIdentifiers.id_RSASSA_PSS, params);\n    }\n\n    /**\n     * Get the stream for writing the message value to be signed.\n     * \n     * @return Stream for writing the message.\n     */\n    @Override\n    public OutputStream getOutputStream() {\n        return out;\n    }\n\n    /**\n     * Sign the message written to the output stream.\n     * <p>\n     * Sign the message written to the stream output in {@link #getOutputStream()}\n     * \n     * @see #getOutputStream()\n     * @return RSA-PSS signature\n     */\n    @Override\n    public byte[] getSignature() {\n        byte[] msg = out.toByteArray();\n        out.reset();\n        try {\n            return sign(msg);\n        } catch (ProtocolException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/tool/DecryptTool.java",
    "content": "package ee.ivxv.key.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.CorrectnessUtil.CiphertextCorrectness;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.common.crypto.rnd.NativeRnd;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.District;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.IBallotBox;\nimport ee.ivxv.common.service.bbox.impl.BboxHelperImpl;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.smartcard.Card;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.key.KeyContext;\nimport ee.ivxv.key.Msg;\nimport ee.ivxv.key.model.Vote;\nimport ee.ivxv.key.protocol.DecryptionProtocol;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.SigningProtocol;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport ee.ivxv.key.protocol.decryption.recover.RecoverDecryption;\nimport ee.ivxv.key.protocol.signing.shoup.ShoupSigning;\nimport ee.ivxv.key.tool.DecryptTool.DecryptArgs;\nimport ee.ivxv.key.util.ElectionResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.CompletionService;\nimport java.util.concurrent.ExecutorCompletionService;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * DecryptTool is a tool for decrypting the encrypted ballots.\n */\npublic class DecryptTool implements Tool.Runner<DecryptArgs> {\n\n    static final Logger log = LoggerFactory.getLogger(DecryptTool.class);\n\n    private final KeyContext ctx;\n    private final I18nConsole console;\n    private final ToolHelper tool;\n\n    public DecryptTool(KeyContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        tool = new ToolHelper(console, ctx.container, new BboxHelperImpl(ctx.conf, ctx.container));\n    }\n\n    @Override\n    public boolean run(DecryptArgs args) throws Exception {\n        tool.checkBbChecksum(args.abb.value(), args.abbChecksum.value());\n        AnonymousBallotBox abb = tool.readJsonAbb(args.abb.value(), IBallotBox.Type.ANONYMIZED);\n        if (args.questionCount.value() != abb.getNumberOfQuestions()) {\n            throw new MessageException(Msg.e_abb_invalid_question_count, args.questionCount.value(),\n                    abb.getNumberOfQuestions());\n        }\n        DistrictList districts = tool.readJsonDistricts(args.districts.value());\n        CandidateList candidates = tool.readJsonCandidates(args.candidates.value(), districts);\n\n        console.println();\n        console.println(Msg.m_abb_dist_verifying);\n        verifyAbb(abb, districts);\n        console.println(Msg.m_abb_dist_ok);\n\n        console.println();\n        if (args.doProvable.value()) {\n            console.println(Msg.m_with_proof);\n        } else {\n            console.println(Msg.m_without_proof);\n        }\n        console.println(Msg.m_protocol_init);\n        DecryptionProtocol dec = null;\n        SigningProtocol signer = null;\n        if (args.recover.isSet()) {\n            ThresholdParameters tparams = new ThresholdParameters(args.dn.value(), args.dm.value());\n            byte[] aid = new byte[] {0x01};\n            byte[] decShareName = new byte[] {0x44, 0x45, 0x43};\n            byte[] signShareName = new byte[] {0x53, 0x49, 0x47, 0x4E};\n            Cards cards = ctx.card.createCards();\n            if (!ctx.card.isPluggableService()) {\n                for (int i = 0; i < tparams.getParties(); i++) {\n                    cards.addCard(String.valueOf(i));\n                }\n            }\n            Set<IndexedBlob> decBlobs = new HashSet<>();\n            Set<IndexedBlob> signBlobs = new HashSet<>();\n            for (int i = 0; i < tparams.getThreshold(); i++) {\n                int retryCount = 0;\n                int maxTries = 2;\n                while (true) {\n                    try {\n                        Card card;\n                        if (ctx.card.isPluggableService()) {\n                            card = ctx.card.createCard(\"-1\");\n                            cards.initUnprocessedCard(card);\n                        } else {\n                            card = cards.getCard(i);\n                        }\n                        IndexedBlob ib = card.getIndexedBlob(aid, decShareName);\n                        if (ib.getIndex() < 1 || ib.getIndex() > tparams.getParties()) {\n                            throw new ProtocolException(\"Indexed blob index mismatch\");\n                        }\n                        decBlobs.add(ib);\n\n                        ib = card.getIndexedBlob(aid, signShareName);\n                        if (ib.getIndex() < 1 || ib.getIndex() > tparams.getParties()) {\n                            throw new ProtocolException(\"Indexed blob index mismatch\");\n                        }\n                        signBlobs.add(ib);\n                        break;\n                    } catch (ProtocolException e) {\n                        throw e;\n                    } catch (Exception e) {\n                        if (++retryCount == maxTries) throw e;\n                    }\n                }\n            }\n\n            dec = new RecoverDecryption(decBlobs, tparams, args.doProvable.value());\n            signer = new ShoupSigning(signBlobs, tparams, new NativeRnd());\n        }\n        console.println(Msg.m_protocol_init_ok);\n\n        console.println();\n        console.println(Msg.m_dec_start);\n        Path out = args.outputPath.value();\n        ElectionResult result = processVotes(abb, dec, candidates, districts,\n                args.doProvable.value(), args.checkDecodable.value(), args.proveInvalid.value(),\n                ctx.args.threads.value());\n        console.println(Msg.m_dec_done);\n\n        console.println();\n        console.println(M.m_out_start, out);\n        Files.createDirectory(out);\n\n        console.println(Msg.m_out_tally);\n        result.outputTally(out, signer);\n\n        console.println(Msg.m_out_plainbb);\n        result.outputPlainBB(out, signer);\n\n        if (args.doProvable.value()) {\n            console.println(Msg.m_out_proof);\n            result.outputProof(out);\n\n            if (args.proveInvalid.value()) {\n                console.println(Msg.m_out_proof_invalid);\n                result.outputInvalidProof(out);\n            }\n        }\n\n        console.println(Msg.m_out_invalid);\n        result.outputInvalid(out);\n\n        console.println(M.m_out_done);\n\n        return true;\n    }\n\n    private ElectionResult processVotes(AnonymousBallotBox abb, DecryptionProtocol dec,\n            CandidateList candidates, DistrictList districts, boolean withProof,\n            boolean checkDecodable, boolean proveInvalid, int threadCount) throws Exception {\n        ElectionResult result =\n                new ElectionResult(abb.getElection(), candidates, districts, withProof, proveInvalid);\n        // WorkerFactory consumer = new WorkerFactory(getDecConsumer(dec, result));\n\n        ExecutorService ioExecutor = Executors.newFixedThreadPool(3);\n        CompletionService<Void> ioCompService = new ExecutorCompletionService<>(ioExecutor);\n\n        ExecutorService decExecutor;\n        threadCount = threadCount > 0 ? threadCount : 1;\n        decExecutor = new ThreadPoolExecutor(threadCount, threadCount, 0L, TimeUnit.MILLISECONDS,\n                new ArrayBlockingQueue<>(threadCount * 2));\n\n        WorkManager manager = new WorkManager(abb, getDecConsumer(dec, result, checkDecodable),\n                decExecutor, result);\n        ioCompService.submit(manager);\n        ioCompService\n                .submit(result.getResultWorker(abb.getNumberOfBallots(), console, ctx.reporter));\n\n        try {\n            for (int done = 0; done < 2; done++) {\n                ioCompService.take().get();\n            }\n        } finally {\n            ioExecutor.shutdown();\n            decExecutor.shutdown();\n        }\n\n        return result;\n    }\n\n    private Consumer<Vote> getDecConsumer(DecryptionProtocol dec, ElectionResult result,\n            boolean checkDecodable) {\n        return (vote) -> {\n            byte[] msg = vote.getVote();\n            // as a defensive measure, assume that the message is not decodable.\n            boolean isCorrect = false;\n            ElGamalDecryptionProof dp;\n            if (checkDecodable) {\n                // decodability check of the ciphertexts is explicitly required\n                try {\n                    if (dec.checkCorrectness(msg) == CiphertextCorrectness.VALID) {\n                        // the ciphertext is correctly encoded\n                        isCorrect = true;\n                    } else {\n                        // ciphertext is not correctly encoded\n                    }\n                } catch (ProtocolException e) {\n                    // catch the exception, but omit the stack-trace as it may contain unique\n                    // information about why the correctness verification failed. This unique\n                    // information could be used to connect the ballot with a voter.\n                }\n            } else {\n                // if decodability check is not explicitly required, then assume that the message is\n                // decodable. This assumption holds when the checks are done in previous steps (i.e.\n                // during processing of the votes).\n                isCorrect = true;\n            }\n            if (isCorrect) {\n                try {\n                    dp = dec.decryptMessage(msg);\n                    vote.setProof(dp);\n                } catch (Exception e) {\n                    // catch the exception, but omit the stack-trace as it may contain identifiable\n                    // information about the error.\n                }\n            }\n            // the vote is added to the result even if it is not correctly encoded - it is counted\n            // towards the invalid vote count\n            result.addVote(vote);\n        };\n    }\n\n    private void verifyAbb(AnonymousBallotBox abb, DistrictList districts) {\n        abb.getDistricts().forEach((d, pMap) -> {\n            District dist = districts.getDistricts().get(d);\n            if (dist == null) {\n                throw new MessageException(Msg.e_illegal_vote_district, d);\n            }\n            pMap.keySet().forEach(s -> {\n                if (!dist.getParish().contains(s)) {\n                    throw new MessageException(Msg.e_illegal_vote_parish, s);\n                }\n            });\n        });\n    }\n\n    public static class DecryptArgs extends Args {\n        Arg<String> identifier = Arg.aString(Msg.arg_identifier);\n        Arg<Path> abb = Arg.aPath(Msg.d_anonballotbox, true, false);\n        Arg<Path> abbChecksum = Arg.aPath(Msg.d_anonballotbox_checksum, true, false);\n        Arg<Integer> questionCount = Arg.anInt(Msg.d_questioncount).setDefault(1);\n        Arg<Path> candidates = Arg.aPath(Msg.d_candidates, true, false);\n        Arg<Path> districts = Arg.aPath(Msg.d_districts, true, false);\n        Arg<Path> outputPath = Arg.aPath(Msg.arg_out, false, null);\n        Arg<Boolean> doProvable = Arg.aFlag(Msg.d_provable).setDefault(true);\n        Arg<Boolean> checkDecodable = Arg.aFlag(Msg.d_check_decodable).setDefault(false);\n        Arg<Boolean> proveInvalid = Arg.aFlag(Msg.d_prove_invalid).setDefault(false);\n\n        // protocols\n\n        Arg<Integer> dm = Arg.anInt(Msg.arg_threshold);\n        Arg<Integer> dn = Arg.anInt(Msg.arg_parties);\n        Arg<Args> recover = new Arg.Tree(Msg.d_recover, dm, dn).setOptional();\n\n        Arg.Tree protocol = new Arg.Tree(Msg.d_protocol, recover).setExclusive();\n\n        public DecryptArgs() {\n            super();\n            args.add(identifier);\n            args.add(abb);\n            args.add(abbChecksum);\n            args.add(questionCount);\n            args.add(candidates);\n            args.add(districts);\n            args.add(outputPath);\n            args.add(doProvable);\n            args.add(checkDecodable);\n            args.add(proveInvalid);\n            args.add(protocol);\n        }\n    }\n\n    private class WorkManager implements Callable<Void> {\n\n        private final AnonymousBallotBox abb;\n        private final Consumer<Vote> consumer;\n        private final ExecutorService decExecutor;\n        private final ElectionResult result;\n\n        WorkManager(AnonymousBallotBox abb, Consumer<Vote> factory, ExecutorService decExecutor,\n                ElectionResult result) {\n            this.abb = abb;\n            this.consumer = factory;\n            this.decExecutor = decExecutor;\n            this.result = result;\n        }\n\n        @Override\n        public Void call() throws Exception {\n            abb.getDistricts().forEach((d, sMap) -> sMap.forEach((s, qMap) -> {\n                qMap.forEach((q, cList) -> cList.forEach(c -> {\n                    boolean taskAdded = false;\n                    do {\n                        try {\n                            decExecutor.execute(() -> consumer.accept(new Vote(d, s, q, c)));\n                            taskAdded = true;\n                        } catch (RejectedExecutionException e) {\n                            try {\n                                Thread.sleep(20);\n                            } catch (InterruptedException e1) {\n                                log.warn(\"Unexpected interruption\", e1);\n                            }\n                        }\n                    } while (!taskAdded);\n                }));\n            }));\n            decExecutor.shutdown();\n            decExecutor.awaitTermination(1, TimeUnit.DAYS);\n            result.setEot();\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/tool/GroupGenTool.java",
    "content": "package ee.ivxv.key.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.ECGroup;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.math.MathException;\nimport ee.ivxv.common.math.ModPGroup;\nimport ee.ivxv.common.math.ModPGroupElement;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.key.KeyContext;\nimport ee.ivxv.key.Msg;\nimport ee.ivxv.key.RandomSourceArg;\nimport ee.ivxv.key.tool.GroupGenTool.GroupGenArgs;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.FutureTask;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\n/**\n * GroupGenTool is a tool for generating ElGamal group parameters.\n */\npublic class GroupGenTool implements Tool.Runner<GroupGenArgs> {\n\n    private static final String MOD_GROUP = \"mod\";\n    private static final String EC_GROUP = \"ec\";\n\n    private final I18nConsole console;\n    private final KeyContext ctx;\n\n    public GroupGenTool(KeyContext ctx) {\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        this.ctx = ctx;\n    }\n\n    @Override\n    public boolean run(GroupGenArgs args) throws Exception {\n        Group group = null;\n        GroupElement generator = null;\n        Rnd rnd = RandomSourceArg.combineFromArgument(args.random);\n        Progress p = console.startInfiniteProgress(1000);\n        try {\n            group = groupGen(args.paramType.value(), args.len.value(), rnd, p);\n            generator = generatorGen(group, rnd);\n        } finally {\n            rnd.close();\n            p.finish();\n        }\n        writeParameters(group, generator, args.initTemplate.value());\n        return true;\n    }\n\n    private Group groupGen(String groupType, int len, Rnd rnd, Progress p) throws Exception {\n        switch (groupType) {\n            case MOD_GROUP:\n                return modpGroupGen(len, rnd, p);\n            case EC_GROUP:\n                return new ECGroup(len);\n            default:\n                // this path should not be executed\n                throw new UnsupportedOperationException(\"Unsupported group type\");\n        }\n    }\n\n    private ModPGroup modpGroupGen(int len, Rnd rnd, Progress p) throws Exception {\n        ModPGroup res = null;\n        ExecutorService executor = Executors.newFixedThreadPool(ctx.args.threads.value());\n        List<Future<ModPGroup>> futures = new ArrayList<>();\n        for (int i = 0; i < ctx.args.threads.value(); i++) {\n            FutureTask<ModPGroup> ft = new FutureTask<>(new Callable<ModPGroup>() {\n                @Override\n                public ModPGroup call() throws IllegalArgumentException, IOException {\n                    ModPGroup g;\n                    while (!Thread.currentThread().isInterrupted()) {\n                        try {\n                            p.increase(1);\n                            g = new ModPGroup(len, rnd, 1);\n                        } catch (MathException e) {\n                            continue;\n                        }\n                        return g;\n                    }\n                    return null;\n                }\n            });\n            executor.submit(ft);\n            futures.add(ft);\n        }\n        Iterator<Future<ModPGroup>> it = futures.iterator();\n        while (it.hasNext()) {\n            Future<ModPGroup> future = it.next();\n            if (!it.hasNext()) {\n                it = futures.iterator();\n            }\n            try {\n                res = future.get(10, TimeUnit.MILLISECONDS);\n            } catch (InterruptedException e) {\n                continue;\n            } catch (ExecutionException e) {\n                throw e;\n            } catch (TimeoutException e) {\n                continue;\n            }\n            break;\n        }\n        executor.shutdownNow();\n        return res;\n    }\n\n    private GroupElement generatorGen(Group group, Rnd rnd) throws IOException {\n        if (group instanceof ModPGroup) {\n            return ((ModPGroup) group).getRandomElement(rnd);\n        } else if (group instanceof ECGroup) {\n            return ((ECGroup) group).getBasePoint();\n        } else {\n            throw new IllegalArgumentException(\"Unknown group\");\n        }\n    }\n\n    private void writeParameters(Group group, GroupElement generator, Path template)\n            throws IOException {\n        String templatestr;\n        if (group instanceof ModPGroup) {\n            templatestr = String.format(\"  paramtype:\\n    mod:\\n      p: %s\\n      g: %s\\n\",\n                    group.getOrder(), ((ModPGroupElement) generator).getValue());\n        } else if (group instanceof ECGroup) {\n            templatestr = String.format(\"  paramtype:\\n    ec:\\n      name: %s\\n\",\n                    ((ECGroup) group).getCurveName());\n        } else {\n            throw new IllegalArgumentException(\"Unknown group\");\n        }\n        Files.write(template, Util.toBytes(templatestr));\n    }\n\n    public static class GroupGenArgs extends Args {\n        Arg<Integer> len = Arg.anInt(Msg.g_length);\n        Arg<String> paramType = Arg.aChoice(Msg.arg_paramtype, MOD_GROUP, EC_GROUP);\n        Arg<Path> initTemplate = Arg.aPath(Msg.g_init_template, false, false);\n        Arg<List<RandomSourceArg.RndListEntry>> random = RandomSourceArg.getArgument();\n\n        public GroupGenArgs() {\n            super();\n            args.add(len);\n            args.add(random);\n            args.add(paramType);\n            args.add(initTemplate);\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/tool/InitTool.java",
    "content": "package ee.ivxv.key.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.ContextFactory;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.SignatureUtil;\nimport ee.ivxv.common.crypto.elgamal.ElGamalParameters;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.ECGroup;\nimport ee.ivxv.common.math.ECGroupElement;\nimport ee.ivxv.common.math.Group.Decodable;\nimport ee.ivxv.common.math.ModPGroup;\nimport ee.ivxv.common.math.ModPGroupElement;\nimport ee.ivxv.common.service.smartcard.Card;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.common.service.smartcard.SmartCardException;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.key.KeyContext;\nimport ee.ivxv.key.Msg;\nimport ee.ivxv.key.RandomSourceArg;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport ee.ivxv.key.protocol.generation.desmedt.DesmedtGeneration;\nimport ee.ivxv.key.protocol.generation.shoup.ShoupGeneration;\nimport ee.ivxv.key.protocol.signing.shoup.ShoupSigning;\nimport ee.ivxv.key.tool.InitTool.InitArgs;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.interfaces.RSAPublicKey;\nimport java.util.Date;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport javax.smartcardio.CardException;\nimport org.bouncycastle.asn1.x500.X500Name;\nimport org.bouncycastle.asn1.x509.BasicConstraints;\nimport org.bouncycastle.asn1.x509.Extension;\nimport org.bouncycastle.asn1.x509.KeyUsage;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.bouncycastle.cert.X509v3CertificateBuilder;\nimport org.bouncycastle.operator.ContentSigner;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * InitTool is a tool for generating ElGamal encryption key pair and RSA signing key pair and\n * storing them on card tokens.\n */\npublic class InitTool implements Tool.Runner<InitArgs> {\n    private static final Logger log = LoggerFactory.getLogger(InitTool.class);\n\n    static final String ENC_CERT_TMPL = \"enc.pem\";\n    static final String ENC_KEY_DER_TMPL = \"pub.der\";\n    static final String ENC_KEY_PEM_TMPL = \"pub.pem\";\n    static final String SIGN_CERT_TMPL = \"sign.pem\";\n\n    static final byte[] AID = new byte[] {0x01};\n    static final byte[] DEC_SHARE_NAME = \"DEC\".getBytes();\n    static final byte[] SIGN_SHARE_NAME = \"SIGN\".getBytes();\n\n    private final KeyContext ctx;\n    private final I18nConsole console;\n\n    public InitTool(KeyContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    @Override\n    public boolean run(InitArgs args) throws Exception {\n        ElGamalParameters params = getParameters(args);\n        log.debug(\"Gen keys with params:{}\", params);\n        Rnd rnd = null;\n        try {\n            rnd = RandomSourceArg.combineFromArgument(args.random);\n        } catch (IOException e) {\n            log.debug(\"Could not initialize random sources:\", e);\n            return false;\n        }\n        try {\n            if (args.desmedt.isSet()) {\n                desmedtGenProtocol(args, rnd, params);\n            }\n        } finally {\n            rnd.close();\n        }\n        return true;\n    }\n\n    private void desmedtGenProtocol(InitArgs args, Rnd rnd, ElGamalParameters params)\n            throws IOException, ProtocolException, CardException, SmartCardException {\n        // PREPARE FOR KEYPAIR GENERATION\n        ThresholdParameters tparams = new ThresholdParameters(args.dn.value(), args.dm.value());\n        byte[][] encshares = new byte[tparams.getParties()][];\n        byte[][] signshares = new byte[tparams.getParties()][];\n        Cards cards = UtilTool.listCards(ctx, tparams);\n        boolean isFastMode =\n                args.enableFastMode.value() && cards.enableFastMode(tparams.getParties());\n\n        Msg fastModeStatus = isFastMode ? Msg.m_fastmode_enabled : Msg.m_fastmode_disabled;\n        console.println(fastModeStatus);\n\n        // COLLECT ENTROPY\n        console.println(Msg.m_collecting_required_randomness, args.requiredRandomness.value());\n        byte[] tmp = new byte[args.requiredRandomness.value()];\n        rnd.mustRead(tmp, 0, tmp.length);\n\n        // GENERATE ENCRYPTION KEYPAIR\n        console.println(Msg.m_generate_decryption_key);\n        DesmedtGeneration gen =\n                new DesmedtGeneration(cards, params, tparams, rnd, AID, DEC_SHARE_NAME, encshares);\n        ElGamalPublicKey pub = new ElGamalPublicKey(gen.generateKey());\n\n        // GENERATE SIGNATURE KEYPAIR\n        console.println(Msg.m_generate_signature_key);\n        ShoupGeneration shoupGen = new ShoupGeneration(cards, args.slen.value(), tparams, rnd, AID,\n                SIGN_SHARE_NAME, signshares);\n\n        RSAPublicKey rsaPub = SignatureUtil.RSA.bytesToRSAPublicKey(shoupGen.generateKey());\n\n        console.println(Msg.m_storing_shares);\n        for (int i = 0; i < encshares.length; i++) {\n\n            // We do not add retry handler to storage operations, it's better to\n            // use different card\n            Card card = cards.getCard(i);\n            card.storeIndexedBlob(AID, DEC_SHARE_NAME, encshares[i], i + 1);\n            card.storeIndexedBlob(AID, SIGN_SHARE_NAME, signshares[i], i + 1);\n        }\n\n        console.println(Msg.m_generating_certificate);\n        // GENERATE CERTIFICATES FOR BOTH KEYPAIRS\n        Set<IndexedBlob> signBlobs = new HashSet<>();\n        for (int i = 0; i < tparams.getThreshold(); i++) {\n            Card card;\n            if (ctx.card.isPluggableService()) {\n                card = ctx.card.createCard(\"-1\");\n                cards.initUnprocessedCard(card);\n            } else {\n                card = cards.getCard(i);\n            }\n            // We do not add retry handler here, since it's part of the storage\n            // operation\n            IndexedBlob ib = card.getIndexedBlob(AID, SIGN_SHARE_NAME);\n            if (ib.getIndex() < 1 || ib.getIndex() > tparams.getParties()) {\n                throw new ProtocolException(\"Indexed blob index mismatch\");\n            }\n            signBlobs.add(ib);\n        }\n        ShoupSigning shoupSign = new ShoupSigning(signBlobs, tparams, rnd);\n\n        Path signCertPath = Util.prefixedPath(args.identifier.value(), SIGN_CERT_TMPL);\n        Path encCertPath = Util.prefixedPath(args.identifier.value(), ENC_CERT_TMPL);\n        byte[] signCert =\n                generateSigningCert(shoupSign, args.signSN.value(), args.signCN.value(), rsaPub);\n        byte[] encCert = generateEncryptionCert(shoupSign, args.encSN.value(), args.encCN.value(),\n                args.signCN.value(), pub);\n        writeCertificate(signCert, args.outputPath.value(), signCertPath);\n        writeCertificate(encCert, args.outputPath.value(), encCertPath);\n        console.println(Msg.m_certificates_generated, signCertPath, encCertPath);\n\n        Path encKeyDerPath = Util.prefixedPath(args.identifier.value(), ENC_KEY_DER_TMPL);\n        Path encKeyPemPath = Util.prefixedPath(args.identifier.value(), ENC_KEY_PEM_TMPL);\n        writeOutEncryptionKey(pub, args.outputPath.value(), encKeyDerPath, encKeyPemPath);\n        console.println(Msg.m_keys_saved, encKeyDerPath, encKeyPemPath);\n\n        if (!args.skipTests.value()) {\n            // TEST THAT THE GENERATED KEY SUCCESSFULLY SIGNS\n            TestKeyTool.allTests(console, log, ctx.card, tparams, rnd, pub, rsaPub);\n        }\n    }\n\n    private byte[] generateSigningCert(ContentSigner signer, BigInteger serial, String name,\n            RSAPublicKey pub) throws IOException {\n        // although the RSA key can realistically only be used with PSS padding, then support for\n        // such certificates is only in the most recent OpenSSL versions (1.1.1+). We use the\n        // default RSA algorithm identifier which does not specify the preferred padding scheme.\n        // SubjectPublicKeyInfo spki = SignatureUtil.RSA.RSA_PSS.getSubjectPublicKeyInfo(pub);\n        SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(pub.getEncoded());\n        boolean is_ca = true;\n        KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature);\n        return generateCert(signer, serial, name, name, spki, is_ca, usage);\n    }\n\n    private byte[] generateEncryptionCert(ContentSigner signer, BigInteger serial, String name,\n            String issuer, ElGamalPublicKey pub) throws IOException {\n        SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(pub.getBytes());\n        boolean is_ca = false;\n        KeyUsage usage = new KeyUsage(KeyUsage.dataEncipherment);\n        return generateCert(signer, serial, name, issuer, spki, is_ca, usage);\n    }\n\n    private byte[] generateCert(ContentSigner signer, BigInteger serial, String name, String issuer,\n            SubjectPublicKeyInfo spki, boolean is_ca, KeyUsage usage) throws IOException {\n        X500Name subjectName = new X500Name(String.format(\"CN=%s\", name));\n        // the signing certificate is self-signed\n        X500Name issuerName = new X500Name(String.format(\"CN=%s\", issuer));\n        // certificate valid from now\n        long now = System.currentTimeMillis();\n        Date notBefore = new Date(now);\n        // the certificate expires in 10 years\n        Date notAfter = new Date(now + 10 * 365 * 24 * 60 * 60 * 1000);\n        X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuerName, serial,\n                notBefore, notAfter, subjectName, spki);\n        // add CA extension (this certificate will also sign the encryption certificate)\n        builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(is_ca));\n        builder.addExtension(Extension.keyUsage, true, usage);\n        X509CertificateHolder cert = builder.build(signer);\n        return cert.getEncoded();\n    }\n\n    private ElGamalParameters getParameters(InitArgs args) {\n        ElGamalParameters params;\n        String electionId = args.identifier.value();\n        if (args.mod.isSet()) {\n            BigInteger p = args.groupP.value();\n            BigInteger g = args.groupG.value();\n\n            ModPGroup group = new ModPGroup(p, true);\n            ModPGroupElement groupG = new ModPGroupElement(group, g);\n            if (group.isGroupElement(groupG) != Decodable.VALID) {\n                throw new IllegalArgumentException(\"Invalid generator\");\n            }\n            params = new ElGamalParameters(group, groupG, electionId);\n        } else {\n            ECGroup group = new ECGroup(args.curveName.value());\n            ECGroupElement groupG = group.getBasePoint();\n            params = new ElGamalParameters(group, groupG, electionId);\n        }\n        return params;\n    }\n\n    private void writeCertificate(byte[] cert, Path dir, Path path) throws IOException {\n        path = dir.resolve(path);\n        Util.createFile(path);\n        String out = Util.encodeCertificate(cert);\n        Files.write(path, Util.toBytes(out));\n    }\n\n    private void writeOutEncryptionKey(ElGamalPublicKey key, Path dir, Path der, Path pem)\n            throws IOException {\n        Path pemPath = dir.resolve(pem);\n        Path derPath = dir.resolve(der);\n        String encodedKey = Util.encodePublicKey(key.getBytes());\n        Util.createFile(pemPath);\n        Files.write(pemPath, Util.toBytes(encodedKey));\n        Util.createFile(derPath);\n        Files.write(derPath, key.getBytes());\n    }\n\n    public static class InitArgs extends Args {\n        Arg<String> identifier = Arg.aString(Msg.arg_identifier);\n        Arg<Path> outputPath = Arg.aPath(Msg.arg_out, false, null);\n        Arg<Boolean> skipTests = Arg.aFlag(Msg.i_skiptest);\n        Arg<List<RandomSourceArg.RndListEntry>> random = RandomSourceArg.getArgument();\n        Arg<Integer> requiredRandomness = Arg.anInt(Msg.i_required_randomness).setDefault(128);\n        Arg<Boolean> enableFastMode = Arg.aFlag(Msg.arg_fastmode).setDefault(true);\n\n        Arg<Integer> slen = Arg.anInt(Msg.i_signaturekeylen);\n        Arg<String> signCN = Arg.aString(Msg.i_signcn);\n        Arg<BigInteger> signSN = Arg.aBigInt(Msg.i_signsn);\n        Arg<String> encCN = Arg.aString(Msg.i_enccn);\n        Arg<BigInteger> encSN = Arg.aBigInt(Msg.i_encsn);\n\n        // ElGamal parameters\n        Arg<BigInteger> groupP = Arg.aBigInt(Msg.i_p);\n        Arg<BigInteger> groupG = Arg.aBigInt(Msg.i_g);\n        Arg<Args> mod = new Arg.Tree(Msg.arg_mod, groupP, groupG).setOptional();\n\n        Arg<String> curveName = null;\n        Arg<Args> ec = null;\n        Arg.Tree paramType = null;\n\n        // GEN protocols\n\n        Arg<Integer> dm = Arg.anInt(Msg.arg_threshold);\n        Arg<Integer> dn = Arg.anInt(Msg.arg_parties);\n        Arg<Args> desmedt = new Arg.Tree(Msg.i_desmedt, dm, dn).setOptional();\n\n        Arg.Tree genProtocol = new Arg.Tree(Msg.i_genprotocol, desmedt).setExclusive();\n\n        public InitArgs() {\n            super();\n            args.add(identifier);\n            args.add(outputPath);\n            args.add(skipTests);\n            args.add(requiredRandomness);\n            args.add(enableFastMode);\n            args.add(random);\n            args.add(slen);\n            args.add(signCN);\n            args.add(signSN);\n            args.add(encCN);\n            args.add(encSN);\n            if (ContextFactory.get().isTestMode()) {\n                curveName = Arg.aChoice(Msg.i_name, ECGroup.P384, ECGroup.P224);\n                ec = new Arg.Tree(Msg.arg_ec, curveName).setOptional();\n                paramType = new Arg.Tree(Msg.arg_paramtype, mod, ec).setExclusive();\n            }\n            else {\n                curveName = Arg.aChoice(Msg.i_name, ECGroup.P384);\n                ec = new Arg.Tree(Msg.arg_ec, curveName).setOptional();\n                paramType = new Arg.Tree(Msg.arg_paramtype, mod, ec).setExclusive();\n            }\n            args.add(paramType);\n            args.add(genProtocol);\n\n        }\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/tool/TestKeyTool.java",
    "content": "package ee.ivxv.key.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.crypto.SignatureUtil;\nimport ee.ivxv.common.crypto.elgamal.ElGamalCiphertext;\nimport ee.ivxv.common.crypto.elgamal.ElGamalDecryptionProof;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.crypto.rnd.NativeRnd;\nimport ee.ivxv.common.crypto.rnd.Rnd;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.smartcard.Card;\nimport ee.ivxv.common.service.smartcard.CardService;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.key.KeyContext;\nimport ee.ivxv.key.Msg;\nimport ee.ivxv.key.protocol.ProtocolException;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport ee.ivxv.key.protocol.decryption.recover.RecoverDecryption;\nimport ee.ivxv.key.protocol.signing.shoup.ShoupSigning;\nimport ee.ivxv.key.tool.TestKeyTool.TestKeyArgs;\nimport ee.ivxv.key.util.QuorumUtil;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.KeyException;\nimport java.security.KeyFactory;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.interfaces.RSAPublicKey;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.X509EncodedKeySpec;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport javax.smartcardio.CardException;\nimport org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;\nimport org.bouncycastle.cert.X509CertificateHolder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * TestKeyTool is a tool for running various helper methods within a key application.\n * <p>\n * Currently the tool supports listing smart card readers, the cards inserted to the readers and\n * reading the card information files from the cards.\n * <p>\n * Also, it is possible to test the functionality of the private key shares stored on the card\n * tokens.\n */\npublic class TestKeyTool implements Tool.Runner<TestKeyArgs> {\n    public static class TestKeyArgs extends Args {\n        Arg<String> identifier = Arg.aString(Msg.arg_identifier);\n        Arg<Path> outputPath = Arg.aPath(Msg.arg_out, true, true);\n        Arg<Integer> dm = Arg.anInt(Msg.arg_threshold);\n        Arg<Integer> dn = Arg.anInt(Msg.arg_parties);\n        Arg<Boolean> fm = Arg.aFlag(Msg.arg_fastmode).setDefault(true);\n\n        public TestKeyArgs() {\n            super();\n            args.add(identifier);\n            args.add(outputPath);\n            args.add(dm);\n            args.add(dn);\n            args.add(fm);\n        }\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(TestKeyTool.class);\n\n    static final Plaintext TEST_MESSAGE = new Plaintext(\"123.321\");\n\n    static void allRecoverTests(I18nConsole console, Logger log, List<Set<IndexedBlob>> quorums,\n            ElGamalPublicKey pub, ThresholdParameters tparams, Rnd rnd, Plaintext message)\n            throws ProtocolException, IOException {\n        console.println(Msg.m_test_decryption_key);\n        ElGamalCiphertext c;\n        try {\n            c = pub.encrypt(message, rnd);\n        } catch (KeyException e) {\n            console.println(Msg.e_testencryption_fail);\n            log.debug(\"Encryption failed\", e);\n            throw new MessageException(Msg.e_testencryption_fail);\n        }\n\n        for (Set<IndexedBlob> quorum : quorums) {\n            recoverTest(console, log, quorum, c, tparams, message);\n        }\n    }\n\n    static void allShoupTests(I18nConsole console, Logger log, List<Set<IndexedBlob>> quorums,\n            RSAPublicKey rsaPub, ThresholdParameters tparams, Rnd rnd, Plaintext message)\n            throws ProtocolException {\n        console.println(Msg.m_test_signature_key);\n        for (Set<IndexedBlob> quorum : quorums) {\n            shoupTest(console, log, quorum, rsaPub, tparams, rnd, message);\n        }\n    }\n\n    static void allTests(I18nConsole console, Logger log, CardService cardService,\n            ThresholdParameters tparams, Rnd rnd, ElGamalPublicKey pub, RSAPublicKey rsaPub)\n            throws ProtocolException, IOException {\n        try {\n            Cards cards = cardService.createCards();\n            if (!cardService.isPluggableService()) {\n                for (int i = 0; i < tparams.getParties(); i++) {\n                    cards.addCard(String.valueOf(i));\n                }\n            }\n            List<IndexedBlob> decList = new ArrayList<>();\n            List<IndexedBlob> signList = new ArrayList<>();\n            for (int i = 0; i < tparams.getParties(); i++) {\n                int retryCount = 0;\n                int maxTries = 2;\n                while (true) {\n                    try {\n                        Card card;\n                        if (cardService.isPluggableService()) {\n                            card = cardService.createCard(\"-1\");\n                            cards.initUnprocessedCard(card);\n                        } else {\n                            card = cards.getCard(i);\n                        }\n                        IndexedBlob ib = card.getIndexedBlob(InitTool.AID, InitTool.DEC_SHARE_NAME);\n                        if (ib.getIndex() < 1 || ib.getIndex() > tparams.getParties()) {\n                            throw new ProtocolException(\"Indexed blob index mismatch\");\n                        }\n                        decList.add(ib);\n\n                        ib = card.getIndexedBlob(InitTool.AID, InitTool.SIGN_SHARE_NAME);\n                        if (ib.getIndex() < 1 || ib.getIndex() > tparams.getParties()) {\n                            throw new ProtocolException(\"Indexed blob index mismatch\");\n                        }\n                        signList.add(ib);\n                        break;\n                    } catch (ProtocolException e) {\n                        throw e;\n                    } catch (Exception e) {\n                        if (++retryCount == maxTries) throw e;\n                    }\n                }\n            }\n\n            List<Set<IndexedBlob>> recoverQuorums =\n                    QuorumUtil.getQuorumList(decList, tparams.getThreshold());\n            List<Set<IndexedBlob>> shoupQuorums =\n                    QuorumUtil.getQuorumList(signList, tparams.getThreshold());\n\n            allRecoverTests(console, log, recoverQuorums, pub, tparams, rnd, TEST_MESSAGE);\n            allShoupTests(console, log, shoupQuorums, rsaPub, tparams, rnd, TEST_MESSAGE);\n        } catch (Exception e) {\n            throw new IOException(e);\n        }\n    }\n\n    static ElGamalPublicKey readDecryptionKey(TestKeyArgs args) throws IOException {\n        Path encCertPath = Util.prefixedPath(args.identifier.value(), InitTool.ENC_CERT_TMPL);\n        Path fullpath = args.outputPath.value().resolve(encCertPath);\n        byte[] raw = readKey(fullpath);\n        ElGamalPublicKey key = new ElGamalPublicKey(raw);\n        return key;\n    }\n\n    private static byte[] readKey(Path fullpath) throws IOException {\n        byte[] content = Files.readAllBytes(fullpath);\n        String certString = new String(content, Util.CHARSET);\n        byte[] certBytes = Util.decodeCertificate(certString);\n        X509CertificateHolder cert = new X509CertificateHolder(certBytes);\n        SubjectPublicKeyInfo key = cert.getSubjectPublicKeyInfo();\n        byte[] ret = key.getEncoded();\n        return ret;\n    }\n\n    static RSAPublicKey readSigningKey(TestKeyArgs args)\n            throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {\n        Path signCertPath = Util.prefixedPath(args.identifier.value(), InitTool.SIGN_CERT_TMPL);\n        Path fullpath = args.outputPath.value().resolve(signCertPath);\n        byte[] raw = readKey(fullpath);\n        RSAPublicKey key = (RSAPublicKey) KeyFactory.getInstance(\"RSA\")\n                .generatePublic(new X509EncodedKeySpec(raw));\n        return key;\n    }\n\n    static void recoverTest(I18nConsole console, Logger log, Set<IndexedBlob> quorum,\n            ElGamalCiphertext c, ThresholdParameters tparams, Plaintext message)\n            throws ProtocolException, IOException {\n        RecoverDecryption dec = new RecoverDecryption(quorum, tparams);\n        ElGamalDecryptionProof decProof = dec.decryptMessage(c.getBytes());\n        boolean res = false;\n        try {\n            GroupElement decrypted = decProof.getDecrypted();\n            Group group = decrypted.getGroup();\n            Plaintext decoded = group.decode(decrypted);\n            res = group.unpad(decoded).equals(message);\n        } catch (IllegalArgumentException e) {\n            e.printStackTrace();\n        }\n        if (res) {\n            console.println(Msg.m_quorum_test_ok, quorum);\n            log.debug(\"Key usage test succeeded with quorum: {}\", quorum);\n        } else {\n            log.debug(\"Key usage test failed with quorum: {}\", quorum);\n            throw new MessageException(Msg.e_quorum_test_fail, quorum);\n        }\n    }\n\n    static void shoupTest(I18nConsole console, Logger log, Set<IndexedBlob> quorum,\n            RSAPublicKey rsaPub, ThresholdParameters tparams, Rnd rnd, Plaintext message)\n            throws ProtocolException {\n        ShoupSigning shoupSign = new ShoupSigning(quorum, tparams, rnd);\n        byte[] signature = shoupSign.sign(message.getMessage());\n        boolean res = SignatureUtil.RSA.RSA_PSS.verifySignature(message.getMessage(), rsaPub, signature);\n        if (res) {\n            console.println(Msg.m_quorum_test_ok, quorum);\n            log.debug(\"Signature creation test succeeded with quorum: {}\", quorum);\n        } else {\n            log.debug(\"Signature creation test failed with quorum: {}\", quorum);\n            throw new MessageException(Msg.e_quorum_test_fail, quorum);\n        }\n    }\n\n    static boolean testReaders(I18nConsole console, Logger log, KeyContext ctx, TestKeyArgs args)\n            throws IOException, ProtocolException, CardException {\n        ThresholdParameters tparams = new ThresholdParameters(args.dn.value(), args.dm.value());\n\n        // boolean isFastMode = args.fm.value() && cards.enableFastMode(tparams.getParties());\n\n        // Msg fastModeStatus = isFastMode ? Msg.m_fastmode_enabled : Msg.m_fastmode_disabled;\n        // console.println(fastModeStatus);\n\n        NativeRnd rnd = new NativeRnd();\n        ElGamalPublicKey pub = readDecryptionKey(args);\n        RSAPublicKey rsaPub;\n        try {\n            rsaPub = readSigningKey(args);\n        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {\n            throw new ProtocolException(\"Invalid RSA encoding\", e);\n        }\n        allTests(console, log, ctx.card, tparams, rnd, pub, rsaPub);\n        return true;\n    }\n\n    private final I18nConsole console;\n\n    private final KeyContext ctx;\n\n\n    public TestKeyTool(KeyContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    @Override\n    public boolean run(TestKeyArgs args) throws Exception {\n        testReaders(console, log, ctx, args);\n        return true;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/tool/UtilTool.java",
    "content": "package ee.ivxv.key.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.service.smartcard.CardInfo;\nimport ee.ivxv.common.service.smartcard.Cards;\nimport ee.ivxv.common.service.smartcard.SmartCardException;\nimport ee.ivxv.common.service.smartcard.pkcs15.PKCS15Card;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.key.KeyContext;\nimport ee.ivxv.key.Msg;\nimport ee.ivxv.key.protocol.ThresholdParameters;\nimport ee.ivxv.key.tool.UtilTool.UtilArgs;\nimport java.util.List;\nimport javax.smartcardio.CardException;\nimport javax.smartcardio.CardTerminal;\nimport javax.smartcardio.TerminalFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * UtilTool is a tool for running various helper methods within a key application.\n * <p>\n * Currently the tool supports listing smart card readers, the cards inserted to the readers and\n * reading the card information files from the cards.\n * <p>\n * Also, it is possible to test the functionality of the private key shares stored on the card\n * tokens.\n */\npublic class UtilTool implements Tool.Runner<UtilArgs> {\n    public static class UtilArgs extends Args {\n        Arg<Boolean> listReaders = Arg.aFlag(Msg.u_listreaders);\n\n        public UtilArgs() {\n            super();\n            args.add(listReaders);\n        }\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(UtilTool.class);\n\n    static Cards listCards(KeyContext ctx, ThresholdParameters tparams) {\n        Cards cards = ctx.card.createCards();\n        for (int i = 0; i < tparams.getParties(); i++) {\n            cards.addCard(String.valueOf(i));\n        }\n        return cards;\n    }\n\n    private final I18nConsole console;\n\n    private final KeyContext ctx;\n\n    public UtilTool(KeyContext ctx) {\n        this.ctx = ctx;\n        this.console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    private void listReaders() throws CardException {\n        TerminalFactory factory = TerminalFactory.getDefault();\n        try {\n            List<CardTerminal> terminals = factory.terminals().list();\n            if (terminals.size() == 0) {\n                console.println(Msg.e_no_cardterminals_found);\n                return;\n            }\n            String idStr = console.i18n.get(Msg.m_id);\n            String nameStr = console.i18n.get(Msg.m_name);\n            String withCardStr = console.i18n.get(Msg.m_with_card);\n            String cardIdStr = console.i18n.get(Msg.m_card_id);\n            String yesStr = console.i18n.get(Msg.m_yes);\n            String noStr = console.i18n.get(Msg.m_no);\n            int maxLen = nameStr.length();\n            for (CardTerminal ct : terminals) {\n                maxLen = Math.max(maxLen, ct.getName().length());\n            }\n            console.console.println(\n                    \"%-\" + idStr.length() + \"s | %-\" + maxLen + \"s | %-\" + withCardStr.length()\n                            + \"s | %-\" + cardIdStr.length() + \"s\",\n                    idStr, nameStr, withCardStr, cardIdStr);\n            for (int i = 0; i < terminals.size(); i++) {\n                CardTerminal ct = terminals.get(i);\n                String cardId = \"-\";\n                if (ct.isCardPresent()) {\n                    PKCS15Card card = new PKCS15Card(\"\", console);\n                    card.setTerminal(i);\n                    try {\n                        card.initialize();\n                        CardInfo info = card.getCardInfo();\n                        cardId = info == null ? \"-\" : info.getId();\n                    } catch (SmartCardException e) {\n                        log.debug(\"Couldn't get cardInfo\", e);\n                        cardId = \"error\";\n                    }\n                }\n                console.console.println(\n                        \"%-\" + idStr.length() + \"d | %-\" + maxLen + \"s | %-\" + withCardStr.length()\n                                + \"s | %s\",\n                        i, ct.getName(), ct.isCardPresent() ? yesStr : noStr, cardId);\n            }\n        } catch (CardException e) {\n            if (e.getCause().getMessage().equals(\"SCARD_E_NO_READERS_AVAILABLE\")) {\n                console.println(Msg.e_no_cardterminals_found);\n            } else {\n                throw e;\n            }\n        }\n    }\n\n    @Override\n    public boolean run(UtilArgs args) throws Exception {\n        if (args.listReaders.value()) {\n            listReaders();\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/util/ElectionResult.java",
    "content": "package ee.ivxv.key.util;\n\nimport ee.ivxv.common.crypto.Plaintext;\nimport ee.ivxv.common.math.Group;\nimport ee.ivxv.common.math.GroupElement;\nimport ee.ivxv.common.model.CandidateList;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.Proof;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.key.model.Invalid;\nimport ee.ivxv.key.model.PlainBallotBox;\nimport ee.ivxv.key.model.Tally;\nimport ee.ivxv.key.model.Vote;\nimport ee.ivxv.key.protocol.SigningProtocol;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.LinkedBlockingQueue;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Helper class for processing election result.\n */\npublic class ElectionResult {\n    static final Logger log = LoggerFactory.getLogger(ElectionResult.class);\n\n    private static final String INVALID_VOTE_PATH_TMPL = \"invalid\";\n    private static final String PROOF_PATH_TMPL = \"proof\";\n    private static final String PROOF_INVALID_PATH_TMPL = \"proof-invalid\";\n    private static final String TALLY_SUFFIX = \".tally\";\n    private static final String PLAIN_SUFFIX = \".plain\";\n    private static final String TALLY_SIG_SUFFIX = TALLY_SUFFIX + \".signature\";\n    private static final String PLAIN_SIG_SUFFIX = PLAIN_SUFFIX + \".signature\";\n\n    private final String electionName;\n    private final CandidateList candidates;\n    private final DistrictList districts;\n    private final boolean withProof;\n    private final boolean proveInvalid;\n    private final BlockingQueue<Object> votes = new LinkedBlockingQueue<>();\n    private final Map<String, Tally> tallySet;\n    private final Map<String, PlainBallotBox> plainSet;\n    private final Proof proof;\n    private final Proof proofInvalid;\n    private final Invalid invalid;\n\n    /**\n     * Initialize using values.\n     *\n     * @param electionName Election identifier\n     * @param candidates   Candidates list\n     * @param districts    Districts list\n     * @param withProof    Boolean indicating if encrypted ballots should be decrypted with proof of\n     *                     correct decryption.\n     * @param proveInvalid Boolean indicating whether proofs of correct decryption should be output\n     *                     for decrypted ballots deemed invalid (e.g., due to incorrect padding).\n     */\n    public ElectionResult(String electionName, CandidateList candidates, DistrictList districts,\n                          boolean withProof, boolean proveInvalid) {\n        this.electionName = electionName;\n        this.candidates = candidates;\n        this.districts = districts;\n        this.withProof = withProof;\n        this.proveInvalid = withProof && proveInvalid;\n        this.tallySet = new HashMap<>();\n        this.plainSet = new HashMap<>();\n        this.proof = withProof ? new Proof(electionName) : null;\n        this.proofInvalid = this.proveInvalid ? new Proof(electionName) : null;\n        this.invalid = new Invalid(electionName);\n    }\n\n    /**\n     * Get the worker for computing the tally.\n     * <p>\n     * The worker works in parallel to decryptions and continuously computes the tally. It separates\n     * votes which are invalid or are given for invalid candidates\n     *\n     * @param voteCount\n     * @param console\n     * @param reporter\n     * @return\n     */\n    public ResultWorker getResultWorker(int voteCount, I18nConsole console, Reporter reporter) {\n        return new ResultWorker(voteCount, console, reporter);\n    }\n\n    /**\n     * Set the end of incoming votes.\n     * <p>\n     * After the last vote has been added, set the end marker indicating the worker thread to stop\n     * waiting for incoming votes.\n     */\n    public void setEot() {\n        votes.add(Util.EOT);\n    }\n\n    /**\n     * Add a vote for worker to process.\n     *\n     * @param vote\n     */\n    public void addVote(Vote vote) {\n        votes.add(vote);\n    }\n\n    /**\n     * Output tally and the corresponding signature.\n     *\n     * @param outDir Output directory to store the tally and signature.\n     * @param signer Protocol for constructing the signature for the tally.\n     * @throws Exception When writing or communication with card tokens fail.\n     */\n    public void outputTally(Path outDir, SigningProtocol signer) throws Exception {\n        for (Map.Entry<String, Tally> tally : tallySet.entrySet()) {\n            ByteArrayOutputStream in = new ByteArrayOutputStream();\n            Json.write(tally.getValue(), in);\n            byte[] signature = signer.sign(in.toByteArray());\n            Files.write(outDir.resolve(Paths.get(tally.getKey() + TALLY_SUFFIX)), in.toByteArray());\n            Files.write(outDir.resolve(Paths.get(tally.getKey() + TALLY_SIG_SUFFIX)), signature);\n        }\n    }\n\n    /**\n     * Output the decrypted ballot box and the corresponding signature.\n     *\n     * @param outDir Output directory to store the decrypted BB and signature.\n     * @param signer Protocol for constructing the signature for the decrypted BB.\n     * @throws Exception When writing or communication with card tokens fail.\n     */\n    public void outputPlainBB(Path outDir, SigningProtocol signer) throws Exception {\n        for (Map.Entry<String, PlainBallotBox> plain: plainSet.entrySet()) {\n            ByteArrayOutputStream in = new ByteArrayOutputStream();\n            Json.write(plain.getValue(), in);\n            byte[] signature = signer.sign(in.toByteArray());\n            Files.write(outDir.resolve(Paths.get(plain.getKey() + PLAIN_SUFFIX)), in.toByteArray());\n            Files.write(outDir.resolve(Paths.get(plain.getKey() + PLAIN_SIG_SUFFIX)), signature);\n        }\n    }\n\n    /**\n     * Output proofs of correct decryptions.\n     *\n     * @param outDir Output directory to store the proofs file.\n     * @throws Exception When writing the proofs file fails.\n     */\n    public void outputProof(Path outDir) throws Exception {\n        if (!withProof) {\n            return;\n        }\n        Json.write(proof, outDir.resolve(Util.prefixedPath(electionName, PROOF_PATH_TMPL)));\n    }\n\n    /**\n     * Output proofs of correct decryption for invalid votes.\n     *\n     * @param outDir Output directory to store the proofs file.\n     * @throws Exception When writing the proofs file fails.\n     */\n    public void outputInvalidProof(Path outDir) throws Exception {\n        if (!proveInvalid) {\n            return;\n        }\n        Json.write(proofInvalid, outDir.resolve(Util.prefixedPath(electionName, PROOF_INVALID_PATH_TMPL)));\n    }\n\n    /**\n     * Output invalid votes.\n     *\n     * @param outDir Output directory to store the invalid votes.\n     * @throws Exception When writing the file fails.\n     */\n    public void outputInvalid(Path outDir) throws Exception {\n        Json.write(invalid,\n                outDir.resolve(Util.prefixedPath(electionName, INVALID_VOTE_PATH_TMPL)));\n    }\n\n    private class ResultWorker implements Callable<Void> {\n        private final int voteCount;\n        private I18nConsole console;\n\n        public ResultWorker(int voteCount, I18nConsole console, Reporter reporter) {\n            this.voteCount = voteCount;\n            this.console = console;\n        }\n\n        @Override\n        public Void call() throws Exception {\n            Progress progress = console.startProgress(voteCount);\n            Object obj;\n            while ((obj = votes.take()) != Util.EOT) {\n                progress.increase(1);\n                Vote vote;\n                if (obj instanceof Vote) {\n                    vote = (Vote) obj;\n                } else {\n                    throw new IllegalArgumentException(\n                            \"Unexpected decryption result type: \" + obj.getClass());\n                }\n                String choice = Tally.INVALID_VOTE_ID;\n                if (vote.getProof() != null) {\n                    // the message has been decrypted\n                    if (isValidChoice(vote)) {\n                        // the message contains a valid choice string.\n                        choice = getCandidateNumber(vote);\n                    } else {\n                        // the message was correctly decrypted, but this does not represent a valid\n                        // choice string\n                        log.warn(\"Choice is not correctly encoded: invalid vote\");\n                    }\n                } else {\n                    // the message has not been decrypted. This can be\n                    // caused by incorrect group elements etc. It was not decrypted\n                    // to prevent any leaks about the key.\n                    log.warn(\"Ciphertext not correctly encoded: invalid vote\");\n                }\n                if (withProof && !choice.equals(Tally.INVALID_VOTE_ID)) {\n                    // output proof of correct decryption only if it is requested and the choice\n                    // string is valid. As the decryption proof also contains the whole encrypted\n                    // message, then this may leak identifiable information\n                    proof.addProof(vote.getProof());\n                }\n                if (choice.equals(Tally.INVALID_VOTE_ID)) {\n                    invalid.getInvalid().add(vote);\n                    if (proveInvalid) {\n                        // Only if so configured, output the proof of correct decryption also for\n                        // votes with invalid choice strings. This includes votes with invalid padding,\n                        // since then the choice string is meaningless (it cannot be reliably extracted).\n                        proofInvalid.addProof(vote.getProof());\n                    }\n                }\n                addVoteToTally(vote, choice);\n            }\n            progress.finish();\n            return null;\n        }\n\n        private String getCandidateNumber(Vote vote) {\n            String candidateCode = Tally.INVALID_VOTE_ID;\n            try {\n                GroupElement decrypted = vote.getProof().getDecrypted();\n                Group group = decrypted.getGroup();\n                Plaintext decoded = group.decode(decrypted);\n                candidateCode = group.unpad(decoded).getUTF8DecodedMessage();\n            } catch (IllegalArgumentException ignored) {\n            }\n            return candidateCode;\n        }\n\n        private boolean isValidChoice(Vote vote) {\n            String candidateCode;\n            try {\n                GroupElement decrypted = vote.getProof().getDecrypted();\n                Group group = decrypted.getGroup();\n                Plaintext decoded = group.decode(decrypted);\n                candidateCode = group.unpad(decoded).getUTF8DecodedMessage();\n            } catch (IllegalArgumentException ignored) {\n                return false;\n            }\n\n            // Get choices list\n            Map<String, Map<String, Map<String, String>>> ds = candidates.getCandidates();\n            // Ensure that vote district exists in a choices list\n            if (!ds.containsKey(vote.getDistrict())) {\n                return false;\n            }\n\n            // Get choices for a specific district (district that vote belongs to)\n            Map<String, Map<String, String>> ps = ds.get(vote.getDistrict());\n            // Ensure that there are choices per that district\n            if (ps.isEmpty()) {\n                return false;\n            }\n\n            // Loop over all choices per that district\n            for (Map<String, String> parties : ps.values()) {\n                // Is there a candidate code that matches candidateCode\n                if (parties.containsKey(candidateCode)) {\n                    return true;\n                }\n            }\n\n            // Choice is not valid\n            return false;\n        }\n\n        private void addVoteToTally(Vote vote, String choice) {\n            tallySet.computeIfAbsent(vote.getQuestion(),\n                            q -> new Tally(electionName, candidates, districts)).getByParish()\n                    .get(vote.getDistrict()).get(vote.getStation())\n                    .compute(choice, (c, count) -> count + 1);\n            plainSet.computeIfAbsent(vote.getQuestion(),\n                    q -> new PlainBallotBox(electionName, candidates, districts)).getByParish()\n                    .get(vote.getDistrict()).get(vote.getStation())\n                    .add(choice);\n        }\n\n    }\n}\n"
  },
  {
    "path": "key/src/main/java/ee/ivxv/key/util/QuorumUtil.java",
    "content": "package ee.ivxv.key.util;\n\nimport ee.ivxv.common.service.smartcard.IndexedBlob;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\npublic class QuorumUtil {\n\n    /**\n     * <pre>\n     * {@literal\n     * quorumSize == count():\n     *  result list size is 1 and contains the object this was called on\n     * quorumSize < count():\n     *  creates 3 quorums such that all blobs are at least in one quorum\n     *  but none are in every quorum.\n     *  1st quorum: first n blobs among all cards\n     *  2nd quorum: last n blobs among all cards\n     *  3rd quorum: first n-1 blobs and the last blob among all blobs\n     *  where n is parameter quorumSize.\n     *  }\n     * </pre>\n     *\n     * @param blobs List of blobs\n     * @param quorumSize number of blobs in a single quorum\n     * @throws IllegalArgumentException if {@code quorumSize > count()}\n     */\n    static public List<Set<IndexedBlob>> getQuorumList(List<IndexedBlob> blobs, int quorumSize) {\n        int size = blobs.size();\n        if (quorumSize > size) {\n            throw new IllegalArgumentException(\"Quorum size has to smaller than total blob count\");\n        }\n        List<Set<IndexedBlob>> res = new ArrayList<>();\n        if (quorumSize == size) {\n            res.add(new HashSet<>(blobs));\n        } else {\n            Set<IndexedBlob> q1 = new HashSet<>(blobs.subList(0, quorumSize));\n\n            Set<IndexedBlob> q2 = new HashSet<>(blobs.subList(size - quorumSize, size));\n\n            Set<IndexedBlob> q3 = new HashSet<>(blobs.subList(0, quorumSize - 1));\n            q3.add(blobs.get(size - 1));\n\n            res.add(q1);\n            res.add(q2);\n            res.add(q3);\n        }\n        return res;\n    }\n}\n"
  },
  {
    "path": "key/tools/pkcs15_initialization/README.rst",
    "content": "Smart card initialization tool\n==============================\n\nThe tool `erase_and_install.sh` formats the smart card PKCS15 file system and\ninitializes authentication tokens. It takes a single argument, which denotes the\nnumber of readers for which to perform the tasks. The readers are indexed by the\nsequence of being plugged into the system.\n\nInternally, the tool uses `pkcs15-init` tool for the tasks which may display\nrequests for entering PIN code. The codes are automatically provided to the tool\nand user does not need to input anything.\n\nThe PIN and PUK codes are given in the file `card_options.conf` and also\ndisplayed to the user. Default PIN is 0000.\n"
  },
  {
    "path": "key/tools/pkcs15_initialization/card_options.conf",
    "content": "export PKCS15_PIN=0000\nexport PKCS15_PUK=123456\nexport PKCS15_SO_PIN=9999\nexport PKCS15_SO_PUK=222333\n"
  },
  {
    "path": "key/tools/pkcs15_initialization/erase_and_install.sh",
    "content": "#!/bin/bash\n\nTOTAL=$(($1-1))\n\nsource card_options.conf\n\nfunction erase {\n    # erase-card will explicitly ignore any PIN from commandline/env, must always enter manually\n    echo $PKCS15_SO_PIN | pkcs15-init --erase-card -r $1 ;\n}\n\nfunction create {\n    pkcs15-init --pin \"env:PKCS15_PIN\" --puk \"env:PKCS15_PUK\" --so-pin \"env:PKCS15_SO_PIN\" --so-puk \"env:PKCS15_SO_PUK\" --create-pkcs15 -r $1;\n    pkcs15-init --pin \"env:PKCS15_PIN\" --puk \"env:PKCS15_PUK\" --auth-id 01 --store-pin --label el -r $1;\n    pkcs15-init --finalize -r $1;\n}\n\necho \"PIN will be $PKCS15_PIN.\"\necho \"During initalization, the pkcs15-init script asks for Security Officer PIN.\"\necho \"This is provided automagically, you don't need to insert anything\".\necho \"Please wait until erasing and initalizing $1 smart cards\"\necho \"===============================\"\n\nfor i in `seq 0 $TOTAL`; do\n  echo \"Erasing card $i\"\n    erase $i;\n  echo \"Creating card $i\"\n    create $i;\n  echo \"Done\"\n  echo\ndone\n\necho \"===============================\"\necho \"All done.\"\necho \"PIN is $PKCS15_PIN.\"\n"
  },
  {
    "path": "key/tools/pkcs15_initialization/erase_pkcs15.sh",
    "content": "#!/bin/bash\n\nTOTAL=$(($1-1))\n\nsource card_options.conf\n\nfunction erase {\n    # erase-card will explicitly ignore any PIN from commandline/env, must always enter manually\n    echo $PKCS15_SO_PIN | pkcs15-init --erase-card -r $1 ;\n}\n\necho \"During erasure, the pkcs15-init script asks for Security Officer PIN.\"\necho \"This is provided automagically, you don't need to insert anything\".\necho \"Please wait until erasing and initalizing $1 smart cards\"\necho \"===============================\"\n\nfor i in `seq 0 $TOTAL`; do\n  echo \"Erasing card $i\"\n    erase $i;\n  echo \"Done\"\n  echo\ndone\n\necho \"===============================\"\necho \"All done.\"\n"
  },
  {
    "path": "mid/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "mid/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "mid/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n----------------------------------\n Mobile-ID REST API helper service\n----------------------------------\n\n<Description of MID helper service.>\n"
  },
  {
    "path": "mid/go.mod",
    "content": "module ivxv.ee/mid\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "mid/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "mid/internal/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value when calling RPC.Authenticate\n\tEmpty = \"\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.AuthenticateStatus\n\tAuthenticate = \"RPC.Authenticate\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.VoterChoices\n\tAuthenticateStatus = \"RPC.AuthenticateStatus\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.GetCertificate\n\tVoterChoices = \"RPC.VoterChoices\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Sign\n\tGetCertificate = \"RPC.GetCertificate\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.SignStatus\n\tSign = \"RPC.Sign\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Vote\n\tSignStatus = \"RPC.SignStatus\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tauthTTL int64\n\tvoteTTL int64\n\tclient  client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != exitCodeOK {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:  tlsDialer,\n\t\tauthTTL: c.Conf.Technical.Status.Session.AuthTTL,\n\t\tvoteTTL: c.Conf.Technical.Status.Session.VoteTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err, Description: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\t// Send request to session status server and verify response\n\tok, err := r.verifyAndUpdateSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndUpdateSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndUpdateSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will update h.Header.SessionID record in the underlying storage\n// by marking session status Caller as serviceMethod.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndUpdateSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\t// NB! Most important part, that prevents any attack on SessionID\n\tvar ok bool\n\tvar ttl string\n\tswitch serviceMethod {\n\tcase Authenticate:\n\t\tok, err = verifyStatusReadResp(&respRead, authenticateHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase AuthenticateStatus:\n\t\tok, err = verifyStatusReadResp(&respRead, authenticateStatusHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase GetCertificate:\n\t\tok, err = verifyStatusReadResp(&respRead, getCertificateHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\t\trespRead.Lease = \"\"\n\tcase Sign:\n\t\tok, err = verifyStatusReadResp(&respRead, signHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\tcase SignStatus:\n\t\tok, err = verifyStatusReadResp(&respRead, signStatusHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\t}\n\tif !ok || err != nil {\n\t\treturn ok, VerifyStatusReadRespError{Err: err, Description: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp, h func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// authenticateHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Authenticate request.\nfunc authenticateHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Authenticate is the very first client request to IVXV,\n\t// so IVXV requires no previous interactions\n\tfirstTime := r.Caller == Empty && r.Auth == client.NoAuth\n\n\tif !(firstTime) {\n\t\treturn false, AuthenticateInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Authenticate,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE,\n\t\t}\n\t}\n\n\tr.Auth = client.MobileIDAuth\n\treturn true, nil\n}\n\n// authenticateStatusHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.AuthenticateStatus request.\nfunc authenticateStatusHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.AuthenticateStatus is the second client request to IVXV,\n\t// so IVXV requires RPC.Authenticate previously interacted\n\tsecondTime := r.Caller == Authenticate && r.Auth == client.MobileIDAuth\n\n\t// If RPC.AuthenticateStatus is still processing Mobile-ID request\n\t// then voting app will still send RPC.AuthenticateStatus request\n\t// to finish Mobile-ID authentication. So client can send\n\t// RPC.AuthenticateStatus queries as many as wants\n\tnTime := r.Caller == AuthenticateStatus && r.Auth == client.MobileIDAuth\n\n\tif !(secondTime) && !(nTime) {\n\t\treturn false, AuthenticateStatusInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      AuthenticateStatus,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE_STATUS,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// getCertificateHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.GetCertificate request.\nfunc getCertificateHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.GetCertificate is the fourth client request to IVXV,\n\t// third interaction should be done with RPC.VoterChoices\n\tfourthTime := r.Caller == VoterChoices && r.Auth == client.MobileIDAuth\n\n\tif !(fourthTime) {\n\t\treturn false, GetCertificateInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      GetCertificate,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// signHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Sign request.\nfunc signHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Sign is the fifth client request to IVXV,\n\t// so IVXV requires RPC.GetCertificate previously interacted\n\tfifthTime := r.Caller == GetCertificate && r.Auth == client.MobileIDAuth\n\n\tif !(fifthTime) {\n\t\treturn false, SignInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Sign,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// signStatusHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.SignStatus request.\nfunc signStatusHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.SignStatus is the sixth client request to IVXV,\n\t// so IVXV requires RPC.Sign previously interacted\n\tsixthTime := r.Caller == Sign && r.Auth == client.MobileIDAuth\n\n\t// If RPC.SignStatus is still processing Mobile-ID request\n\t// then voting app will still send RPC.SignStatus request\n\t// to check whether document is signed with Mobile-ID. So\n\t// client can send RPC.SignStatus queries as many as wants\n\tnTime := r.Caller == SignStatus && r.Auth == client.MobileIDAuth\n\n\tif !(sixthTime) && !(nTime) {\n\t\treturn false, SignStatusInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      SignStatus,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN_STATUS,\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "mid/internal/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ                    = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER           = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT            = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_TLS_DIAL                                 = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                              = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_UPDATE_FAIL                              = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE        = \"SessionID has been attempted to tamper at RPC.Authenticate\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE_STATUS = \"SessionID has been attempted to tamper at RPC.AuthenticateStatus\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE     = \"SessionID has been attempted to tamper at RPC.GetCertificate\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN                = \"SessionID has been attempted to tamper at RPC.Sign\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN_STATUS         = \"SessionID has been attempted to tamper at RPC.SignStatus\"\n)\n"
  },
  {
    "path": "mid/service/mid/log_desc.go",
    "content": "package main\n\nconst (\n\t_MID_EXPIRED_AUTH_SESSION = \"Request timeout expired for this voter between RPC.Authenticate and RPC.AuthenticateStatus\"\n\t_MID_AUTHREQ              = \"RPC.AuthenticateReq\"\n\t_MID_EXPIRED              = \"Current time >= 'electionstoptime' from election configuration file\"\n\t_MID_SESSION_ID           = \"Malformed SessionID\"\n\t_MID_SESSION_ID_EXPIRED   = \"SessionID has been expired\"\n\t_MID_AUTH_RESP            = \"Mobile-ID provider responded with error\"\n\t_MID_NO_RESP              = \"Cannot reach Mobile-ID provider\"\n\t_MID_DATA_TOKEN_CREATE    = \"Failed to create data token cookie (AES cipher)\"\n\t_MID_SAME_AUTH_SESSION    = \"Mobile-ID provider has been issued duplicate authentication session code\"\n\t_MID_AUTHRESP             = \"RPC.AuthenticateResp\"\n\t_MID_AUTHSTATUSREQ        = \"RPC.AuthenticateStatusReq\"\n\t_MID_UNKNOWN_AUTH_SESSION = \"Voter send non-existing Mobile-ID authentication session code\"\n\t_MID_SIG_RESP             = \"Mobile-ID provider returned back voter's signature\"\n\t_MID_NO_CERT_RESP         = \"Mobile-ID provider didn't return back voter's certificate\"\n\t_MID_CERT_RESP            = \"Mobile-ID provider returned back voter's certificate\"\n\t_MID_VERIFY_SIG           = \"Failed to verify voter's signature\"\n\t_MID_VOTER_ID             = \"Cannot extract voter's personal code from certificate's 'Subject' field\"\n\t_MID_AUTH_TOKEN_CREATE    = \"Failed to create IVXV authentication token cookie (AES cipher)\"\n\t_MID_AUTHSTATUSRESP       = \"RPC.AuthenticateStatusResp\"\n\t_MID_GETCERTREQ           = \"RPC.GetCertificateReq\"\n\t_MID_VOTER_NO_AUTH        = \"Voter has not been authenticated\"\n\t_MID_VOTER_NO_PHONENR     = \"Authenticated voter should have a phone nr. parsed from IVXV authentication token cookie\"\n\t_MID_CERT                 = \"Mobile-ID signing certificate of a voter\"\n\t_MID_GETCERTRESP          = \"RPC.GetCertificateResp\"\n\t_MID_SIGNREQ              = \"RPC.SignReq\"\n\t_MID_SIGNRESP             = \"RPC.SignResp\"\n\t_MID_SIGNSTATUSREQ        = \"RPC.SignStatusReq\"\n\t_MID_SIGNSTATUSRESP       = \"RPC.SignStatusResp\"\n\t_MID_START                = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_MID_AUTH_STOP            = \"Failed to transform authentication stop time from election configuration file to RFC3339 format\"\n\t_MID_STOP                 = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_MID_CLIENT_CONF          = \"Failed to configure Mobile-ID API client\"\n\t_MID_TICKET_AUTH          = \"Mobile-ID service should be configured for ticket based authentication\"\n\t_MID_TICKET               = \"Failed to create a Mobile-ID ticket (AES cipher) that is used for cookie signing\"\n\t_MID_AUTH                 = \"Failed to parse authentication configuration for mid service\"\n\t_MID_SERVER               = \"Failed to parse server configuration for mid service\"\n\t_MID_SERVER_SERVE         = \"Failed to serve mid service to clients\"\n)\n"
  },
  {
    "path": "mid/service/mid/main.go",
    "content": "/*\nThe mid service performs Mobile-ID authentication and intermediates requests\nfor Mobile-ID signing using the Mobile-ID REST API.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/auth/ticket\"\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/mid\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tinternal \"ivxv.ee/mid/internal/sessionstatus/rpc\"\n\t//ivxv:modules common/collector/container\n)\n\nconst (\n\t// StatusPoll is returned as Status from AuthenticateStatus and\n\t// SignStatus if a Mobile-ID session has not yet finished and the\n\t// client needs to poll again.\n\tStatusPoll = \"POLL\"\n\n\t// StatusOK is returned as Status from AuthenticateStatus and\n\t// SignStatus if a Mobile-ID session has finished successfully.\n\tStatusOK = \"OK\"\n)\n\n// midToServerError maps an ivxv.ee/common/collector/mid package error to an ivxv.ee/common/collector/server\n// error to return to the client. It returns nil if err is not a recognized mid\n// error, i.e., it was caused by an internal server error.\nfunc midToServerError(err error) error {\n\tswitch {\n\tcase errors.CausedBy(err, new(mid.InputError)) != nil:\n\t\treturn server.ErrBadRequest\n\tcase errors.CausedBy(err, new(mid.NotMIDUserError)) != nil:\n\t\treturn server.ErrMIDNotUser\n\tcase errors.CausedBy(err, new(mid.SIMError)) != nil:\n\t\treturn server.ErrMIDOperator\n\tcase errors.CausedBy(err, new(mid.AbsentError)) != nil:\n\t\treturn server.ErrMIDAbsent\n\tcase errors.CausedBy(err, new(mid.CanceledError)) != nil:\n\t\treturn server.ErrMIDCanceled\n\tcase errors.CausedBy(err, new(mid.ExpiredError)) != nil:\n\t\treturn server.ErrMIDExpired\n\tcase errors.CausedBy(err, new(mid.CertificateError)) != nil:\n\t\treturn server.ErrMIDCertificate\n\tcase errors.CausedBy(err, new(mid.StatusError)) != nil:\n\t\treturn server.ErrMIDGeneral\n\t}\n\treturn nil\n}\n\n// session is an outstanding Mobile-ID authentication session.\ntype session struct {\n\tcreated      time.Time\n\tchallengeRnd []byte\n}\n\n// RPC is the handler for Mobile-ID service calls.\ntype RPC struct {\n\tstatus   client.Verifier\n\tauthEnd  time.Time\n\tmid      *mid.Client\n\tticket   *ticket.T\n\tidentify identity.Identifier\n\n\t// sessions maps session codes to outstanding authentication sessions.\n\t// Not used for signing sessions because we have no state for those.\n\tsessions    map[string]session\n\tsessionLock sync.Mutex\n}\n\n// startCleaner starts a separate goroutine which removes sessions older than 5\n// minutes from r.sessions every minute.\nfunc (r *RPC) startCleaner(ctx context.Context) {\n\tgo func() {\n\t\tfor range time.Tick(1 * time.Minute) {\n\t\t\tr.sessionLock.Lock()\n\t\t\tearliest := time.Now().Add(-5 * time.Minute)\n\t\t\tfor code, sess := range r.sessions {\n\t\t\t\tif sess.created.Before(earliest) {\n\t\t\t\t\t// Session removed due to expiry\n\t\t\t\t\tlog.Debug(ctx, SessionTimeout{SessionCode: code,\n\t\t\t\t\t\tDescription: _MID_EXPIRED_AUTH_SESSION})\n\t\t\t\t\tdelete(r.sessions, code)\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.sessionLock.Unlock()\n\t\t}\n\t}()\n}\n\n// AuthArgs are the arguments provided to a call of RPC.Authenticate.\ntype AuthArgs struct {\n\tserver.Header\n\tIDCode  string `size:\"11\"`\n\tPhoneNo string `size:\"20\"`\n}\n\n// AuthResponse is the response returned by RPC.Authenticate.\ntype AuthResponse struct {\n\tserver.Header\n\tSessionCode string\n\tChallenge   []byte\n\tDataToken   []byte\n}\n\n// Authenticate is the remote procedure call performed by clients to start a\n// Mobile-ID authentication session.\nfunc (r *RPC) Authenticate(args AuthArgs, resp *AuthResponse) (err error) {\n\tlog.Log(args.Ctx, AuthenticateReq{PhoneNo: args.PhoneNo, Description: _MID_AUTHREQ})\n\n\t// The server filter for voting end will only enable once we stop\n\t// serving signing requests, so we must manually check if we should\n\t// still serve authentication requests and refuse those requests when\n\t// not served anymore\n\tif !time.Now().Before(r.authEnd) { // not before == equal or after\n\t\tlog.Log(args.Ctx, AuthenticateVotingEnded{Description: _MID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Authenticate).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, AuthenticateVerifySessionIDError{Err: err, Description: _MID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, AuthenticateUpdateSessionIDError{Description: _MID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tsess := session{created: time.Now()}\n\tresp.SessionCode, sess.challengeRnd, resp.Challenge, err =\n\t\tr.mid.MobileAuthenticate(args.Ctx, args.IDCode, args.PhoneNo)\n\tif err != nil {\n\t\tif clierr := midToServerError(err); clierr != nil {\n\t\t\t// Log known mID service error about failed authentication\n\t\t\tlog.Error(args.Ctx, AuthenticateMIDError{Err: err, Description: _MID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown mID service error about failed authentication\n\t\tlog.Error(args.Ctx, AuthenticateError{Err: log.Alert(err), Description: _MID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\tif resp.DataToken, err = r.ticket.CreateData([]byte(args.PhoneNo)); err != nil {\n\t\t// Error creating data token\n\t\tlog.Error(args.Ctx, DataTicketError{Err: err, Description: _MID_DATA_TOKEN_CREATE})\n\t\treturn server.ErrInternal\n\t}\n\tr.sessionLock.Lock()\n\tdefer r.sessionLock.Unlock()\n\tif _, ok := r.sessions[resp.SessionCode]; ok {\n\t\t// Mobile-ID service has issued a session code that has already been issued.\n\t\t// This is bug of Mobile-ID service if this ever happens\n\t\tlog.Error(args.Ctx, DuplicateSessionCodeError{Code: resp.SessionCode,\n\t\t\tDescription: _MID_SAME_AUTH_SESSION})\n\t\treturn server.ErrInternal\n\t}\n\tr.sessions[resp.SessionCode] = sess\n\n\t// Authentication process successfully initiated\n\tlog.Log(args.Ctx, AuthenticateResp{\n\t\tSessionCode: resp.SessionCode,\n\t\tChallenge:   resp.Challenge,\n\t\tDescription: _MID_AUTHRESP,\n\t})\n\treturn nil\n}\n\n// AuthStatusArgs are the arguments provided to a call of RPC.AuthenticateStatus.\ntype AuthStatusArgs struct {\n\tserver.Header\n\tSessionCode string `size:\"36\"`\n}\n\n// AuthStatusResponse is the response returned by RPC.AuthenticateStatus.\ntype AuthStatusResponse struct {\n\tserver.Header\n\tStatus       string\n\tGivenName    string\n\tSurname      string\n\tPersonalCode string\n\tAuthToken    []byte\n}\n\n// AuthenticateStatus is the remote procedure call performed by clients to\n// check the status of a Mobile-ID authentication session.\nfunc (r *RPC) AuthenticateStatus(args AuthStatusArgs, resp *AuthStatusResponse) error {\n\tlog.Log(args.Ctx, AuthenticateStatusReq{SessionCode: args.SessionCode, Description: _MID_AUTHSTATUSREQ})\n\n\t// The server filter for voting end will only enable once we stop\n\t// serving signing requests, so we must manually check if we should\n\t// still serve authentication requests and refuse those requests when\n\t// not served anymore\n\tif !time.Now().Before(r.authEnd) { // not before == equal or after\n\t\tlog.Log(args.Ctx, AuthenticateStatusVotingEnded{Description: _MID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.AuthenticateStatus).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, AuthenticateStatusVerifySessionIDError{Err: err, Description: _MID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, AuthenticateStatusUpdateSessionIDError{Description: _MID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tr.sessionLock.Lock()\n\tsess, ok := r.sessions[args.SessionCode]\n\tr.sessionLock.Unlock()\n\tif !ok {\n\t\t// Client has presented \"RPC.AuthenticateStatus.SessionCode\" argument that\n\t\t// hasn't been authenticated in RPC.Authenticate\n\t\tlog.Error(args.Ctx, UnknownSessionCodeError{Description: _MID_UNKNOWN_AUTH_SESSION})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tcert, algorithm, signature, err := r.mid.GetMobileAuthenticateStatus(args.Ctx, args.SessionCode)\n\tif err != nil {\n\t\tr.sessionLock.Lock()\n\t\tdelete(r.sessions, args.SessionCode)\n\t\tr.sessionLock.Unlock()\n\n\t\tif clierr := midToServerError(err); clierr != nil {\n\t\t\t// Log known mID service error about failed authentication\n\t\t\tlog.Error(args.Ctx, AuthenticateStatusMIDError{Err: err, Description: _MID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown mID service error about failed authentication\n\t\tlog.Error(args.Ctx, AuthenticateStatusError{Err: log.Alert(err), Description: _MID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\tresp.Status = StatusPoll\n\tif len(signature) > 0 {\n\t\t// AuthenticationStatus has provided a signed result\n\t\tlog.Log(args.Ctx, AuthenticationSignature{Signature: signature, Description: _MID_SIG_RESP})\n\t\tif cert == nil {\n\t\t\t// Signature missing from the authentication reply with signature\n\t\t\tlog.Error(args.Ctx, AuthenticationCertificateMissingError{Err: err,\n\t\t\t\tDescription: _MID_NO_CERT_RESP})\n\t\t\treturn server.ErrMIDGeneral\n\t\t}\n\t\t// Certificate received from the signed response\n\t\tlog.Log(args.Ctx, AuthenticationCertificate{Certificate: cert, Description: _MID_CERT_RESP})\n\n\t\tr.sessionLock.Lock()\n\t\tdelete(r.sessions, args.SessionCode)\n\t\tr.sessionLock.Unlock()\n\n\t\t// Verify signature of authentication reply, refuse to proceed, if fails\n\t\tif err = mid.VerifyAuthenticationSignature(\n\t\t\tcert, algorithm, sess.challengeRnd, signature); err != nil {\n\t\t\tlog.Error(args.Ctx, AuthenticationSignatureError{Err: err, Description: _MID_VERIFY_SIG})\n\t\t\treturn server.ErrMIDGeneral\n\t\t}\n\n\t\tresp.Status = StatusOK\n\t\tresp.GivenName = findName(&cert.Subject, asn1.ObjectIdentifier{2, 5, 4, 42})\n\t\tresp.Surname = findName(&cert.Subject, asn1.ObjectIdentifier{2, 5, 4, 4})\n\t\tif resp.PersonalCode, err = r.identify(&cert.Subject); err != nil {\n\t\t\t// Backend cannot extract voter's personal code from certificate's Subject field\n\t\t\tlog.Error(args.Ctx, AuthenticationSubjectIdentityError{Err: err,\n\t\t\t\tDescription: _MID_VOTER_ID})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\tif resp.AuthToken, err = r.ticket.Create(cert.Subject); err != nil {\n\t\t\t// Backend internal error, cannot create AES shared secret that is used as\n\t\t\t// authenitcation cookie in ticket-based methods\n\t\t\tlog.Error(args.Ctx, AuthenticationTicketError{Err: err, Description: _MID_AUTH_TOKEN_CREATE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\t}\n\n\t// Successful authentication, log information, handle AuthToken as sensitive\n\tlog.Log(args.Ctx, AuthenticateStatusResp{\n\t\tStatus:       resp.Status,\n\t\tGivenName:    resp.GivenName,\n\t\tSurname:      resp.Surname,\n\t\tPersonalCode: resp.PersonalCode,\n\t\tAuthToken:    log.Sensitive(resp.AuthToken),\n\t\tDescription:  _MID_AUTHSTATUSRESP,\n\t})\n\treturn nil\n}\n\n// findName searches name for oid and returns the value for that oid or an\n// empty string. Panics if the value for the oid is not a string.\nfunc findName(name *pkix.Name, oid asn1.ObjectIdentifier) string {\n\tfor _, n := range name.Names {\n\t\tif n.Type.Equal(oid) {\n\t\t\treturn n.Value.(string)\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// CertificateArgs are the arguments provided to a call of RPC.GetCertificate.\ntype CertificateArgs struct {\n\tserver.Header\n}\n\n// CertificateResponse is the response returned by RPC.GetCertificate.\ntype CertificateResponse struct {\n\tserver.Header\n\tCertificate []byte\n}\n\n// GetCertificate is the remote procedure call performed by clients to get the\n// Mobile-ID signing certificate that will be used to sign the vote.\nfunc (r *RPC) GetCertificate(args CertificateArgs, resp *CertificateResponse) error {\n\tlog.Log(args.Ctx, GetCertificateReq{Description: _MID_GETCERTREQ})\n\n\t// Get the voter serial number. If empty, then the request is not\n\t// authenticated.\n\tidentity := server.VoterIdentity(args.Ctx)\n\tif len(identity) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedGetCertificateError{Description: _MID_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Get voter phone no from the ticket, not authenticated if does not exist\n\tphoneno := server.VoterNumber(args.Ctx)\n\tif len(phoneno) == 0 {\n\t\tlog.Error(args.Ctx, MissingDataGetCertificateError{Description: _MID_VOTER_NO_PHONENR})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.GetCertificate).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, GetCertificateVerifySessionIDError{Err: err, Description: _MID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, GetCertificateUpdateSessionIDError{Description: _MID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tc, err := r.mid.GetMobileCertificate(args.Ctx, identity, phoneno)\n\tif err != nil {\n\t\tif clierr := midToServerError(err); clierr != nil {\n\t\t\t// Log known mID service error about failed authentication\n\t\t\tlog.Error(args.Ctx, GetCertificateMIDError{Err: err, Description: _MID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown mID service error about failed authentication\n\t\tlog.Error(args.Ctx, GetCertificateError{Err: log.Alert(err), Description: _MID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Log signing certificate.\n\t// TODO: identical to GetCertificateResp, why both?\n\tlog.Log(args.Ctx, SigningCertificate{Certificate: c, Description: _MID_CERT})\n\tresp.Certificate = c.Raw\n\n\t// Log successful reply\n\t// TODO: identical to SigningCertificate, why both?\n\tlog.Log(args.Ctx, GetCertificateResp{\n\t\tCertificate: resp.Certificate,\n\t\tDescription: _MID_GETCERTRESP,\n\t})\n\treturn nil\n}\n\n// SignArgs are the arguments provided to a call of RPC.Sign.\ntype SignArgs struct {\n\tserver.Header\n\tHash     []byte `size:\"64\"` // Hash of the data to sign.\n\tHashType string `size:\"10\"` // Allowed values described in 'ivxv.ee/common/collector/mid' package.\n}\n\n// SignResponse is the response returned by RPC.Sign.\ntype SignResponse struct {\n\tserver.Header\n\tSessionCode string\n}\n\n// Sign is the remote procedure call performed by clients to start a Mobile-ID\n// signing session.\nfunc (r *RPC) Sign(args SignArgs, resp *SignResponse) (err error) {\n\tlog.Log(args.Ctx, SignReq{Hash: args.Hash, Description: _MID_SIGNREQ})\n\n\t// Get the voter serial number. If empty, then the request is not\n\t// authenticated.\n\tidentity := server.VoterIdentity(args.Ctx)\n\tif len(identity) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedSignError{Description: _MID_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Get voter phone no from the ticket, not authenticated if does not exist\n\tphoneno := server.VoterNumber(args.Ctx)\n\tif len(phoneno) == 0 {\n\t\tlog.Error(args.Ctx, MissingDataSignError{Description: _MID_VOTER_NO_PHONENR})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Sign).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, SignVerifySessionIDError{Err: err, Description: _MID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, SignUpdateSessionIDError{Description: _MID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tresp.SessionCode, err = r.mid.MobileSignHash(\n\t\targs.Ctx, identity, phoneno, args.Hash, args.HashType)\n\tif err != nil {\n\t\tif clierr := midToServerError(err); clierr != nil {\n\t\t\t// Log known mID service error about failed authentication\n\t\t\tlog.Error(args.Ctx, SignMIDError{Err: err, Description: _MID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown mID service error about failed authentication\n\t\tlog.Error(args.Ctx, SignError{Err: log.Alert(err), Description: _MID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Successfully initiated signing\n\tlog.Log(args.Ctx, SignResp{\n\t\tSessionCode: resp.SessionCode,\n\t\tDescription: _MID_SIGNRESP,\n\t})\n\treturn\n}\n\n// SignStatusArgs are the arguments provided to a call of RPC.SignStatus.\ntype SignStatusArgs struct {\n\tserver.Header\n\tSessionCode string `size:\"36\"`\n}\n\n// SignStatusResponse is the response returned by RPC.SignStatus.\ntype SignStatusResponse struct {\n\tserver.Header\n\tStatus    string\n\tSignature []byte\n\tAlgorithm string // The signature algorithm. Allowed values described in 'ivxv.ee/common/collector/mid' package.\n}\n\n// SignStatus is the remote procedure call performed by clients to check the\n// status of a Mobile-ID signing session.\nfunc (r *RPC) SignStatus(args SignStatusArgs, resp *SignStatusResponse) (err error) {\n\tlog.Log(args.Ctx, SignStatusReq{SessionCode: args.SessionCode, Description: _MID_SIGNSTATUSREQ})\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.SignStatus).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, SignStatusVerifySessionIDError{Err: err, Description: _MID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, SignStatusUpdateSessionIDError{Description: _MID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tresp.Algorithm, resp.Signature, err = r.mid.GetMobileSignHashStatus(args.Ctx, args.SessionCode)\n\tif err != nil {\n\t\tif clierr := midToServerError(err); clierr != nil {\n\t\t\t// Log known mID service error about failed authentication\n\t\t\tlog.Error(args.Ctx, SignStatusMIDError{Err: err, Description: _MID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown mID service error about failed authentication\n\t\tlog.Error(args.Ctx, SignStatusError{Err: log.Alert(err), Description: _MID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\tresp.Status = StatusPoll\n\tif len(resp.Signature) > 0 {\n\t\tresp.Status = StatusOK\n\t}\n\n\t// Log sucessfully received signature\n\tlog.Log(args.Ctx, SignStatusResp{\n\t\tStatus:      resp.Status,\n\t\tSignature:   resp.Signature,\n\t\tDescription: _MID_SIGNSTATUSRESP,\n\t})\n\treturn\n}\n\nfunc main() {\n\t// Call midmain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(midmain())\n}\n\nfunc midmain() (code int) {\n\tc := command.NewWithoutStorage(\"ivxv-mid\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := internal.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Create new RPC instance and start the session cleaner.\n\trpc := &RPC{sessions: make(map[string]session), status: statusClient}\n\trpc.startCleaner(c.Ctx)\n\n\tvar start, stop time.Time\n\tvar authConf server.AuthConf\n\tvar err error\n\n\tif c.Conf.Election != nil {\n\t\t// Check election configuration time values - service start\n\t\tif start, err = c.Conf.Election.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err, Description: _MID_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - election stop\n\t\tif rpc.authEnd, err = c.Conf.Election.ElectionStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ElectionStopTimeError{Err: err, Description: _MID_AUTH_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - service stop\n\t\tif stop, err = c.Conf.Election.ServiceStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ServiceStopTimeError{Err: err, Description: _MID_STOP},\n\t\t\t\t\"bad service stop time:\", err)\n\t\t}\n\n\t\t// Configure the MID-REST API client.\n\t\tif rpc.mid, err = mid.New(&c.Conf.Election.MID); err != nil {\n\t\t\treturn c.Error(exit.Config, MIDConfError{Err: err, Description: _MID_CLIENT_CONF},\n\t\t\t\t\"failed to configure MID-REST API client:\", err)\n\t\t}\n\n\t\t// Configure the ticket manager for issuing authentication\n\t\t// tickets.\n\t\tticketConf, ok := c.Conf.Election.Auth[auth.Ticket]\n\t\tif !ok {\n\t\t\treturn c.Error(exit.Config, TicketAuthError{Description: _MID_TICKET_AUTH},\n\t\t\t\t\"ticket authentication is mandatory for mid\")\n\t\t}\n\t\tif rpc.ticket, err = ticket.NewFromSystem(); err != nil {\n\t\t\treturn c.Error(exit.Config, TicketConfError{Err: err, Description: _MID_TICKET},\n\t\t\t\t\"failed to configure ticket manager:\", err)\n\t\t}\n\n\t\t// Parse configuration for authenticating with tickets issued\n\t\t// by this server.\n\t\tif authConf, err = server.NewAuthConf(auth.Conf{auth.Ticket: ticketConf},\n\t\t\tc.Conf.Election.Identity, nil); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ServerAuthConfError{Err: err, Description: _MID_AUTH},\n\t\t\t\t\"failed to configure client authentication:\", err)\n\t\t}\n\n\t\t// Store the voter identifier for signer identification.\n\t\trpc.identify = authConf.Identity\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err, Description: _MID_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.WithAuth(authConf).ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err, Description: _MID_SERVER_SERVE},\n\t\t\t\t\"failed to serve mid service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "processor/.gitignore",
    "content": ".classpath\n.gradle/\n.idea/\n.project\nbuild/\n/log/\n.settings/\n/bin/\n"
  },
  {
    "path": "processor/Makefile",
    "content": "include ../common/java/common.mk\n"
  },
  {
    "path": "processor/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------------\n Processor application\n-----------------------\n\nProcessor is a command line application for validating and post-processing ballot box in off-line mode.\n\nProcessor functionality is provided by the tools described below. The tools should be executed in the given order, each tool providing input for the next. The tools:\n\n* *check* - validates the ballot box and ballot signatures and compares it to the provided voters lists, voting districts lists and registration data.\n* *squash* - removes all but the latest ballot per each voter.\n* *revoke* - revokes voter ballots according to the provided revocation lists.\n* *anonymize* - removes voters data from the ballot box and provides anonymized ballot box ready for the *key application*.\n\nBuilding\n--------\n\nIVXV java applications have 2 levels of build systems:\n\n* *make* - the build system facade. Must be installed on the user's machine.\n* *gradle* - the implementation of the build system. Gradle is located under ``common/external/gradle-8.11``, with the executable ``bin/gradle(.bat)``.\n\nBuilding:\n\n* ``make`` or\n* ``make all`` or\n* ``gradle build installDist`` - build and test the application.\n* ``make clean`` or\n* ``gradle clean`` - clean build resources, i.e. the directory ``build``.\n\nApplication executable is ``build/install/processor/bin/processor(.bat)``.\n\nDistributable application packages are provided under ``build/distributions/``.\n\nExecuting\n---------\n\nUsage:\n\n* ``processor -h`` - show application help\n* ``processor <tool> -h`` - show the tool help\n* ``processor <tool> --conf certs.bdoc --params processor.yaml.bdoc`` - execute a tool with the *--conf* and *--params* arguments\n* ``processor check --conf certs.bdoc --params processor.yaml.bdoc``\n* ``processor squash --conf certs.bdoc --params processor.yaml.bdoc``\n* ``PROCESSOR_OPTS=-Xmx16G processor squash --conf certs.bdoc --params processor.yaml.bdoc`` - the same as above, but with increased maximal memory usage to 16GB, which is necessary for complex, parallel computations such like 'processor'.\n\nThe ``certs.bdoc`` container is expected to contain all CA, OCSP and TSA certificates required for validating any signed input (including the ``certs.bdoc`` itself) and the file ``ivxv.properties`` that refers to the certificates. Example of ``ivxv.properties``:\n\n::\n\n  ca = cert1.pem, cert2.pem\n  ocsp = ocsp.pem\n  tsa = tsa.pem\n\nExample of ``processor.yaml``:\n\n.. code-block:: yaml\n\n  check:\n    ballotbox: votes.zip\n    districts: TESTKOV2017.districts.json\n    registrationlist: ocsp.zip\n    signingkey: voterfile.pub.key\n    voterlists:\n      -\n        path: 00.TESTKOV2017.gen.voters\n        signature: 00.TESTKOV2017.gen.voters.signature\n      -\n        path: 03.TESTKOV2017.gen.voters\n        signature: 03.TESTKOV2017.gen.voters.signature\n      -\n        path: 06.TESTKOV2017.gen.voters\n        signature: 06.TESTKOV2017.gen.voters.signature\n      -\n        path: 09.TESTKOV2017.gen.voters\n        signature: 09.TESTKOV2017.gen.voters.signature\n    out: out-1\n\n  squash:\n    ballotbox: out-1/bb-1.json\n    out: out-2\n\n  revoke:\n    ballotbox: out-2/bb-2.json\n    revocationlists:\n      - 12.TESTKOV2017.gen.revoke.json\n      - 13.TESTKOV2017.gen.revoke.json\n      - 14.TESTKOV2017.gen.revoke.json\n      - 15.TESTKOV2017.gen.revoke.json\n    out: out-3\n\n  anonymize:\n    ballotbox: out-3/bb-3.json\n    out: out-4\n"
  },
  {
    "path": "processor/build.gradle",
    "content": "buildscript {\n    ext.base = '../'\n    apply from: \"${base}/common/java/common-buildscript.gradle\", to: buildscript\n}\n\napply from: \"${base}/common/java/common-build.gradle\"\napply plugin: 'application'\n\ndependencies {\n    implementation project(\":common\")\n\n    testImplementation testFixtures(project(\":common\"))\n}\n\napplication {\n\tmainClass = \"ee.ivxv.processor.Processor\"\n\tapplicationDefaultJvmArgs = [\"-Xmx8G\"]\n}\n\n"
  },
  {
    "path": "processor/settings.gradle",
    "content": "include \"common\"\nproject(\":common\").projectDir = file(\"../common/java\")\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/Msg.java",
    "content": "package ee.ivxv.processor;\n\nimport ch.qos.cal10n.BaseName;\nimport ch.qos.cal10n.LocaleData;\nimport ee.ivxv.common.util.NameHolder;\n\n@BaseName(\"i18n.processor-msg\")\n@LocaleData(defaultCharset = \"UTF-8\", value = {})\npublic enum Msg implements NameHolder {\n    /*-\n     * The part of the enum name until the first '_' (including) is excluded from the getName().\n     * This is a means to provide multiple translations for the same tool or argument name.\n     */\n\n    // App\n    app_processor,\n\n    // Tools\n    tool_check, tool_squash, tool_revoke, tool_anonymize, tool_export, tool_stats, tool_statsdiff,\n    tool_checkAndSquash, tool_revokeAndAnonymize,\n    // Tool arguments\n    arg_ballotbox(\"bb\"), arg_ballotbox_checksum(\"bbcs\"), arg_signed_ballot_max_size_bytes, //\n    arg_registrationlist, arg_registrationlist_checksum, //\n    arg_districts, arg_revocationlists, //\n    arg_tskey, arg_vlkey, //\n    arg_voterlists, arg_path, arg_signature, arg_skip_cmd, //\n    arg_districts_mapping, //\n    arg_voter_id, //\n    arg_election_start, //\n    arg_voterforeignehak, //\n    arg_enckey, //\n    arg_election_day, arg_period_start, arg_period_end, //\n    arg_compare, arg_to, arg_diff, //\n    arg_out(\"o\"),\n\n    // Error messages\n    e_tehcnical_error,\n\n    e_vl_voterlists_missing, e_vl_vlkey_missing, //\n    e_vl_first_not_initial, e_vl_initial_not_first, //\n    e_vl_read_error, e_vl_signature_error, e_vl_election_id, //\n    e_vl_invalid_header, e_vl_invalid_voter_row, e_vl_invalid_row_number, //\n    e_vl_invalid_changeset, e_vl_invalid_version, e_vl_invalid_time, //\n    e_vl_invalid_district, e_vl_invalid_parish, //\n    e_vl_voter_already_removed, e_vl_removed_voter_missing, //\n    e_vl_voter_already_added, e_vl_added_voter_exists, //\n    e_vl_fictive_single_district_and_parish_required, //\n    e_vl_error_report,\n\n    e_dist_mapping_invalid_row,\n    e_skip_cmd_loading,\n    e_reg_checksum_missing, //\n    e_bb_read_error, e_bb_ballot_processing, e_reg_record_processing, //\n    e_bb_invalid_file_name, e_bb_invalid_file_size, e_bb_missing_file, e_bb_repeated_file, e_bb_unknown_file_type, //\n    e_ballot_signature_invalid, e_ballot_missing_voter_signature, //\n    e_active_voter_not_found, e_active_voterlist_not_found, //\n    e_time_before_start, //\n    e_reg_resp_invalid, e_reg_req_invalid, //\n    e_reg_resp_not_unique, e_reg_req_not_unique, //\n    e_reg_resp_no_nonce, e_reg_resp_nonce_not_sig, e_reg_resp_nonce_alg_mismatch, //\n    e_reg_resp_nonce_sig_invalid, //\n    e_unknown_file_in_vote_container, //\n    e_reg_resp_req_unmatch, e_reg_req_without_ballot, e_ballot_without_reg_req, //\n    e_same_time_as_latest, e_invalid_signature_profile, //\n    e_bb_error_report,\n    e_invalid_error_report,\n\n    e_rl_read_error, e_rl_election_id, //\n    e_rl_processing_error, //\n    e_rl_voter_not_found_in_bb, e_rl_ballot_already_revoked, e_rl_ballot_already_restored,\n\n    e_bb_ciphertext_checking, //\n    e_ciphertext_invalid, //\n    e_ciphertext_invalid_bytes, //\n    e_ciphertext_invalid_group, //\n    e_ciphertext_invalid_point, //\n    e_ciphertext_invalid_qr, //\n    e_ciphertext_invalid_range, //\n\n    e_writing_ivoter_list, //\n    e_writing_revocation_report, //\n    e_writing_log_n, //\n    e_writing_error_report,\n\n    e_stats_code_not_estonian,\n\n    // Messages\n    m_output_file, //\n    m_read, //\n\n    m_bb_unsigned_skipping_output, //\n    m_reg_skipping_compare, //\n    m_bb_grouping_votes_by_voter, //\n\n    m_vl_reading, //\n    m_vl, //\n    m_vl_type, //\n    m_vl_skipped, //\n    m_vl_total_added, //\n    m_vl_total_removed, //\n    m_vl_fictive_warning, //\n    m_vl_fictive_voter_name,\n\n    m_rl_loading, //\n    m_rl_loaded, //\n    m_rl_arg_for_cont, //\n    m_rl_checking_integrity, //\n    m_rl_data_is_integrous, //\n    m_rl_revoke_total, //\n    m_rl_restore_total, //\n    m_rl_revoke_start, //\n    m_rl_revoke_ballots_before, //\n    m_rl_revoke_count, //\n    m_rl_revoke_ballots_after, //\n    m_rl_revoke_done, //\n    m_rl_restore_start, //\n    m_rl_restore_ballots_before, //\n    m_rl_restore_count, //\n    m_rl_restore_ballots_after, //\n    m_rl_restore_done, //\n\n    m_dist_mapping_loading, //\n    m_dist_mapping_arg_for_cont, //\n    m_dist_mapping_loaded, //\n\n    m_removing_recurrent_votes, //\n    m_removing_invalid_ciphertexts, //\n    m_applying_revocation_lists, //\n    m_anonymizing_ballot_box,\n\n    m_writing_ivoter_list, //\n    m_writing_revocation_report, //\n    m_writing_log_n, //\n\n    m_stats_generating, m_stats_generated, m_stats_ballot_errors, m_stats_valid_ballots, //\n    m_stats_json_saved, m_stats_csv_saved, m_stats_diff_saved, //\n\n    ;\n\n    private final String shortName;\n\n    private Msg() {\n        this(null);\n    }\n\n    private Msg(String shortName) {\n        this.shortName = shortName;\n    }\n\n    @Override\n    public String getShortName() {\n        return shortName;\n    }\n\n    @Override\n    public String getName() {\n        return extractName(name());\n    }\n\n    @Override\n    public Enum<?> getKey() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/Processor.java",
    "content": "package ee.ivxv.processor;\n\nimport ee.ivxv.common.cli.AppRunner;\n\npublic class Processor {\n\n    public static void main(String[] args) {\n        ProcessorApp app = new ProcessorApp();\n        AppRunner<ProcessorContext> runner = new AppRunner<>(app);\n\n        if (!runner.run(args)) {\n            System.exit(1);\n        }\n    }\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/ProcessorApp.java",
    "content": "package ee.ivxv.processor;\n\nimport ee.ivxv.common.cli.App;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.cli.VerifyTool;\nimport ee.ivxv.common.cli.VerifyTool.VerifyArgs;\nimport ee.ivxv.common.conf.Conf;\nimport ee.ivxv.processor.tool.AnonymizeTool;\nimport ee.ivxv.processor.tool.AnonymizeTool.AnonymizeArgs;\nimport ee.ivxv.processor.tool.CheckTool;\nimport ee.ivxv.processor.tool.CheckTool.CheckArgs;\nimport ee.ivxv.processor.tool.ExportTool;\nimport ee.ivxv.processor.tool.ExportTool.ExportArgs;\nimport ee.ivxv.processor.tool.RevokeTool;\nimport ee.ivxv.processor.tool.RevokeTool.RevokeArgs;\nimport ee.ivxv.processor.tool.SquashTool;\nimport ee.ivxv.processor.tool.SquashTool.SquashArgs;\nimport ee.ivxv.processor.tool.StatsDiffTool;\nimport ee.ivxv.processor.tool.StatsDiffTool.StatsDiffArgs;\nimport ee.ivxv.processor.tool.StatsTool;\nimport ee.ivxv.processor.tool.StatsTool.StatsArgs;\nimport ee.ivxv.processor.tool.CheckAndSquashTool;\nimport ee.ivxv.processor.tool.CheckAndSquashTool.CheckAndSquashArgs;\nimport ee.ivxv.processor.tool.RevokeAndAnonymizeTool;\nimport ee.ivxv.processor.tool.RevokeAndAnonymizeTool.RevokeAndAnonymizeArgs;\nimport java.util.Arrays;\nimport java.util.List;\n\nclass ProcessorApp extends App<ProcessorContext> {\n\n    ProcessorApp() {\n        super(Msg.app_processor, createTools());\n    }\n\n    private static List<Tool<ProcessorContext, ?>> createTools() {\n        return Arrays.asList( //\n                new Tool<>(Msg.tool_check, CheckArgs::new, CheckTool::new),\n                new Tool<>(Msg.tool_squash, SquashArgs::new, SquashTool::new),\n                new Tool<>(Msg.tool_revoke, RevokeArgs::new, RevokeTool::new),\n                new Tool<>(Msg.tool_anonymize, AnonymizeArgs::new, AnonymizeTool::new),\n                new Tool<>(Msg.tool_export, ExportArgs::new, ExportTool::new),\n                new Tool<>(ee.ivxv.common.cli.Msg.tool_verify, VerifyArgs::new, VerifyTool::new),\n                new Tool<>(Msg.tool_stats, StatsArgs::new, StatsTool::new),\n                new Tool<>(Msg.tool_statsdiff, StatsDiffArgs::new, StatsDiffTool::new),\n                new Tool<>(Msg.tool_checkAndSquash, CheckAndSquashArgs::new, CheckAndSquashTool::new),\n                new Tool<>(Msg.tool_revokeAndAnonymize, RevokeAndAnonymizeArgs::new, RevokeAndAnonymizeTool::new));\n    }\n\n    @Override\n    public ProcessorContext createContext(InitialContext ctx, Conf conf, CommonArgs args) {\n        return new ProcessorContext(ctx, conf, args);\n    }\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/ProcessorContext.java",
    "content": "package ee.ivxv.processor;\n\nimport ee.ivxv.common.cli.AppContext;\nimport ee.ivxv.common.cli.CommonArgs;\nimport ee.ivxv.common.cli.InitialContext;\nimport ee.ivxv.common.conf.Conf;\n\npublic class ProcessorContext extends AppContext<Conf> {\n\n    public ProcessorContext(InitialContext i, Conf conf, CommonArgs args) {\n        super(i, conf, args);\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/AnonymizeTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.model.AnonymousBallotBox;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.AnonymizeTool.AnonymizeArgs;\nimport ee.ivxv.processor.util.ReportHelper;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class AnonymizeTool implements Tool.Runner<AnonymizeArgs> {\n\n    private static final String OUT_BB_TMPL = \"bb-4.json\";\n    private static final String OUT_LOG_DISCRIMINATOR = \"anonymize\";\n\n    private static final Map<String, Object> EMPTY = new HashMap<>();\n\n    final I18nConsole console;\n    final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    final Map<String, Map<String, Object>> excluded = new ConcurrentHashMap<>();\n\n    public AnonymizeTool(ProcessorContext ctx) {\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(AnonymizeArgs args) throws Exception {\n        tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n\n        BallotBox bb = tool.readJsonBb(args.bb.value(), BallotBox.Type.DOUBLE_VOTERS_REMOVED);\n        AnonymousBallotBox abb = anonymize(bb);\n\n        reporter.writeBbErrors(args.out.value());\n\n        // There should still be an empty .log3 file even if there are no anonymised records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR, Reporter.LogType.LOG3, bb);\n        reporter.writeLog3(args.out.value(), bb, OUT_LOG_DISCRIMINATOR,\n                (voterId, qid) -> !excluded.getOrDefault(voterId, EMPTY).containsKey(qid));\n\n        Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n        tool.writeJsonBb(abb, args.out.value().resolve(OUT_BB));\n\n        return true;\n    }\n\n    private AnonymousBallotBox anonymize(BallotBox bb) {\n        console.println();\n        console.println(Msg.m_anonymizing_ballot_box);\n\n        return bb.anonymize();\n    }\n\n    public static class AnonymizeArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false);\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public AnonymizeArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(out);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/CheckAndSquashTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Arg.TreeList;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.CorrectnessUtil;\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.model.*;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataRef;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Ref.RegRef;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.util.ContainerHelper;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.common.util.log.PerformanceLog;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.util.DistrictsMapper;\nimport ee.ivxv.processor.util.ReportHelper;\nimport ee.ivxv.processor.util.VotersUtil;\n\nimport java.io.Closeable;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.function.Supplier;\n\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x9.X9ObjectIdentifiers;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n/**\n * Class is used to combine Check and Squash tools functionality.\n * What have been done:\n * a) removed WRITE out-1 directory\n * b) reuse input for \"check\" tool in \"squash\" tool\n * c) add key processing\n * d) WRITE out-2 as \"squash\" tool output\n */\npublic class CheckAndSquashTool implements Tool.Runner<CheckAndSquashTool.CheckAndSquashArgs> {\n\n    private static final Logger log = LoggerFactory.getLogger(CheckAndSquashTool.class);\n\n    static final ASN1ObjectIdentifier TS_KEY_ALG_ID = PKCSObjectIdentifiers.rsaEncryption;\n    static final ASN1ObjectIdentifier VL_KEY_ALG_ID = X9ObjectIdentifiers.id_ecPublicKey;\n    private static final String OUT_BB_TMPL = \"bb-2.json\";\n    private static final String OUT_IVLJSON_TMPL = \"ivoterlist.json\";\n    private static final String OUT_IVLPDF_TMPL = \"ivoterlist.pdf\";\n    private static final String OUT_RR_TMPL = \"revocation-report.csv\";\n    private static final String OUT_LOG_DISCRIMINATOR_CHECK = \"check\";\n    private static final String OUT_LOG_DISCRIMINATOR_SQUASH = \"squash\";\n    private static final Long DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES = 32768L;\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    private final List<Reporter.Record> revocationRecords = new ArrayList<>();\n    private final List<Reporter.LogNRecord> log2Records = new ArrayList<>();\n\n    public CheckAndSquashTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(CheckAndSquashArgs args) throws Exception {\n        DistrictList dl = tool.readJsonDistricts(args.districts.value());\n        VoterProvider vp = getVoterProvider(args, dl);\n        reporter.writeVlErrors(args.out.value());\n\n        boolean signed = args.bbChecksum.isSet();\n        if (!signed) {\n            console.println(Msg.m_bb_unsigned_skipping_output);\n        }\n\n\n        BallotBox bb = readBallotBox(args, vp, dl.getElection());\n        reporter.writeBbErrors(args.out.value());\n\n        // There should still be an empty .log1 file even if there are no accepted records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR_CHECK, Reporter.LogType.LOG1, bb);\n        reporter.writeLog1(args.out.value(), bb, OUT_LOG_DISCRIMINATOR_CHECK);\n\n        ElGamalPublicKey pub = new ElGamalPublicKey(args.encKey.value());\n\n        removeRecurrentVotes(bb);\n        removeInvalidCiphertexts(bb, pub);\n        reporter.writeInvalidVotesErrors(args.out.value());\n\n        Path OUT_IVLJSON = Util.prefixedPath(bb.getElection(), OUT_IVLJSON_TMPL);\n        Path OUT_IVLPDF = Util.prefixedPath(bb.getElection(), OUT_IVLPDF_TMPL);\n        Path OUT_RR = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL);\n        Path OUT_RR_ANONYMOUS = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL + \".anonymous\");\n        Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n\n        reporter.writeIVoterList(args.out.value().resolve(OUT_IVLJSON), args.out.value().resolve(OUT_IVLPDF), bb, dl);\n        reporter.writeRevocationReport(args.out.value().resolve(OUT_RR), bb.getElection(), revocationRecords,\n                Reporter.AnonymousFormatter.NOT_ANONYMOUS);\n        reporter.writeRevocationReport(args.out.value().resolve(OUT_RR_ANONYMOUS), bb.getElection(), revocationRecords,\n                Reporter.AnonymousFormatter.REVOCATION_REPORT_CSV);\n\n        // There should still be an empty .log2 file even if there are no squashed records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR_SQUASH, Reporter.LogType.LOG2, bb);\n        reporter.writeLog2(args.out.value(), bb.getElection(), OUT_LOG_DISCRIMINATOR_SQUASH, log2Records);\n\n        tool.writeJsonBb(bb, args.out.value().resolve(OUT_BB));\n\n        return true;\n    }\n\n    private VoterProvider getVoterProvider(CheckAndSquashArgs args, DistrictList dl) throws Exception {\n        if (args.voterLists.isSet()) {\n            VoterList vl = readVoterLists(args, dl, getDistrictsMapper(args.distMapping.value()));\n            return vl::find;\n        }\n\n        // Voter lists not given - using fictive voter list that accepts all\n\n        if (dl.getDistricts().size() != 1) {\n            throw new MessageException(Msg.e_vl_fictive_single_district_and_parish_required);\n        }\n        Map.Entry<String, District> de = dl.getDistricts().entrySet().iterator().next();\n        if (de.getValue().getParish().size() != 1) {\n            throw new MessageException(Msg.e_vl_fictive_single_district_and_parish_required);\n        }\n        String parish = de.getValue().getParish().iterator().next();\n\n        console.println(Msg.m_vl_fictive_warning, Msg.m_vl_fictive_voter_name);\n\n        String voterName = ctx.i.i18n.get(Msg.m_vl_fictive_voter_name);\n        LName district = new LName(de.getKey());\n        return (vid, version) -> new Voter(vid, voterName, null, parish, district);\n    }\n\n    private DistrictsMapper getDistrictsMapper(Path path) throws Exception {\n        if (path == null) {\n            return new DistrictsMapper();\n        }\n        console.println();\n        console.println(Msg.m_dist_mapping_loading, path);\n        ctx.container.requireContainer(path);\n        Container c = ctx.container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(Msg.m_dist_mapping_arg_for_cont);\n        console.println(Msg.m_dist_mapping_loaded, path);\n\n        return new DistrictsMapper(file.getStream());\n    }\n\n    private VoterList readVoterLists(CheckAndSquashArgs args, DistrictList dl, DistrictsMapper mapper) {\n        if (!args.vlKey.isSet()) {\n            throw new MessageException(Msg.e_vl_vlkey_missing);\n        }\n\n        console.println();\n        console.println(Msg.m_vl_reading);\n        VotersUtil.Loader loader =\n                VotersUtil.getLoader(args.vlKey.value(), dl, mapper, reporter::reportVlErrors);\n\n        console.println();\n        console.println(Msg.m_read);\n        // NB! Must process voter lists in certain order. Using the order of input values.\n        args.voterLists.value().forEach(vl -> {\n            SkipCommand skip_cmd = null;\n            if (vl.skip_cmd.value() != null) {\n                try {\n                    skip_cmd = tool.readSkipCommand(vl.skip_cmd.value());\n                } catch (Exception e) {\n                    throw new MessageException(Msg.e_skip_cmd_loading);\n                }\n            }\n\n            VoterList list = loader.load(vl.path.value(), vl.signature.value(),\n                    vl.skip_cmd.value(), skip_cmd, args.foreignEHAK.value());\n            console.println(Msg.m_vl, list.getName());\n            console.println(Msg.m_vl_type, list.getChangeset());\n            if (skip_cmd != null) {\n                console.println(Msg.m_vl_skipped);\n            }\n            console.println(Msg.m_vl_total_added, list.getAdded().size());\n            console.println(Msg.m_vl_total_removed, list.getRemoved().size());\n            console.println();\n        });\n\n        return loader.getCurrent();\n    }\n\n    private BallotBox readBallotBox(CheckAndSquashArgs args, VoterProvider vp, String eid) throws Exception {\n        try {\n            if (args.bbChecksum.isSet()) {\n                tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n            }\n            if (args.rl.isSet()) {\n                if (!args.rlChecksum.isSet()) {\n                    throw new MessageException(Msg.e_reg_checksum_missing);\n                }\n                tool.checkRegChecksum(args.rl.value(), args.rlChecksum.value());\n            }\n\n            int tc = ctx.args.threads.value();\n            BboxHelper.Loader<?> loader =\n                    ctx.bbox.getLoader(args.bb.value(), console::startProgress, tc);\n\n            BallotBox bb = load(args, vp, eid, loader);\n\n            console.println(M.m_bb_total_checked_ballots, bb.getNumberOfBallots());\n\n            return bb;\n        } catch (InvalidBboxException e) {\n            throw new MessageException(e, Msg.e_bb_read_error, e.path, e);\n        }\n    }\n\n    private <T> BallotBox load(CheckAndSquashArgs args, VoterProvider vp, String eid,\n                               BboxHelper.Loader<T> loader) {\n        boolean haveRegData = args.rl.isSet();\n        BboxHelper.BallotsChecked<T> bc = getCheckedBallots(args.bb.value(), vp, args.tsKey.value(),\n                args.elStart.value(), loader, (ref, res, va) -> {\n                    // Ignore BALLOT_WITHOUT_REG_REQ if there is no registration data.\n                    if (haveRegData || res != Result.BALLOT_WITHOUT_REG_REQ) {\n                        reporter.reportBbError(ref, res, va);\n                    }\n                }, args.maxSignedBallotSizeInBytes.value());\n\n        if (!haveRegData) {\n            console.println();\n            console.println(Msg.m_reg_skipping_compare);\n\n            // Even if there is no reg data, we still need to run checkRegData,\n            // because we cannot skip ballotbox stages.\n            console.println();\n            console.println(Msg.m_bb_grouping_votes_by_voter);\n            return exec(() -> bc.checkRegData(new EmptyRegDataLoaderResult<T>())).getBallotBox(eid);\n        }\n\n        BboxHelper.RegDataLoaderResult<T> rdlr = getRegData(args.rl.value(), loader);\n\n        console.println();\n        console.println(M.m_bb_compare_with_reg);\n        BboxHelper.BboxLoaderResult res = exec(() -> bc.checkRegData(rdlr));\n\n        long bwr = reporter.countBbErrors(Result.BALLOT_WITHOUT_REG_REQ);\n        console.println(M.m_bb_ballot_missing_reg, bwr);\n        if (bwr == 0) {\n            console.println(M.m_bb_in_compliance_with_reg);\n        }\n        long rwb = reporter.countBbErrors(Result.REG_REQ_WITHOUT_BALLOT);\n        console.println(M.m_bb_reg_missing_ballot, rwb);\n        if (rwb == 0) {\n            console.println(M.m_reg_in_compliance_with_bb);\n        }\n\n        return res.getBallotBox(eid);\n    }\n\n    private <T> BboxHelper.BallotsChecked<T> getCheckedBallots(Path path, VoterProvider vp,\n                                                               PublicKeyHolder tsKey, Instant elStart, BboxHelper.Loader<T> l,\n                                                               BboxHelper.Reporter<Ref.BbRef> reporter,\n                                                               long maxSignedBallotSizeInBytes) {\n        console.println();\n        console.println(M.m_bb_loading, path);\n        BboxHelper.BboxLoader<T> loader = exec(() -> l.getBboxLoader(path, reporter, maxSignedBallotSizeInBytes));\n        console.println(M.m_bb_loaded);\n        console.println(M.m_bb_checking_type);\n        // If no error has occurred so far, the file structure must be correct and type UNORGANIZED\n        console.println(M.m_bb_type, BallotBox.Type.UNORGANIZED);\n        console.println(M.m_bb_numof_collector_ballots, loader.getNumberOfValidBallots());\n\n        console.println(M.m_bb_checking_integrity);\n        BboxHelper.IntegrityChecked<T> ic = exec(() -> loader.checkIntegrity());\n        console.println(M.m_bb_data_is_integrous);\n        console.println(M.m_bb_numof_ballots, ic.getNumberOfValidBallots());\n\n        console.println(M.m_bb_checking_ballot_sig);\n        BboxHelper.BallotsChecked<T> bc = exec(() -> ic.checkBallots(vp, tsKey, elStart));\n        console.println(M.m_bb_total_ballots, ic.getNumberOfValidBallots());\n        console.println(M.m_bb_numof_ballots_sig_valid, bc.getNumberOfValidBallots());\n        console.println(M.m_bb_numof_ballots_sig_invalid, bc.getNumberOfInvalidBallots());\n        if (bc.getNumberOfInvalidBallots() == 0) {\n            console.println(M.m_bb_all_ballots_sig_valid);\n        }\n\n        return bc;\n    }\n\n    private <T> BboxHelper.RegDataLoaderResult<T> getRegData(Path path, BboxHelper.Loader<T> l) {\n        console.println();\n        console.println(M.m_reg_loading, path);\n        BboxHelper.RegDataLoader<T> rdl =\n                exec(() -> l.getRegDataLoader(path, reporter::reportRegError));\n        console.println(M.m_reg_loaded);\n\n        console.println(M.m_reg_checking_integrity);\n        BboxHelper.RegDataIntegrityChecked<T> rdic = exec(() -> rdl.checkIntegrity());\n        console.println(M.m_reg_data_is_integrous);\n        console.println(M.m_reg_numof_records, rdic.getNumberOfValidBallots());\n\n        return exec(() -> rdic.getRegData());\n    }\n\n    private <T extends BboxHelper.Stage> T exec(Supplier<T> task) {\n        long t = System.currentTimeMillis();\n        T result = task.get();\n\n        log(result, t);\n\n        return result;\n    }\n\n    private void log(BboxHelper.Stage stage, long startTime) {\n        long t = System.currentTimeMillis() - startTime;\n        String name = stage.getClass().getSimpleName();\n\n        PerformanceLog.log.info(\"{} #BALLOTS: {}\", name, stage.getNumberOfValidBallots());\n        PerformanceLog.log.info(\"{}     TIME: {} ms\", name, t);\n        log.info(\"{} #Ballots: {}\", name, stage.getNumberOfValidBallots());\n        log.info(\"{} #Invalid: {}\", name, stage.getNumberOfInvalidBallots());\n    }\n\n    static class EmptyRegDataLoaderResult<T> implements BboxHelper.RegDataLoaderResult<T> {\n        @Override\n        public int getNumberOfValidBallots() {\n            return 0;\n        }\n\n        @Override\n        public int getNumberOfInvalidBallots() {\n            return 0;\n        }\n\n        @Override\n        public void report(RegRef ref, Result res, Object... args) {\n            // Ignore any errors.\n        }\n\n        @Override\n        public Map<Object, RegDataRef<T>> getRegData() {\n            return new LinkedHashMap<>();\n        }\n    }\n\n    public static class CheckAndSquashArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false).setOptional();\n        Arg<Long> maxSignedBallotSizeInBytes = Arg\n                .aLong(Msg.arg_signed_ballot_max_size_bytes)\n                .setOptional()\n                .setDefault(DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false);\n        Arg<Path> rl = Arg.aPath(Msg.arg_registrationlist, true, false).setOptional();\n        Arg<Path> rlChecksum =\n                Arg.aPath(Msg.arg_registrationlist_checksum, true, false).setOptional();\n        Arg<PublicKeyHolder> tsKey = Arg.aPublicKey(Msg.arg_tskey, TS_KEY_ALG_ID);\n        Arg<PublicKeyHolder> vlKey = Arg.aPublicKey(Msg.arg_vlkey, VL_KEY_ALG_ID).setOptional();\n        Arg<List<VoterListEntry>> voterLists =\n                new TreeList<>(Msg.arg_voterlists, VoterListEntry::new).setOptional();\n        Arg<Path> distMapping = Arg.aPath(Msg.arg_districts_mapping, true, false).setOptional();\n        Arg<Instant> elStart = Arg.anInstant(Msg.arg_election_start);\n        Arg<String> foreignEHAK = Arg.aString(Msg.arg_voterforeignehak).setOptional();\n        Arg<Path> encKey = Arg.aPath(Msg.arg_enckey, true, false);\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public CheckAndSquashArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(maxSignedBallotSizeInBytes);\n            args.add(districts);\n            args.add(rl);\n            args.add(rlChecksum);\n            args.add(tsKey);\n            args.add(vlKey);\n            args.add(voterLists);\n            args.add(distMapping);\n            args.add(elStart);\n            args.add(foreignEHAK);\n            args.add(encKey);\n            args.add(out);\n        }\n\n    }\n\n    static class VoterListEntry extends Args {\n        Arg<Path> path = Arg.aPath(Msg.arg_path, true, false);\n        Arg<Path> signature = Arg.aPath(Msg.arg_signature, true, false);\n        Arg<Path> skip_cmd = Arg.aPath(Msg.arg_skip_cmd, true, false).setOptional();\n\n        VoterListEntry() {\n            args.add(path);\n            args.add(signature);\n            args.add(skip_cmd);\n        }\n    }\n\n    private void removeRecurrentVotes(BallotBox bb) {\n        console.println();\n        console.println(Msg.m_removing_recurrent_votes);\n\n        bb.removeRecurrentVotes(this::collect);\n\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private void removeInvalidCiphertexts(BallotBox bb, ElGamalPublicKey pk) {\n        console.println();\n        console.println(Msg.m_removing_invalid_ciphertexts);\n\n        try (CiphertextFilter filter = new CiphertextFilter(pk, bb.getNumberOfBallots())) {\n            bb.removeInvalidCiphertexts(filter::accept);\n        }\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private void collect(String vid, Ballot b) {\n        revocationRecords.add(ctx.reporter.newRevocationRecordForRecurrentVote(vid, b));\n        log2Records.add(ctx.reporter.newLog123Record(vid, b));\n    }\n\n    private void collectinvalid(String vid, Ballot b, String qid, CorrectnessUtil.CiphertextCorrectness cc, byte[] vote) {\n        revocationRecords.add(ctx.reporter.newRevocationRecordForInvalidVote(vid, b));\n        log2Records.add(ctx.reporter.newLog123Record(vid, b));\n        reporter.reportAbbError(vid, b, qid, cc);\n    }\n\n    private class CiphertextFilter implements BallotBox.VoteFilter, Closeable {\n\n        final ElGamalPublicKey pk;\n        final Progress p;\n\n        public CiphertextFilter(ElGamalPublicKey pk, int total) {\n            this.pk = pk;\n            this.p = console.startProgress(total);\n        }\n\n        @Override\n        public boolean accept(String voterId, Ballot b, String qid, byte[] vote) {\n            CorrectnessUtil.CiphertextCorrectness res = CorrectnessUtil.isValidCiphertext(pk, vote);\n            boolean isValid = res == CorrectnessUtil.CiphertextCorrectness.VALID;\n\n            p.increase(1);\n\n            if (!isValid) {\n                collectinvalid(voterId, b, qid, res, vote);\n            }\n\n            return isValid;\n        }\n\n        @Override\n        public void close() {\n            p.finish();\n        }\n\n    }\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/CheckTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Arg.TreeList;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.District;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.SkipCommand;\nimport ee.ivxv.common.model.LName;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.model.VoterList;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.BboxHelper.RegDataRef;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Ref.RegRef;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.util.ContainerHelper;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.common.util.log.PerformanceLog;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.CheckTool.CheckArgs;\nimport ee.ivxv.processor.util.DistrictsMapper;\nimport ee.ivxv.processor.util.ReportHelper;\nimport ee.ivxv.processor.util.VotersUtil;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\nimport org.bouncycastle.asn1.x9.X9ObjectIdentifiers;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class CheckTool implements Tool.Runner<CheckArgs> {\n\n    private static final Logger log = LoggerFactory.getLogger(CheckTool.class);\n\n    static final ASN1ObjectIdentifier TS_KEY_ALG_ID = PKCSObjectIdentifiers.rsaEncryption;\n    static final ASN1ObjectIdentifier VL_KEY_ALG_ID = X9ObjectIdentifiers.id_ecPublicKey;\n\n    private static final String OUT_BB_TMPL = \"bb-1.json\";\n    private static final String OUT_LOG_DISCRIMINATOR = \"check\";\n    private static final Long DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES = 32768L;\n\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    public CheckTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(CheckArgs args) throws Exception {\n        DistrictList dl = tool.readJsonDistricts(args.districts.value());\n        VoterProvider vp = getVoterProvider(args, dl);\n        reporter.writeVlErrors(args.out.value());\n\n        boolean signed = args.bbChecksum.isSet();\n        if (!signed) {\n            console.println(Msg.m_bb_unsigned_skipping_output);\n        }\n\n        BallotBox bb = readBallotBox(args, vp, dl.getElection());\n        reporter.writeBbErrors(args.out.value());\n\n        // There should still be an empty .log1 file even if there are no accepted records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR, Reporter.LogType.LOG1, bb);\n        reporter.writeLog1(args.out.value(), bb, OUT_LOG_DISCRIMINATOR);\n\n        if (signed) {\n            Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n            tool.writeJsonBb(bb, args.out.value().resolve(OUT_BB));\n        }\n        return true;\n    }\n\n    private VoterProvider getVoterProvider(CheckArgs args, DistrictList dl) throws Exception {\n        if (args.voterLists.isSet()) {\n            VoterList vl = readVoterLists(args, dl, getDistrictsMapper(args.distMapping.value()));\n            return vl::find;\n        }\n\n        // Voter lists not given - using fictive voter list that accepts all\n\n        if (dl.getDistricts().size() != 1) {\n            throw new MessageException(Msg.e_vl_fictive_single_district_and_parish_required);\n        }\n        Map.Entry<String, District> de = dl.getDistricts().entrySet().iterator().next();\n        if (de.getValue().getParish().size() != 1) {\n            throw new MessageException(Msg.e_vl_fictive_single_district_and_parish_required);\n        }\n        String parish = de.getValue().getParish().iterator().next();\n\n        console.println(Msg.m_vl_fictive_warning, Msg.m_vl_fictive_voter_name);\n\n        String voterName = ctx.i.i18n.get(Msg.m_vl_fictive_voter_name);\n        LName district = new LName(de.getKey());\n        return (vid, version) -> new Voter(vid, voterName, null, parish, district);\n    }\n\n    private DistrictsMapper getDistrictsMapper(Path path) throws Exception {\n        if (path == null) {\n            return new DistrictsMapper();\n        }\n        console.println();\n        console.println(Msg.m_dist_mapping_loading, path);\n        ctx.container.requireContainer(path);\n        Container c = ctx.container.read(path.toString());\n        ContainerHelper ch = new ContainerHelper(console, c);\n        DataFile file = ch.getSingleFileAndReport(Msg.m_dist_mapping_arg_for_cont);\n        console.println(Msg.m_dist_mapping_loaded, path);\n\n        return new DistrictsMapper(file.getStream());\n    }\n\n    private VoterList readVoterLists(CheckArgs args, DistrictList dl, DistrictsMapper mapper) {\n        if (!args.vlKey.isSet()) {\n            throw new MessageException(Msg.e_vl_vlkey_missing);\n        }\n\n        console.println();\n        console.println(Msg.m_vl_reading);\n        VotersUtil.Loader loader =\n                VotersUtil.getLoader(args.vlKey.value(), dl, mapper, reporter::reportVlErrors);\n\n        console.println();\n        console.println(Msg.m_read);\n        // NB! Must process voter lists in certain order. Using the order of input values.\n        args.voterLists.value().forEach(vl -> {\n            SkipCommand skip_cmd = null;\n            if (vl.skip_cmd.value() != null) {\n                try {\n                    skip_cmd = tool.readSkipCommand(vl.skip_cmd.value());\n                }\n                catch (Exception e) {\n                    throw new MessageException(Msg.e_skip_cmd_loading);\n                }\n            }\n\n            VoterList list = loader.load(vl.path.value(), vl.signature.value(),\n                    vl.skip_cmd.value(), skip_cmd, args.foreignEHAK.value());\n            console.println(Msg.m_vl, list.getName());\n            console.println(Msg.m_vl_type, list.getChangeset());\n            if (skip_cmd != null) {\n                console.println(Msg.m_vl_skipped);\n            }\n            console.println(Msg.m_vl_total_added, list.getAdded().size());\n            console.println(Msg.m_vl_total_removed, list.getRemoved().size());\n            console.println();\n        });\n\n        return loader.getCurrent();\n    }\n\n    private BallotBox readBallotBox(CheckArgs args, VoterProvider vp, String eid) throws Exception {\n        try {\n            if (args.bbChecksum.isSet()) {\n                tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n            }\n            if (args.rl.isSet()) {\n                if (!args.rlChecksum.isSet()) {\n                    throw new MessageException(Msg.e_reg_checksum_missing);\n                }\n                tool.checkRegChecksum(args.rl.value(), args.rlChecksum.value());\n            }\n\n            int tc = ctx.args.threads.value();\n            BboxHelper.Loader<?> loader =\n                    ctx.bbox.getLoader(args.bb.value(), console::startProgress, tc);\n\n            BallotBox bb = load(args, vp, eid, loader);\n\n            console.println(M.m_bb_total_checked_ballots, bb.getNumberOfBallots());\n\n            return bb;\n        } catch (InvalidBboxException e) {\n            throw new MessageException(e, Msg.e_bb_read_error, e.path, e);\n        }\n    }\n\n    private <T> BallotBox load(CheckArgs args, VoterProvider vp, String eid,\n            BboxHelper.Loader<T> loader) {\n        boolean haveRegData = args.rl.isSet();\n        BboxHelper.BallotsChecked<T> bc = getCheckedBallots(args.bb.value(), vp, args.tsKey.value(),\n                args.elStart.value(), loader, (ref, res, va) -> {\n                    // Ignore BALLOT_WITHOUT_REG_REQ if there is no registration data.\n                    if (haveRegData || res != Result.BALLOT_WITHOUT_REG_REQ) {\n                        reporter.reportBbError(ref, res, va);\n                    }\n                }, args.maxSignedBallotSizeInBytes.value());\n\n        if (!haveRegData) {\n            console.println();\n            console.println(Msg.m_reg_skipping_compare);\n\n            // Even if there is no reg data, we still need to run checkRegData,\n            // because we cannot skip ballotbox stages.\n            console.println();\n            console.println(Msg.m_bb_grouping_votes_by_voter);\n            return exec(() -> bc.checkRegData(new EmptyRegDataLoaderResult<T>())).getBallotBox(eid);\n        }\n\n        BboxHelper.RegDataLoaderResult<T> rdlr = getRegData(args.rl.value(), loader);\n\n        console.println();\n        console.println(M.m_bb_compare_with_reg);\n        BboxHelper.BboxLoaderResult res = exec(() -> bc.checkRegData(rdlr));\n\n        long bwr = reporter.countBbErrors(Result.BALLOT_WITHOUT_REG_REQ);\n        console.println(M.m_bb_ballot_missing_reg, bwr);\n        if (bwr == 0) {\n            console.println(M.m_bb_in_compliance_with_reg);\n        }\n        long rwb = reporter.countBbErrors(Result.REG_REQ_WITHOUT_BALLOT);\n        console.println(M.m_bb_reg_missing_ballot, rwb);\n        if (rwb == 0) {\n            console.println(M.m_reg_in_compliance_with_bb);\n        }\n\n        return res.getBallotBox(eid);\n    }\n\n    private <T> BboxHelper.BallotsChecked<T> getCheckedBallots(Path path, VoterProvider vp,\n            PublicKeyHolder tsKey, Instant elStart, BboxHelper.Loader<T> l,\n            BboxHelper.Reporter<Ref.BbRef> reporter, long maxSignedBallotSizeInBytes) {\n        console.println();\n        console.println(M.m_bb_loading, path);\n        BboxHelper.BboxLoader<T> loader = exec(() -> l.getBboxLoader(path, reporter, maxSignedBallotSizeInBytes));\n        console.println(M.m_bb_loaded);\n        console.println(M.m_bb_checking_type);\n        // If no error has occurred so far, the file structure must be correct and type UNORGANIZED\n        console.println(M.m_bb_type, BallotBox.Type.UNORGANIZED);\n        console.println(M.m_bb_numof_collector_ballots, loader.getNumberOfValidBallots());\n\n        console.println(M.m_bb_checking_integrity);\n        BboxHelper.IntegrityChecked<T> ic = exec(() -> loader.checkIntegrity());\n        console.println(M.m_bb_data_is_integrous);\n        console.println(M.m_bb_numof_ballots, ic.getNumberOfValidBallots());\n\n        console.println(M.m_bb_checking_ballot_sig);\n        BboxHelper.BallotsChecked<T> bc = exec(() -> ic.checkBallots(vp, tsKey, elStart));\n        console.println(M.m_bb_total_ballots, ic.getNumberOfValidBallots());\n        console.println(M.m_bb_numof_ballots_sig_valid, bc.getNumberOfValidBallots());\n        console.println(M.m_bb_numof_ballots_sig_invalid, bc.getNumberOfInvalidBallots());\n        if (bc.getNumberOfInvalidBallots() == 0) {\n            console.println(M.m_bb_all_ballots_sig_valid);\n        }\n\n        return bc;\n    }\n\n    private <T> BboxHelper.RegDataLoaderResult<T> getRegData(Path path, BboxHelper.Loader<T> l) {\n        console.println();\n        console.println(M.m_reg_loading, path);\n        BboxHelper.RegDataLoader<T> rdl =\n                exec(() -> l.getRegDataLoader(path, reporter::reportRegError));\n        console.println(M.m_reg_loaded);\n\n        console.println(M.m_reg_checking_integrity);\n        BboxHelper.RegDataIntegrityChecked<T> rdic = exec(() -> rdl.checkIntegrity());\n        console.println(M.m_reg_data_is_integrous);\n        console.println(M.m_reg_numof_records, rdic.getNumberOfValidBallots());\n\n        return exec(() -> rdic.getRegData());\n    }\n\n    private <T extends BboxHelper.Stage> T exec(Supplier<T> task) {\n        long t = System.currentTimeMillis();\n        T result = task.get();\n\n        log(result, t);\n\n        return result;\n    }\n\n    private void log(BboxHelper.Stage stage, long startTime) {\n        long t = System.currentTimeMillis() - startTime;\n        String name = stage.getClass().getSimpleName();\n\n        PerformanceLog.log.info(\"{} #BALLOTS: {}\", name, stage.getNumberOfValidBallots());\n        PerformanceLog.log.info(\"{}     TIME: {} ms\", name, t);\n        log.info(\"{} #Ballots: {}\", name, stage.getNumberOfValidBallots());\n        log.info(\"{} #Invalid: {}\", name, stage.getNumberOfInvalidBallots());\n    }\n\n    static class EmptyRegDataLoaderResult<T> implements BboxHelper.RegDataLoaderResult<T> {\n        @Override\n        public int getNumberOfValidBallots() {\n            return 0;\n        }\n\n        @Override\n        public int getNumberOfInvalidBallots() {\n            return 0;\n        }\n\n        @Override\n        public void report(RegRef ref, Result res, Object... args) {\n            // Ignore any errors.\n        }\n\n        @Override\n        public Map<Object, RegDataRef<T>> getRegData() {\n            return new LinkedHashMap<>();\n        }\n    }\n\n    public static class CheckArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false).setOptional();\n        Arg<Long> maxSignedBallotSizeInBytes = Arg\n                .aLong(Msg.arg_signed_ballot_max_size_bytes)\n                .setOptional()\n                .setDefault(DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false);\n        Arg<Path> rl = Arg.aPath(Msg.arg_registrationlist, true, false).setOptional();\n        Arg<Path> rlChecksum =\n                Arg.aPath(Msg.arg_registrationlist_checksum, true, false).setOptional();\n        Arg<PublicKeyHolder> tsKey = Arg.aPublicKey(Msg.arg_tskey, TS_KEY_ALG_ID);\n        Arg<PublicKeyHolder> vlKey = Arg.aPublicKey(Msg.arg_vlkey, VL_KEY_ALG_ID).setOptional();\n        Arg<List<VoterListEntry>> voterLists =\n                new TreeList<>(Msg.arg_voterlists, VoterListEntry::new).setOptional();\n        Arg<Path> distMapping = Arg.aPath(Msg.arg_districts_mapping, true, false).setOptional();\n        Arg<Instant> elStart = Arg.anInstant(Msg.arg_election_start);\n        Arg<String> foreignEHAK = Arg.aString(Msg.arg_voterforeignehak).setOptional();\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public CheckArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(maxSignedBallotSizeInBytes);\n            args.add(districts);\n            args.add(rl);\n            args.add(rlChecksum);\n            args.add(tsKey);\n            args.add(vlKey);\n            args.add(voterLists);\n            args.add(distMapping);\n            args.add(elStart);\n            args.add(foreignEHAK);\n            args.add(out);\n        }\n\n    }\n\n    static class VoterListEntry extends Args {\n        Arg<Path> path = Arg.aPath(Msg.arg_path, true, false);\n        Arg<Path> signature = Arg.aPath(Msg.arg_signature, true, false);\n        Arg<Path> skip_cmd = Arg.aPath(Msg.arg_skip_cmd, true, false).setOptional();\n\n        VoterListEntry() {\n            args.add(path);\n            args.add(signature);\n            args.add(skip_cmd);\n        }\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/ExportTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.ExportTool.ExportArgs;\nimport ee.ivxv.processor.util.ReportHelper;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic class ExportTool implements Tool.Runner<ExportArgs> {\n\n    private static final String OUT_EXP = \"export\";\n    private static final String EXPORT_VOTE_EXTENSION = \"asice\";\n    private static final Long DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES = 32768L;\n\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    public ExportTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(ExportArgs args) throws Exception {\n        export(args);\n        reporter.writeBbErrors(args.out.value());\n\n        return true;\n    }\n\n    private void export(ExportArgs args) throws Exception {\n        try {\n            tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n\n            int tc = ctx.args.threads.value();\n            BboxHelper.Loader<?> loader =\n                    ctx.bbox.getLoader(args.bb.value(), console::startProgress, tc);\n\n            export(args.bb.value(), args.voter.value(), loader, args.out.value().resolve(OUT_EXP), args.maxSignedBallotSizeInBytes.value());\n        } catch (InvalidBboxException e) {\n            throw new MessageException(e, Msg.e_bb_read_error, e.path, e);\n        }\n    }\n\n    private <T> void export(Path path, String voterId, BboxHelper.Loader<T> l, Path out, long maxSignedBallotSizeInBytes) {\n        console.println();\n        console.println(M.m_bb_loading, path);\n        BboxHelper.BboxLoader<T> loader = l.getBboxLoader(path, reporter::reportBbError, maxSignedBallotSizeInBytes);\n        console.println(M.m_bb_loaded);\n        console.println(M.m_bb_checking_type);\n        // If no error has occurred so far, the file structure must be correct and type UNORGANIZED\n        console.println(M.m_bb_type, BallotBox.Type.UNORGANIZED);\n\n        console.println(M.m_bb_checking_integrity);\n        BboxHelper.IntegrityChecked<T> ic = loader.checkIntegrity();\n        console.println(M.m_bb_data_is_integrous);\n        console.println(M.m_bb_numof_ballots, ic.getNumberOfValidBallots());\n\n        console.println();\n        if (voterId != null) {\n            console.println(M.m_bb_exporting_voter, voterId, out);\n        } else {\n            console.println(M.m_bb_exporting, out);\n        }\n        ic.export(Optional.ofNullable(voterId), (ref, bytes) -> writeContainer(ref, bytes, out));\n        console.println(M.m_bb_exported);\n    }\n\n    private void writeContainer(Ref.BbRef ref, byte[] bytes, Path out) {\n        try {\n            String fileName = String.format(\"%s.%s\", ref.ballot, EXPORT_VOTE_EXTENSION);\n            Path ballotPath = out.resolve(ref.voter).resolve(fileName);\n\n            Files.createDirectories(ballotPath.getParent());\n            Files.write(ballotPath, bytes);\n        } catch (Exception e) {\n            // Let the BboxLoader take care of error reporting\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static class ExportArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false);\n        Arg<Long> maxSignedBallotSizeInBytes = Arg\n                .aLong(Msg.arg_signed_ballot_max_size_bytes)\n                .setOptional()\n                .setDefault(DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES);\n        Arg<String> voter = Arg.aString(Msg.arg_voter_id).setOptional();\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public ExportArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(maxSignedBallotSizeInBytes);\n            args.add(voter);\n            args.add(out);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/RevokeAndAnonymizeTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.model.*;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.report.Reporter.RevokeAction;\nimport ee.ivxv.common.util.*;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.util.ReportHelper;\n\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\n\npublic class RevokeAndAnonymizeTool implements Tool.Runner<RevokeAndAnonymizeTool.RevokeAndAnonymizeArgs> {\n\n    private static final String OUT_BB_TMPL = \"bb-4.json\";\n    private static final String OUT_RR_TMPL = \"revocation-report.csv\";\n    private static final String OUT_IVLJSON_TMPL = \"ivoterlist.json\";\n    private static final String OUT_LOG_DISCRIMINATOR_REVOKE = \"revoke\";\n    private static final String OUT_LOG_DISCRIMINATOR_ANONYMIZE = \"anonymize\";\n    private static final Map<String, Object> EMPTY = new HashMap<>();\n    final ProcessorContext ctx;\n    final I18nConsole console;\n    final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    final Map<String, Map<String, Object>> excluded = new ConcurrentHashMap<>();\n\n    public RevokeAndAnonymizeTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(RevokeAndAnonymizeArgs args) throws Exception {\n        tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n\n        BallotBox bb = tool.readJsonBb(args.bb.value(), BallotBox.Type.INVALID_CIPHERTEXTS_REMOVED);\n        DistrictList dl = tool.readJsonDistricts(args.districts.value());\n        RlLoader loader = new RlLoader(bb);\n        Path out = args.out.value();\n\n        applyRevocationLists(bb, args.revLists.value(), loader);\n\n        Path OUT_IVLJSON = Util.prefixedPath(bb.getElection(), OUT_IVLJSON_TMPL);\n        Path OUT_RR = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL);\n        Path OUT_RR_ANONYMOUS = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL + \".anonymous\");\n        Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n\n        reporter.writeIVoterList(out.resolve(OUT_IVLJSON), null, bb, dl);\n        reporter.writeRevocationReport(out.resolve(OUT_RR), bb.getElection(), loader.revRecords,\n                Reporter.AnonymousFormatter.NOT_ANONYMOUS);\n        reporter.writeRevocationReport(out.resolve(OUT_RR_ANONYMOUS), bb.getElection(), loader.revRecords,\n                Reporter.AnonymousFormatter.REVOCATION_REPORT_CSV);\n\n        // There should still be an empty .log2 file even if there are no revoked records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR_REVOKE, Reporter.LogType.LOG2, bb);\n        reporter.writeLog2(out, bb.getElection(), OUT_LOG_DISCRIMINATOR_REVOKE, loader.getLog2Records());\n\n        // There should still be an empty .log3 file even if there are no anonymised records\n        reporter.writeEmptyLogFiles(args.out.value(), OUT_LOG_DISCRIMINATOR_ANONYMIZE, Reporter.LogType.LOG3, bb);\n        reporter.writeLog3(args.out.value(), bb, OUT_LOG_DISCRIMINATOR_ANONYMIZE,\n                (voterId, qid) -> !excluded.getOrDefault(voterId, EMPTY).containsKey(qid));\n\n        AnonymousBallotBox abb = anonymize(bb);\n\n        tool.writeJsonBb(abb, out.resolve(OUT_BB));\n\n        return true;\n    }\n\n    private AnonymousBallotBox anonymize(BallotBox bb) {\n        console.println();\n        console.println(Msg.m_anonymizing_ballot_box);\n\n        return bb.anonymize();\n    }\n\n    private void applyRevocationLists(BallotBox bb, List<Path> paths, RlLoader loader) {\n        console.println();\n        console.println(Msg.m_applying_revocation_lists);\n\n        bb.revokeDoubleVotes(paths.stream().map(p -> () -> loader.load(p)), loader::collect);\n        loader.reportAfterLoading();\n\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private class RlLoader {\n\n        private final BallotBox bb;\n        final List<Reporter.Record> revRecords = new ArrayList<>();\n        /**\n         * The LOG2 records. At most one entry per ballot. Using ordered map to retain the original\n         * iteration order in the output.\n         */\n        private final Map<String, Reporter.LogNRecord> log2Records = new LinkedHashMap<>();\n\n        int ballotCount;\n        RevocationList current;\n        String operator;\n        int currentCount;\n\n        RlLoader(BallotBox bb) {\n            this.bb = bb;\n            ballotCount = bb.getNumberOfBallots();\n            bb.getBallots().keySet().forEach(vid -> log2Records.put(vid, null));\n        }\n\n        RevocationList load(Path path) {\n            reportAfterLoading();\n\n            loadRevocationList(path);\n\n            reportBeforeLoading();\n\n            currentCount = 0;\n\n            return current;\n        }\n\n        private void reportBeforeLoading() {\n            if (current.isRevoke()) {\n                console.println(Msg.m_rl_revoke_start);\n                console.println(Msg.m_rl_revoke_ballots_before, ballotCount);\n            } else {\n                console.println(Msg.m_rl_restore_start);\n                console.println(Msg.m_rl_restore_ballots_before, ballotCount);\n            }\n        }\n\n        void reportAfterLoading() {\n            if (current != null) {\n                if (current.isRevoke()) {\n                    ballotCount -= currentCount;\n                    console.println(Msg.m_rl_revoke_count, currentCount);\n                    console.println(Msg.m_rl_revoke_ballots_after, ballotCount);\n                    console.println(Msg.m_rl_revoke_done);\n                } else {\n                    ballotCount += currentCount;\n                    console.println(Msg.m_rl_restore_count, currentCount);\n                    console.println(Msg.m_rl_restore_ballots_after, ballotCount);\n                    console.println(Msg.m_rl_restore_done);\n                }\n            }\n        }\n\n        private void loadRevocationList(Path path) {\n            try {\n                console.println();\n                console.println(Msg.m_rl_loading, path);\n                ctx.container.requireContainer(path);\n                Container c = ctx.container.read(path.toString());\n                console.println(Msg.m_rl_loaded);\n\n                ContainerHelper ch = new ContainerHelper(console, c);\n                DataFile file = ch.getSingleFileAndReport(Msg.m_rl_arg_for_cont);\n\n                console.println(Msg.m_rl_checking_integrity);\n                RevocationList rl = Json.read(file.getStream(), RevocationList.class);\n                if (rl.getElection() != null && !rl.getElection().equals(bb.getElection())) {\n                    throw new MessageException(Msg.e_rl_election_id, rl.getElection(),\n                            bb.getElection());\n                }\n                console.println(Msg.m_rl_data_is_integrous);\n\n                Msg totalMsg = rl.isRevoke() ? Msg.m_rl_revoke_total : Msg.m_rl_restore_total;\n                console.println(totalMsg, rl.getPersons().size());\n\n                current = rl;\n                operator = ch.getSignerNames();\n            } catch (Exception e) {\n                throw new MessageException(e, Msg.e_rl_read_error, path, e);\n            }\n        }\n\n        void collect(String voterId, Ballot b, boolean revoke, boolean success) {\n            if (success) {\n                RevokeAction action = revoke ? RevokeAction.REVOKED : RevokeAction.RESTORED;\n\n                revRecords.add(ctx.reporter.newRevocationRecord(action, voterId, b, operator));\n                log2Records.put(voterId, revoke ? ctx.reporter.newLog123Record(voterId, b) : null);\n                currentCount++;\n            } else {\n                Msg key = b == null ? Msg.e_rl_voter_not_found_in_bb\n                        : revoke ? Msg.e_rl_ballot_already_revoked\n                        : Msg.e_rl_ballot_already_restored;\n                Message innerMsg = new Message(key, voterId);\n                Message msg = new Message(Msg.e_rl_processing_error, innerMsg);\n                console.println(msg.key, msg.args);\n            }\n        }\n\n        List<Reporter.LogNRecord> getLog2Records() {\n            return log2Records.values().stream().filter(r -> r != null)\n                    .collect(Collectors.toList());\n        }\n    }\n\n    public static class RevokeAndAnonymizeArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false);\n        Arg<List<Path>> revLists =\n                Arg.listOfPaths(Msg.arg_revocationlists, true, false).setOptional();\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public RevokeAndAnonymizeArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(districts);\n            args.add(revLists);\n            args.add(out);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/RevokeTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.RevocationList;\nimport ee.ivxv.common.service.container.Container;\nimport ee.ivxv.common.service.container.DataFile;\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.report.Reporter.RevokeAction;\nimport ee.ivxv.common.util.ContainerHelper;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.RevokeTool.RevokeArgs;\nimport ee.ivxv.processor.util.ReportHelper;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class RevokeTool implements Tool.Runner<RevokeArgs> {\n\n    private static final String OUT_BB_TMPL = \"bb-3.json\";\n    private static final String OUT_RR_TMPL = \"revocation-report.csv\";\n    private static final String OUT_IVLJSON_TMPL = \"ivoterlist.json\";\n    private static final String OUT_LOG_DISCRIMINATOR = \"revoke\";\n\n    final ProcessorContext ctx;\n    final I18nConsole console;\n    final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    public RevokeTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(RevokeArgs args) throws Exception {\n        tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n\n        BallotBox bb = tool.readJsonBb(args.bb.value(), BallotBox.Type.INVALID_CIPHERTEXTS_REMOVED);\n        DistrictList dl = tool.readJsonDistricts(args.districts.value());\n        RlLoader loader = new RlLoader(bb);\n        Path out = args.out.value();\n\n        applyRevocationLists(bb, args.revLists.value(), loader);\n\n        Path OUT_IVLJSON = Util.prefixedPath(bb.getElection(), OUT_IVLJSON_TMPL);\n        Path OUT_RR = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL);\n        Path OUT_RR_ANONYMOUS = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL + \".anonymous\");\n        Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n\n        reporter.writeIVoterList(out.resolve(OUT_IVLJSON), null, bb, dl);\n        reporter.writeRevocationReport(out.resolve(OUT_RR), bb.getElection(), loader.revRecords,\n                Reporter.AnonymousFormatter.NOT_ANONYMOUS);\n        reporter.writeRevocationReport(out.resolve(OUT_RR_ANONYMOUS), bb.getElection(), loader.revRecords,\n                Reporter.AnonymousFormatter.REVOCATION_REPORT_CSV);\n\n        // There should still be an empty .log2 file even if there are no revoked records\n        reporter.writeEmptyLogFiles(out, OUT_LOG_DISCRIMINATOR, Reporter.LogType.LOG2, bb);\n        reporter.writeLog2(out, bb.getElection(), OUT_LOG_DISCRIMINATOR, loader.getLog2Records());\n\n        tool.writeJsonBb(bb, out.resolve(OUT_BB));\n\n        return true;\n    }\n\n    private void applyRevocationLists(BallotBox bb, List<Path> paths, RlLoader loader) {\n        console.println();\n        console.println(Msg.m_applying_revocation_lists);\n\n        bb.revokeDoubleVotes(paths.stream().map(p -> () -> loader.load(p)), loader::collect);\n        loader.reportAfterLoading();\n\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private class RlLoader {\n\n        private final BallotBox bb;\n        final List<Reporter.Record> revRecords = new ArrayList<>();\n        /**\n         * The LOG2 records. At most one entry per ballot. Using ordered map to retain the original\n         * iteration order in the output.\n         */\n        private final Map<String, Reporter.LogNRecord> log2Records = new LinkedHashMap<>();\n\n        int ballotCount;\n        RevocationList current;\n        String operator;\n        int currentCount;\n\n        RlLoader(BallotBox bb) {\n            this.bb = bb;\n            ballotCount = bb.getNumberOfBallots();\n            bb.getBallots().keySet().forEach(vid -> log2Records.put(vid, null));\n        }\n\n        RevocationList load(Path path) {\n            reportAfterLoading();\n\n            loadRevocationList(path);\n\n            reportBeforeLoading();\n\n            currentCount = 0;\n\n            return current;\n        }\n\n        private void reportBeforeLoading() {\n            if (current.isRevoke()) {\n                console.println(Msg.m_rl_revoke_start);\n                console.println(Msg.m_rl_revoke_ballots_before, ballotCount);\n            } else {\n                console.println(Msg.m_rl_restore_start);\n                console.println(Msg.m_rl_restore_ballots_before, ballotCount);\n            }\n        }\n\n        void reportAfterLoading() {\n            if (current != null) {\n                if (current.isRevoke()) {\n                    ballotCount -= currentCount;\n                    console.println(Msg.m_rl_revoke_count, currentCount);\n                    console.println(Msg.m_rl_revoke_ballots_after, ballotCount);\n                    console.println(Msg.m_rl_revoke_done);\n                } else {\n                    ballotCount += currentCount;\n                    console.println(Msg.m_rl_restore_count, currentCount);\n                    console.println(Msg.m_rl_restore_ballots_after, ballotCount);\n                    console.println(Msg.m_rl_restore_done);\n                }\n            }\n        }\n\n        private void loadRevocationList(Path path) {\n            try {\n                console.println();\n                console.println(Msg.m_rl_loading, path);\n                ctx.container.requireContainer(path);\n                Container c = ctx.container.read(path.toString());\n                console.println(Msg.m_rl_loaded);\n\n                ContainerHelper ch = new ContainerHelper(console, c);\n                DataFile file = ch.getSingleFileAndReport(Msg.m_rl_arg_for_cont);\n\n                console.println(Msg.m_rl_checking_integrity);\n                RevocationList rl = Json.read(file.getStream(), RevocationList.class);\n                if (rl.getElection() != null && !rl.getElection().equals(bb.getElection())) {\n                    throw new MessageException(Msg.e_rl_election_id, rl.getElection(),\n                            bb.getElection());\n                }\n                console.println(Msg.m_rl_data_is_integrous);\n\n                Msg totalMsg = rl.isRevoke() ? Msg.m_rl_revoke_total : Msg.m_rl_restore_total;\n                console.println(totalMsg, rl.getPersons().size());\n\n                current = rl;\n                operator = ch.getSignerNames();\n            } catch (Exception e) {\n                throw new MessageException(e, Msg.e_rl_read_error, path, e);\n            }\n        }\n\n        void collect(String voterId, Ballot b, boolean revoke, boolean success) {\n            if (success) {\n                RevokeAction action = revoke ? RevokeAction.REVOKED : RevokeAction.RESTORED;\n\n                revRecords.add(ctx.reporter.newRevocationRecord(action, voterId, b, operator));\n                log2Records.put(voterId, revoke ? ctx.reporter.newLog123Record(voterId, b) : null);\n                currentCount++;\n            } else {\n                Msg key = b == null ? Msg.e_rl_voter_not_found_in_bb\n                        : revoke ? Msg.e_rl_ballot_already_revoked\n                                : Msg.e_rl_ballot_already_restored;\n                Message innerMsg = new Message(key, voterId);\n                Message msg = new Message(Msg.e_rl_processing_error, innerMsg);\n                console.println(msg.key, msg.args);\n            }\n        }\n\n        List<Reporter.LogNRecord> getLog2Records() {\n            return log2Records.values().stream().filter(r -> r != null)\n                    .collect(Collectors.toList());\n        }\n    }\n\n    public static class RevokeArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false);\n        Arg<List<Path>> revLists =\n                Arg.listOfPaths(Msg.arg_revocationlists, true, false).setOptional();\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public RevokeArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(districts);\n            args.add(revLists);\n            args.add(out);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/SquashTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.CorrectnessUtil;\nimport ee.ivxv.common.crypto.elgamal.ElGamalPublicKey;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.service.console.Progress;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.SquashTool.SquashArgs;\nimport ee.ivxv.processor.util.ReportHelper;\n\nimport java.io.Closeable;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SquashTool implements Tool.Runner<SquashArgs> {\n\n    private static final String OUT_BB_TMPL = \"bb-2.json\";\n    private static final String OUT_IVLJSON_TMPL = \"ivoterlist.json\";\n    private static final String OUT_IVLPDF_TMPL = \"ivoterlist.pdf\";\n    private static final String OUT_RR_TMPL = \"revocation-report.csv\";\n    private static final String OUT_LOG_DISCRIMINATOR = \"squash\";\n\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    private final List<Reporter.Record> revocationRecords = new ArrayList<>();\n    private final List<Reporter.LogNRecord> log2Records = new ArrayList<>();\n\n    public SquashTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(SquashArgs args) throws Exception {\n        tool.checkBbChecksum(args.bb.value(), args.bbChecksum.value());\n\n        BallotBox bb = tool.readJsonBb(args.bb.value(), BallotBox.Type.INTEGRITY_CONTROLLED);\n        DistrictList dl = tool.readJsonDistricts(args.districts.value());\n        Path out = args.out.value();\n        ElGamalPublicKey pub = new ElGamalPublicKey(args.encKey.value());\n\n        removeRecurrentVotes(bb);\n        removeInvalidCiphertexts(bb, pub);\n        reporter.writeInvalidVotesErrors(args.out.value());\n\n        Path OUT_IVLJSON = Util.prefixedPath(bb.getElection(), OUT_IVLJSON_TMPL);\n        Path OUT_IVLPDF = Util.prefixedPath(bb.getElection(), OUT_IVLPDF_TMPL);\n        Path OUT_RR = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL);\n        Path OUT_RR_ANONYMOUS = Util.prefixedPath(bb.getElection(), OUT_RR_TMPL + \".anonymous\");\n        Path OUT_BB = Util.prefixedPath(bb.getElection(), OUT_BB_TMPL);\n\n        reporter.writeIVoterList(out.resolve(OUT_IVLJSON), out.resolve(OUT_IVLPDF), bb, dl);\n        reporter.writeRevocationReport(out.resolve(OUT_RR), bb.getElection(), revocationRecords,\n                Reporter.AnonymousFormatter.NOT_ANONYMOUS);\n        reporter.writeRevocationReport(out.resolve(OUT_RR_ANONYMOUS), bb.getElection(), revocationRecords,\n                Reporter.AnonymousFormatter.REVOCATION_REPORT_CSV);\n\n        // There should still be an empty .log2 file even if there are no squashed records\n        reporter.writeEmptyLogFiles(out, OUT_LOG_DISCRIMINATOR, Reporter.LogType.LOG2, bb);\n        reporter.writeLog2(out, bb.getElection(), OUT_LOG_DISCRIMINATOR, log2Records);\n\n        tool.writeJsonBb(bb, out.resolve(OUT_BB));\n\n        return true;\n    }\n\n    private void removeRecurrentVotes(BallotBox bb) {\n        console.println();\n        console.println(Msg.m_removing_recurrent_votes);\n\n        bb.removeRecurrentVotes(this::collect);\n\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private void removeInvalidCiphertexts(BallotBox bb, ElGamalPublicKey pk) {\n        console.println();\n        console.println(Msg.m_removing_invalid_ciphertexts);\n\n        try (CiphertextFilter filter = new CiphertextFilter(pk, bb.getNumberOfBallots())) {\n            bb.removeInvalidCiphertexts(filter::accept);\n        }\n        console.println();\n        console.println(M.m_bb_type, bb.getType());\n        console.println(M.m_bb_numof_ballots, bb.getNumberOfBallots());\n    }\n\n    private void collect(String vid, Ballot b) {\n        revocationRecords.add(ctx.reporter.newRevocationRecordForRecurrentVote(vid, b));\n        log2Records.add(ctx.reporter.newLog123Record(vid, b));\n    }\n\n    private void collectinvalid(String vid, Ballot b, String qid, CorrectnessUtil.CiphertextCorrectness cc, byte[] vote) {\n        revocationRecords.add(ctx.reporter.newRevocationRecordForInvalidVote(vid, b));\n        log2Records.add(ctx.reporter.newLog123Record(vid, b));\n        reporter.reportAbbError(vid, b, qid, cc);\n    }\n\n    private class CiphertextFilter implements BallotBox.VoteFilter, Closeable {\n\n        final ElGamalPublicKey pk;\n        final Progress p;\n\n        public CiphertextFilter(ElGamalPublicKey pk, int total) {\n            this.pk = pk;\n            this.p = console.startProgress(total);\n        }\n\n        @Override\n        public boolean accept(String voterId, Ballot b, String qid, byte[] vote) {\n            CorrectnessUtil.CiphertextCorrectness res = CorrectnessUtil.isValidCiphertext(pk, vote);\n            boolean isValid = res == CorrectnessUtil.CiphertextCorrectness.VALID;\n\n            p.increase(1);\n\n            if (!isValid) {\n                collectinvalid(voterId, b, qid, res, vote);\n            }\n\n            return isValid;\n        }\n\n        @Override\n        public void close() {\n            p.finish();\n        }\n\n    } // class CiphertextFilter\n\n    public static class SquashArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Path> bbChecksum = Arg.aPath(Msg.arg_ballotbox_checksum, true, false);\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false);\n        Arg<Path> encKey = Arg.aPath(Msg.arg_enckey, true, false);\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public SquashArgs() {\n            args.add(bb);\n            args.add(bbChecksum);\n            args.add(districts);\n            args.add(encKey);\n            args.add(out);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/StatsDiffTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.StatsDiffTool.StatsDiffArgs;\nimport ee.ivxv.processor.util.Statistics;\nimport java.nio.file.Path;\n\npublic class StatsDiffTool implements Tool.Runner<StatsDiffArgs> {\n\n    private final I18nConsole console;\n\n    public StatsDiffTool(ProcessorContext ctx) {\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n    }\n\n    @Override\n    public boolean run(StatsDiffArgs args) throws Exception {\n        Statistics compare = Json.read(args.compare.value(), Statistics.class);\n        Statistics to = Json.read(args.to.value(), Statistics.class);\n\n        Statistics diff = new Statistics(compare, to);\n        diff.writeJSON(args.diff.value());\n        console.println(Msg.m_stats_diff_saved, args.diff.value());\n\n        return true;\n    }\n\n    public static class StatsDiffArgs extends Args {\n\n        Arg<Path> compare = Arg.aPath(Msg.arg_compare, true, false);\n        Arg<Path> to = Arg.aPath(Msg.arg_to, true, false);\n        Arg<Path> diff = Arg.aPath(Msg.arg_diff, false, null);\n\n        public StatsDiffArgs() {\n            args.add(compare);\n            args.add(to);\n            args.add(diff);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/tool/StatsTool.java",
    "content": "package ee.ivxv.processor.tool;\n\nimport ee.ivxv.common.M;\nimport ee.ivxv.common.cli.Arg;\nimport ee.ivxv.common.cli.Arg.TreeList;\nimport ee.ivxv.common.cli.Args;\nimport ee.ivxv.common.cli.Tool;\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.SkipCommand;\nimport ee.ivxv.common.model.LName;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.model.VoterList;\nimport ee.ivxv.common.service.bbox.BboxHelper;\nimport ee.ivxv.common.service.bbox.BboxHelper.VoterProvider;\nimport ee.ivxv.common.service.bbox.InvalidBboxException;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.common.util.ToolHelper;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport ee.ivxv.processor.tool.StatsTool.StatsArgs;\nimport ee.ivxv.processor.util.DistrictsMapper;\nimport ee.ivxv.processor.util.ReportHelper;\nimport ee.ivxv.processor.util.Statistics;\nimport ee.ivxv.processor.util.VotersUtil;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.time.LocalDate;\nimport java.util.List;\nimport org.bouncycastle.asn1.ASN1ObjectIdentifier;\nimport org.bouncycastle.asn1.x9.X9ObjectIdentifiers;\n\npublic class StatsTool implements Tool.Runner<StatsArgs> {\n\n    private static final ASN1ObjectIdentifier VL_KEY_ALG_ID = X9ObjectIdentifiers.id_ecPublicKey;\n    private static final LName DUMMY_LNAME = new LName(\"0.0\");\n\n    private static final String OUT_JSON_TMPL = \"stats.json\";\n    private static final String OUT_CSV_TMPL = \"stats.csv\";\n    private static final Long DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES = 32768L;\n\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final ReportHelper reporter;\n    private final ToolHelper tool;\n\n    public StatsTool(ProcessorContext ctx) {\n        this.ctx = ctx;\n        console = new I18nConsole(ctx.i.console, ctx.i.i18n);\n        reporter = new ReportHelper(ctx, console);\n        tool = new ToolHelper(console, ctx.container, ctx.bbox);\n    }\n\n    @Override\n    public boolean run(StatsArgs args) throws Exception {\n        String elid = \"ELECTION\";\n\n        // If there is no district list, then only report total statistics. Otherwise statistics are\n        // reported per district.\n        DistrictList dl = null;\n        if (args.districts.isSet()) {\n            dl = tool.readJsonDistricts(args.districts.value());\n            elid = dl.getElection();\n        }\n\n        Statistics stats;\n        if (args.bb.value().toString().endsWith(\".json\")) {\n            // Do not use tool.readJsonBb, since it forces us to specify a ballot box type,\n            // but we want to be able to compute statistics from any type.\n            BallotBox bb = readJsonBallotBox(args.bb.value());\n            elid = bb.getElection();\n            stats = generateStatistics(args, dl, bb);\n        } else {\n            VoterProvider vp = getVoterProvider(args, dl);\n            reporter.writeVlErrors(args.out.value());\n\n            BboxHelper.IntegrityChecked<?> bb = checkBallotBox(args.bb.value(), args.maxSignedBallotSizeInBytes.value());\n            reporter.writeBbErrors(args.out.value());\n            stats = generateStatistics(args, vp, dl, bb);\n        }\n\n        Path OUT_JSON = Util.prefixedPath(elid, OUT_JSON_TMPL);\n        Path path = args.out.value().resolve(OUT_JSON);\n        stats.writeJSON(path);\n        console.println(Msg.m_stats_json_saved, path);\n\n        Path OUT_CSV = Util.prefixedPath(elid, OUT_CSV_TMPL);\n        path = args.out.value().resolve(OUT_CSV);\n        stats.writeCSV(path);\n        console.println(Msg.m_stats_csv_saved, path);\n\n        return true;\n    }\n\n    private BallotBox readJsonBallotBox(Path path) throws Exception {\n        console.println();\n        console.println(M.m_bb_loading, path);\n        BallotBox bb = Json.read(path, BallotBox.class);\n        console.println(M.m_bb_loaded);\n        console.println(M.m_bb_total_ballots, bb.getNumberOfBallots());\n        return bb;\n    }\n\n    private Statistics generateStatistics(StatsArgs args, DistrictList dl, BallotBox bb) {\n        console.println();\n        console.println(Msg.m_stats_generating);\n        Statistics stats = new Statistics(args.elDay.value(), dl);\n        bb.getBallots().forEach((vid, vb) -> vb.getBallots().forEach(ballot -> {\n            if (args.start.isSet() && ballot.getTime().isBefore(args.start.value())\n                    || args.end.isSet() && ballot.getTime().isAfter(args.end.value())) {\n                return;\n            }\n\n            // Ballot does not contain voter code so assemble new voter. Cannot reuse for\n            // all ballots, since district or station may change.\n            Voter v = new Voter(vid, ballot.getName(), null,\n                    ballot.getParish(), ballot.getDistrict());\n            stats.countVoteFrom(v);\n        }));\n        console.println(Msg.m_stats_generated);\n\n        return stats;\n    }\n\n    private VoterProvider getVoterProvider(StatsArgs args, DistrictList dl) {\n        if (dl == null) {\n            // Use dummy VoterProvider for reporting total statistics only.\n            return (voter, version) -> new Voter(voter, \"\", \"\", \"\",new LName(\"\",\"\"));\n        }\n\n        if (!args.voterLists.isSet()) {\n            // Cannot report per district statistics for Zip ballot box without voter lists.\n            throw new MessageException(Msg.e_vl_voterlists_missing);\n        }\n\n        if (!args.vlKey.isSet()) {\n            throw new MessageException(Msg.e_vl_vlkey_missing);\n        }\n\n        console.println();\n        console.println(Msg.m_vl_reading);\n        VotersUtil.Loader loader = VotersUtil.getLoader(args.vlKey.value(), dl,\n                new DistrictsMapper(), reporter::reportVlErrors);\n\n        console.println();\n        console.println(Msg.m_read);\n        // NB! Must process voter lists in certain order. Using the order of input values.\n        args.voterLists.value().forEach(vl -> {\n            SkipCommand skip_cmd = null;\n            if (vl.skip_cmd.value() != null) {\n                try {\n                    skip_cmd = tool.readSkipCommand(vl.skip_cmd.value());\n                }\n                catch (Exception e) {\n                    throw new MessageException(Msg.e_skip_cmd_loading);\n                }\n            }\n\n            VoterList list = loader.load(vl.path.value(), vl.signature.value(),\n                    vl.skip_cmd.value(), skip_cmd, args.foreignEHAK.value());\n            console.println(Msg.m_vl, list.getName());\n            console.println(Msg.m_vl_type, list.getChangeset());\n            if (skip_cmd != null) {\n                console.println(Msg.m_vl_skipped);\n            }\n            console.println(Msg.m_vl_total_added, list.getAdded().size());\n            console.println(Msg.m_vl_total_removed, list.getRemoved().size());\n            console.println();\n        });\n\n        return loader.getCurrent()::find;\n    }\n\n    private BboxHelper.IntegrityChecked<?> checkBallotBox(Path path, long maxSignedBallotSizeInBytes) {\n        try {\n            int tc = ctx.args.threads.value();\n            BboxHelper.Loader<?> loader = ctx.bbox.getLoader(path, console::startProgress, tc);\n\n            console.println();\n            console.println(M.m_bb_loading, path);\n            BboxHelper.BboxLoader<?> bbLoader = loader.getBboxLoader(path, reporter::reportBbError, maxSignedBallotSizeInBytes);\n            console.println(M.m_bb_loaded);\n            console.println(M.m_bb_numof_collector_ballots, bbLoader.getNumberOfValidBallots());\n\n            console.println(M.m_bb_checking_integrity);\n            BboxHelper.IntegrityChecked<?> bb = bbLoader.checkIntegrity();\n            console.println(M.m_bb_data_is_integrous);\n            console.println(M.m_bb_total_ballots, bb.getNumberOfValidBallots());\n\n            return bb;\n        } catch (InvalidBboxException e) {\n            throw new MessageException(e, Msg.e_bb_read_error, e.path, e);\n        }\n    }\n\n    private Statistics generateStatistics(StatsArgs args, VoterProvider vp, DistrictList dl,\n            BboxHelper.IntegrityChecked<?> bbox) {\n        console.println();\n        console.println(Msg.m_stats_generating);\n        Statistics stats = new Statistics(args.elDay.value(), dl);\n        bbox.listVoters(args.start.value(), args.end.value(), vp, stats::countVoteFrom);\n        console.println(Msg.m_stats_generated);\n        console.println(Msg.m_stats_ballot_errors, reporter.countBbErrors());\n        console.println(Msg.m_stats_valid_ballots, stats.getTotalCount().get());\n\n        return stats;\n    }\n\n    public static class StatsArgs extends Args {\n\n        Arg<Path> bb = Arg.aPath(Msg.arg_ballotbox, true, false);\n        Arg<Long> maxSignedBallotSizeInBytes = Arg\n                .aLong(Msg.arg_signed_ballot_max_size_bytes)\n                .setOptional()\n                .setDefault(DEFAULT_MAX_VOTE_BDOC_SIZE_IN_BYTES);\n        Arg<LocalDate> elDay = Arg.aLocalDate(Msg.arg_election_day);\n        Arg<Instant> start = Arg.anInstant(Msg.arg_period_start).setOptional();\n        Arg<Instant> end = Arg.anInstant(Msg.arg_period_end).setOptional();\n        Arg<Path> districts = Arg.aPath(Msg.arg_districts, true, false).setOptional();\n        Arg<PublicKeyHolder> vlKey = Arg.aPublicKey(Msg.arg_vlkey, VL_KEY_ALG_ID).setOptional();\n        Arg<List<VoterListEntry>> voterLists =\n                new TreeList<>(Msg.arg_voterlists, VoterListEntry::new).setOptional();\n        Arg<String> foreignEHAK = Arg.aString(Msg.arg_voterforeignehak).setOptional();\n\n        Arg<Path> out = Arg.aPath(Msg.arg_out, false, null);\n\n        public StatsArgs() {\n            args.add(bb);\n            args.add(maxSignedBallotSizeInBytes);\n            args.add(elDay);\n            args.add(start);\n            args.add(end);\n            args.add(districts);\n            args.add(vlKey);\n            args.add(voterLists);\n            args.add(foreignEHAK);\n            args.add(out);\n        }\n\n    }\n\n    static class VoterListEntry extends Args {\n        Arg<Path> path = Arg.aPath(Msg.arg_path, true, false);\n        Arg<Path> signature = Arg.aPath(Msg.arg_signature, true, false);\n        Arg<Path> skip_cmd = Arg.aPath(Msg.arg_skip_cmd, true, false).setOptional();\n\n        VoterListEntry() {\n            args.add(path);\n            args.add(signature);\n            args.add(skip_cmd);\n        }\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/util/DistrictsMapper.java",
    "content": "package ee.ivxv.processor.util;\n\nimport ee.ivxv.common.model.LName;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class DistrictsMapper {\n\n    private static final Logger log = LoggerFactory.getLogger(DistrictsMapper.class);\n\n    private static final String SEPARATOR = \"\\t\";\n\n    /** Map from district id to map from station id to district-station pair. */\n    private final Map<String, Map<String, LocationPair>> mapping = new LinkedHashMap<>();\n\n    public DistrictsMapper() {\n        // Default constructor, no re-mapping\n    }\n\n    public DistrictsMapper(InputStream in) throws Exception {\n        try (BufferedReader br = new BufferedReader(new InputStreamReader(in, Util.CHARSET))) {\n            br.lines() //\n                    .filter(s -> !s.isEmpty()) //\n                    .forEach(s -> {\n                        String[] r = s.split(SEPARATOR);\n\n                        if (r.length != 8) {\n                            throw new MessageException(Msg.e_dist_mapping_invalid_row, s);\n                        }\n                        LName fromStat = new LName(r[0], r[1]);\n                        LName fromDist = new LName(r[2], r[3]);\n\n                        LName toStat = new LName(r[4], r[5]);\n                        LName toDist = new LName(r[6], r[7]);\n\n                        mapping.computeIfAbsent(fromDist.getId(), x -> new LinkedHashMap<>())\n                                .put(fromStat.getId(), new LocationPair(toDist, toStat));\n                    });\n        }\n        mapping.forEach((d, smap) -> smap\n                .forEach((s, res) -> log.info(\"Mapping district / station {} {} -> {} {}\", d, s,\n                        res.district.getId(), res.station.getId())));\n    }\n\n    public LocationPair get(String districtId, String stationId) {\n        return Optional.ofNullable(mapping.get(districtId)) //\n                .map(d -> d.get(stationId))\n                .orElse(new LocationPair(new LName(districtId), new LName(stationId)));\n    }\n\n    public static class LocationPair {\n        public final LName district;\n        public final LName station;\n\n        public LocationPair(LName district, LName station) {\n            this.district = district;\n            this.station = station;\n        }\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/util/ReportHelper.java",
    "content": "package ee.ivxv.processor.util;\n\nimport ee.ivxv.common.crypto.CorrectnessUtil.CiphertextCorrectness;\nimport ee.ivxv.common.model.Ballot;\nimport ee.ivxv.common.model.BallotBox;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.service.bbox.Ref;\nimport ee.ivxv.common.service.bbox.Result;\nimport ee.ivxv.common.service.i18n.Message;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.service.report.Reporter;\nimport ee.ivxv.common.service.report.Reporter.LogType;\nimport ee.ivxv.common.service.report.Reporter.Record;\nimport ee.ivxv.common.util.I18nConsole;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\nimport ee.ivxv.processor.ProcessorContext;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.atomic.LongAdder;\nimport java.util.function.BiPredicate;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class ReportHelper {\n\n    private static final Logger log = LoggerFactory.getLogger(ReportHelper.class);\n\n    public static final String OUT_VL_ERR = \"voterlist_errors.txt\";\n    public static final String OUT_BB_ERR = \"ballotbox_errors.txt\";\n    public static final String OUT_INVALID_ERR = \"invalid_votes.txt\";\n\n    private final ProcessorContext ctx;\n    private final I18nConsole console;\n    private final Map<Result, LongAdder> bbErrors = new ConcurrentHashMap<>();\n    private final Map<String, Queue<String>> errors = new ConcurrentHashMap<>();\n\n    public ReportHelper(ProcessorContext ctx, I18nConsole console) {\n        this.ctx = ctx;\n        this.console = console;\n    }\n\n    public void writeLog1(Path dir, BallotBox bb, String disc) {\n        writeLogN(dir, bb, disc, LogType.LOG1, (x, y) -> true);\n    }\n\n    public void writeLog2(Path dir, BallotBox bb, String disc, BiPredicate<String, String> filter) {\n        writeLogN(dir, bb, disc, LogType.LOG2, filter);\n    }\n\n    public void writeLog3(Path dir, BallotBox bb, String disc, BiPredicate<String, String> filter) {\n        writeLogN(dir, bb, disc, LogType.LOG3, filter);\n    }\n\n    public void writeLog2(Path dir, String eid, String disc, List<Reporter.LogNRecord> records) {\n        writeLogN(dir, eid, disc, LogType.LOG2, records.stream());\n    }\n\n    public void writeEmptyLogFiles(Path outputDir, String logDiscriminator, LogType type, BallotBox ballotBox) {\n        Set<String> questionIDs = new HashSet<>();\n\n        ballotBox.getBallots().forEach((voterId, voterBallots) -> voterBallots.getBallots()\n                .forEach(voterBallot -> questionIDs.addAll(voterBallot.getVotes().keySet())));\n\n        // Does not print the output files to the console to avoid double printing.\n        // Potential improvement: get rid of the double printing issue to consistently print the\n        // output filename.\n        questionIDs.forEach(questionID -> ctx.reporter.writeEmptyLog(outputDir, ballotBox.getElection(),\n                logDiscriminator, type, questionID));\n        }\n\n    private void writeLogN(Path dir, String eid, String disc, LogType type,\n            Stream<Reporter.LogNRecord> records) {\n        try {\n            console.println();\n            console.println(Msg.m_writing_log_n, type.value);\n            Map<String, Path> paths = ctx.reporter.writeLogN(dir, eid, disc, type, records);\n            paths.values().forEach(p -> console.println(Msg.m_output_file, p));\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_writing_log_n, type.value, dir, e);\n        }\n    }\n\n    private void writeLogN(Path dir, BallotBox bb, String disc, LogType type,\n            BiPredicate<String, String> filter) {\n        Map<String, List<Record>> rmap = new LinkedHashMap<>();\n\n        bb.getBallots()\n                .forEach((voterId, vb) -> vb.getBallots() //\n                        .forEach(b -> b.getVotes().keySet().stream()\n                                .filter(qid -> filter.test(voterId, qid)) //\n                                .forEach(qid -> rmap.computeIfAbsent(qid, x -> new ArrayList<>())\n                                        .add(ctx.reporter.newLog123Record(voterId, b, qid)))));\n\n        try {\n            console.println();\n            console.println(Msg.m_writing_log_n, type.value);\n            Map<String, Path> paths = ctx.reporter.writeRecords(dir, bb.getElection(), disc, type, rmap);\n            paths.values().forEach(p -> console.println(Msg.m_output_file, p));\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_writing_log_n, type.value, dir, e);\n        }\n    }\n\n    public void writeIVoterList(Path jsonOut, Path pdfOut, BallotBox bb, DistrictList dl) {\n        try {\n            console.println();\n            console.println(Msg.m_writing_ivoter_list);\n            ctx.reporter.writeIVoterList(jsonOut, pdfOut, bb, dl);\n            console.println(Msg.m_output_file, jsonOut);\n            if (pdfOut != null) {\n                console.println(Msg.m_output_file, pdfOut);\n            }\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_writing_ivoter_list, e);\n        }\n    }\n\n    public void writeRevocationReport(Path out, String electionId, List<Reporter.Record> records, Reporter.AnonymousFormatter formatter) {\n        try {\n            console.println();\n            console.println(Msg.m_writing_revocation_report);\n            ctx.reporter.write(out, electionId, records, formatter);\n            console.println(Msg.m_output_file, out);\n        } catch (Exception e) {\n            throw new MessageException(e, Msg.e_writing_revocation_report, out, e);\n        }\n    }\n\n    public void reportVlErrors(Enum<?> key, Object... args) {\n        reportErrors(OUT_VL_ERR, key, args);\n    }\n\n    public void reportBbError(Ref.BbRef ref, Result res, Object... args) {\n        String voter = ref == null ? \"?\" : ref.voter;\n        String ballot = ref == null ? \"?\" : ref.ballot;\n        String r = String.format(\"%s/%s\", voter, ballot);\n        log.error(\"Error while reading ballot box: {}, {}, {}\", voter, ballot, res);\n        reportBbError(m -> new Message(Msg.e_bb_ballot_processing, voter, ballot, m), r, res, args);\n    }\n\n    public void reportRegError(Ref.RegRef ref, Result res, Object... args) {\n        String r = ref == null ? \"?\" : ref.ref;\n        log.error(\"Error while reading registration data: {}, {}\", r, res);\n        reportBbError(m -> new Message(Msg.e_reg_record_processing, r, m), r, res, args);\n    }\n\n    private void reportBbError(Function<Message, Message> provider, String ref, Result res,\n            Object... args) {\n        bbErrors.computeIfAbsent(res, r -> new LongAdder()).increment();\n        Msg key = translate(res);\n        if (key == null) {\n            return;\n        }\n        Message innerMsg = new Message(key, args);\n        Message msg = provider.apply(innerMsg);\n        reportErrors(OUT_BB_ERR, s -> String.format(\"%s\\t%s\\t%s\", ref, res, s), msg.key, msg.args);\n    }\n\n    public void reportAbbError(String voterId, Ballot b, String qid, CiphertextCorrectness res) {\n        Msg key = translate(res);\n        if (key == null) {\n            return;\n        }\n        Message innerMsg = new Message(key);\n        Message msg = new Message(Msg.e_bb_ciphertext_checking, voterId, b.getId(), qid, innerMsg);\n        String ref = String.format(\"%s/%s\", voterId, b.getId());\n\n        reportErrors(OUT_INVALID_ERR, s -> String.format(\"%s\\t%s\\t%s\", ref, res, s), msg.key, msg.args);\n    }\n\n    private void reportErrors(String type, Enum<?> key, Object... args) {\n        reportErrors(type, s -> s, key, args);\n    }\n\n    private void reportErrors(String type, Function<String, String> fmt, Enum<?> key,\n            Object... args) {\n        if (!ctx.args.quiet.value()) {\n            console.println(key, args);\n        }\n        errors.computeIfAbsent(type, x -> new ConcurrentLinkedQueue<>())\n                .add(fmt.apply(console.i18n.get(key, args)));\n    }\n\n    private Msg translate(Result res) {\n        switch (res) {\n            case INVALID_FILE_NAME:\n                return Msg.e_bb_invalid_file_name;\n            case INVALID_FILE_SIZE:\n                return Msg.e_bb_invalid_file_size;\n            case MISSING_FILE:\n                return Msg.e_bb_missing_file;\n            case REPEATED_FILE:\n                return Msg.e_bb_repeated_file;\n            case UNKNOWN_FILE_TYPE:\n                return Msg.e_bb_unknown_file_type;\n            case INVALID_BALLOT_SIGNATURE:\n                return Msg.e_ballot_signature_invalid;\n            case MISSING_VOTER_SIGNATURE:\n                return Msg.e_ballot_missing_voter_signature;\n            case VOTER_NOT_FOUND:\n                return Msg.e_active_voter_not_found;\n            case VOTERLIST_NOT_FOUND:\n                return Msg.e_active_voterlist_not_found;\n            case TIME_BEFORE_START:\n                return Msg.e_time_before_start;\n            case REG_RESP_INVALID:\n                return Msg.e_reg_resp_invalid;\n            case REG_REQ_INVALID:\n                return Msg.e_reg_req_invalid;\n            case REG_RESP_NOT_UNIQUE:\n                return Msg.e_reg_resp_not_unique;\n            case REG_REQ_NOT_UNIQUE:\n                return Msg.e_reg_req_not_unique;\n            case REG_NO_NONCE:\n                return Msg.e_reg_resp_no_nonce;\n            case REG_NONCE_NOT_SIG:\n                return Msg.e_reg_resp_nonce_not_sig;\n            case REG_NONCE_ALG_MISMATCH:\n                return Msg.e_reg_resp_nonce_alg_mismatch;\n            case REG_NONCE_SIG_INVALID:\n                return Msg.e_reg_resp_nonce_sig_invalid;\n            case UNKNOWN_FILE_IN_VOTE_CONTAINER:\n                return Msg.e_unknown_file_in_vote_container;\n            case TECHNICAL_ERROR:\n                return Msg.e_tehcnical_error;\n            case REG_RESP_REQ_UNMATCH:\n                return Msg.e_reg_resp_req_unmatch;\n            case REG_REQ_WITHOUT_BALLOT:\n                return Msg.e_reg_req_without_ballot;\n            case BALLOT_WITHOUT_REG_REQ:\n                return Msg.e_ballot_without_reg_req;\n            case SAME_TIME_AS_LATEST:\n                return Msg.e_same_time_as_latest;\n            case INVALID_SIGNATURE_PROFILE:\n                return Msg.e_invalid_signature_profile;\n            case OK:\n                return null;\n            default:\n                throw new RuntimeException(\"Unhandled ballot box processing result: \" + res);\n        }\n    }\n\n    private Msg translate(CiphertextCorrectness res) {\n        switch (res) {\n            case INVALID:\n                return Msg.e_ciphertext_invalid;\n            case INVALID_BYTES:\n                return Msg.e_ciphertext_invalid_bytes;\n            case INVALID_GROUP:\n                return Msg.e_ciphertext_invalid_group;\n            case INVALID_POINT:\n                return Msg.e_ciphertext_invalid_point;\n            case INVALID_QR:\n                return Msg.e_ciphertext_invalid_qr;\n            case INVALID_RANGE:\n                return Msg.e_ciphertext_invalid_range;\n            case VALID:\n                return null;\n            default:\n                throw new RuntimeException(\"Unhandled ciphertext correctness result: \" + res);\n        }\n    }\n\n    public long countBbErrors() {\n        return bbErrors.values().stream().mapToLong(LongAdder::sum).sum();\n    }\n\n    public long countBbErrors(Result type) {\n        return bbErrors.getOrDefault(type, new LongAdder()).sum();\n    }\n\n    public void writeVlErrors(Path out) {\n        writeErrors(out, OUT_VL_ERR, Msg.e_vl_error_report);\n    }\n\n    public void writeBbErrors(Path out) {\n        writeErrors(out, OUT_BB_ERR, Msg.e_bb_error_report);\n    }\n\n    public void writeInvalidVotesErrors(Path out) {\n        writeErrors(out, OUT_INVALID_ERR, Msg.e_invalid_error_report);\n    }\n\n    private void writeErrors(Path out, String file, Enum<?> key) {\n        Path path = out.resolve(file);\n        try {\n            // The file should always be created for consistency...\n            Util.createFile(path);\n\n            // ...however the file may be empty if there is nothing to report.\n            Queue<String> errs = errors.get(file);\n            if (errs == null || errs.isEmpty()) {\n                return;\n            }\n\n            // Only mention that there were errors if there indeed were any.\n            console.println(key, path);\n            Files.write(path, errs);\n        } catch (Exception e) {\n            log.error(\"Error occurred while writing error report {}: {}\", file, e.getMessage(), e);\n            throw new MessageException(Msg.e_writing_error_report, path, e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/util/Statistics.java",
    "content": "package ee.ivxv.processor.util;\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.Json;\nimport ee.ivxv.processor.Msg;\nimport java.io.IOException;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.LocalDate;\nimport java.time.Period;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.time.format.ResolverStyle;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport java.util.stream.Collectors;\n\n@JsonIgnoreProperties({\"meta\"})\npublic class Statistics {\n\n    public static final String TOTAL_DISTRICT = \"TOTAL\";\n\n    private static final DateTimeFormatter ESTONIAN_DOB_FMT =\n            DateTimeFormatter.ofPattern(\"uuuuMMdd\").withResolverStyle(ResolverStyle.STRICT);\n    private static final String CSV_SEPARATOR = \",\";\n    private static final String CSV_QUOTE = \"\\\"\";\n\n    private static final String AGE_GROUP_16_17 = \"age_group_16-17\";\n    private static final String AGE_GROUP_18_24 = \"age_group_18-24\";\n    private static final String AGE_GROUP_25_34 = \"age_group_25-34\";\n    private static final String AGE_GROUP_35_44 = \"age_group_35-44\";\n    private static final String AGE_GROUP_45_54 = \"age_group_45-54\";\n    private static final String AGE_GROUP_55_64 = \"age_group_55-64\";\n    private static final String AGE_GROUP_65_74 = \"age_group_65-74\";\n    private static final String AGE_GROUP_75PLUS = \"age_group_75plus\";\n    private static final String REVOTERS_2_TIMES = \"revoters-2-times\";\n    private static final String REVOTERS_3_TIMES = \"revoters-3-times\";\n    private static final String REVOTERS_MORE_THAN_3_TIMES = \"revoters-more-than-3-times\";\n    private static final String REVOTERS_TOTAL = \"revoters-total\";\n    private static final String TOTAL_VOTERS = \"total-voters\";\n    private static final String TOTAL_VOTES_COLLECTED = \"total-votes-collected\";\n    private static final String VOTERS_FEMALES = \"voters-females\";\n    private static final String VOTERS_MALES = \"voters-males\";\n\n    private final boolean readOnly;\n    private final LocalDate ageDate;\n    private final SortedMap<String, Statistics.Block> districts;\n    private final boolean withDistricts;\n\n    public Statistics(LocalDate ageDate, DistrictList dl) {\n        this.readOnly = false;\n        this.ageDate = ageDate;\n        this.districts = new TreeMap<>();\n        this.districts.put(TOTAL_DISTRICT, new Block());\n        this.withDistricts = dl != null;\n        if (withDistricts) {\n            dl.getDistricts().keySet().forEach(district -> districts.put(district, new Block()));\n        }\n    }\n\n    /**\n     * Statistics created via this constructor are read-only and calls to countVoteFrom are no-ops.\n     */\n    @JsonCreator\n    public Statistics(@JsonProperty(\"data\") Map<String, Statistics.Block> districts) {\n        this.readOnly = true;\n        this.ageDate = null; // Unused in read-only mode.\n        this.districts = new TreeMap<>(districts);\n        this.withDistricts = false; // Unused in read-only mode.\n    }\n\n    /**\n     * Constructs a new read-only Statistics which represents the difference between two Statistics.\n     * <p>\n     * Each district is the result of subtracting values of the <tt>to</tt> Block from the values of\n     * the corresponding <tt>compare</tt> Block. If a district is missing from either parameter,\n     * then it is counted as a Block with all zero values.\n     * \n     * @param compare the base for the comparison\n     * @param to the Statistics to compare to\n     */\n    public Statistics(Statistics compare, Statistics to) {\n        this.readOnly = true;\n        this.ageDate = null; // Unused in read-only mode.\n        this.districts = new TreeMap<>();\n        this.withDistricts = false; // Unused in read-only mode.\n\n        Set<String> allDistricts = new HashSet<>(compare.districts.keySet());\n        allDistricts.addAll(to.districts.keySet());\n\n        for (String district : allDistricts) {\n            districts.put(district, new Block(//\n                    compare.districts.getOrDefault(district, new Block()),\n                    to.districts.getOrDefault(district, new Block())));\n        }\n    }\n\n    public void countVoteFrom(Voter voter) {\n        if (readOnly) {\n            return;\n        }\n        String code = voter.getCode();\n        int age = getAgeFromEstonianCode(code, ageDate);\n        boolean female = isFemaleFromEstonianCode(code);\n        districts.get(TOTAL_DISTRICT).countVoteFrom(code, age, female);\n        if (withDistricts) {\n            districts.get(voter.getDistrict().getId()).countVoteFrom(code, age, female);\n        }\n    }\n\n    /**\n     * @param code Estonian national person number\n     * @param date\n     * @return the age of the person relative to date\n     */\n    private static int getAgeFromEstonianCode(String code, LocalDate date) {\n        if (code.length() != 11) {\n            throw new MessageException(Msg.e_stats_code_not_estonian, code);\n        }\n        String yearPrefix;\n        switch (code.codePointAt(0)) {\n            case '1':\n            case '2':\n                yearPrefix = \"18\";\n                break;\n            case '3':\n            case '4':\n                yearPrefix = \"19\";\n                break;\n            case '5':\n            case '6':\n                yearPrefix = \"20\";\n                break;\n            case '7':\n            case '8':\n                yearPrefix = \"21\";\n                break;\n            default:\n                throw new MessageException(Msg.e_stats_code_not_estonian, code);\n        }\n        try {\n            LocalDate dob = LocalDate.parse(yearPrefix + code.substring(1, 7), ESTONIAN_DOB_FMT);\n            return Period.between(dob, date).getYears();\n        } catch (DateTimeParseException e) {\n            throw new MessageException(e, Msg.e_stats_code_not_estonian, code);\n        }\n    }\n\n    /**\n     * @param code Estonian national person number\n     * @return if the person is female\n     */\n    private static boolean isFemaleFromEstonianCode(String code) {\n        if (code.length() != 11) {\n            throw new MessageException(Msg.e_stats_code_not_estonian, code);\n        }\n        switch (code.codePointAt(0)) {\n            case '1':\n            case '3':\n            case '5':\n            case '7':\n                return false;\n            case '2':\n            case '4':\n            case '6':\n            case '8':\n                return true;\n            default:\n                throw new MessageException(Msg.e_stats_code_not_estonian, code);\n        }\n    }\n\n    /**\n     * @return number of total votes collected, or empty Optional if no \"TOTAL\" district.\n     */\n    @JsonIgnore\n    public Optional<Integer> getTotalCount() {\n        return Optional.ofNullable(districts.get(TOTAL_DISTRICT))\n                .map(Block::getTotalVotesCollected);\n    }\n\n    @JsonProperty(\"data\")\n    public Map<String, Statistics.Block> getDistricts() {\n        return Collections.unmodifiableMap(districts);\n    }\n\n    public void writeJSON(Path path) throws Exception {\n        Json.write(this, path);\n    }\n\n    public void writeCSV(Path path) throws Exception {\n        if (path.getParent() != null) {\n            Files.createDirectories(path.getParent());\n        }\n        try (Writer writer =\n                new OutputStreamWriter(Files.newOutputStream(path), StandardCharsets.UTF_8)) {\n            writeCSVHeader(writer);\n            for (String district : districts.keySet()) {\n                writeCSVRecord(writer, district, districts.get(district));\n            }\n        }\n    }\n\n    private static void writeCSVHeader(Writer writer) throws IOException {\n        writer.append(CSV_QUOTE)\n                .append(String.join(CSV_QUOTE + CSV_SEPARATOR + CSV_QUOTE, \"district\",\n                        AGE_GROUP_16_17, AGE_GROUP_18_24, AGE_GROUP_25_34, AGE_GROUP_35_44,\n                        AGE_GROUP_45_54, AGE_GROUP_55_64, AGE_GROUP_65_74, AGE_GROUP_75PLUS,\n                        REVOTERS_2_TIMES, REVOTERS_3_TIMES, REVOTERS_MORE_THAN_3_TIMES,\n                        REVOTERS_TOTAL, TOTAL_VOTERS, TOTAL_VOTES_COLLECTED, VOTERS_FEMALES,\n                        VOTERS_MALES))\n                .append(CSV_QUOTE).append('\\n');\n    }\n\n    private static void writeCSVRecord(Writer writer, String district, Block block)\n            throws IOException {\n        writer.append(CSV_QUOTE).append(district).append(CSV_QUOTE).append(CSV_SEPARATOR)\n                .append(Arrays.asList(block.getAgeGroup1617(), block.getAgeGroup1824(),\n                        block.getAgeGroup2534(), block.getAgeGroup3544(), block.getAgeGroup4554(),\n                        block.getAgeGroup5564(), block.getAgeGroup6574(), block.getAgeGroup75Plus(),\n                        block.getRevoters2Times(), block.getRevoters3Times(),\n                        block.getRevotersMoreThan3Times(), block.getRevotersTotal(),\n                        block.getTotalVoters(), block.getTotalVotesCollected(),\n                        block.getVotersFemales(), block.getVotersMales()) //\n                        .stream().map(String::valueOf).collect(Collectors.joining(CSV_SEPARATOR)))\n                .append('\\n');\n    }\n\n    @JsonPropertyOrder({AGE_GROUP_16_17, AGE_GROUP_18_24, AGE_GROUP_25_34, AGE_GROUP_35_44,\n            AGE_GROUP_45_54, AGE_GROUP_55_64, AGE_GROUP_65_74, AGE_GROUP_75PLUS, REVOTERS_2_TIMES,\n            REVOTERS_3_TIMES, REVOTERS_MORE_THAN_3_TIMES, REVOTERS_TOTAL, TOTAL_VOTERS,\n            TOTAL_VOTES_COLLECTED, VOTERS_FEMALES, VOTERS_MALES})\n    @JsonIgnoreProperties(ignoreUnknown = true) // Allow for Blocks with extra data.\n    public static class Block {\n\n        private final boolean readOnly;\n        private final Map<String, Integer> voters;\n        private final Map<Integer, Integer> ages;\n        private int revoters2Times;\n        private int revoters3Times;\n        private int revotersMoreThan3Times;\n        private int revotersTotal;\n        private int totalVoters;\n        private int totalVotesCollected;\n        private int votersFemales;\n        private int votersMales;\n\n        private Block() {\n            this.readOnly = false;\n            this.voters = new HashMap<>();\n            this.ages = new HashMap<>();\n        }\n\n        @JsonCreator\n        public Block(//\n                @JsonProperty(AGE_GROUP_16_17) int ageGroup1617,\n                @JsonProperty(AGE_GROUP_18_24) int ageGroup1824,\n                @JsonProperty(AGE_GROUP_25_34) int ageGroup2534,\n                @JsonProperty(AGE_GROUP_35_44) int ageGroup3544,\n                @JsonProperty(AGE_GROUP_45_54) int ageGroup4554,\n                @JsonProperty(AGE_GROUP_55_64) int ageGroup5564,\n                @JsonProperty(AGE_GROUP_65_74) int ageGroup6574,\n                @JsonProperty(AGE_GROUP_75PLUS) int ageGroup75plus,\n                @JsonProperty(REVOTERS_2_TIMES) int revoters2Times,\n                @JsonProperty(REVOTERS_3_TIMES) int revoters3Times,\n                @JsonProperty(REVOTERS_MORE_THAN_3_TIMES) int revotersMoreThan3Times,\n                @JsonProperty(REVOTERS_TOTAL) int revotersTotal,\n                @JsonProperty(TOTAL_VOTERS) int totalVoters,\n                @JsonProperty(TOTAL_VOTES_COLLECTED) int totalVotesCollected,\n                @JsonProperty(VOTERS_FEMALES) int votersFemales,\n                @JsonProperty(VOTERS_MALES) int votersMales) {\n            this.readOnly = true;\n            this.voters = null; // Unused in read-only mode.\n            this.ages = new HashMap<>();\n            this.ages.put(16, ageGroup1617);\n            this.ages.put(18, ageGroup1824);\n            this.ages.put(25, ageGroup2534);\n            this.ages.put(35, ageGroup3544);\n            this.ages.put(45, ageGroup4554);\n            this.ages.put(55, ageGroup5564);\n            this.ages.put(65, ageGroup6574);\n            this.ages.put(75, ageGroup75plus);\n            this.revoters2Times = revoters2Times;\n            this.revoters3Times = revoters3Times;\n            this.revotersMoreThan3Times = revotersMoreThan3Times;\n            this.revotersTotal = revotersTotal;\n            this.totalVoters = totalVoters;\n            this.totalVotesCollected = totalVotesCollected;\n            this.votersFemales = votersFemales;\n            this.votersMales = votersMales;\n        }\n\n        private Block(Block compare, Block to) {\n            this.readOnly = true;\n            this.voters = null; // Unused in read-only mode.\n            this.ages = new HashMap<>(compare.ages);\n            for (Map.Entry<Integer, Integer> age : to.ages.entrySet()) {\n                this.ages.merge(age.getKey(), -age.getValue(), Integer::sum);\n            }\n            this.revoters2Times = compare.revoters2Times - to.revoters2Times;\n            this.revoters3Times = compare.revoters3Times - to.revoters3Times;\n            this.revotersMoreThan3Times =\n                    compare.revotersMoreThan3Times - to.revotersMoreThan3Times;\n            this.revotersTotal = compare.revotersTotal - to.revotersTotal;\n            this.totalVoters = compare.totalVoters - to.totalVoters;\n            this.totalVotesCollected = compare.totalVotesCollected - to.totalVotesCollected;\n            this.votersFemales = compare.votersFemales - to.votersFemales;\n            this.votersMales = compare.votersMales - to.votersMales;\n        }\n\n        private void countVoteFrom(String code, int age, boolean female) {\n            if (readOnly) {\n                return;\n            }\n            switch (voters.merge(code, 1, Integer::sum)) {\n                case 1:\n                    ages.merge(age, 1, Integer::sum);\n                    totalVoters++;\n                    if (female) {\n                        votersFemales++;\n                    } else {\n                        votersMales++;\n                    }\n                    break;\n                case 2:\n                    revoters2Times++;\n                    revotersTotal++;\n                    break;\n                case 3:\n                    revoters2Times--;\n                    revoters3Times++;\n                    break;\n                case 4:\n                    revoters3Times--;\n                    revotersMoreThan3Times++;\n                    break;\n            }\n            totalVotesCollected++;\n        }\n\n        private int getAgeGroup(Integer min, Integer max) {\n            return ages.entrySet().stream() //\n                    .filter(entry -> min == null || min <= entry.getKey())\n                    .filter(entry -> max == null || max >= entry.getKey())\n                    .mapToInt(entry -> entry.getValue()).sum();\n        }\n\n        /*\n         * Fields to include in the statistics output.\n         */\n\n        @JsonProperty(AGE_GROUP_16_17)\n        public int getAgeGroup1617() {\n            return getAgeGroup(16, 17);\n        }\n\n        @JsonProperty(AGE_GROUP_18_24)\n        public int getAgeGroup1824() {\n            return getAgeGroup(18, 24);\n        }\n\n        @JsonProperty(AGE_GROUP_25_34)\n        public int getAgeGroup2534() {\n            return getAgeGroup(25, 34);\n        }\n\n        @JsonProperty(AGE_GROUP_35_44)\n        public int getAgeGroup3544() {\n            return getAgeGroup(35, 44);\n        }\n\n        @JsonProperty(AGE_GROUP_45_54)\n        public int getAgeGroup4554() {\n            return getAgeGroup(45, 54);\n        }\n\n        @JsonProperty(AGE_GROUP_55_64)\n        public int getAgeGroup5564() {\n            return getAgeGroup(55, 64);\n        }\n\n        @JsonProperty(AGE_GROUP_65_74)\n        public int getAgeGroup6574() {\n            return getAgeGroup(65, 74);\n        }\n\n        @JsonProperty(AGE_GROUP_75PLUS)\n        public int getAgeGroup75Plus() {\n            return getAgeGroup(75, null);\n        }\n\n        @JsonProperty(REVOTERS_2_TIMES)\n        public int getRevoters2Times() {\n            return revoters2Times;\n        }\n\n        @JsonProperty(REVOTERS_3_TIMES)\n        public int getRevoters3Times() {\n            return revoters3Times;\n        }\n\n        @JsonProperty(REVOTERS_MORE_THAN_3_TIMES)\n        public int getRevotersMoreThan3Times() {\n            return revotersMoreThan3Times;\n        }\n\n        @JsonProperty(REVOTERS_TOTAL)\n        public int getRevotersTotal() {\n            return revotersTotal;\n        }\n\n        @JsonProperty(TOTAL_VOTERS)\n        public int getTotalVoters() {\n            return totalVoters;\n        }\n\n        @JsonProperty(TOTAL_VOTES_COLLECTED)\n        public int getTotalVotesCollected() {\n            return totalVotesCollected;\n        }\n\n        @JsonProperty(VOTERS_FEMALES)\n        public int getVotersFemales() {\n            return votersFemales;\n        }\n\n        @JsonProperty(VOTERS_MALES)\n        public int getVotersMales() {\n            return votersMales;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "processor/src/main/java/ee/ivxv/processor/util/VotersUtil.java",
    "content": "package ee.ivxv.processor.util;\n\nimport ee.ivxv.common.crypto.CryptoUtil.PublicKeyHolder;\nimport ee.ivxv.common.model.District;\nimport ee.ivxv.common.model.DistrictList;\nimport ee.ivxv.common.model.LName;\nimport ee.ivxv.common.model.Voter;\nimport ee.ivxv.common.model.VoterList;\nimport ee.ivxv.common.model.SkipCommand;\nimport ee.ivxv.common.service.i18n.MessageException;\nimport ee.ivxv.common.util.Util;\nimport ee.ivxv.processor.Msg;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeParseException;\nimport java.util.HashSet;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.Arrays;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport org.bouncycastle.asn1.x9.X9ObjectIdentifiers;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class VotersUtil {\n\n    static final Logger log = LoggerFactory.getLogger(VotersUtil.class);\n\n    private static final String SEPARATOR = \"\\t\";\n    private static final String VERSION = \"2\";\n    private static final String FOREIGN = \"FOREIGN\";\n    private static final String DEFAULT_EHAK = \"0000\";\n    private static final String KUSTUTAMINE = \"kustutamine\";\n\n    public static Loader getLoader(PublicKeyHolder key, DistrictList dl, DistrictsMapper mapper,\n                                   Reporter reporter) {\n        return new Loader(key, dl, mapper, reporter);\n    }\n\n\n    static String readHeaderRow(String s) {\n        if (s == null) {\n            throw new MessageException(Msg.e_vl_invalid_header);\n        }\n        if (s.contains(SEPARATOR)) {\n            String[] times = s.split(SEPARATOR);\n            if (times.length != 2) {\n                throw new MessageException(Msg.e_vl_invalid_header);\n            }\n            for (String time : times) {\n                if (time.contains(\" \")) {\n                    time = time.replace(\" \", \"T\");\n                }\n                try {\n                    ZonedDateTime.parse(time);\n                } catch (DateTimeParseException e) {\n                    throw new MessageException(Msg.e_vl_invalid_time);\n                }\n            }\n        }\n        return s;\n    }\n\n    public static class Loader {\n\n        private final PublicKeyHolder key;\n        private final DistrictList dl;\n        private final DistrictsMapper mapper;\n        private final Reporter rep;\n        private VoterList current;\n\n        Loader(PublicKeyHolder key, DistrictList dl, DistrictsMapper mapper, Reporter rep) {\n            this.key = key;\n            this.dl = dl;\n            this.mapper = mapper;\n            this.rep = rep;\n        }\n\n        public VoterList load(Path path, Path signature, Path skippath, SkipCommand skip, String... arg) {\n            if (skippath == null) {\n                verifySignature(path, signature);\n                current = readVoterList(path, arg);\n                validate(current);\n            }\n            else {\n                List<Voter> voters = new ArrayList<>();\n\n                String name = path.getFileName().toString() + \" / \" + skippath.getFileName().toString();\n\n                current = new VoterList(current, name,\n                        current.getVersionNumber(), skip.getElection(), skip.getChangeset(), voters);\n\n            }\n            return current;\n        }\n\n        public VoterList getCurrent() {\n            return current;\n        }\n\n        /* For testing only. */\n        void setCurrent(VoterList current) {\n            this.current = current;\n        }\n\n        void verifySignature(Path path, Path signature) {\n            try {\n                byte[] vl = Files.readAllBytes(path);\n                byte[] s = Files.readAllBytes(signature);\n\n                if (!key.verify(vl, s, X9ObjectIdentifiers.ecdsa_with_SHA256)) {\n                    throw new MessageException(Msg.e_vl_signature_error, signature);\n                }\n            } catch (Exception e) {\n                throw new MessageException(e, Msg.e_vl_read_error, path, e);\n            }\n        }\n\n        VoterList readVoterList(Path path, String... arg) {\n            try (InputStream in = Files.newInputStream(path)) {\n                String name = path.getFileName().toString();\n\n                return readVoterList(name, in, arg);\n            } catch (Exception e) {\n                throw new MessageException(e, Msg.e_vl_read_error, path, e);\n            }\n        }\n\n        /*-\n        valijate-nimekiri = versiooninumber LF valimise-identifikaator LF tüüp LF period LF *valija\n\n        versiooninumber = 1*2DIGIT\n        valimise-identifikaator = 1*28CHAR\n        tüüp = DIGIT\n        rfc3339_from = RFC3339 time\n        rfc3339_to = RFC3339 time\n        period = rfc3339_from TAB rfc3339_to\n         */\n        VoterList readVoterList(String name, InputStream in, String... arg) throws Exception {\n            try (BufferedReader br = new BufferedReader(new InputStreamReader(in, Util.CHARSET))) {\n                String version = readHeaderRow(br.readLine());\n                if (!version.equals(VERSION)) {\n                    throw new MessageException(Msg.e_vl_invalid_version, version);\n                }\n                String electionId = readHeaderRow(br.readLine());\n                String type = readHeaderRow(br.readLine());\n                String times = readHeaderRow(br.readLine());\n                List<Voter> voters =\n                        br.lines().map(s -> parseVoter(s, arg)).collect(Collectors.toList());\n\n                validateVoters(voters, name);\n\n                VoterList vl =\n                        new VoterList(current, name, version, electionId, type, voters);\n\n                return vl;\n            }\n        }\n\n        /*-\n        valija = isikukood TAB nimi TAB tegevus TAB valijaringkond LF\n\n        isikukood = 11*11DIGIT\n        nimi = 1*100UTF-8-CHAR\n        tegevus = \"lisamine\" | \"kustutamine\"\n\n        valijaringkond = omavalitsuse-ehak-kood TAB ringkond\n        omavalitsuse-ehak-kood = ehak-kood\n        jaoskonna-number-omavalitsuses = 1*10 DIGIT\n\n        ringkond =  ringkonna-number-omavalitsuses\n        ringkonna-number-omavalitsuses = 1*10DIGIT\n\n        ehak-kood = 1*10DIGIT\n\n         */\n        Voter parseVoter(String csv, String... arg) {\n            String[] r = csv.split(SEPARATOR, -1);\n            if (r.length == 5) {\n                // \"lisamine\" or \"muutmine\"\n            } else if (r.length == 2 && Arrays.asList(r).contains(KUSTUTAMINE)) {\n                // potential error for NullPointerException\n                return new Voter(r[1], null, r[0], \"\", null);\n            }\n            else {\n                throw new MessageException(Msg.e_vl_invalid_voter_row, csv);\n            }\n            LName district = new LName(\"\", \"\");\n            String ehak = r[3];\n            if (ehak.equals(FOREIGN)) {\n                if (arg.length == 1 && arg[0] != null) {\n                    ehak = arg[0];\n                } else {\n                    ehak = DEFAULT_EHAK;\n                }\n            }\n\n            List<String> dists = dl.getDistricts().keySet().stream()\n                    .filter(x -> x.endsWith(\".\" + r[4]))\n                    .collect(Collectors.toList());\n\n            for (String key : dists) {\n                List<String> parishes = dl.getDistricts().get(key).getParish();\n                if (parishes.contains(ehak)) {\n                    String[] d = key.split(\"\\\\.\");\n                    district = new LName(d[0], r[4]);\n                    break;\n                }\n            }\n\n            return new Voter(r[1], r[2], r[0], ehak, district);\n        }\n\n        void validate(VoterList vl) {\n            if (vl.getParent() == null && !vl.isInitial()) {\n                log.error(\"The first voter list {} is not initial list\", vl.getName());\n                throw new MessageException(Msg.e_vl_first_not_initial, vl.getName());\n            }\n            if (vl.getParent() != null && vl.isInitial()) {\n                log.error(\"Initial voter list {} is not the first\", vl.getName());\n                throw new MessageException(Msg.e_vl_initial_not_first, vl.getName());\n            }\n\n            if (vl.getParent() != null) {\n                if (Integer.parseInt(vl.getChangeset()) <= Integer.parseInt(vl.getParent().getChangeset())) {\n                    log.error(\"Voter list {} order is wrong\", vl.getName());\n                    throw new MessageException(Msg.e_vl_invalid_changeset);\n                }\n            }\n\n            if (vl.getElectionId() == null || !vl.getElectionId().equals(dl.getElection())) {\n                throw new MessageException(Msg.e_vl_election_id, vl.getName(), vl.getElectionId(),\n                        dl.getElection());\n            }\n        }\n\n        /**\n         * Removes invalid voters from the list and reports about errors.\n         *\n         * <p>\n         * The processing logic:\n         * <ul>\n         * <li>If there is an error with a record of a voter X, all records of voter X in this voter\n         * list are removed.\n         * <li>Consider that moving voter from one district/parish to another requires 1 removal\n         * and 1 addition record NOT necessarily in correct order.\n         * <li>The list of voters is checked in 3 rounds: districts/parish, removals, additions.\n         * <li>For each record: if the district or parish is invalid, report error.\n         * <li>For each removal record X: if there are multiple removal records for X or the voter X\n         * does not have valid record, report error.\n         * <li>For each addition record X: if there are multiple addition records for X or the voter\n         * already has valid record, report error.\n         * </ul>\n         *\n         * @param voters\n         * @param vlName The voter list display name for reporting.\n         */\n        void validateVoters(List<Voter> voters, String vlName) {\n            Set<String> removed = new HashSet<>();\n            Set<String> added = new HashSet<>();\n            Set<String> invalid = new HashSet<>();\n\n            try {\n                voters.forEach(v -> {\n                    // if here NullPointerException occurs, then it's a \"kustutamine\"\n                    District d = dl.getDistricts().get(v.getDistrict().getId());\n                    if (d == null) {\n                        rep.report(Msg.e_vl_invalid_district, vlName, v.getCode(), v.getName(),\n                                v.getDistrict().getId());\n                        invalid.add(v.getCode());\n                    } else if (!d.getParish().contains(v.getParish())) {\n                        rep.report(Msg.e_vl_invalid_parish, vlName, v.getCode(), v.getName(),\n                                v.getParish());\n                        invalid.add(v.getCode());\n                    }\n                });\n            } catch (NullPointerException npe) {\n                // \"kustutamine\", do nothing\n            }\n\n            voters.forEach(v -> {\n                if (invalid.contains(v.getCode())) {\n                    return;\n                }\n                if (added.contains(v.getCode()) && removed.contains(v.getCode())) {\n                    removed.remove(v.getCode());\n                    added.remove(v.getCode());\n                }\n                if (v.isAddition()) {\n                    if (!added.add(v.getCode())) {\n                        rep.report(Msg.e_vl_voter_already_added, vlName, v.getCode(), v.getName());\n                        invalid.add(v.getCode());\n                    } else {\n                        if (current != null && current.find(v.getCode()) != null && !removed.contains(v.getCode())) {\n                            rep.report(Msg.e_vl_added_voter_exists, vlName, v.getCode(), v.getName());\n                            invalid.add(v.getCode());\n                        }\n                    }\n                } else {\n                    if (!removed.add(v.getCode())) {\n                        rep.report(Msg.e_vl_voter_already_removed, vlName, v.getCode(), v.getName());\n                        invalid.add(v.getCode());\n                    } else {\n                        if ((current == null || current.find(v.getCode()) == null) && (!added.contains(v.getCode()))) {\n                            rep.report(Msg.e_vl_removed_voter_missing, vlName, v.getCode(), v.getName());\n                            invalid.add(v.getCode());\n                        }\n                    }\n                }\n            });\n\n            voters.removeIf(v -> invalid.contains(v.getCode()));\n        }\n\n    } // class Loader\n\n    public interface Reporter {\n        void report(Enum<?> key, Object... args);\n    }\n\n}\n"
  },
  {
    "path": "proxy/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "proxy/Makefile",
    "content": "EXTRADATA := service/proxy/haproxy.cfg.tmpl service/proxy/haproxy-rsyslog.conf\n\ninclude ../common/go/common.mk\n"
  },
  {
    "path": "proxy/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------\n Proxy service\n-----------------\n\n<Description of proxy service.>\n"
  },
  {
    "path": "proxy/go.mod",
    "content": "module ivxv.ee/proxy\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nreplace ivxv.ee/common/collector => ../common/collector\n"
  },
  {
    "path": "proxy/service/proxy/haproxy-rsyslog.conf",
    "content": "# IVXV Internet voting framework\n#\n# Logging configuration for haproxy.\n# /usr/share/ivxv/haproxy-rsyslog.conf\n# symlinked to /etc/rsyslog.d/49-haproxy.conf\n\n# This file is originally installed by haproxy package\n# and is overriden by ivxv-proxy package.\n\n# Create an additional socket in haproxy's chroot in order to allow logging via\n# /dev/log to chroot'ed HAProxy processes\ninput(\n    type=\"imuxsock\"\n    Socket=\"/var/lib/haproxy/dev/log\"\n)\n"
  },
  {
    "path": "proxy/service/proxy/haproxy.cfg.tmpl",
    "content": "{{/* vim: set ft=gotexttmpl: */ -}}\nglobal\n\tlog /dev/log local0{{if (not .Debug)}} info{{end}}\n\tchroot /var/lib/haproxy\n\tuser haproxy\n\tgroup haproxy\n\tdaemon\n\ndefaults\n\tlog global\n\ttimeout connect 7000\n\ttimeout client  60000\n\ttimeout server  60000\n\nfrontend {{replace .Service.ID \"@\" \"-\" -1}}\n\t# Listen on all interfaces instead of only {{.Service.Address}}.\n\tbind *:{{port .Service.Address}}\n\toption tcplog\n\n\t# Only accept TLS ClientHello messages.\n\ttcp-request inspect-delay 3000\n\ttcp-request content accept if { req.ssl_hello_type 1 }\n{{range .Backends}}\n\tuse_backend {{.Name}} if { req.ssl_sni {{.Name}}.{{.SniDomain}} }\n{{- end}}\n{{range .Backends}}\nbackend {{.Name}}\n\t{{- range .Servers}}\n\tserver {{replace .ID \"@\" \"-\" -1}} {{.Address}} check inter 1m send-proxy-v2\n\t{{- end}}\n{{end -}}\n"
  },
  {
    "path": "proxy/service/proxy/haproxy.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"html/template\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/log\"\n)\n\ntype data struct {\n\tDebug    bool\n\tService  *conf.Service\n\tBackends []*backend\n}\n\ntype backend struct {\n\tName      string\n\tSniDomain string\n\tServers   []*conf.Service\n}\n\n// generate creates a HAProxy configuration based on the technical configuration\n// and a template.\nfunc generate(c *conf.Technical, network string, service *conf.Service, tmpl string) (\n\tcfg []byte, code int, err error) {\n\n\tt, err := template.New(filepath.Base(tmpl)).Funcs(template.FuncMap{\n\t\t\"port\": func(addr string) (port string, err error) {\n\t\t\t_, port, err = net.SplitHostPort(addr)\n\t\t\treturn\n\t\t},\n\t\t\"replace\": strings.Replace,\n\t}).ParseFiles(tmpl)\n\tif err != nil {\n\t\treturn nil, exit.DataErr, ParseTemplateError{Path: tmpl, Err: err,\n\t\t\tDescription: _PROXY_PARSE_TEMPLATE_READ}\n\t}\n\n\td := data{\n\t\tDebug:   c.Debug,\n\t\tService: service,\n\t}\n\tservices := c.Services(network)\n\trt := reflect.TypeOf(services).Elem()\n\trv := reflect.ValueOf(services).Elem()\n\tfor i := 0; i < rt.NumField(); i++ {\n\t\tswitch rt.Field(i).Name {\n\t\tcase \"Proxy\", \"Storage\", \"Session\":\n\t\t\tcontinue // Do not proxy to other proxies or storage.\n\t\t}\n\t\td.Backends = append(d.Backends, &backend{\n\t\t\tName:      strings.ToLower(rt.Field(i).Name),\n\t\t\tSniDomain: c.SniDomain,\n\t\t\tServers:   rv.Field(i).Interface().([]*conf.Service),\n\t\t})\n\t}\n\n\tvar buf bytes.Buffer\n\tif err = t.Execute(&buf, d); err != nil {\n\t\treturn nil, exit.DataErr, ExecuteTemplateError{Err: err, Description: _PROXY_PARSE_TEMPLATE_EXEC}\n\t}\n\n\treturn buf.Bytes(), exit.OK, nil\n}\n\n// check calls the HAProxy binary to verify that cfg is valid.\nfunc check(ctx context.Context, cfg []byte) (code int, err error) {\n\tcmd := exec.CommandContext(ctx, \"/usr/sbin/haproxy\", \"-c\", \"--\", \"/dev/stdin\")\n\tcmd.Stdin = bytes.NewReader(cfg)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn exit.DataErr, CheckConfigurationError{\n\t\t\tConfiguration: string(cfg),\n\t\t\tOutput:        string(out),\n\t\t\tErr:           err,\n\t\t\tDescription:   _PROXY_PARSE_TEMPLATE_VERIFY,\n\t\t}\n\t}\n\treturn\n}\n\n// readPIDFile opens pidfile and reads the PID of the master process.\nfunc readPIDFile(pidfile string) (pid int, code int, err error) {\n\tpidb, err := os.ReadFile(pidfile)\n\tif err != nil {\n\t\treturn 0, exit.NoInput, ReadPIDFileError{Path: pidfile, Err: err,\n\t\t\tDescription: _PROXY_HAPROXY_PIDFILE}\n\t}\n\teol := bytes.IndexByte(pidb, '\\n')\n\tif eol < 0 {\n\t\teol = len(pidb)\n\t}\n\tpid, err = strconv.Atoi(string(pidb[:eol]))\n\tif err != nil {\n\t\treturn 0, exit.DataErr, ParsePIDFilePIDError{PIDFile: string(pidb), Err: err,\n\t\t\tDescription: _PROXY_HAPROXY_PIDFILE_INVALID}\n\t}\n\treturn pid, exit.OK, nil\n}\n\n// Since we do not have enough permissions to reload HAProxy we have to\n// improvise a bit: we expect to be the same user that HAProxy child processes\n// are running under and send USR1 to them. This will cause them to gracefully\n// close and HAProxy to stop. The service manager will pick up on this and\n// restart HAProxy, now with the new configuration.\nfunc restart(ctx context.Context, proc string, pid int) error {\n\tcpids, err := childPIDs(proc, pid)\n\tif err != nil {\n\t\treturn ChildPIDsError{ParentPID: pid, Err: err, Description: _PROXY_HAPROXY_CHILD}\n\t}\n\tif len(cpids) == 0 {\n\t\treturn NoChildPIDsError{ParentPID: pid, Description: _PROXY_HAPROXY_NO_CHILD}\n\t}\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\t// Start a goroutine per child PID. Create a buffered channel to hold\n\t// all the results without blocking.\n\tc := make(chan error, len(cpids))\n\tfor _, pid := range cpids {\n\t\tgo func(pid int) { c <- stopPID(ctx, pid) }(pid)\n\t}\n\n\t// Read up to as many results as goroutines were started.\n\tfor range cpids {\n\t\tif err := <-c; err != nil {\n\t\t\treturn StopPIDError{Err: err, Description: _PROXY_HAPROXY_CHILD_STOP}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc stopPID(ctx context.Context, pid int) error {\n\tif err := syscall.Kill(pid, syscall.SIGUSR1); err != nil {\n\t\treturn SignalPIDError{PID: pid, Err: err, Description: _PROXY_HAPROXY_SIGUSR1}\n\t}\n\n\t// Signal the PID with 0 until it no longer succeeds. There is a chance\n\t// that between two signals, the old process stops and a new process\n\t// starts under the same user with the same PID, but this is negligible\n\t// enough that we ignore it.\n\tlog.Log(ctx, WaitHAProxyStop{PID: pid, Description: _PROXY_HAPROXY_STOP})\n\tsleep := time.NewTimer(0)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-sleep.C:\n\t\t\tif err := syscall.Kill(pid, syscall.Signal(0)); err != nil {\n\t\t\t\tlog.Log(ctx, HAProxyStopped{PID: pid, Description: _PROXY_HAPROXY_STOPPED})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tsleep.Reset(200 * time.Millisecond)\n\t\t}\n\t}\n}\n\n// childPIDs scans through proc looking for processes whose parent PID is pid.\nfunc childPIDs(proc string, pid int) ([]int, error) {\n\tppid := regexp.MustCompile(\"\\nPPid:\\t\" + strconv.Itoa(pid) + \"\\n\")\n\n\tdir, err := os.Open(proc)\n\tif err != nil {\n\t\treturn nil, OpenProcError{Proc: proc, Err: err, Description: _PROXY_PROC_DIR}\n\t}\n\tdefer dir.Close()\n\n\tnames, err := dir.Readdirnames(-1)\n\tif err != nil {\n\t\treturn nil, ReadProcError{Proc: proc, Err: err, Description: _PROXY_PROC}\n\t}\n\tvar cpids []int\n\tfor _, name := range names {\n\t\tcpid, err := strconv.Atoi(name)\n\t\tif err != nil {\n\t\t\tcontinue // Skip non-integer directory names.\n\t\t}\n\t\tstatus, err := os.ReadFile(filepath.Join(proc, name, \"status\"))\n\t\tif err != nil {\n\t\t\t// Skip processes where we cannot read status (probably\n\t\t\t// exited after Readdirnames).\n\t\t\tcontinue\n\t\t}\n\t\tif ppid.Match(status) {\n\t\t\tcpids = append(cpids, cpid)\n\t\t}\n\t}\n\treturn cpids, nil\n}\n\n// checkPID checks proc if the process PID still exists.\nfunc checkPID(proc string, pid int) error {\n\tif _, err := os.Stat(filepath.Join(proc, strconv.Itoa(pid), \"status\")); err != nil {\n\t\treturn StatProcStatusError{PID: pid, Err: err, Description: _PROXY_NO_PID}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "proxy/service/proxy/log_desc.go",
    "content": "package main\n\nconst (\n\t_PROXY_PARSE_TEMPLATE_READ     = \"Failed to read HAProxy template file\"\n\t_PROXY_PARSE_TEMPLATE_EXEC     = \"Failed to generate HAProxy configuration file\"\n\t_PROXY_PARSE_TEMPLATE_VERIFY   = \"Failed to verify correctness of a HAProxy configuration file\"\n\t_PROXY_HAPROXY_PIDFILE         = \"Failed to read HAProxy PID file\"\n\t_PROXY_HAPROXY_PIDFILE_INVALID = \"PID of a HAProxy is not a base-10 integer\"\n\t_PROXY_HAPROXY_CHILD           = \"Unable to locate HAProxy child processes\"\n\t_PROXY_HAPROXY_NO_CHILD        = \"No HAProxy child processes found\"\n\t_PROXY_HAPROXY_CHILD_STOP      = \"Failed to stop HAProxy child processes\"\n\t_PROXY_HAPROXY_SIGUSR1         = \"Failed to send SIGUSR1 to systemd\"\n\t_PROXY_HAPROXY_STOP            = \"Waiting for HAProxy to stop gracefully\"\n\t_PROXY_HAPROXY_STOPPED         = \"HAProxy has stopped gracefully\"\n\t_PROXY_PROC_DIR                = \"Unable to access /proc/ directory\"\n\t_PROXY_PROC                    = \"Unable to open HAProxy process files from /proc/\"\n\t_PROXY_NO_PID                  = \"No HAProxy PID file found\"\n\t_PROXY_HAPROXY_WRITE           = \"Failed to write HAProxy configuration file to filesystem\"\n\t_PROXY_HAPROXY_UPDATE          = \"HAProxy will be reloaded now\"\n\t_PROXY_HAPROXY_RESTART         = \"Failed to restart HAProxy\"\n\t_PROXY_HAPROXY_RESTARTED       = \"HAProxy has been restarted successfully\"\n\t_PROXY_HAPROXY_PID             = \"HAProxy PID info\"\n\t_PROXY_HAPROXY_RELOAD          = \"Failed to reload HAProxy\"\n\t_PROXY_HAPROXY_CONTROL         = \"Failed to configure proxy service\"\n\t_PROXY_HAPROXY_SERVE           = \"Failed to serve proxy service\"\n)\n"
  },
  {
    "path": "proxy/service/proxy/main.go",
    "content": "/*\nThe proxy service controls a locally running HAProxy instance.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t//ivxv:modules common/collector/container\n)\n\nvar (\n\tcfgp = flag.String(\"haproxy-config\", \"/etc/haproxy/haproxy.cfg\",\n\t\t\"`path` where to write the HAProxy configuration file\\n\")\n\n\tpidp = flag.String(\"haproxy-pid\", \"/run/haproxy.pid\", \"`path` to the HAProxy pid file\")\n\n\tprocp = flag.String(\"proc\", \"/proc\", \"`path` where proc filesystem is mounted\")\n\n\ttmplp = flag.String(\"haproxy-template\", \"/usr/share/ivxv/haproxy.cfg.tmpl\",\n\t\t\"`path` to the HAProxy configuration template\\n\")\n)\n\n// reload updates the HAProxy configuration and restarts HAProxy to make it use\n// the new configuration.\nfunc reload(ctx context.Context, c *conf.Technical,\n\tnetwork string, service *conf.Service, until int) (code int, err error) {\n\n\tcfg, code, err := generate(c, network, service, *tmplp)\n\tif err != nil {\n\t\treturn code, GenerateHAProxyConfigurationError{Err: err, Description: _PROXY_PARSE_TEMPLATE_EXEC}\n\t}\n\n\tif code, err = check(ctx, cfg); err != nil {\n\t\treturn code, CheckHAProxyConfigurationError{Err: err, Description: _PROXY_PARSE_TEMPLATE_VERIFY}\n\t}\n\n\tif until >= command.Execute {\n\t\t//nolint:gosec // Keep the configuration file permissions.\n\t\tif err = os.WriteFile(*cfgp, cfg, 0644); err != nil {\n\t\t\treturn exit.CantCreate, WriteConfigurationError{Err: err, Description: _PROXY_HAPROXY_WRITE}\n\t\t}\n\t\tlog.Log(ctx, UpdatedHAProxyConfiguration{Path: *cfgp, Description: _PROXY_HAPROXY_UPDATE})\n\n\t\t// It is possible here that the just created configuration file\n\t\t// gets replaced before HAProxy is restarted. This should not\n\t\t// happen during normal operation and there is little point in\n\t\t// worrying about someone doing this maliciously, because then\n\t\t// they can just replace it and restart HAProxy whenever they\n\t\t// want.\n\n\t\tpid, code, err := readPIDFile(*pidp)\n\t\tif err != nil {\n\t\t\treturn code, ReadHAProxyPIDError{Err: err, Description: _PROXY_HAPROXY_PIDFILE}\n\t\t}\n\t\tif err = restart(ctx, *procp, pid); err != nil {\n\t\t\treturn exit.Unavailable, RestartHAProxyError{Err: err, Description: _PROXY_HAPROXY_RESTART}\n\t\t}\n\t\tlog.Log(ctx, HAProxyRestarted{Description: _PROXY_HAPROXY_RESTARTED})\n\t}\n\treturn\n}\n\n// hapid is the PID of the HAProxy master process.\ntype hapid int\n\n// start sets p to the PID of the HAProxy master process.\nfunc (p *hapid) start(ctx context.Context) error {\n\t// We need to wait until the service manager has restarted\n\t// HAProxy. Replace this with something that actually ensures new\n\t// instances of HAProxy are up.\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// Get the PID of the HAProxy master processes.\n\tpid, _, err := readPIDFile(*pidp)\n\tif err != nil {\n\t\treturn EnableReadPIDError{Err: err, Description: _PROXY_HAPROXY_PIDFILE}\n\t}\n\t*p = hapid(pid)\n\tlog.Log(ctx, HAProxyPID{PID: *p, Description: _PROXY_HAPROXY_PID})\n\treturn nil\n}\n\n// check checks if the HAProxy master process still exists.\nfunc (p *hapid) check(_ context.Context) error {\n\tif err := checkPID(*procp, int(*p)); err != nil {\n\t\treturn CheckHAProxyError{PID: *p, Err: err, Description: _PROXY_NO_PID}\n\t}\n\treturn nil\n}\n\n// stop does nothing, because we do not have permissions to stop HAProxy\n// instances.\nfunc (p *hapid) stop(_ context.Context) error {\n\treturn nil\n}\n\nfunc main() {\n\t// Call proxymain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(proxymain())\n}\n\nfunc proxymain() (code int) {\n\tc := command.NewWithoutStorage(\"ivxv-proxy\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tvar s *server.Controller\n\tvar err error\n\tif c.Conf.Technical != nil {\n\t\t// Create new HAProxy configuration and reload the service.\n\t\tif code, err = reload(c.Ctx, c.Conf.Technical,\n\t\t\tc.Network, c.Service, c.Until); err != nil {\n\n\t\t\treturn c.Error(code, ReloadConfigurationError{Err: err, Description: _PROXY_HAPROXY_RELOAD},\n\t\t\t\t\"reloading new configuration failed:\", err)\n\t\t}\n\n\t\t// Configure a new proxy service controller. This actually does\n\t\t// not control anything and just monitors if the HAProxy\n\t\t// processes are still running or not.\n\t\tvar p hapid\n\t\tif s, err = server.NewController(&c.Conf.Version,\n\t\t\tp.start, p.check, p.stop); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ControllerConfError{Err: err, Description: _PROXY_HAPROXY_CONTROL},\n\t\t\t\t\"failed to configure controller:\", err)\n\t\t}\n\t}\n\tif c.Until >= command.Execute {\n\t\tif err = s.Control(c.Ctx); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ControlError{Err: err, Description: _PROXY_HAPROXY_SERVE},\n\t\t\t\t\"failed to control proxy service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "scripts/goimports.sh",
    "content": "#!/bin/bash\n# import: import statement generator for IVXV modules.\n#\n# import currently assumes that all sub-packages of a package are its modules.\n# If this changes, then we need to start parsing Go source code to determine\n# the list of modules.\n\nset -e\n\n# We have quite a lot of unquoted variables in this script: this is\n# intentional, as we want to split output of commands and functions. However we\n# only want to do this on newlines and not spaces, so override IFS.\nIFS=\"\n\"\n\ndirective=\"//ivxv:modules\"\ndevelopment=\"//ivxv:development\"\nimportfile=\"gen_import.go\"\ndevelopmentfile=\"gen_import_dev.go\"\n\nusage () {\n\tcat <<HERE\nusage: $0 [-vh] [<package>...]\n\nWe use a registration-based approach for IVXV package modules, which means that\nwhen they are imported, they register themselves in their parent package\n(similar to what the \"crypto\" package does with hashes). This way the main\nbinary can control which implementations are linked and the parent package\ndispatches to the implementations.\n\nimport helps out by creating an import statement for all modules of a package.\nIt takes a list of packages as input and searches files in those packages for\nthe string \"$directive\". If found, it generates imports for all submodules\nof ivxv.ee packages listed after that string under a separate file.\n\nFor example, if invoked as \"import ivxv.ee/service/...\", it will look through\nall service packages and if it finds \"$directive auth\" in a file, it\ncreates a new file under that package which imports all submodules of\nivxv.ee/auth.\n\nIf a submodule contains the string \"$development\", then it is considered\na development-only submodule. These submodules will be put in a separate file\nfrom other imports and have the \"development\" build constraint set, i.e., those\nmodules will only be imported if the binary is built with the \"development\"\nbuild tag.\nHERE\n}\n\nverbose=\"false\"\nwhile getopts \":hv\" opt; do\n\tcase \"$opt\" in\n\t\t\"h\")\n\t\t\tusage\n\t\t\texit 0\n\t\t\t;;\n\t\t\"v\")\n\t\t\tverbose=\"true\"\n\t\t\t;;\n\t\t\"?\")\n\t\t\techo \"error: unknown option -$OPTARG\" >&2\n\t\t\tusage\n\t\t\texit 1\n\t\t\t;;\n\tesac\ndone\nshift $((OPTIND-1))\n\n# find_directive greps through all directories given as arguments looking for\n# the directive and prints any packages listed in such directives.\nfind_directive () {\n\t# We need to declare the local variable beforehand, because local\n\t# always returns 0.\n\n\tlocal directives\n\tfor dir in \"$@\"; do\n\t\t# Disable exiting on errors to check grep return value.\n\t\tset +e\n\t\tdirectives=$(grep -hd skip \"$directive\" \"$dir\"/*)\n\t\trc=$?\n\t\tcase $rc in\n\t\t\t[0-1])\n\t\t\t\t# Expected return values.\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\texit $rc\n\t\t\t\t;;\n\t\tesac\n\t\tset -e\n\t\techo \"$directives\" | sed -n \"s;.*$directive \\\\(.*\\\\);\\\\1;p\"\n\tdone\n}\n\n# list_modules prints a string of the form \"importpath:dir\" for each subpackage\n# of the IVXV Go packages given as arguments.\nlist_modules () {\n\t# Use a local variable instead of piping go into grep so that this\n\t# function fails if go fails.\n\n\tlocal modules\n\tfor parent in \"$@\"; do\n\t\tmodules=$(${GO:-go} list -f '{{.ImportPath}}:{{.Dir}}' \"ivxv.ee/$parent/...\")\n\t\techo \"$modules\" | grep -v \"^ivxv.ee/$parent:\"\n\tdone\n}\n\n# dev_modules takes a boolean argument and a list of Go package of the form\n# \"importpath:dir\". It prints the import paths of all Go packages that contain\n# the development directive somewhere in their source code. If the boolean\n# value is false, then it prints the import paths of packages which do not have\n# the development directive instead.\ndev_modules () {\n\tlocal greprc=0\n\tif [ \"$1\" = \"false\" ]; then\n\t\tgreprc=1\n\tfi\n\tshift 1\n\n\tlocal import\n\tlocal dir\n\tfor pkg in \"$@\"; do\n\t\timport=$(echo \"$pkg\" | cut -d: -f 1)\n\t\tdir=$(echo \"$pkg\" | cut -d: -f 2)\n\n\t\t# Disable exiting on errors to check grep return value.\n\t\tset +e\n\t\tgrep -hd skip \"$development\" \"$dir\"/* > /dev/null\n\t\trc=$?\n\t\tcase $rc in\n\t\t\t[0-1])\n\t\t\t\tif [ $rc -eq $greprc ]; then\n\t\t\t\t\techo \"$import\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\texit $rc\n\t\t\t\t;;\n\t\tesac\n\t\tset -e\n\tdone\n}\n\n# create_import takes a file path, a package name, a boolean, and a list of\n# modules as arguments. It creates a Go source file at the path with the\n# provided package name which imports all of the listed modules. If the boolean\n# value is true, then it adds a \"development\" build constraint to the source\n# file.\n#\n# If no modules are given, then the file is not created.\ncreate_import () {\n\tlocal path=\"$1\"\n\tlocal name=\"$2\"\n\tlocal dev=\"$3\"\n\tshift 3\n\n\t# Do not create empty file.\n\tif [ $# -eq 0 ]; then\n\t\treturn 0\n\tfi\n\n\tcat > \"$path\" <<HERE\n// Generated by ivxv.ee/cmd/import. DO NOT EDIT!\n$([ \"$dev\" = \"true\" ] && echo \"// +build development\")\n\npackage $name\n\nimport (\n$(for module in \"$@\"; do echo \"$module\" | sed 's/\\(.*\\)/\\t_ \"\\1\"/'; done)\n)\nHERE\n\t${GO:-go} fmt \"$path\" > /dev/null\n}\n\n# create_imports takes a package name, source directory, and list of modules\n# and creates release and development import files in that directory.\ncreate_imports () {\n\tlocal name=\"$1\"\n\tlocal dir=\"$2\"\n\tshift 2\n\n\tcreate_import \"$dir/$importfile\"      \"$name\" \"false\" $(dev_modules \"false\" \"$@\")\n\tcreate_import \"$dir/$developmentfile\" \"$name\" \"true\"  $(dev_modules \"true\"  \"$@\")\n}\n\nfor pkg in $(${GO:-go} list -f '{{.Name}}:{{.ImportPath}}:{{.Dir}}' \"$@\"); do\n\tname=$(echo \"$pkg\" | cut -d: -f 1)\n\timport=$(echo \"$pkg\" | cut -d: -f 2)\n\tdir=$(echo \"$pkg\" | cut -d: -f 3)\n\n\tparents=$(find_directive \"$dir\")\n\tif [ -n \"$parents\" ]; then\n\t\t[ \"$verbose\" = \"false\" ] || echo \"$import: importing\" $parents\n\t\tcreate_imports \"$name\" \"$dir\" $(list_modules $parents)\n\tfi\ndone\n"
  },
  {
    "path": "sessionstatus/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "sessionstatus/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "sessionstatus/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------\n Session status service\n-----------------\n\n<Description of sessionstatus service.>\n"
  },
  {
    "path": "sessionstatus/api/Makefile",
    "content": "include ../../common/go/common.mk\n"
  },
  {
    "path": "sessionstatus/api/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------\n Session status client API\n-----------------\n\n<Description of session status client API.>\n"
  },
  {
    "path": "sessionstatus/api/go.mod",
    "content": "module ivxv.ee/sessionstatus/api\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../../common/collector\n"
  },
  {
    "path": "sessionstatus/api/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "sessionstatus/api/rpc/builder.go",
    "content": "package rpc\n\nimport \"ivxv.ee/common/collector/server\"\n\ntype StatusReadReqBuilder struct {\n\theader server.Header\n}\n\n// NewSessionStatusReadReqBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusReadReq.\nfunc NewSessionStatusReadReqBuilder() *StatusReadReqBuilder {\n\treturn &StatusReadReqBuilder{}\n}\n\nfunc (srrb *StatusReadReqBuilder) WithHeader(h server.Header) *StatusReadReqBuilder {\n\tsrrb.header = h\n\treturn srrb\n}\n\n// Build returns StatusReadReq.\nfunc (srrb *StatusReadReqBuilder) Build() StatusReadReq {\n\treturn StatusReadReq{Header: srrb.header}\n}\n\ntype StatusReadRespBuilder struct {\n\tresponse map[string]any\n}\n\n// NewSessionStatusReadRespBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusReadResp.\nfunc NewSessionStatusReadRespBuilder() *StatusReadRespBuilder {\n\treturn &StatusReadRespBuilder{}\n}\n\nfunc (srrb *StatusReadRespBuilder) WithResponse(r map[string]any) *StatusReadRespBuilder {\n\tsrrb.response = r\n\treturn srrb\n}\n\n// Build returns StatusReadResp.\nfunc (srrb *StatusReadRespBuilder) Build() StatusReadResp {\n\treturn StatusReadResp{\n\t\tHeader: server.Header{\n\t\t\tSessionID: srrb.response[sessionIDField].(string),\n\t\t},\n\t\tCaller: srrb.response[callerField].(string),\n\t\tAuth:   srrb.response[authField].(string),\n\t\tLease:  srrb.response[leaseField].(string),\n\t}\n}\n\ntype StatusUpdateReqBuilder struct {\n\theader server.Header\n\tcaller string\n\tauth   string\n\tlease  string\n\tttl    string\n}\n\n// NewSessionStatusUpdateReqBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusUpdateReq.\nfunc NewSessionStatusUpdateReqBuilder() *StatusUpdateReqBuilder {\n\treturn &StatusUpdateReqBuilder{}\n}\n\nfunc (surb *StatusUpdateReqBuilder) WithHeader(h server.Header) *StatusUpdateReqBuilder {\n\tsurb.header = h\n\treturn surb\n}\n\nfunc (surb *StatusUpdateReqBuilder) WithCaller(c string) *StatusUpdateReqBuilder {\n\tsurb.caller = c\n\treturn surb\n}\n\nfunc (surb *StatusUpdateReqBuilder) WithAuth(a string) *StatusUpdateReqBuilder {\n\tsurb.auth = a\n\treturn surb\n}\n\nfunc (surb *StatusUpdateReqBuilder) WithLease(l string) *StatusUpdateReqBuilder {\n\tsurb.lease = l\n\treturn surb\n}\n\nfunc (surb *StatusUpdateReqBuilder) WithTTL(t string) *StatusUpdateReqBuilder {\n\tsurb.ttl = t\n\treturn surb\n}\n\n// Build returns StatusUpdateReq.\nfunc (surb *StatusUpdateReqBuilder) Build() StatusUpdateReq {\n\treturn StatusUpdateReq{\n\t\tHeader: surb.header,\n\t\tCaller: surb.caller,\n\t\tAuth:   surb.auth,\n\t\tLease:  surb.lease,\n\t\tTTL:    surb.ttl,\n\t}\n}\n\ntype StatusUpdateRespBuilder struct {\n\tresponse map[string]any\n}\n\n// NewSessionStatusUpdateRespBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusUpdateResp.\nfunc NewSessionStatusUpdateRespBuilder() *StatusUpdateRespBuilder {\n\treturn &StatusUpdateRespBuilder{}\n}\n\nfunc (surb *StatusUpdateRespBuilder) WithResponse(r map[string]any) *StatusUpdateRespBuilder {\n\tsurb.response = r\n\treturn surb\n}\n\n// Build returns StatusUpdateResp.\nfunc (surb *StatusUpdateRespBuilder) Build() StatusUpdateResp {\n\treturn StatusUpdateResp{\n\t\tOk: surb.response[okField].(bool),\n\t}\n}\n\ntype StatusDeleteReqBuilder struct {\n\theader server.Header\n}\n\n// NewSessionStatusDeleteReqBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusDeleteReq.\nfunc NewSessionStatusDeleteReqBuilder() *StatusDeleteReqBuilder {\n\treturn new(StatusDeleteReqBuilder)\n}\n\nfunc (sdrb *StatusDeleteReqBuilder) WithHeader(h server.Header) *StatusDeleteReqBuilder {\n\tsdrb.header = h\n\treturn sdrb\n}\n\n// Build returns StatusDeleteReq.\nfunc (sdrb *StatusDeleteReqBuilder) Build() StatusDeleteReq {\n\treturn StatusDeleteReq{Header: sdrb.header}\n}\n\ntype StatusDeleteRespBuilder struct {\n\tresponse map[string]any\n}\n\n// NewSessionStatusDeleteRespBuilder is a Builder-pattern constructor, which is used\n// to prepare a StatusDeleteResp.\nfunc NewSessionStatusDeleteRespBuilder() *StatusDeleteRespBuilder {\n\treturn new(StatusDeleteRespBuilder)\n}\n\nfunc (sdrb *StatusDeleteRespBuilder) WithResponse(r map[string]any) *StatusDeleteRespBuilder {\n\tsdrb.response = r\n\treturn sdrb\n}\n\n// Build returns StatusDeleteResp.\nfunc (sdrb *StatusDeleteRespBuilder) Build() StatusDeleteResp {\n\treturn StatusDeleteResp{\n\t\tOk: sdrb.response[okField].(bool),\n\t}\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/builder_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tstatus \"ivxv.ee/common/collector/status/client\"\n)\n\n// If any new RPC endpoint is about to appear in a IVXV online workflow\n// then it should be added here, in case you wish that endpoint to\n// receive status checks.\nconst (\n\t// Empty RPC\n\tEmpty = \"\"\n\n\t// Mobile-ID or Smart-ID authentication\n\tAuthenticate       = \"RPC.Authenticate\"\n\tAuthenticateStatus = \"RPC.AuthenticateStatus\"\n\n\t// Web eID authentication\n\tChallenge = \"RPC.Challenge\"\n\tToken     = \"RPC.Token\"\n\n\t// Choices list\n\tVoterChoices = \"RPC.VoterChoices\"\n\n\t// Mobile-ID or Smart-ID signing certificate REST query\n\tGetCertificate = \"RPC.GetCertificate\"\n\t// Smart-ID signing certificate received status check\n\tGetCertificateStatus = \"RPC.GetCertificateStatus\"\n\n\t// Mobile-ID or Smart-ID signing\n\tSign       = \"RPC.Sign\"\n\tSignStatus = \"RPC.SignStatus\"\n\n\t// Voting, i.e. storing a vote in a database\n\tVote = \"RPC.Vote\"\n)\n\nvar commonErrTxt3 = \"Two structs are not equal. Expected %v, got %v\\n\"\n\n// Example of StatusReadReq\nvar headers = []server.Header{\n\t// Example of any RPC request Header against IVXV backend. That kind of\n\t// Header will see any RPC method\n\t{\n\t\tCtx:        context.Background(),\n\t\tSessionID:  \"0101e9342abab1577b8b2844d6a1d317\",\n\t\tOS:         \"Ubuntu Jammy 22.04 LTS\",\n\t\tAuthMethod: \"\",\n\t\tAuthToken:  nil,\n\t\tDataToken:  nil,\n\t},\n\t// Example of SessionID tamper attack to bypass backend SessionID generation.\n\t// Can be bypassed if SessionID is len(SessionID) != 0 and valid hex\n\t{\n\t\tCtx:        context.Background(),\n\t\tSessionID:  \"0\",\n\t\tOS:         \"Ubuntu Jammy 22.04 LTS\",\n\t\tAuthMethod: \"\",\n\t\tAuthToken:  nil,\n\t\tDataToken:  nil,\n\t},\n\t// Example of OS JavaScript injection attack which could impact IVXV\n\t// logmonitor, that will display results on Web\n\t{\n\t\tCtx:        context.Background(),\n\t\tSessionID:  \"\",\n\t\tOS:         `<script>alert(\"Hello World\")</script>`,\n\t\tAuthMethod: \"\",\n\t\tAuthToken:  nil,\n\t\tDataToken:  nil,\n\t},\n}\n\nvar sessionStatusReadResps = []map[string]any{\n\t// Example of RPC.Authenticate request with Mobile-ID\n\t// For given SessionID there SHOULD NOT be any session status\n\t// records in a database\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    Empty,\n\t\t\"Auth\":      status.NoAuth,\n\t\t\"Lease\":     \"0\",\n\t},\n\t// Example of RPC.AuthenticateStatus request with Mobile-ID\n\t// This time we have updated the session status record in a database\n\t// and Caller is the previous RPC method that has been invoked and\n\t// Auth is determined to be \"mid\". Lease ID also has a value\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    Authenticate,\n\t\t\"Auth\":      status.MobileIDAuth,\n\t\t// int64 = 3367438597345620447\n\t\t\"Lease\": \"2ebb8b9015b9b1df\",\n\t},\n\t// Example of Web eID RPC.Token request\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    Challenge,\n\t\t\"Auth\":      status.WebeIDAuth,\n\t\t// int64 = 1936286082718970961\n\t\t\"Lease\": \"1adf11eeefff3451\",\n\t},\n\t// Example of ID-card RPC.VoterChoices. In case of ID card it is a\n\t// first interaction with session status database\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    Empty,\n\t\t\"Auth\":      status.NoAuth,\n\t\t\"Lease\":     \"\",\n\t},\n\t// Example of ID-card RPC.Vote\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    VoterChoices,\n\t\t\"Auth\":      status.IDcardAuth,\n\t\t// int64 = 768699984047453265 aaaf8900fff3451\n\t\t\"Lease\": \"aaaf8900fff3451\",\n\t},\n\t// Example of Web eID RPC.Challenge\n\t{\n\t\t\"SessionID\": \"0101e9342abab1577b8b2844d6a1d317\",\n\t\t\"Caller\":    status.NoAuth,\n\t\t\"Auth\":      status.WebeIDAuth,\n\t\t\"Lease\":     \"\",\n\t},\n}\n\nvar sessionStatusUpdateReqs = []map[string]any{\n\t// Example of RPC.Authenticate Update request with Mobile-ID\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": Authenticate,\n\t\t\"Auth\":   status.MobileIDAuth,\n\t\t\"Lease\":  \"0\",\n\t},\n\t// Example of RPC.AuthenticateStatus request with Mobile-ID\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": AuthenticateStatus,\n\t\t\"Auth\":   status.MobileIDAuth,\n\t\t// int64 = 3367438597345620447\n\t\t\"Lease\": \"2ebb8b9015b9b1df\",\n\t},\n\t// Example of Web eID RPC.Token request\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": Token,\n\t\t\"Auth\":   status.WebeIDAuth,\n\t\t// int64 = 1936286082718970961\n\t\t\"Lease\": \"1adf11eeefff3451\",\n\t},\n\t// Example of ID-card RPC.VoterChoices\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": VoterChoices,\n\t\t\"Auth\":   status.IDcardAuth,\n\t\t\"Lease\":  \"0\",\n\t},\n\t// Example of ID-card RPC.Vote\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": Vote,\n\t\t\"Auth\":   status.IDcardAuth,\n\t\t// int64 = 768699984047453265\n\t\t\"Lease\": \"aaaf8900fff34511\",\n\t},\n\t// Example of Web eID RPC.Challenge\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Caller\": Challenge,\n\t\t\"Auth\":   status.WebeIDAuth,\n\t\t\"Lease\":  \"0\",\n\t},\n}\n\nvar sessionStatusUpdateResps = []map[string]any{\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Ok\":     true,\n\t},\n\t{\n\t\t\"Header\": headers[0],\n\t\t\"Ok\":     false,\n\t},\n}\n\n// Responses for Update and Delete are literally same\nvar sessionStatusDeleteResps = sessionStatusUpdateResps\n\n// Proves that SessionStatusReadReqBuilder can produce correct StatusReadReq.\nfunc TestSessionStatusReadReqBuilder(t *testing.T) {\n\tfor _, h := range headers {\n\t\texpected := StatusReadReq{Header: h}\n\n\t\tgot := NewSessionStatusReadReqBuilder().\n\t\t\tWithHeader(h).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n\n// Proves that SessionStatusReadRespBuilder can produce correct StatusReadResp.\nfunc TestSessionStatusReadRespBuilder(t *testing.T) {\n\tfor _, sessionStatusReadResp := range sessionStatusReadResps {\n\t\texpected := StatusReadResp{\n\t\t\tHeader: server.Header{\n\t\t\t\tSessionID: sessionStatusReadResp[\"SessionID\"].(string),\n\t\t\t},\n\t\t\tCaller: sessionStatusReadResp[\"Caller\"].(string),\n\t\t\tAuth:   sessionStatusReadResp[\"Auth\"].(string),\n\t\t\tLease:  sessionStatusReadResp[\"Lease\"].(string),\n\t\t}\n\n\t\tgot := NewSessionStatusReadRespBuilder().\n\t\t\tWithResponse(sessionStatusReadResp).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n\n// Proves that SessionStatusUpdateReqBuilder can produce correct StatusUpdateReq.\nfunc TestSessionStatusUpdateReqBuilder(t *testing.T) {\n\tfor _, sessionStatusUpdateReq := range sessionStatusUpdateReqs {\n\t\texpected := StatusUpdateReq{\n\t\t\tHeader: sessionStatusUpdateReq[\"Header\"].(server.Header),\n\t\t\tCaller: sessionStatusUpdateReq[\"Caller\"].(string),\n\t\t\tAuth:   sessionStatusUpdateReq[\"Auth\"].(string),\n\t\t\tLease:  sessionStatusUpdateReq[\"Lease\"].(string),\n\t\t}\n\n\t\tgot := NewSessionStatusUpdateReqBuilder().\n\t\t\tWithHeader(sessionStatusUpdateReq[\"Header\"].(server.Header)).\n\t\t\tWithCaller(sessionStatusUpdateReq[\"Caller\"].(string)).\n\t\t\tWithAuth(sessionStatusUpdateReq[\"Auth\"].(string)).\n\t\t\tWithLease(sessionStatusUpdateReq[\"Lease\"].(string)).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n\n// Proves that SessionStatusUpdateRespBuilder can produce correct StatusUpdateResp.\nfunc TestNewSessionStatusUpdateRespBuilder(t *testing.T) {\n\tfor _, sessionStatusUpdateResp := range sessionStatusUpdateResps {\n\t\texpected := StatusUpdateResp{\n\t\t\tOk: sessionStatusUpdateResp[\"Ok\"].(bool),\n\t\t}\n\n\t\tgot := NewSessionStatusUpdateRespBuilder().\n\t\t\tWithResponse(sessionStatusUpdateResp).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n\n// Proves that StatusDeleteReqBuilder can produce correct StatusDeleteReq.\nfunc TestSessionStatusDeleteReqBuilder(t *testing.T) {\n\tfor _, h := range headers {\n\t\texpected := StatusDeleteReq{Header: h}\n\n\t\tgot := NewSessionStatusDeleteReqBuilder().\n\t\t\tWithHeader(h).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n\n// Proves that SessionStatusDeleteRespBuilder can produce correct StatusDeleteResp.\nfunc TestNewSessionStatusDeleteRespBuilder(t *testing.T) {\n\tfor _, sessionStatusDeleteResp := range sessionStatusDeleteResps {\n\t\texpected := StatusDeleteResp{\n\t\t\tOk: sessionStatusDeleteResp[\"Ok\"].(bool),\n\t\t}\n\n\t\tgot := NewSessionStatusDeleteRespBuilder().\n\t\t\tWithResponse(sessionStatusDeleteResp).\n\t\t\tBuild()\n\n\t\tif !reflect.DeepEqual(expected, got) {\n\t\t\tt.Errorf(commonErrTxt3, expected, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"crypto/tls\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\tstatus \"ivxv.ee/common/collector/status/client\"\n\tclient \"ivxv.ee/common/collector/status/client/rpc\"\n\t\"ivxv.ee/common/collector/storage/etcd\"\n\t\"ivxv.ee/common/collector/yaml\"\n)\n\ntype Client struct {\n\tstatus.TLSDialer\n}\n\n// NewClient configures session status TLS client to communicate with a\n// session status service.\nfunc NewClient(c *command.C) (status.TLSDialer, int) {\n\t// Get session status client configuration from technical.yml\n\tobservable := c.Conf.Technical.Status.Session\n\tif observable == nil {\n\t\treturn nil, c.Error(exit.Config, SessionObservableNotConfiguredError{\n\t\t\tDescription: _SESSIONSTATUS_CONF,\n\t\t},\n\t\t\t\"failed to read session observable client from configuration\")\n\t}\n\n\t// Get storage CA certificate from technical.yml (storage:conf:ca)\n\tvar storageConf etcd.Conf\n\terr := yaml.Apply(c.Conf.Technical.Storage.Conf, &storageConf)\n\tif err != nil {\n\t\treturn nil, c.Error(exit.Config, ReadStorageCAFromTechicalConfigError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_STORAGE_CONF},\n\t\t\t\"failed to read CA certificate from storage configuration:\", err)\n\t}\n\n\t// Add CA certificate to in-memory certificate pool\n\tcertPool, err := cryptoutil.PEMCertificatePool(storageConf.CA)\n\tif err != nil {\n\t\treturn nil, c.Error(exit.Config, AddStorageCAToCAPoolError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_STORAGE_CA},\n\t\t\t\"failed to add storage CA to certificate pool:\", err)\n\t}\n\n\t// Get filepath of a client TLS cert and key\n\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\n\t// Parse client TLS certificate-key pair\n\ttlsCert, err := tls.LoadX509KeyPair(cert, key)\n\tif err != nil {\n\t\treturn nil, c.Error(exit.Config, ParseTLSKeyPairError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_KEY_PAIR},\n\t\t\t\"failed to parse TLS client certificate-key pair:\", err)\n\t}\n\n\t// Get network segment of a client\n\tnetwork, _ := c.Conf.Technical.Service(c.Service.ID)\n\t// List of services for a given network segment\n\tservices := c.Conf.Technical.Services(network)\n\t// Read session status service address from services\n\taddr := services.SessionStatus[0].Address\n\n\t// Create session status RPC TLS client\n\treturn &Client{\n\t\tTLSDialer: client.NewTLSClient(addr, &tls.Config{\n\t\t\tRootCAs:      certPool,\n\t\t\tCertificates: []tls.Certificate{tlsCert},\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\tServerName:   observable.ServerName,\n\t\t}),\n\t}, 0\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/dto.go",
    "content": "package rpc\n\nimport \"ivxv.ee/common/collector/server\"\n\n// StatusReadReq is a data to pass to RPC.SessionStatusRead.\ntype StatusReadReq struct {\n\tserver.Header\n}\n\n// StatusReadResp is a data that RPC.SessionStatusRead responds back.\ntype StatusReadResp struct {\n\tserver.Header\n\n\t// Caller is an RPC method which was previously called.\n\tCaller string\n\n\t// Auth is an authentication method being used.\n\tAuth string\n\n\t// Lease is an optional field that indicates an ID of TTL timer.\n\t// It may present or may not, depending on implementation.\n\tLease string\n}\n\n// StatusUpdateReq is a data to pass to RPC.SessionStatusUpdate.\n//\n// For each field description see StatusReadResp.\ntype StatusUpdateReq struct {\n\tserver.Header\n\t// Caller is an RPC method which was currently called.\n\tCaller string\n\n\t// Auth is an authentication method being used.\n\tAuth string\n\n\t// Lease is an optional field that indicates an ID of TTL timer.\n\tLease string\n\n\t// Lease is an optional field that indicates a TTL in seconds.\n\tTTL string\n}\n\n// StatusUpdateResp is a data that RPC.SessionStatusUpdate responds back.\ntype StatusUpdateResp struct {\n\tserver.Header\n\n\t// Ok is true if updating was successful.\n\tOk bool\n}\n\n// StatusDeleteReq is a data to pass to RPC.SessionStatusDelete.\ntype StatusDeleteReq struct {\n\tserver.Header\n}\n\n// StatusDeleteResp is a data that RPC.SessionStatusDelete responds back.\ntype StatusDeleteResp struct {\n\tserver.Header\n\n\t// Ok is true if updating was successful.\n\tOk bool\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/endpoints.go",
    "content": "package rpc\n\n// Endpoint is a collection of available session status server RPC endpoints.\nvar Endpoint = struct {\n\tSessionStatusRead   string\n\tSessionStatusUpdate string\n\tSessionStatusDelete string\n}{\n\t\"RPC.SessionStatusRead\",\n\t\"RPC.SessionStatusUpdate\",\n\t\"RPC.SessionStatusDelete\",\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CONF                           = \"Failed to parse sessionstatus service configuration\"\n\t_SESSIONSTATUS_STORAGE_CONF                   = \"Failed to parse storage service configuration\"\n\t_SESSIONSTATUS_STORAGE_CA                     = \"Failed to add storage service CA certificate to CA pool\"\n\t_SESSIONSTATUS_KEY_PAIR                       = \"Failed to parse sessionstatus service key and certificate\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER = \"Cannot cast VerifyReq to server.Header\"\n)\n"
  },
  {
    "path": "sessionstatus/api/rpc/rpc.go",
    "content": "// Package rpc defines an API for any implementation services that\n// wish to communicate with a session status reporting service (server).\n//\n// This API is not supposed to be a part of common/collector packages collection,\n// since this API is the session status service specific and therefore cannot\n// be reused elsewhere.\npackage rpc\n\nconst (\n\tsessionIDField = \"SessionID\"\n\tcallerField    = \"Caller\"\n\tauthField      = \"Auth\"\n\tleaseField     = \"Lease\"\n\tokField        = \"Ok\"\n)\n"
  },
  {
    "path": "sessionstatus/api/rpc/util.go",
    "content": "package rpc\n\nimport (\n\t\"reflect\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tstatusRpc \"ivxv.ee/common/collector/status/client/rpc\"\n)\n\nconst expectedCastForServerHeader = \"server.Header\"\n\n// CastVerifyRequestToServerHeader tries to cast req to server.Header.\nfunc CastVerifyRequestToServerHeader(req *statusRpc.VerifyReq) (*server.Header, error) {\n\t// request.Request should cast to server.Header\n\theader, ok := req.Request.(server.Header)\n\tif !ok {\n\t\treturn nil, CastVerifyReqToServerHeaderError{\n\t\t\tExpected:    expectedCastForServerHeader,\n\t\t\tGot:         reflect.TypeOf(req.Request),\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER,\n\t\t}\n\t}\n\n\treturn &header, nil\n}\n"
  },
  {
    "path": "sessionstatus/api/rpc/util_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tclient \"ivxv.ee/common/collector/status/client/rpc\"\n)\n\nconst (\n\tsomeServiceMethod = \"RPC.SomeServiceMethod\"\n\tmsgExpectNoErrors = \"Expected no errors, got %v\\n\"\n)\n\nvar goodVerifyReqs = []*client.VerifyReq{\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: server.Header{\n\t\t\tSessionID:  \"b64\",\n\t\t\tOS:         \"My perfect OS\",\n\t\t\tAuthMethod: \"tls\",\n\t\t},\n\t},\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: server.Header{\n\t\t\tSessionID: \"b64\",\n\t\t},\n\t},\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest:       server.Header{},\n\t},\n}\n\nvar badVerifyReqs = []*client.VerifyReq{\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: struct {\n\t\t\tCtx        context.Context\n\t\t\tSessionID  string\n\t\t\tOS         string\n\t\t\tAuthMethod string\n\t\t\tAuthToken  []byte\n\t\t\tDataToken  []byte\n\t\t}{\n\t\t\tSessionID:  \"b64\",\n\t\t\tOS:         \"My perfect OS\",\n\t\t\tAuthMethod: \"tls\",\n\t\t},\n\t},\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest: struct {\n\t\t\tSessionID  string\n\t\t\tOS         string\n\t\t\tAuthMethod string\n\t\t}{\n\t\t\tSessionID:  \"b64\",\n\t\t\tOS:         \"My perfect OS\",\n\t\t\tAuthMethod: \"tls\",\n\t\t},\n\t},\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest:       struct{}{},\n\t},\n\t{\n\t\tServiceMethod: someServiceMethod,\n\t\tRequest:       nil,\n\t},\n}\n\nfunc TestCastVerifyRequestToServerHeader(t *testing.T) {\n\tfor _, goodverifyReq := range goodVerifyReqs {\n\t\theader, err := CastVerifyRequestToServerHeader(goodverifyReq)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\n\t\texpectedHeader := goodverifyReq.Request.(server.Header)\n\n\t\tif !reflect.DeepEqual(*header, expectedHeader) {\n\t\t\tmsg := \"Expected v1 == v2, got v1: %v, v2: %v\\n\"\n\t\t\tt.Errorf(msg, *header, expectedHeader)\n\t\t}\n\t}\n\n\tfor _, badverifyReq := range badVerifyReqs {\n\t\theader, err := CastVerifyRequestToServerHeader(badverifyReq)\n\n\t\tmsg := \"Expected CastVerifyReqToServerHeaderError, got %v\\n\"\n\t\tif err == nil || header != nil {\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\n\t\texpected := new(CastVerifyReqToServerHeaderError)\n\t\texpected.Expected = expectedCastForServerHeader\n\t\texpected.Got = reflect.TypeOf(badverifyReq.Request)\n\t\texpected.Description = _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER\n\n\t\tif !reflect.DeepEqual(*expected, err) {\n\t\t\tmsg = \"Expected %v, got %v\\n\"\n\t\t\tt.Errorf(msg, expected, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "sessionstatus/go.mod",
    "content": "module ivxv.ee/sessionstatus\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ./api\n"
  },
  {
    "path": "sessionstatus/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "sessionstatus/internal/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSREADREQ    = \"Cannot cast any to SessionStatusReadReq\"\n\t_SESSIONSTATUS_READ                             = \"Failed to read session status data from a database\"\n\t_SESSIONSTATUS_RESP                             = \"Invalid session status format fetched from a database\"\n\t_SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSUPDATEDREQ = \"Cannot cast any to SessionStatusUpdateReq\"\n\t_SESSIONSTATUS_PUT                              = \"Failed to update session status data in a database\"\n\t_SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSDELETEDREQ = \"Cannot cast any to SessionStatusDeleteReq\"\n\t_SESSIONSTATUS_DEL                              = \"Failed to delete session status data in a database\"\n\t_SESSIONSTATUS_SESSSTATREADREQ                  = \"RPC.SessionStatusReadReq\"\n\t_SESSIONSTATUS_CAST_ANY_TO_SESSSTATREADRESP     = \"Cannot cast any to SessionStatusReadResp\"\n\t_SESSIONSTATUS_SESSSTATREADRESP                 = \"RPC.SessionStatusReadResp\"\n\t_SESSIONSTATUS_SESSSTATUPDATEREQ                = \"RPC.SessionStatusUpdateReq\"\n\t_SESSIONSTATUS_SESSSTATUPDATERESP               = \"RPC.SessionStatusUpdateResp\"\n\t_SESSIONSTATUS_SESSSTATDELETEREQ                = \"RPC.SessionStatusDeleteReq\"\n\t_SESSIONSTATUS_SESSSTATDELETERESP               = \"RPC.SessionStatusDeleteResp\"\n\t_SESSIONSTATUS_PARSE_B64                        = \"Invalid base-64 session status format\"\n\t_SESSIONSTATUS_PARSE                            = \"Invalid session status format\"\n)\n"
  },
  {
    "path": "sessionstatus/internal/rpc/repository.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\n\t\"ivxv.ee/common/collector/status\"\n\t\"ivxv.ee/common/collector/storage\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\tsessionIDPrefix             = \"/session\"\n\tseparator                   = \"\\x1f\"\n\tstatusReadRespDBRecordCount = 2\n)\n\ntype client struct {\n\trepository storage.SessionStatusRepository\n}\n\n// NewStatusRepository initializes storage client r for a session\n// status server. TTL value can be used for assigning expiration time\n// for a key in a database.\nfunc NewStatusRepository(r storage.SessionStatusRepository) status.Status {\n\treturn &client{repository: r}\n}\n\nfunc (c *client) Read(ctx context.Context, data interface{}) (interface{}, error) {\n\t// Any data that is passed here, must cast to *SessionStatusReadReq\n\treq, err := castAnyToSessionStatusReadReq(data)\n\tif err != nil {\n\t\treturn nil, CastAnyToSessionStatusReadReqError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSREADREQ}\n\t}\n\n\tkey := toSessionStorageKey(req.Header.SessionID)\n\n\t// GET database query.\n\t// val can be either nil or []byte.\n\t// nil means an error or absence of a value\n\tval, lease, err := c.repository.GetWithLease(ctx, key)\n\tif err != nil {\n\t\treturn nil, GetWithLeaseError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _SESSIONSTATUS_READ,\n\t\t}\n\t}\n\n\t// nil means that val doesn't exist in a database,\n\t// for a caller it means either req.Header.SessionID key\n\t// is a brand-new session ID or existing but tampered session ID.\n\t// Decision should be made by a caller!\n\tif val == nil {\n\t\treturn &api.StatusReadResp{Header: req.Header}, nil\n\t}\n\n\t// Verify val correctness\n\tarray, err := parseSessionStatus(val)\n\tif err != nil {\n\t\treturn nil, ParseSessionStatusError{\n\t\t\tValue:       val,\n\t\t\tErr:         err,\n\t\t\tDescription: _SESSIONSTATUS_RESP,\n\t\t}\n\t}\n\n\treturn &api.StatusReadResp{\n\t\tHeader: req.Header,\n\t\tCaller: array[0],\n\t\tAuth:   array[1],\n\t\tLease:  lease,\n\t}, nil\n}\n\nfunc (c *client) Update(ctx context.Context, data interface{}) error {\n\t// Any data that is passed here, must cast to *SessionStatusUpdateReq\n\treq, err := castAnyToSessionStatusUpdateReq(data)\n\tif err != nil {\n\t\treturn CastAnyToSessionStatusUpdateReqError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSUPDATEDREQ}\n\t}\n\n\t// Value should be stored in a database as\n\t// base64(Caller\\x1FAuth)\n\tconcat := req.Caller + separator + req.Auth\n\tval := []byte(base64.StdEncoding.EncodeToString([]byte(concat)))\n\n\t// TTL value is int and measured in seconds!\n\tttl := &storage.PutOpOptionWithTTL{TTL: req.TTL, LeaseID: req.Lease}\n\n\tkey := toSessionStorageKey(req.Header.SessionID)\n\n\t// PUT query to a database, where key will be deleted after ttl amount\n\t// of seconds\n\terr = c.repository.PutForceWithOpts(ctx, key, val, ttl)\n\tif err != nil {\n\t\treturn PutForceWithOptsError{\n\t\t\tKey:         key,\n\t\t\tValue:       val,\n\t\t\tErr:         err,\n\t\t\tDescription: _SESSIONSTATUS_PUT,\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *client) Delete(ctx context.Context, data interface{}) error {\n\t// Any data that is passed here, must cast to *SessionStatusDeleteReq\n\treq, err := castAnyToSessionStatusDeleteReq(data)\n\tif err != nil {\n\t\treturn CastAnyToSessionStatusDeleteReqError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSDELETEDREQ}\n\t}\n\n\tkey := toSessionStorageKey(req.Header.SessionID)\n\n\t// DELETE query to a database\n\terr = c.repository.Delete(ctx, key)\n\tif err != nil {\n\t\treturn DeleteError{\n\t\t\tKey:         key,\n\t\t\tErr:         err,\n\t\t\tDescription: _SESSIONSTATUS_DEL,\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "sessionstatus/internal/rpc/server.go",
    "content": "package rpc\n\nimport (\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/status\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\n// NewHandler returns an RPC handler that is ready to pass as rcvr\n// parameter into rpc.NewServer().Register(rcvr), which in turn means\n// that all rules applied to rcvr must also apply to a returned handler.\nfunc NewHandler(status status.Status) *RPC {\n\treturn &RPC{status: status}\n}\n\n// RPC is a handler to process microservices' session status requests.\ntype RPC struct {\n\tstatus status.Status\n}\n\n// SessionStatusRead is an RPC endpoint to provide an information\n// about server.Header.SessionID of the client.\n//\n// Client should pass a request req, that includes req.server.Header.SessionID,\n// then response resp will be an information includes previous RPC.Method being\n// called with this server.Header.SessionID and Auth, which is a detailed\n// authentication method being used (\"id\" for ID-card, \"mid\" for Mobile-ID,\n// \"sid\" for Smart-ID, \"wid\" for Web eID). Lease is used to keep track of\n// the TTL value of that particular server.Header.SessionID in a database.\nfunc (r *RPC) SessionStatusRead(req api.StatusReadReq, resp *api.StatusReadResp) error {\n\tlog.Log(req.Ctx, SessionStatusReadReq{Description: _SESSIONSTATUS_SESSSTATREADREQ})\n\n\t// s is either nil or *StatusReadResp.\n\t// nil s is only possible if error\n\ts, err := r.status.Read(req.Ctx, &req)\n\tif err != nil {\n\t\treturn SessionStatusReadError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_READ}\n\t}\n\n\t// s should be castable to *SessionStatusReadResp.\n\t// There is no way to s being non-castable\n\t// to *StatusReadResp, if so, then code has bugs\n\treadResp, err := castAnyToSessionStatusReadResp(s)\n\tif err != nil {\n\t\treturn CastAnyToSessionStatusReadRespError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATREADRESP}\n\t}\n\n\tresp.Header = readResp.Header\n\tresp.Caller = readResp.Caller\n\tresp.Auth = readResp.Auth\n\tresp.Lease = readResp.Lease\n\n\tlog.Log(req.Ctx, SessionStatusReadResp{\n\t\tCaller:      resp.Caller,\n\t\tAuth:        resp.Auth,\n\t\tLeaseID:     resp.Lease,\n\t\tDescription: _SESSIONSTATUS_SESSSTATREADRESP,\n\t})\n\treturn nil\n}\n\n// SessionStatusUpdate is an RPC endpoint to update current information of a\n// server.Header.SessionID client in the database. Information to be updated\n// is passed in req. On success resp is returned, otherwise error.\nfunc (r *RPC) SessionStatusUpdate(req api.StatusUpdateReq, resp *api.StatusUpdateResp) error {\n\tlog.Log(req.Ctx, SessionStatusUpdateReq{\n\t\tCaller:      req.Caller,\n\t\tAuth:        req.Auth,\n\t\tLeaseID:     req.Lease,\n\t\tDescription: _SESSIONSTATUS_SESSSTATUPDATEREQ,\n\t})\n\n\t// Update session status in a database\n\terr := r.status.Update(req.Ctx, &req)\n\tif err != nil {\n\t\treturn SessionStatusUpdateError{\n\t\t\tSessionID:   req.SessionID,\n\t\t\tCaller:      req.Caller,\n\t\t\tAuth:        req.Auth,\n\t\t\tErr:         err,\n\t\t\tDescription: _SESSIONSTATUS_PUT,\n\t\t}\n\t}\n\n\tresp.Ok = true\n\n\tlog.Log(req.Ctx, SessionStatusUpdateResp{Success: resp.Ok,\n\t\tDescription: _SESSIONSTATUS_SESSSTATUPDATERESP})\n\treturn nil\n}\n\n// SessionStatusDelete is an RPC endpoint to delete current information of a\n// server.Header.SessionID client in the database. Information to be deleted\n// is passed in req. On success resp is returned, otherwise error.\nfunc (r *RPC) SessionStatusDelete(req api.StatusDeleteReq, resp *api.StatusDeleteResp) error {\n\tlog.Log(req.Ctx, SessionStatusDeleteReq{Description: _SESSIONSTATUS_SESSSTATDELETEREQ})\n\n\t// Delete session status in a database\n\terr := r.status.Delete(req.Ctx, &req)\n\tif err != nil {\n\t\treturn SessionStatusDeleteError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_DEL}\n\t}\n\n\tresp.Ok = true\n\n\tlog.Log(req.Ctx, SessionStatusDeleteResp{Success: resp.Ok,\n\t\tDescription: _SESSIONSTATUS_SESSSTATDELETERESP})\n\treturn nil\n}\n"
  },
  {
    "path": "sessionstatus/internal/rpc/util.go",
    "content": "package rpc\n\nimport (\n\t\"encoding/base64\"\n\t\"reflect\"\n\t\"strings\"\n\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\texpectedCastForStatusReadReq   = \"*api.StatusReadReq\"\n\texpectedCastForStatusReadResp  = \"*api.StatusReadResp\"\n\texpectedCastForStatusUpdateReq = \"*api.StatusUpdateReq\"\n\texpectedCastForStatusDeleteReq = \"*api.StatusDeleteReq\"\n)\n\n// parseSessionStatus parses val. This function expects val to be\n// base64(\"string\" + \"\\x1F\" + \"string\"), otherwise returns nil and error.\nfunc parseSessionStatus(val []byte) ([]string, error) {\n\t// val is always in a form of:\n\t// base64(\"RPC.Method\" + \"\\x1F\" + \"Auth\")\n\t// Auth is \"id\" or \"mid\" or \"sid\" or \"wid\"\n\tsessionStatus, err := base64.StdEncoding.DecodeString(string(val))\n\tif err != nil {\n\t\treturn nil, Base64DecodeSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_PARSE_B64}\n\t}\n\n\tsessionStatusStr := string(sessionStatus)\n\n\tarray := strings.Split(sessionStatusStr, separator)\n\tif len(array) != statusReadRespDBRecordCount {\n\t\treturn nil, InvalidReadStatusDatabaseRecordCountError{\n\t\t\tExpected:    statusReadRespDBRecordCount,\n\t\t\tGot:         len(array),\n\t\t\tRecord:      sessionStatusStr,\n\t\t\tDescription: _SESSIONSTATUS_PARSE,\n\t\t}\n\t}\n\n\treturn array, nil\n}\n\n// castAnyToSessionStatusReadReq tries to cast req to *StatusReadReq.\nfunc castAnyToSessionStatusReadReq(req interface{}) (*api.StatusReadReq, error) {\n\t// Cast to *StatusReadReq\n\tsessionStatusReadReq, ok := req.(*api.StatusReadReq)\n\tif !ok {\n\t\treturn nil, CastToStatusReadReqError{\n\t\t\tExpected:    expectedCastForStatusReadReq,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSREADREQ,\n\t\t}\n\t}\n\n\treturn sessionStatusReadReq, nil\n}\n\n// castAnyToSessionStatusReadResp tries to cast req to *StatusReadResp.\nfunc castAnyToSessionStatusReadResp(req interface{}) (*api.StatusReadResp, error) {\n\t// Cast to *StatusReadResp\n\tsessionStatusReadResp, ok := req.(*api.StatusReadResp)\n\tif !ok {\n\t\treturn nil, CastToStatusReadRespError{\n\t\t\tExpected:    expectedCastForStatusReadResp,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATREADRESP,\n\t\t}\n\t}\n\n\treturn sessionStatusReadResp, nil\n}\n\n// castAnyToSessionStatusUpdateReq tries to cast req to *StatusUpdateReq.\nfunc castAnyToSessionStatusUpdateReq(req interface{}) (*api.StatusUpdateReq, error) {\n\t// Cast to *StatusUpdateReq\n\tsessionStatusUpdateReq, ok := req.(*api.StatusUpdateReq)\n\tif !ok {\n\t\treturn nil, CastToStatusUpdateReqError{\n\t\t\tExpected:    expectedCastForStatusUpdateReq,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSUPDATEDREQ,\n\t\t}\n\t}\n\n\treturn sessionStatusUpdateReq, nil\n}\n\n// castAnyToSessionStatusDeleteReq tries to cast req to *StatusDeleteReq.\nfunc castAnyToSessionStatusDeleteReq(req interface{}) (*api.StatusDeleteReq, error) {\n\t// Cast to *StatusDeleteReq\n\tsessionStatusDeleteReq, ok := req.(*api.StatusDeleteReq)\n\tif !ok {\n\t\treturn nil, CastToStatusDeleteReqError{\n\t\t\tExpected:    expectedCastForStatusDeleteReq,\n\t\t\tGot:         reflect.TypeOf(req),\n\t\t\tDescription: _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSDELETEDREQ,\n\t\t}\n\t}\n\n\treturn sessionStatusDeleteReq, nil\n}\n\n// toSessionStorageKey returns NoSQL repository key for a given sessionID.\nfunc toSessionStorageKey(sessionID string) string {\n\treturn sessionIDPrefix + \"/\" + sessionID\n}\n"
  },
  {
    "path": "sessionstatus/internal/rpc/util_test.go",
    "content": "package rpc\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/server\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\tmsgExpectNoErrors        = \"Expected no errors, got %v\\n\"\n\tmsgExpectOneGotAnother   = \"Excepted %v, got %v\\n\"\n\tmsgExpectNilAndError     = \"Expected value nil and error, got value: %v, error: %v\\n\"\n\tmsgExpectValueAndNoError = \"Expected value %v and no error, got value: %v, error: %v\\n\"\n\tsomeServiceMethodCaller  = \"RPC.SomeServiceMethodCaller\"\n\tsomAuth                  = \"SomeAuth\"\n)\n\nvar goodSessionStatuses = [][]byte{\n\t// b64(\"Hello\" + separator + \"World\")\n\t[]byte(\"SGVsbG8fV29ybGQ=\"),\n\t// b64(\"Hello\" + separator + \"\")\n\t[]byte(\"SGVsbG8f\"),\n\t// b64(\"\" + separator + \"World\")\n\t[]byte(\"H1dvcmxk\"),\n\t// b64(\"\" + separator + \"\")\n\t[]byte(\"Hw==\"),\n}\n\nvar goodSessionStatusResults = [][]string{\n\t{\"Hello\", \"World\"},\n\t{\"Hello\", \"\"},\n\t{\"\", \"World\"},\n\t{\"\", \"\"},\n}\n\nvar badFormatVals = [][]byte{\n\t// b64(\"Hello\" + \"\\xF1\" + \"World\")\n\t[]byte(\"SGVsbG/xV29ybGQ=\"),\n\t// b64(\"Hello\" + \"\\xFF\" + \"World\")\n\t[]byte(\"SGVsbG//V29ybGQ=\"),\n\t// b64(\"Hello\" + \"\\\\s\" + \"World\")\n\t[]byte(\"SGVsbG9cc1dvcmxk\"),\n\t// b64(\"Hello\" + \"\\\\r\" + \"World\")\n\t[]byte(\"SGVsbG9ccldvcmxk\"),\n\t// b64(\"Hello\" + \" \" + \"World\")\n\t[]byte(\"SGVsbG8gV29ybGQ=\"),\n\t// b64(\"Hello\" + \"World\")\n\t[]byte(\"SGVsbG9Xb3JsZA==\"),\n\t// b64(\"\")\n\t[]byte(\"\"),\n\t// b64(nil)\n\t[]byte(nil),\n}\n\nvar badBase64Vals = [][]byte{\n\t[]byte(\"13rf34545545434343452224\"),\n\t[]byte(\"X\"),\n\t[]byte(\"x\"),\n\t[]byte(\"Äratus!\"),\n}\n\nvar header = server.Header{\n\t// Example of any RPC request Header against IVXV backend. That kind of\n\t// Header will see any RPC method\n\tCtx:        context.Background(),\n\tSessionID:  \"0101e9342abab1577b8b2844d6a1d317\",\n\tOS:         \"Ubuntu Jammy 22.04 LTS\",\n\tAuthMethod: \"\",\n\tAuthToken:  nil,\n\tDataToken:  nil,\n}\n\nvar goodStatusReadReqs = []*api.StatusReadReq{\n\t{Header: header},\n\t{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t{Header: server.Header{SessionID: \"A\", AuthMethod: \"\"}},\n\t{Header: server.Header{}},\n}\n\nvar badStatusReadReqs = []any{\n\t// not a *StatusReadReq\n\t&api.StatusUpdateReq{Header: server.Header{Ctx: context.Background()}},\n\t// not a *StatusReadReq\n\tapi.StatusUpdateResp{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t// not a pointer!\n\tapi.StatusReadReq{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t// emptiness\n\tnil,\n}\n\nvar goodStatusReadResps = []*api.StatusReadResp{\n\t{\n\t\tHeader: server.Header{Ctx: context.Background()},\n\t\tCaller: someServiceMethodCaller,\n\t\tAuth:   somAuth,\n\t},\n\t{\n\t\tHeader: server.Header{Ctx: context.Background()},\n\t\tCaller: \"\",\n\t\tAuth:   \"\",\n\t},\n\t{\n\t\tHeader: server.Header{Ctx: context.Background()},\n\t},\n\t{\n\t\tHeader: server.Header{Ctx: context.Background()},\n\t\tAuth:   somAuth,\n\t},\n}\n\nvar badStatusReadResps = []any{\n\t&api.StatusReadReq{Header: server.Header{Ctx: context.Background()}},\n\tapi.StatusReadReq{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t// anonymous struct\n\tstruct{ hello string }{hello: \"world\"},\n\t// not a pointer!\n\tapi.StatusReadResp{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\tnil,\n}\n\nvar goodStatusUpdateReqs = []*api.StatusUpdateReq{\n\t{\n\t\tHeader: header,\n\t\tCaller: someServiceMethodCaller,\n\t\tAuth:   somAuth,\n\t\tLease:  \"0\",\n\t},\n\t{\n\t\tHeader: header,\n\t\tCaller: someServiceMethodCaller,\n\t\tAuth:   somAuth,\n\t\t// int64 = 768699984047453265\n\t\tLease: \"aaaf8900fff3451\",\n\t},\n}\n\nvar badStatusUpdateReqs = []any{\n\t// not a *StatusUpdateReq\n\t&api.StatusReadReq{Header: header},\n\t// not a *StatusUpdateReq\n\tapi.StatusReadReq{Header: header},\n\t// anonymous struct\n\tstruct{ hello string }{hello: \"world\"},\n\t// not a pointer!\n\tapi.StatusUpdateReq{Header: server.Header{SessionID: \"\"}},\n\tnil,\n}\n\nvar goodStatusDeleteReqs = []*api.StatusDeleteReq{\n\t{Header: header},\n\t{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t{Header: server.Header{SessionID: \"A\", AuthMethod: \"\"}},\n\t{Header: server.Header{}},\n}\n\nvar badStatusDeleteReqs = []any{\n\t// not a *StatusDeleteReq\n\t&api.StatusUpdateReq{Header: server.Header{Ctx: context.Background()}},\n\t// not a *StatusDeleteReq\n\tapi.StatusUpdateResp{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t// not a pointer!\n\tapi.StatusDeleteReq{Header: server.Header{SessionID: \"AAABBBCCCDDD\"}},\n\t// emptiness\n\tnil,\n}\n\nfunc TestParseSessionStatus(t *testing.T) {\n\t// Only good values from a database\n\tfor i, goodSessionStatus := range goodSessionStatuses {\n\t\tparsed, err := parseSessionStatus(goodSessionStatus)\n\t\tif err != nil {\n\t\t\tt.Errorf(msgExpectNoErrors, err)\n\t\t}\n\t\tif parsed[0] != goodSessionStatusResults[i][0] {\n\t\t\tt.Errorf(msgExpectOneGotAnother, goodSessionStatusResults[0], parsed[0])\n\t\t}\n\t\tif parsed[1] != goodSessionStatusResults[i][1] {\n\t\t\tt.Errorf(msgExpectOneGotAnother, goodSessionStatusResults[1], parsed[1])\n\t\t}\n\t}\n\n\t// Correctly base64 encoded but incorrectly formatted values\n\tfor _, badFormatVal := range badFormatVals {\n\t\tparsed, err := parseSessionStatus(badFormatVal)\n\t\tif err == nil || parsed != nil {\n\t\t\tmsg := \"Excepted error, got %v\\n\"\n\t\t\tt.Errorf(msg, err)\n\t\t}\n\t\tb64, err := base64.StdEncoding.DecodeString(string(badFormatVal))\n\t\tif err != nil {\n\t\t\tmsg := \"Excepted value %v to be base64 decodable, got %v\\n\"\n\t\t\tt.Errorf(msg, string(badFormatVal), err)\n\t\t}\n\t\texpected := new(InvalidReadStatusDatabaseRecordCountError)\n\t\texpected.Expected = statusReadRespDBRecordCount\n\t\texpected.Got = 1\n\t\texpected.Record = b64\n\t\texpected.Description = _SESSIONSTATUS_PARSE\n\t\tif reflect.DeepEqual(err, *expected) {\n\t\t\tt.Errorf(msgExpectOneGotAnother, expected, err)\n\t\t}\n\t}\n\n\t// Incorrectly base64 encoded values\n\tfor _, badBase64Val := range badBase64Vals {\n\t\tr, err := parseSessionStatus(badBase64Val)\n\t\tif err == nil || r != nil {\n\t\t\tt.Errorf(msgExpectNilAndError, r, err)\n\t\t}\n\t}\n}\n\nfunc TestCastAnyToSessionStatusReadReq(t *testing.T) {\n\t// Only good values from a database Read are allowed\n\tfor _, goodStatusReadReq := range goodStatusReadReqs {\n\t\tparsed, err := castAnyToSessionStatusReadReq(goodStatusReadReq)\n\t\tif parsed == nil || err != nil {\n\t\t\tt.Errorf(msgExpectValueAndNoError, goodStatusReadReq, parsed, err)\n\t\t}\n\t}\n\n\t// Bad values from a database Read\n\tfor _, badStatusReadReq := range badStatusReadReqs {\n\t\tparsed, err := castAnyToSessionStatusReadReq(badStatusReadReq)\n\t\tif parsed != nil || err == nil {\n\t\t\tt.Errorf(msgExpectNilAndError, parsed, err)\n\t\t}\n\t\texpected := new(CastToStatusReadReqError)\n\t\texpected.Expected = expectedCastForStatusReadReq\n\t\texpected.Got = reflect.TypeOf(badStatusReadReq)\n\t\texpected.Description = _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSREADREQ\n\t\tif !reflect.DeepEqual(err, *expected) {\n\t\t\tt.Errorf(msgExpectOneGotAnother, expected, err)\n\t\t}\n\t}\n}\n\nfunc TestCastAnyToSessionStatusReadResp(t *testing.T) {\n\t// Only good values from a database Read are allowed\n\tfor _, goodStatusReadResp := range goodStatusReadResps {\n\t\tparsed, err := castAnyToSessionStatusReadResp(goodStatusReadResp)\n\t\tif parsed == nil || err != nil {\n\t\t\tt.Errorf(msgExpectValueAndNoError, goodStatusReadResp, parsed, err)\n\t\t}\n\t}\n\n\t// Bad values from a database Read\n\tfor _, badStatusReadResp := range badStatusReadResps {\n\t\tparsed, err := castAnyToSessionStatusReadResp(badStatusReadResp)\n\t\tif parsed != nil || err == nil {\n\t\t\tt.Errorf(msgExpectNilAndError, parsed, err)\n\t\t}\n\n\t\texpected := new(CastToStatusReadRespError)\n\t\texpected.Expected = expectedCastForStatusReadResp\n\t\texpected.Got = reflect.TypeOf(badStatusReadResp)\n\t\texpected.Description = _SESSIONSTATUS_CAST_ANY_TO_SESSSTATREADRESP\n\t\tif !reflect.DeepEqual(err, *expected) {\n\t\t\tt.Errorf(msgExpectOneGotAnother, expected, err)\n\t\t}\n\t}\n}\n\nfunc TestCastAnyToSessionStatusUpdateReq(t *testing.T) {\n\t// Only good values from a database Update are allowed\n\tfor _, goodStatusUpdateReq := range goodStatusUpdateReqs {\n\t\tparsed, err := castAnyToSessionStatusUpdateReq(goodStatusUpdateReq)\n\t\tif parsed == nil || err != nil {\n\t\t\tt.Errorf(msgExpectValueAndNoError, goodStatusUpdateReq, parsed, err)\n\t\t}\n\t}\n\n\t// Bad values from a database Update\n\tfor _, badStatusUpdateReq := range badStatusUpdateReqs {\n\t\tparsed, err := castAnyToSessionStatusUpdateReq(badStatusUpdateReq)\n\t\tif parsed != nil || err == nil {\n\t\t\tt.Errorf(msgExpectNilAndError, parsed, err)\n\t\t}\n\t\texpected := new(CastToStatusUpdateReqError)\n\t\texpected.Expected = expectedCastForStatusUpdateReq\n\t\texpected.Got = reflect.TypeOf(badStatusUpdateReq)\n\t\texpected.Description = _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSUPDATEDREQ\n\t\tif !reflect.DeepEqual(err, *expected) {\n\t\t\tt.Errorf(msgExpectOneGotAnother, expected, err)\n\t\t}\n\t}\n}\n\nfunc TestCastAnyToSessionStatusDeleteReq(t *testing.T) {\n\t// Only good values from a database Delete are allowed\n\tfor _, goodStatusDeleteReq := range goodStatusDeleteReqs {\n\t\tparsed, err := castAnyToSessionStatusDeleteReq(goodStatusDeleteReq)\n\t\tif parsed == nil || err != nil {\n\t\t\tt.Errorf(msgExpectValueAndNoError, goodStatusDeleteReq, parsed, err)\n\t\t}\n\t}\n\n\t// Bad values from a database Delete\n\tfor _, badStatusDeleteReq := range badStatusDeleteReqs {\n\t\tparsed, err := castAnyToSessionStatusDeleteReq(badStatusDeleteReq)\n\t\tif parsed != nil || err == nil {\n\t\t\tt.Errorf(msgExpectNilAndError, parsed, err)\n\t\t}\n\t\texpected := new(CastToStatusDeleteReqError)\n\t\texpected.Expected = expectedCastForStatusDeleteReq\n\t\texpected.Got = reflect.TypeOf(badStatusDeleteReq)\n\t\texpected.Description = _SESSIONSTATUS_CAST_ANY_TO_SESSSTATUSDELETEDREQ\n\t\tif !reflect.DeepEqual(err, *expected) {\n\t\t\tt.Errorf(msgExpectOneGotAnother, expected, err)\n\t\t}\n\t}\n}\n\nfunc TestToSessionStorageKey(t *testing.T) {\n\texpected := \"/session/123456789abc\"\n\tgot := toSessionStorageKey(\"123456789abc\")\n\tif expected != got {\n\t\tmsg := \"Expected s1 == s2, got s1: %v, s2: %v\\n\"\n\t\tt.Errorf(msg, expected, got)\n\t}\n}\n"
  },
  {
    "path": "sessionstatus/service/sessionstatus/log_desc.go",
    "content": "package main\n\nconst (\n\t_SESSIONSTATUS_START        = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_SESSIONSTATUS_STOP         = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_SESSIONSTATUS_STORAGE_CONF = \"Failed to parse storage service configuration\"\n\t_SESSIONSTATUS_SERVER       = \"Failed to parse server configuration for sessionstatus service\"\n\t_SESSIONSTATUS_SERVER_SERVE = \"Failed to serve sessionstatus service to clients\"\n)\n"
  },
  {
    "path": "sessionstatus/service/sessionstatus/main.go",
    "content": "// The sessionstatus service (Session status) is used to provide a secure\n// way of transferring SessionID during client-server communication.\n// Session status service is an internal-network microservice.\npackage main\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/storage/etcd\"\n\t\"ivxv.ee/common/collector/yaml\"\n\tinternal \"ivxv.ee/sessionstatus/internal/rpc\"\n\t//ivxv:modules common/collector/auth\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nfunc main() {\n\t// Call sessionstatus in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(sessionstatus())\n}\n\nfunc sessionstatus() (code int) {\n\tc := command.New(\"ivxv-sessionstatus\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tvar start, stop time.Time\n\tvar err error\n\n\tif elec := c.Conf.Election; elec != nil {\n\t\t// Status server starts in a test-voting period\n\t\tif start, err = elec.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err,\n\t\t\t\tDescription: _SESSIONSTATUS_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\t// Status server shuts down at the same time as all services do\n\t\tif stop, err = elec.ServiceStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StopTimeError{Err: err,\n\t\t\t\tDescription: _SESSIONSTATUS_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\t}\n\n\t// Extract x509 CA certificate of a storage service\n\t// from election.yml `storage:conf:ca`.\n\t// All IVXV services have common x509 CA certificate,\n\t// which means we can use same CA for a status server as\n\t// ClientCAs and as RootCAs for any IVXV services which\n\t// will establish TLS connection with sessionstatus service\n\tvar storageConf etcd.Conf\n\terr = yaml.Apply(c.Conf.Technical.Storage.Conf, &storageConf)\n\tif err != nil {\n\t\treturn c.Error(exit.Config, GetStorageCAError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_STORAGE_CONF},\n\t\t\t\"failed to get CA cert from a storage config:\", err)\n\t}\n\n\t// Register Session status server as an RPC server\n\tvar rpc *internal.RPC\n\n\t// Create desired repository for the server\n\tr := c.Storage.SessionStatusRepository()\n\trepository := internal.NewStatusRepository(r)\n\t// Create desired handler for the server\n\trpc = internal.NewHandler(repository)\n\n\tvar s *server.S\n\n\tif c.Conf.Technical != nil {\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\t// Ensure that cert's `X509v3 Subject Alternative Name:DNS:`\n\t\t\t// value is THE SAME AS `sessionStatusConf.ServerName`,\n\t\t\t// e.g. if your cert has\n\t\t\t// X509v3 Subject Alternative Name:DNS:session.status.inttest.ivxv.ee\n\t\t\t// and\n\t\t\t// sessionStatusConf.ServerName == session.status.inttest.ivxv.ee\n\t\t\t// then OK\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t\t// will set `tls.RequireAndVerifyClientCert` to the server TLS,\n\t\t\t// which means that any client should include RootCAs in their\n\t\t\t// TLS configuration\n\t\t\tClientCA: storageConf.CA,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err,\n\t\t\t\tDescription: _SESSIONSTATUS_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err,\n\t\t\t\tDescription: _SESSIONSTATUS_SERVER_SERVE},\n\t\t\t\t\"failed to serve choices service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "setup.cfg",
    "content": "# IVXV Internet voting framework\n# Flake8 configuration for python code checks\n\n[flake8]\nmax-line-length = 88\n\nper-file-ignores =\n    # undefined imports defined by behave: given, when, then, ...\n    tests/features/steps/*.py: F821\n"
  },
  {
    "path": "setup.py",
    "content": "# IVXV Internet voting framework\n\"\"\"\nSetup of Collector Management Service.\n\"\"\"\n\nimport os\n\nfrom setuptools import setup\nfrom setuptools.command.build_py import build_py\n\n\nclass IvxvPackageBuilder(build_py):\n    \"\"\"Customized build_py.\"\"\"\n\n    def build_package_data(self):\n        \"\"\"Copy data files into build directory.\"\"\"\n        super().build_package_data()\n\n        # install jsonschema files\n        src_dir = 'Documentation/common/schema'\n        tgt_dir = os.path.join(self.build_lib, 'ivxv_admin', 'jsonschema')\n        self.mkpath(tgt_dir)\n        self.copy_file(os.path.join(src_dir, 'ivxv.choices.schema'), tgt_dir)\n        self.copy_file(os.path.join(src_dir, 'ivxv.districts.schema'), tgt_dir)\n\n\nsetup(\n    name='IVXVCollectorAdminDaemon',\n    version='1.10.3',\n    description='IVXV Collector Management Service',\n    author='IVXV Developer',\n    author_email='info@ivotingcentre.ee',\n    install_requires=[\n        'bottle',\n        'docopt',\n        'jinja2',\n        'jsonschema',\n        'pyopenssl',\n        'fasteners',\n        'python-crontab',\n        'python-dateutil',\n        'python-debian',\n        'pyyaml',\n        'setuptools',\n    ],\n    cmdclass={'build_py': IvxvPackageBuilder},\n    packages=[\n        'ivxv_admin',\n        'ivxv_admin.cli_utils',\n        'ivxv_admin.cli_utils.config_utils',\n        'ivxv_admin.config_validator',\n        'ivxv_admin.lib',\n        'ivxv_admin.service',\n    ],\n    package_dir={'': 'collector-admin'},\n    package_data={'ivxv_admin': ['templates/*.jinja', 'templates/*.json']},\n    entry_points={\n        'console_scripts': [\n            # collector management (storage utilities)\n            'ivxv-collector-init'\n            '=ivxv_admin.cli_utils.admin_storage_utils:ivxv_collector_init_util',\n\n            'ivxv-create-data-dirs'\n            '=ivxv_admin.cli_utils.admin_storage_utils:ivxv_create_data_dirs_util',\n\n            # management service database (storage utilities)\n            'ivxv-db=ivxv_admin.cli_utils.admin_storage_utils:database_util',\n\n            'ivxv-db-dump'\n            '=ivxv_admin.cli_utils.admin_storage_utils:database_dump_util',\n\n            'ivxv-db-reset'\n            '=ivxv_admin.cli_utils.admin_storage_utils:database_reset_util',\n\n            # user management\n            'ivxv-users-list=ivxv_admin.cli_utils.status_utils:users_list_util',\n\n            # config management\n            'ivxv-config-apply=ivxv_admin.cli_utils.config_utils.config_apply:main',\n\n            'ivxv-secret-load'\n            '=ivxv_admin.cli_utils.config_utils.load_secret_data_file:main',\n\n            'ivxv-config-validate'\n            '=ivxv_admin.cli_utils.config_utils.config_validate:main',\n\n            'ivxv-voter-list-download'\n            '=ivxv_admin.cli_utils.config_utils.voter_list_download:main',\n\n            # service management\n            'ivxv-backup=ivxv_admin.cli_utils.backup_utils:backup_util',\n\n            'ivxv-backup-crontab'\n            '=ivxv_admin.cli_utils.backup_utils:backup_crontab_generator_util',\n\n            'ivxv-detail-stats-crontab'\n            '=ivxv_admin.cli_utils.service_utils:detail_stats_crontab_editor',\n\n            'ivxv-voting-facts-crontab'\n            '=ivxv_admin.cli_utils.service_utils:voting_facts_crontab_editor',\n\n            'ivxv-copy-log-to-logmon'\n            '=ivxv_admin.cli_utils.service_utils:copy_logs_to_logmon_util',\n\n            'ivxv-voterstats=ivxv_admin.cli_utils.service_utils:voterstats_util',\n\n            'ivxv-voting-facts=ivxv_admin.cli_utils.service_utils:voting_facts_util',\n\n            'ivxv-status=ivxv_admin.cli_utils.status_utils:status_util',\n\n            'ivxv-service=ivxv_admin.cli_utils.service_utils:manage_service',\n\n            'ivxv-export-votes=ivxv_admin.cli_utils.service_utils:export_votes_util',\n\n            'ivxv-update-packages'\n            '=ivxv_admin.cli_utils.service_utils:update_software_pkg_util',\n            \"ivxv-generate-processor-input\"\n            \"=ivxv_admin.cli_utils.service_utils:generate_processor_input_util\",\n\n            \"ivxv-voting-sessions\"\n            \"=ivxv_admin.cli_utils.service_utils:voting_sessions_util\",\n\n            # command loading\n            'ivxv-cmd-load=ivxv_admin.cli_utils.config_utils.command_load:main',\n\n            # logging\n            'ivxv-eventlog-dump=ivxv_admin.event_log:event_log_dump_util',\n\n            # daemons\n            'ivxv-admin-httpd=ivxv_admin.http_daemon:daemon',\n\n            'ivxv-agent-daemon=ivxv_admin.agent_daemon:main_loop',\n        ],\n    },\n)\n"
  },
  {
    "path": "smartid/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "smartid/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "smartid/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n----------------------------------\n Smart-ID REST API helper service\n----------------------------------\n\n<Description of SmartID helper service.>\n"
  },
  {
    "path": "smartid/go.mod",
    "content": "module ivxv.ee/smartid\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "smartid/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "smartid/internal/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value when calling RPC.Challenge\n\tEmpty = \"\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Authenticate\n\tChallenge = \"RPC.Challenge\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.AuthenticateStatus\n\tAuthenticate = \"RPC.Authenticate\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.VoterChoices\n\tAuthenticateStatus = \"RPC.AuthenticateStatus\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.GetCertificate\n\tVoterChoices = \"RPC.VoterChoices\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.GetCertificateStatus\n\tGetCertificate = \"RPC.GetCertificate\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Sign\n\tGetCertificateStatus = \"RPC.GetCertificateStatus\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.SignStatus\n\tSign = \"RPC.Sign\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Vote\n\tSignStatus = \"RPC.SignStatus\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tauthTTL int64\n\tvoteTTL int64\n\tclient  client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != exitCodeOK {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:  tlsDialer,\n\t\tauthTTL: c.Conf.Technical.Status.Session.AuthTTL,\n\t\tvoteTTL: c.Conf.Technical.Status.Session.VoteTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err, Description: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\tok, err := r.verifyAndUpdateSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndUpdateSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndUpdateSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will update h.Header.SessionID record in the underlying storage\n// by marking session status Caller as serviceMethod.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndUpdateSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\t// NB! Most important part, that prevents any attack on SessionID\n\tvar ok bool\n\tvar ttl string\n\tswitch serviceMethod {\n\tcase Challenge:\n\t\tok, err = verifyStatusReadResp(&respRead, challengeHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase Authenticate:\n\t\tok, err = verifyStatusReadResp(&respRead, authenticateHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase AuthenticateStatus:\n\t\tok, err = verifyStatusReadResp(&respRead, authenticateStatusHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase GetCertificate:\n\t\tok, err = verifyStatusReadResp(&respRead, getCertificateHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\t\trespRead.Lease = \"\"\n\tcase GetCertificateStatus:\n\t\tok, err = verifyStatusReadResp(&respRead, getCertificateStatusHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\tcase Sign:\n\t\tok, err = verifyStatusReadResp(&respRead, signHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\tcase SignStatus:\n\t\tok, err = verifyStatusReadResp(&respRead, signStatusHandler)\n\t\tttl = strconv.FormatInt(r.voteTTL, 10)\n\t}\n\tif !ok || err != nil {\n\t\treturn ok, VerifyStatusReadRespError{Err: err, Description: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp,\n\th func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// challengeHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Challenge request.\nfunc challengeHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Challenge is the very first client request to IVXV,\n\t// so IVXV requires no previous interactions\n\tfirstTime := r.Caller == Empty && r.Auth == client.NoAuth\n\n\tif !(firstTime) {\n\t\treturn false, ChallengeInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Challenge,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_CHAL,\n\t\t}\n\t}\n\n\tr.Auth = client.SmartIDAuth\n\treturn true, nil\n}\n\n// authenticateHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Authenticate request.\nfunc authenticateHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Authenticate is the second client request to IVXV,\n\t// so IVXV requires no previous interactions\n\tfirstTime := r.Caller == Challenge && r.Auth == client.SmartIDAuth\n\n\tif !(firstTime) {\n\t\treturn false, AuthenticateInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Authenticate,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// authenticateStatusHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.AuthenticateStatus request.\nfunc authenticateStatusHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.AuthenticateStatus is the third client request to IVXV,\n\t// so IVXV requires RPC.Authenticate previously interacted\n\tsecondTime := r.Caller == Authenticate && r.Auth == client.SmartIDAuth\n\n\t// If RPC.AuthenticateStatus is still processing Smart-ID request\n\t// then voting app will still send RPC.AuthenticateStatus request\n\t// to finish Smart-ID authentication. So client can send\n\t// RPC.AuthenticateStatus queries as many as wants\n\tnTime := r.Caller == AuthenticateStatus && r.Auth == client.SmartIDAuth\n\n\tif !(secondTime) && !(nTime) {\n\t\treturn false, AuthenticateStatusInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      AuthenticateStatus,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE_STATUS,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// getCertificateHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.GetCertificate request.\nfunc getCertificateHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.GetCertificate is the fourth client request to IVXV,\n\t// third interaction should be done with RPC.VoterChoices\n\tfourthTime := r.Caller == VoterChoices && r.Auth == client.SmartIDAuth\n\n\tif !(fourthTime) {\n\t\treturn false, GetCertificateInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      GetCertificate,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// getCertificateStatusHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.GetCertificateStatus request.\nfunc getCertificateStatusHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.GetCertificateStatus is the fifth client request to IVXV,\n\t// fourth interaction should be done with RPC.GetCertificate\n\tfifthTime := r.Caller == GetCertificate && r.Auth == client.SmartIDAuth\n\n\t// If RPC.GetCertificateStatus is still processing Smart-ID request\n\t// then voting app will still send RPC.GetCertificateStatus request\n\t// to receive Smart-ID signing certificate. So client can send\n\t// RPC.GetCertificateStatus queries as many as wants\n\tnTime := r.Caller == GetCertificateStatus && r.Auth == client.SmartIDAuth\n\n\tif !(fifthTime) && !(nTime) {\n\t\treturn false, GetCertificateStatusInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      GetCertificateStatus,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE_STATUS,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// signHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Sign request.\nfunc signHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Sign is the sixth client request to IVXV,\n\t// so IVXV requires RPC.GetCertificateStatus previously interacted\n\tsixthTime := r.Caller == GetCertificateStatus && r.Auth == client.SmartIDAuth\n\n\tif !(sixthTime) {\n\t\treturn false, SignInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Sign,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN,\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// signStatusHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.SignStatus request.\nfunc signStatusHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Sign is the seventh client request to IVXV,\n\t// so IVXV requires RPC.Sign previously interacted\n\tseventhTime := r.Caller == Sign && r.Auth == client.SmartIDAuth\n\n\t// If RPC.SignStatus is still processing Smart-ID request\n\t// then voting app will still send RPC.SignStatus request\n\t// to check whether document is signed with Smart-ID. So\n\t// client can send RPC.SignStatus queries as many as wants\n\tnTime := r.Caller == SignStatus && r.Auth == client.SmartIDAuth\n\n\tif !(seventhTime) && !(nTime) {\n\t\treturn false, SignStatusInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      SignStatus,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN_STATUS,\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "smartid/internal/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ                       = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER              = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT               = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_TLS_DIAL                                    = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                                 = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_UPDATE_FAIL                                 = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_CHAL                   = \"SessionID has been attempted to tamper at RPC.Challenge\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE           = \"SessionID has been attempted to tamper at RPC.Authenticate\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_AUTHENTICATE_STATUS    = \"SessionID has been attempted to tamper at RPC.AuthenticateStatus\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE        = \"SessionID has been attempted to tamper at RPC.GetCertificate\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_GET_CERTIFICATE_STATUS = \"SessionID has been attempted to tamper at RPC.GetCertificateStatus\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN                   = \"SessionID has been attempted to tamper at RPC.Sign\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_SIGN_STATUS            = \"SessionID has been attempted to tamper at RPC.SignStatus\"\n)\n"
  },
  {
    "path": "smartid/service/smartid/log_desc.go",
    "content": "package main\n\nconst (\n\t_SMARTID_EXPIRED_AUTH_SESSION = \"Request timeout expired for this voter between RPC.Authenticate and RPC.AuthenticateStatus\"\n\t_SMARTID_CHALREQ              = \"RPC.ChallengeReq\"\n\t_SMARTID_CHAL_COOKIE_MARSHAL  = \"Failed to ASN.1 DER marshal a cookie data\"\n\t_SMARTID_CHAL_COOKIE          = \"Failed to encrypt a cookie\"\n\t_SMARTID_CHALRESP             = \"RPC.ChallengeResp\"\n\t_SMARTID_AUTHREQ              = \"RPC.AuthenticateReq\"\n\t_SMARTID_EXPIRED              = \"Current time >= 'electionstoptime' from election configuration file\"\n\t_SMARTID_SESSION_ID           = \"Malformed SessionID\"\n\t_SMARTID_SESSION_ID_EXPIRED   = \"SessionID has been expired\"\n\t_SMARTID_CHAL                 = \"Failed to create a challenge\"\n\t_SMARTID_COOKIE_EXTRACT       = \"Failed to decrypt a cookie\"\n\t_SMARTID_COOKIE_UNMARSHAL     = \"Failed to ASN.1 DER unmarshal a cookie data\"\n\t_SMARTID_COOKIE_SESSIONID     = \"Tampered cookie SessionID\"\n\t_SMARTID_AUTH_RESP            = \"Smart-ID provider responded with error\"\n\t_SMARTID_NO_RESP              = \"Cannot reach Smart-ID provider\"\n\t_SMARTID_DATA_TOKEN_CREATE    = \"Failed to create data token cookie (AES cipher)\"\n\t_SMARTID_SAME_AUTH_SESSION    = \"Smart-ID provider has been issued duplicate authentication session code\"\n\t_SMARTID_AUTHRESP             = \"RPC.AuthenticateResp\"\n\t_SMARTID_AUTHSTATUSREQ        = \"RPC.AuthenticateStatusReq\"\n\t_SMARTID_UNKNOWN_AUTH_SESSION = \"Voter send non-existing Smart-ID authentication session code\"\n\t_SMARTID_SIG_RESP             = \"Smart-ID provider returned back voter's signature\"\n\t_SMARTID_NO_CERT_RESP         = \"Smart-ID provider didn't return back voter's certificate\"\n\t_SMARTID_CERT_RESP            = \"Smart-ID provider returned back voter's certificate\"\n\t_SMARTID_VERIFY_SIG           = \"Failed to verify voter's signature\"\n\t_SMARTID_VOTER_ID             = \"Cannot extract voter's personal code from certificate's 'Subject' field\"\n\t_SMARTID_AUTH_TOKEN_CREATE    = \"Failed to create IVXV authentication token cookie (AES cipher)\"\n\t_SMARTID_AUTHSTATUSRESP       = \"RPC.AuthenticateStatusResp\"\n\t_SMARTID_GETCERTREQ           = \"RPC.GetCertificateReq\"\n\t_SMARTID_VOTER_NO_AUTH        = \"Voter has not been authenticated\"\n\t_SMARTID_VOTER_NO_PHONENR     = \"Authenticated voter should have a phone nr. parsed from IVXV authentication token cookie\"\n\t_SMARTID_CERT                 = \"Smart-ID signing certificate of a voter\"\n\t_SMARTID_GETCERTRESP          = \"RPC.GetCertificateResp\"\n\t_SMARTID_GETCERTSTATUSREQ     = \"RPC.GetCertificateStatusReq\"\n\t_SMARTID_GETCERTSTATUSRESP    = \"RPC.GetCertificateStatusReq\"\n\t_SMARTID_SIGNREQ              = \"RPC.SignReq\"\n\t_SMARTID_SIGNRESP             = \"RPC.SignResp\"\n\t_SMARTID_SIGNSTATUSREQ        = \"RPC.SignStatusReq\"\n\t_SMARTID_SIGNSTATUSRESP       = \"RPC.SignStatusResp\"\n\t_SMARTID_START                = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_SMARTID_AUTH_STOP            = \"Failed to transform authentication stop time from election configuration file to RFC3339 format\"\n\t_SMARTID_STOP                 = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_SMARTID_CLIENT_CONF          = \"Failed to configure Smart-ID API client\"\n\t_SMARTID_TICKET_AUTH          = \"Smart-ID service should be configured for ticket based authentication\"\n\t_SMARTID_TICKET               = \"Failed to create a Smart-ID ticket (AES cipher) that is used for cookie signing\"\n\t_SMARTID_AUTH                 = \"Failed to parse authentication configuration for smartid service\"\n\t_SMARTID_SERVER               = \"Failed to parse server configuration for smartid service\"\n\t_SMARTID_SERVER_SERVE         = \"Failed to serve smartid service to clients\"\n)\n"
  },
  {
    "path": "smartid/service/smartid/main.go",
    "content": "/*\nThe smartid service performs Smart-ID authentication and intermediates requests\nfor Smart-ID signing using the Smart-ID REST API.\n*/\npackage main\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/auth/ticket\"\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/smartid\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tinternal \"ivxv.ee/smartid/internal/sessionstatus/rpc\"\n\t//ivxv:modules common/collector/container\n)\n\nconst (\n\t// StatusPoll is returned as Status from AuthenticateStatus and\n\t// SignStatus if a Smart-ID session has not yet finished and the\n\t// client needs to poll again.\n\tStatusPoll = \"POLL\"\n\n\t// StatusOK is returned as Status from AuthenticateStatus and\n\t// SignStatus if a Smart-ID session has finished successfully.\n\tStatusOK = \"OK\"\n)\n\n// smartidToServerError maps an ivxv.ee/common/collector/smartid package error to an ivxv.ee/common/collector/server\n// error to return to the client. It returns nil if err is not a recognized smartid\n// error, i.e., it was caused by an internal server error.\nfunc smartidToServerError(err error) error {\n\tswitch {\n\tcase errors.CausedBy(err, new(smartid.InputError)) != nil:\n\t\treturn server.ErrBadRequest\n\tcase errors.CausedBy(err, new(smartid.VerificationError)) != nil:\n\t\treturn server.ErrSmartIDVerification\n\tcase errors.CausedBy(err, new(smartid.AccountError)) != nil:\n\t\treturn server.ErrSmartIDAccount\n\tcase errors.CausedBy(err, new(smartid.CanceledError)) != nil:\n\t\treturn server.ErrSmartIDCanceled\n\tcase errors.CausedBy(err, new(smartid.ExpiredError)) != nil:\n\t\treturn server.ErrSmartIDExpired\n\tcase errors.CausedBy(err, new(smartid.CertificateError)) != nil:\n\t\treturn server.ErrSmartIDCertificate\n\tcase errors.CausedBy(err, new(smartid.StatusError)) != nil:\n\t\treturn server.ErrSmartIDGeneral\n\t}\n\treturn nil\n}\n\n// RPC is the handler for Smart-ID service calls.\ntype RPC struct {\n\tstatus   client.Verifier\n\tauthEnd  time.Time\n\tsmartid  *smartid.Client\n\tticket   *ticket.T\n\tidentify identity.Identifier\n\n\tsessionTimeout time.Duration\n}\n\ntype ChallengeArgs struct {\n\tserver.Header\n}\n\ntype ChallengeResponse struct {\n\tserver.Header\n\tChallenge    []byte\n\tXSmartIDAuth []byte\n}\n\ntype challengeCookie struct {\n\tChallenge []byte\n\tSessionID string\n\tExpiresAt time.Time\n}\n\n// Challenge is the remote procedure call performed by clients to generate a Smart-ID verification code.\nfunc (r *RPC) Challenge(args ChallengeArgs, resp *ChallengeResponse) (err error) {\n\tlog.Log(args.Ctx, ChallengeReq{Description: _SMARTID_CHALREQ})\n\n\tif !time.Now().Before(r.authEnd) {\n\t\tlog.Log(args.Ctx, ChallengeVotingEnded{Description: _SMARTID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Challenge).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, ChallengeVerifySessionIDError{Err: err, Description: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, ChallengeUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tchallenge, challengeDigest, err := r.smartid.Challenge()\n\tif err != nil {\n\t\tlog.Error(args.Ctx, ChallengeError{Err: err, Description: _SMARTID_CHAL})\n\t\treturn server.ErrInternal\n\t}\n\n\tb, err := asn1.Marshal(challengeCookie{\n\t\tChallenge: challenge,\n\t\tSessionID: args.SessionID,\n\t\tExpiresAt: time.Now().Add(r.sessionTimeout).UTC(),\n\t})\n\tif err != nil {\n\t\tlog.Error(args.Ctx, ChallengeCookieMarshalError{Err: err, Description: _SMARTID_CHAL_COOKIE_MARSHAL})\n\t\treturn server.ErrInternal\n\t}\n\n\tcookie, err := r.ticket.CreateData(b)\n\tif err != nil {\n\t\tlog.Error(args.Ctx, ChallengeCookieError{Err: err, Description: _SMARTID_CHAL_COOKIE})\n\t\treturn server.ErrInternal\n\t}\n\n\tresp.Challenge = challengeDigest\n\tresp.XSmartIDAuth = cookie\n\n\tlog.Log(args.Ctx, ChallengeResp{\n\t\tChallenge:   resp.Challenge,\n\t\tDescription: _SMARTID_CHALRESP,\n\t})\n\treturn nil\n}\n\n// AuthArgs are the arguments provided to a call of RPC.Authenticate.\ntype AuthArgs struct {\n\tserver.Header\n\tIdentifier string `size:\"11\"`\n}\n\n// AuthResponse is the response returned by RPC.Authenticate.\ntype AuthResponse struct {\n\tserver.Header\n\tSessionCode string\n}\n\n// Authenticate is the remote procedure call performed by clients to start a\n// Smart-ID authentication session.\nfunc (r *RPC) Authenticate(args AuthArgs, resp *AuthResponse) (err error) {\n\tlog.Log(args.Ctx, AuthenticateReq{Identifier: args.Identifier, Description: _SMARTID_AUTHREQ})\n\n\t// The server filter for voting end will only enable once we stop\n\t// serving signing requests, so we must manually check if we should\n\t// still serve authentication requests and refuse those requests when\n\t// not served anymore\n\tif !time.Now().Before(r.authEnd) { // not before == equal or after\n\t\tlog.Log(args.Ctx, AuthenticateVotingEnded{Description: _SMARTID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Authenticate).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, AuthenticateVerifySessionIDError{Err: err, Description: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, AuthenticateUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tdata, err := r.ticket.TokenData(args.XSmartIDAuth)\n\tif err != nil {\n\t\tlog.Error(args.Ctx, AuthenticateExtractCookieError{Err: err, Description: _SMARTID_COOKIE_EXTRACT})\n\t\treturn server.ErrInternal\n\t}\n\tvar cookie challengeCookie\n\trest, err := asn1.Unmarshal(data, &cookie)\n\tif err != nil || len(rest) != 0 {\n\t\tlog.Error(args.Ctx, AuthenticateUnmarshalCookieError{Err: err, Description: _SMARTID_COOKIE_UNMARSHAL})\n\t\treturn server.ErrBadRequest\n\t}\n\tif cookie.SessionID != args.SessionID {\n\t\tlog.Error(args.Ctx, AuthenticateCookieSessionIDError{\n\t\t\tCookieSessionID: cookie.SessionID,\n\t\t\tHeaderSessionID: args.SessionID,\n\t\t\tDescription:     _SMARTID_COOKIE_SESSIONID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif time.Now().UTC().After(cookie.ExpiresAt) {\n\t\tlog.Debug(args.Ctx, SessionTimeout{SessionID: args.SessionID,\n\t\t\tDescription: _SMARTID_EXPIRED_AUTH_SESSION})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tresp.SessionCode, err = r.smartid.Authenticate(args.Ctx, args.Identifier, cookie.Challenge)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, AuthenticateSmartIDError{Err: err, Description: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, AuthenticateError{Err: log.Alert(err), Description: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Authentication has been successfully initiated\n\tlog.Log(args.Ctx, AuthenticateResp{\n\t\tSessionCode: resp.SessionCode,\n\t\tDescription: _SMARTID_AUTHRESP,\n\t})\n\treturn nil\n}\n\n// AuthStatusArgs are the arguments provided to a call of RPC.AuthenticateStatus.\ntype AuthStatusArgs struct {\n\tserver.Header\n\tSessionCode string `size:\"36\"`\n}\n\n// AuthStatusResponse is the response returned by RPC.AuthenticateStatus.\ntype AuthStatusResponse struct {\n\tserver.Header\n\tStatus       string\n\tGivenName    string\n\tSurname      string\n\tPersonalCode string\n\tAuthToken    []byte\n\tDataToken    []byte\n}\n\n// AuthenticateStatus is the remote procedure call performed by clients to\n// check the status of a Smart-ID authentication session.\nfunc (r *RPC) AuthenticateStatus(args AuthStatusArgs, resp *AuthStatusResponse) error {\n\tlog.Log(args.Ctx, AuthenticateStatusReq{SessionCode: args.SessionCode, Description: _SMARTID_AUTHSTATUSREQ})\n\n\t// The server filter for voting end will only enable once we stop\n\t// serving signing requests, so we must manually check if we should\n\t// still serve authentication requests and refuse those requests when\n\t// not served anymore\n\tif !time.Now().Before(r.authEnd) { // not before == equal or after\n\t\tlog.Log(args.Ctx, AuthenticateStatusVotingEnded{Description: _SMARTID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.AuthenticateStatus).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, AuthenticateStatusVerifySessionIDError{Err: err, Description: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, AuthenticateStatusUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tdata, err := r.ticket.TokenData(args.XSmartIDAuth)\n\tif err != nil {\n\t\tlog.Error(args.Ctx, AuthenticateStatusExtractCookieError{\n\t\t\tErr: err, Description: _SMARTID_COOKIE_EXTRACT})\n\t\treturn server.ErrInternal\n\t}\n\tvar cookie challengeCookie\n\trest, err := asn1.Unmarshal(data, &cookie)\n\tif err != nil || len(rest) != 0 {\n\t\tlog.Error(args.Ctx, AuthenticateStatusUnmarshalCookieError{\n\t\t\tErr: err, Description: _SMARTID_COOKIE_UNMARSHAL})\n\t\treturn server.ErrBadRequest\n\t}\n\tif cookie.SessionID != args.SessionID {\n\t\tlog.Error(args.Ctx, AuthenticateStatusCookieSessionIDError{\n\t\t\tCookieSessionID: cookie.SessionID,\n\t\t\tHeaderSessionID: args.SessionID,\n\t\t\tDescription:     _SMARTID_COOKIE_SESSIONID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif time.Now().UTC().After(cookie.ExpiresAt) {\n\t\tvar sessTimeout SessionTimeout\n\t\tsessTimeout.SessionID = args.SessionID\n\t\tlog.Debug(args.Ctx, sessTimeout)\n\t\treturn server.ErrBadRequest\n\t}\n\n\tdocumentno, cert, algorithm, signature, err := r.smartid.GetAuthenticateStatus(args.Ctx, args.SessionCode)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, AuthenticateStatusSmartIDError{Err: err, Description: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, AuthenticateStatusError{Err: log.Alert(err), Description: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\tresp.Status = StatusPoll\n\tif len(signature) > 0 {\n\t\t// Signed authentication response received\n\t\tlog.Log(args.Ctx, AuthenticationSignature{Signature: signature, Description: _SMARTID_SIG_RESP})\n\t\tif cert == nil {\n\t\t\t// Certificate missing from the signed response\n\t\t\tlog.Error(args.Ctx, AuthenticationCertificateMissingError{Err: err,\n\t\t\t\tDescription: _SMARTID_NO_CERT_RESP})\n\t\t\treturn server.ErrSmartIDGeneral\n\t\t}\n\t\t// Log detected certificate\n\t\tlog.Log(args.Ctx, AuthenticationCertificate{Certificate: cert, Description: _SMARTID_CERT_RESP})\n\n\t\tif err = smartid.VerifyAuthenticationSignature(\n\t\t\tcert, algorithm, cookie.Challenge, signature); err != nil {\n\t\t\t// Error in verifying the signed response\n\t\t\tlog.Error(args.Ctx, AuthenticationSignatureError{Err: err, Description: _SMARTID_VERIFY_SIG})\n\t\t\treturn server.ErrSmartIDGeneral\n\t\t}\n\n\t\tresp.Status = StatusOK\n\t\tresp.GivenName = findName(&cert.Subject, asn1.ObjectIdentifier{2, 5, 4, 42})\n\t\tresp.Surname = findName(&cert.Subject, asn1.ObjectIdentifier{2, 5, 4, 4})\n\t\tif resp.PersonalCode, err = r.identify(&cert.Subject); err != nil {\n\t\t\t// Backend could not extract voter's personal code from certificate's Subject field\n\t\t\tlog.Error(args.Ctx, AuthenticationSubjectIdentityError{Err: err,\n\t\t\t\tDescription: _SMARTID_VOTER_ID})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\tif resp.AuthToken, err = r.ticket.Create(cert.Subject); err != nil {\n\t\t\t// Error in authentication ticket creation\n\t\t\tlog.Error(args.Ctx, AuthenticationTicketError{Err: err,\n\t\t\t\tDescription: _SMARTID_AUTH_TOKEN_CREATE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\tif resp.DataToken, err = r.ticket.CreateData([]byte(documentno)); err != nil {\n\t\t\t// Error in creating the ticket\n\t\t\tlog.Error(args.Ctx, DataTicketError{Err: err, Description: _SMARTID_DATA_TOKEN_CREATE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\t}\n\n\t// Successful AuthenticateStatusResp, AuthToken is treated as sensitive\n\tlog.Log(args.Ctx, AuthenticateStatusResp{\n\t\tStatus:       resp.Status,\n\t\tGivenName:    resp.GivenName,\n\t\tSurname:      resp.Surname,\n\t\tPersonalCode: resp.PersonalCode,\n\t\tAuthToken:    log.Sensitive(resp.AuthToken),\n\t\tDataToken:    log.Sensitive(resp.DataToken),\n\t\tDescription:  _SMARTID_AUTHSTATUSRESP,\n\t})\n\treturn nil\n}\n\n// findName searches name for oid and returns the value for that oid or an\n// empty string. Panics if the value for the oid is not a string.\nfunc findName(name *pkix.Name, oid asn1.ObjectIdentifier) string {\n\tfor _, n := range name.Names {\n\t\tif n.Type.Equal(oid) {\n\t\t\treturn n.Value.(string)\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// CertificateChoiceArgs are the arguments provided to a call of RPC.GetCertificate.\ntype CertificateChoiceArgs struct {\n\tserver.Header\n}\n\n// CertificateChoiceResponse is the response returned by RPC.GetCertificateChoice.\ntype CertificateChoiceResponse struct {\n\tserver.Header\n\tSessionCode string\n}\n\n// GetCertificateChoice is the remote procedure call performed by clients to get the\n// Smart-ID signing certificate choice session that will be used to sign the vote.\nfunc (r *RPC) GetCertificateChoice(args CertificateChoiceArgs, resp *CertificateChoiceResponse) error {\n\tlog.Log(args.Ctx, GetCertificateChoiceReq{Description: _SMARTID_GETCERTREQ})\n\n\t// Get the voter serial number. If empty, then the request is not\n\t// authenticated.\n\tpersonalCode := server.VoterIdentity(args.Ctx)\n\tif len(personalCode) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedGetCertificateError{Description: _SMARTID_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.GetCertificate).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, GetCertificateVerifySessionIDError{Err: err, Description: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, GetCertificateUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tdocumentno := server.VoterNumber(args.Ctx)\n\tif len(documentno) == 0 {\n\t\tvar missingDocumentNo MissingDataSignError\n\t\tmissingDocumentNo.Description = _SMARTID_VOTER_NO_PHONENR\n\t\t// Could not detect documentno from the token\n\t\tlog.Error(args.Ctx, missingDocumentNo)\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\ts, err := r.smartid.GetCertificateChoice(args.Ctx, documentno)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, GetCertificateChoiceSmartIDError{Err: err, Description: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, GetCertificateChoiceError{Err: log.Alert(err), Description: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\t// GetCertificateChoice successfully initiated\n\tlog.Log(args.Ctx, GetCertificateChoiceResp{\n\t\tSession:     resp.SessionCode,\n\t\tDescription: _SMARTID_GETCERTRESP,\n\t})\n\tresp.SessionCode = s\n\treturn nil\n}\n\n// CertificateChoiceStatusArgs is the response returned by RPC.GetCertificateChoiceStatus.\ntype CertificateChoiceStatusArgs struct {\n\tserver.Header\n\tSessionCode string\n}\n\n// CertificateChoiceStatusResponse is the response returned by RPC.GetCertificateChoiceStatus.\ntype CertificateChoiceStatusResponse struct {\n\tserver.Header\n\tCertificate []byte\n\tStatus      string\n}\n\n// GetCertificateChoiceStatus is the remote procedure call performed by clients to get the\n// Smart-ID signing certificate that will be used to sign the vote.\nfunc (r *RPC) GetCertificateChoiceStatus(args CertificateChoiceStatusArgs,\n\tresp *CertificateChoiceStatusResponse) error {\n\tlog.Log(args.Ctx, GetCertificateChoiceStatusReq{Session: args.SessionCode,\n\t\tDescription: _SMARTID_GETCERTSTATUSREQ})\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.GetCertificateStatus).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, GetCertificateStatusVerifySessionIDError{Err: err,\n\t\t\tDescription: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, GetCertificateStatusUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t_, c, err := r.smartid.GetCertificateChoiceStatus(args.Ctx, args.SessionCode)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, GetCertificateChoiceStatusSmartIDError{Err: err,\n\t\t\t\tDescription: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, GetCertificateChoiceStatusError{Err: log.Alert(err),\n\t\t\tDescription: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\t// Signing certificate successfully retrieved\n\tlog.Log(args.Ctx, SigningCertificate{Certificate: c, Description: _SMARTID_CERT})\n\n\tresp.Status = StatusPoll\n\tif c != nil {\n\t\tresp.Status = StatusOK\n\t\tresp.Certificate = c.Raw\n\t}\n\n\t// Log successful GetCertificateResp\n\tlog.Log(args.Ctx, GetCertificateResp{\n\t\tCertificate: resp.Certificate,\n\t\tStatus:      resp.Status,\n\t\tDescription: _SMARTID_GETCERTSTATUSRESP,\n\t})\n\treturn nil\n}\n\n// SignArgs are the arguments provided to a call of RPC.Sign.\ntype SignArgs struct {\n\tserver.Header\n\tHash     []byte `size:\"64\"` // Hash of the data to sign.\n\tHashType string `size:\"10\"` // Allowed values described in 'ivxv.ee/common/collector/smartid' package.\n}\n\n// SignResponse is the response returned by RPC.Sign.\ntype SignResponse struct {\n\tserver.Header\n\tSessionCode string\n}\n\n// Sign is the remote procedure call performed by clients to start a Smart-ID\n// signing session.\nfunc (r *RPC) Sign(args SignArgs, resp *SignResponse) (err error) {\n\tlog.Log(args.Ctx, SignReq{HashType: args.HashType, Hash: args.Hash,\n\t\tDescription: _SMARTID_SIGNREQ})\n\n\t// Get the voter serial number. If empty, then the request is not\n\t// authenticated.\n\tidentity := server.VoterIdentity(args.Ctx)\n\tif len(identity) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedSignError{Description: _SMARTID_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Sign).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, SignVerifySessionIDError{Err: err, Description: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, SignUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tdocumentno := server.VoterNumber(args.Ctx)\n\tif len(documentno) == 0 {\n\t\t// Could not detect documentno from the token\n\t\tlog.Error(args.Ctx, MissingDataSignError{Description: _SMARTID_VOTER_NO_PHONENR})\n\t\treturn server.ErrUnauthenticated\n\t}\n\tresp.SessionCode, err = r.smartid.SignHash(\n\t\targs.Ctx, documentno, args.Hash, args.HashType)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, SignSmartIDError{Err: err, Description: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, SignError{Err: log.Alert(err), Description: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Signing successfully initiated\n\n\tlog.Log(args.Ctx, SignResp{\n\t\tSessionCode: resp.SessionCode,\n\t\tDescription: _SMARTID_SIGNRESP,\n\t})\n\treturn\n}\n\n// SignStatusArgs are the arguments provided to a call of RPC.SignStatus.\ntype SignStatusArgs struct {\n\tserver.Header\n\tSessionCode string `size:\"36\"`\n}\n\n// SignStatusResponse is the response returned by RPC.SignStatus.\ntype SignStatusResponse struct {\n\tserver.Header\n\tStatus    string\n\tSignature []byte\n\t//nolint: lll\n\tAlgorithm string // The signature algorithm. Allowed values described in 'ivxv.ee/common/collector/smartid' package.\n}\n\n// SignStatus is the remote procedure call performed by clients to check the\n// status of a Smart-ID signing session.\nfunc (r *RPC) SignStatus(args SignStatusArgs, resp *SignStatusResponse) (err error) {\n\tlog.Log(args.Ctx, SignStatusReq{SessionCode: args.SessionCode,\n\t\tDescription: _SMARTID_SIGNSTATUSREQ})\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.SignStatus).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, SignStatusVerifySessionIDError{Err: err,\n\t\t\tDescription: _SMARTID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, SignStatusUpdateSessionIDError{Description: _SMARTID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\tresp.Algorithm, resp.Signature, err = r.smartid.GetSignHashStatus(args.Ctx, args.SessionCode)\n\tif err != nil {\n\t\tif clierr := smartidToServerError(err); clierr != nil {\n\t\t\t// Log known smartid service error about failed authentication\n\t\t\tlog.Error(args.Ctx, SignStatusSmartIDError{Err: err, Description: _SMARTID_AUTH_RESP})\n\t\t\treturn clierr\n\t\t}\n\t\t// Log unknown smartid service error about failed authentication\n\t\tlog.Error(args.Ctx, SignStatusError{Err: log.Alert(err), Description: _SMARTID_NO_RESP})\n\t\treturn server.ErrInternal\n\t}\n\n\tresp.Status = StatusPoll\n\tif len(resp.Signature) > 0 {\n\t\tresp.Status = StatusOK\n\t}\n\n\t// Signed response successfully retrieved\n\n\tlog.Log(args.Ctx, SignStatusResp{\n\t\tStatus:      resp.Status,\n\t\tSignature:   resp.Signature,\n\t\tDescription: _SMARTID_SIGNSTATUSRESP,\n\t})\n\treturn\n}\n\nfunc main() {\n\t// Call smartidmain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(smartidmain())\n}\n\nfunc smartidmain() (code int) {\n\tc := command.NewWithoutStorage(\"ivxv-smartid\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := internal.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Create new RPC instance and start the session cleaner.\n\trpc := &RPC{sessionTimeout: time.Minute * 5, status: statusClient}\n\n\tvar start, stop time.Time\n\tvar authConf server.AuthConf\n\tvar err error\n\n\tif c.Conf.Election != nil {\n\t\t// Check election configuration time values - service start\n\t\tif start, err = c.Conf.Election.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err, Description: _SMARTID_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - election stop\n\t\tif rpc.authEnd, err = c.Conf.Election.ElectionStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ElectionStopTimeError{Err: err, Description: _SMARTID_AUTH_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - service stop\n\t\tif stop, err = c.Conf.Election.ServiceStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ServiceStopTimeError{Err: err, Description: _SMARTID_STOP},\n\t\t\t\t\"bad service stop time:\", err)\n\t\t}\n\n\t\t// Configure the Smart-ID REST API client.\n\t\tif rpc.smartid, err = smartid.New(&c.Conf.Election.SmartID); err != nil {\n\t\t\treturn c.Error(exit.Config, SmartIDConfError{Err: err, Description: _SMARTID_CLIENT_CONF},\n\t\t\t\t\"failed to configure SmartID-REST API client:\", err)\n\t\t}\n\n\t\t// Configure the ticket manager for issuing authentication\n\t\t// tickets.\n\t\tticketConf, ok := c.Conf.Election.Auth[auth.Ticket]\n\t\tif !ok {\n\t\t\treturn c.Error(exit.Config, TicketAuthError{Description: _SMARTID_TICKET_AUTH},\n\t\t\t\t\"ticket authentication is mandatory for smartid\")\n\t\t}\n\t\tif rpc.ticket, err = ticket.NewFromSystem(); err != nil {\n\t\t\treturn c.Error(exit.Config, TicketConfError{Err: err, Description: _SMARTID_TICKET},\n\t\t\t\t\"failed to configure ticket manager:\", err)\n\t\t}\n\n\t\t// Parse configuration for authenticating with tickets issued\n\t\t// by this server.\n\t\tif authConf, err = server.NewAuthConf(auth.Conf{auth.Ticket: ticketConf},\n\t\t\tc.Conf.Election.Identity, nil); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ServerAuthConfError{Err: err, Description: _SMARTID_AUTH},\n\t\t\t\t\"failed to configure client authentication:\", err)\n\t\t}\n\n\t\t// Store the voter identifier for signer identification.\n\t\trpc.identify = authConf.Identity\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err, Description: _SMARTID_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.WithAuth(authConf).ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err, Description: _SMARTID_SERVER_SERVE},\n\t\t\t\t\"failed to serve smartid service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "storage/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "storage/Makefile",
    "content": "include ../common/go/common.mk\n\ndb=etcd\n\n.PHONY:ivxv_storage_db\nivxv_storage_db:\n\tcp $(ROOTDIR)/common/external/database/$(db)/ivxv-storage_db_install.sh bin/\n\tcp $(ROOTDIR)/common/external/database/$(db)/ivxv-storage_db_uninstall.sh bin/\n\tcp $(ROOTDIR)/common/external/database/$(db)/ivxv-storage_db.tar.gz bin/\n"
  },
  {
    "path": "storage/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------\n Storage service\n-----------------\n\n<Description of storage service.>\n"
  },
  {
    "path": "storage/cmd/storageidx/log_desc.go",
    "content": "package main\n\nconst (\n\t_STORAGEIDX_START         = \"Failed to transform election start time from election configuration file to RFC3339 format\"\n\t_STORAGEIDX_REBUILD       = \"Failed to rebuild voted statistics in a database\"\n\t_STORAGEIDX_REBUILD_START = \"Starting voted statistics re-calculation (rebuild)\"\n)\n"
  },
  {
    "path": "storage/cmd/storageidx/main.go",
    "content": "/*\nThe storageidx application rebuilds indexes in the storage service.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/q11n\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `storageidx rebuilds indexes in the storage service.\n\nAn index consists of a set of keys in the storage service which do not contain\nprimary data, but build upon existing values, e.g., by constructing secondary\nlookup tables or aggregating data. Indices help optimize operations which\nwould be too slow when operating on primary data only.\n\nIt is usually not necessary to manually rebuild an index in the storage\nservice, but use of this application might help in cases where index corruption\nis detected, e.g., when new data was stored but a following index update\nfailed.\n\nSince the storage service does not allow deletion of any keys, the application\ndoes not clear the indexes before starting the rebuild, but simply overwrites\nall values.`\n\nvar (\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call storageidxmain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(storageidxmain())\n}\n\nfunc storageidxmain() (code int) {\n\tc := command.New(\"ivxv-storageidx\", usage)\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\tstart, err := c.Conf.Election.ElectionStartTime()\n\tif err != nil {\n\t\treturn c.Error(exit.Config, ElectionStartTimeError{Err: err,\n\t\t\tDescription: _STORAGEIDX_START},\n\t\t\t\"failed to parse election start time:\", err)\n\t}\n\n\tvar qps []q11n.Protocol\n\tfor _, q := range c.Conf.Election.Qualification {\n\t\tqps = append(qps, q.Protocol)\n\t}\n\n\tif c.Until < command.Execute {\n\t\treturn exit.OK\n\t}\n\n\tif err := rebuildVotedStats(c.Ctx, start, qps, c.Storage); err != nil {\n\t\treturn c.Error(exit.Unavailable, RebuildVotedStatsError{Err: err,\n\t\t\tDescription: _STORAGEIDX_REBUILD},\n\t\t\t\"failed to rebuild successful voter index:\", err)\n\t}\n\n\treturn\n}\n\nfunc rebuildVotedStats(ctx context.Context, start time.Time, qps []q11n.Protocol,\n\ts *storage.Client) error {\n\n\tlog.Log(ctx, RebuildingVotedStats{Description: _STORAGEIDX_REBUILD_START})\n\tprogress.Static(\"Rebuilding successful voter statistics index:\")\n\taddvoter := progress.Count(0, false)\n\tprogress.Static(\"voters with\")\n\taddprogress := progress.Count(0, true)\n\tprogress.Static(\"votes\")\n\tprogress.Redraw()\n\tdefer progress.Keep()\n\n\t// To rebuild the successful voter statistics index retrieve all\n\t// complete votes from storage and re-mark them as voted. The SetVoted\n\t// method will compare the vote with the existing index value for that\n\t// voter and update if necessary: the order of calls does not matter.\n\tc, errc := s.GetVotes(ctx, qps, nil)\n\tvoters := make(map[string]struct{})\n\n\t// Init Transaction\n\ttransaction := s.Txn()\n\n\tfor {\n\t\tselect {\n\t\tcase vote, ok := <-c:\n\t\t\tif !ok {\n\t\t\t\t// Replace closed channel with nil so that it\n\t\t\t\t// is not selected anymore. Keep reading from\n\t\t\t\t// error channel.\n\t\t\t\tc = nil\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tctime := vote.Time\n\t\t\tif q11nTime, err := q11n.CanonicalTime(vote.Qualification); err != nil {\n\t\t\t\treturn err\n\t\t\t} else if !q11nTime.IsZero() {\n\t\t\t\tctime = q11nTime\n\t\t\t}\n\n\t\t\ttestVote := ctime.Before(start)\n\n\t\t\t// BEGIN\n\t\t\ttxnOp, err := transaction.Begin(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// SET AUTOCOMMIT ON\n\t\t\ttransaction.AutoCommit(ctx, txnOp)\n\n\t\t\tif err := s.TxnSetVoted(ctx, txnOp, vote.VoteID, \"\", ctime, testVote); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, ok := voters[vote.Voter]; !ok {\n\t\t\t\tvoters[vote.Voter] = struct{}{}\n\t\t\t\taddvoter(1)\n\t\t\t}\n\t\t\taddprogress(1)\n\t\tcase err, ok := <-errc:\n\t\t\tif !ok {\n\t\t\t\t// Error channel was closed: we are done.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif errors.CausedBy(err, (new(storage.GetVotesFatalError))) == nil {\n\t\t\t\t// Ignore any non-fatal errors. They are not\n\t\t\t\t// the problem of this application.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "storage/cmd/storageorder/log_desc.go",
    "content": "package main\n\nconst (\n\t_STORAGEORDER_FILE      = \"Missing filename argument for this command\"\n\t_STORAGEORDER_FILE_OPEN = \"Failed to open a file\"\n\t_STORAGEORDER_READ_CSV  = \"Failed to read CSV file\"\n\t_STORAGEORDER_CSV       = \"Invalid CSV format\"\n\t_STORAGEORDER_ORDER     = \"Failed to put missing votes into database votes order table\"\n)\n"
  },
  {
    "path": "storage/cmd/storageorder/main.go",
    "content": "/*\nThe storageorder application adds votes to order table in the storage service.\n*/\npackage main\n\nimport (\n\t\"encoding/csv\"\n\t\"flag\"\n\t\"io\"\n\t\"os\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/q11n\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `storageorder adds votes to order table in the storage service.\n\nIt is usually not necessary to add votes to votes order table,\nbut use of this application might help in cases where adding vote is\nunsuccessful. This might happen when too many votes arrive same time.\n\nInput is csv file in this order:\n<name>,<voterid>,<adminCode>,<district>\n`\n\nvar (\n\tfile = flag.String(\"file\", \"\", \"csv file\")\n)\n\nfunc main() {\n\t// Call storageordermain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(storageordermain())\n}\n\nfunc storageordermain() (code int) {\n\tc := command.New(\"ivxv-storageorder\", usage)\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif *file == \"\" {\n\t\treturn c.Error(exit.Usage, CmdAddVoteOrderArgError{\n\t\t\tDescription: _STORAGEORDER_FILE}, \"missing file argument\")\n\t}\n\n\tf, err := os.Open(*file)\n\tif err != nil {\n\t\treturn c.Error(exit.Usage, CmdAddVoteOrderFileOpenError{Err: err,\n\t\t\tDescription: _STORAGEORDER_FILE_OPEN}, \"failed to open file\")\n\t}\n\n\tdefer f.Close()\n\n\tcsvReader := csv.NewReader(f)\n\tfor {\n\t\trec, err := csvReader.Read()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn c.Error(exit.Usage, CmdAddVoteOrderLineReadError{Err: err,\n\t\t\t\tDescription: _STORAGEORDER_READ_CSV}, \"failed to read file\")\n\t\t}\n\t\tif len(rec) != 4 {\n\t\t\treturn c.Error(exit.Usage, CmdAddVoteOrderLineError{\n\t\t\t\tDescription: _STORAGEORDER_CSV}, \"wrong number of fields in line\")\n\t\t}\n\t\tif err := c.Storage.AddVoteOrder(c.Ctx, rec[0], rec[1], rec[3], rec[2]); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, CmdAddVoteOrderError{Err: err,\n\t\t\t\tDescription: _STORAGEORDER_ORDER},\n\t\t\t\t\"failed to add vote to order table:\", err)\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "storage/go.mod",
    "content": "module ivxv.ee/storage\n\ngo 1.23\n\nrequire (\n\tgo.etcd.io/etcd/client/v3 v3.5.17\n\tivxv.ee/common/collector v1.9.11\n)\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n"
  },
  {
    "path": "storage/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "storage/service/storage/cluster.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage/etcd\"\n)\n\n// protocol prepends the used etcd protocol (https://) to the address.\nfunc protocol(addr string) string {\n\treturn \"https://\" + addr\n}\n\n// bootstrap reports if id is one of the storage services responsible for\n// bootstrapping the cluster.\nfunc bootstrap(id string, c *etcd.Conf) bool {\n\tfor _, b := range c.Bootstrap {\n\t\tif b == id {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// walDir returns the directory to use for etcd write-ahead logs. It returns\n// the default etcd location, but make it explicit to protect against future\n// changes.\nfunc walDir(wd string) string {\n\treturn filepath.Join(wd, \"etcd\", \"member\", \"wal\")\n}\n\n// firstBoot reports if this is the first time booting this storage server. It\n// uses the same approach as etcd internally, i.e., checks if the write-ahead\n// log directory contains any files.\nfunc firstBoot(wd string) (bool, error) {\n\tf, err := os.Open(walDir(wd))\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn true, nil\n\t\t}\n\t\treturn false, OpenWALDirError{Err: err, Description: _STORAGE_WAL_OPEN}\n\t}\n\tdefer f.Close()\n\tfiles, err := f.Readdirnames(-1)\n\tif err != nil {\n\t\treturn false, ReadWALDirError{Err: err, Description: _STORAGE_ETCD_OPEN}\n\t}\n\treturn len(files) == 0, nil\n}\n\ntype member struct {\n\tid        uint64\n\tname      string\n\tclientURL string\n\tpeerURL   string\n}\n\nfunc hexID(id uint64) string {\n\treturn fmt.Sprintf(\"%x\", id)\n}\n\n// members retrieves the current cluster member list.\nfunc members(ctx context.Context, client *clientv3.Client, optime time.Duration) (\n\t[]member, error) {\n\n\tlog.Log(ctx, ListingMembers{Description: _STORAGE_ETCD_MEM})\n\tctx, cancel := context.WithTimeout(ctx, optime)\n\tdefer cancel()\n\tlist, err := client.MemberList(ctx)\n\tif err != nil {\n\t\treturn nil, MemberListError{Err: err, Description: _STORAGE_ETCD_MEM_FAIL}\n\t}\n\tlog.Log(ctx, MemberList{Members: list.Members, Description: _STORAGE_ETCD_MEM_LIST})\n\n\tmembers := make([]member, len(list.Members))\n\tfor i, pbm := range list.Members {\n\t\tm := member{id: pbm.ID, name: pbm.Name}\n\n\t\tswitch len(pbm.ClientURLs) {\n\t\tcase 0: // Member has not been started yet.\n\t\tcase 1:\n\t\t\tm.clientURL = pbm.ClientURLs[0]\n\t\tdefault:\n\t\t\t// Multiple client URLs are not supported.\n\t\t\treturn nil, UnexpectedMemberClientURLCountError{\n\t\t\t\tID:          hexID(m.id),\n\t\t\t\tName:        m.name,\n\t\t\t\tURLs:        pbm.ClientURLs,\n\t\t\t\tDescription: _STORAGE_SAME_ID_CLIENT,\n\t\t\t}\n\t\t}\n\n\t\t// Multiple peer URLs are not supported.\n\t\tif len(pbm.PeerURLs) != 1 {\n\t\t\treturn nil, UnexpectedMemberPeerURLCountError{\n\t\t\t\tID:          hexID(m.id),\n\t\t\t\tName:        m.name,\n\t\t\t\tURLs:        pbm.PeerURLs,\n\t\t\t\tDescription: _STORAGE_SAME_ID,\n\t\t\t}\n\t\t}\n\t\tm.peerURL = pbm.PeerURLs[0]\n\t\tmembers[i] = m\n\t}\n\treturn members, nil\n}\n\n// pruneMembers removes all members from the cluster that are not configured.\nfunc pruneMembers(ctx context.Context, client *clientv3.Client, optime time.Duration,\n\tconf clientv3.Config, members []member, configured []*conf.Service) error {\n\nnext:\n\tfor _, m := range members {\n\t\t// Check if the member is still configured.\n\t\tfor _, service := range configured {\n\t\t\tif m.peerURL == protocol(service.PeerAddress) {\n\t\t\t\tcontinue next\n\t\t\t}\n\t\t}\n\n\t\t// First ping the member's client URL and ensure that it does\n\t\t// not respond. If the member has no client URL, then it has\n\t\t// not been started yet and can be freely pruned.\n\t\tif len(m.clientURL) > 0 {\n\t\t\tlog.Log(ctx, PingingClientURL{URL: m.clientURL,\n\t\t\t\tDescription: _STORAGE_PING_CLIENT})\n\n\t\t\t// Since this member is no longer configured, its\n\t\t\t// client URL is not among the existing client's\n\t\t\t// endpoints. We must create a new ping client for\n\t\t\t// connecting to this client URL.\n\t\t\tconf.Endpoints = []string{m.clientURL}\n\t\t\tpingc, err := clientv3.New(conf)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debug(ctx, PingClientError{Err: err,\n\t\t\t\t\tDescription: _STORAGE_PING_CLIENT_CFG})\n\t\t\t} else {\n\t\t\t\tdefer pingc.Close()\n\n\t\t\t\topctx, cancel := context.WithTimeout(ctx, optime)\n\t\t\t\tdefer cancel()\n\t\t\t\tstatus, err := pingc.Status(opctx, m.clientURL)\n\t\t\t\tif err == nil {\n\t\t\t\t\tlog.Debug(ctx, PingStatus{Status: status,\n\t\t\t\t\t\tDescription: _STORAGE_PING_CLIENT_PRUNED})\n\t\t\t\t\treturn RemovedMemberStillAliveError{\n\t\t\t\t\t\tID:          hexID(m.id),\n\t\t\t\t\t\tName:        m.name,\n\t\t\t\t\t\tClientURL:   m.clientURL,\n\t\t\t\t\t\tDescription: _STORAGE_PING_CLIENT_PRUNED,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlog.Debug(ctx, PingError{Err: err, Description: _STORAGE_PING_CLIENT_FAIL})\n\t\t\t}\n\t\t}\n\n\t\t// Remove stopped and deconfigured member from cluster.\n\t\tlog.Log(ctx, PruningMember{ID: hexID(m.id), Name: m.name, URL: m.peerURL,\n\t\t\tDescription: _STORAGE_PRUNE_NODE})\n\t\topctx, cancel := context.WithTimeout(ctx, optime)\n\t\tdefer cancel()\n\t\tif _, err := client.MemberRemove(opctx, m.id); err != nil {\n\t\t\treturn RemoveMemberError{Err: err,\n\t\t\t\tDescription: _STORAGE_PRUNE_NODE_FAIL}\n\t\t}\n\t\tlog.Log(ctx, PrunedMember{ID: hexID(m.id), Description: _STORAGE_PRUNE_NODE_OK})\n\t}\n\treturn nil\n}\n\n// addMember adds a member to the cluster if not already there.\nfunc addMember(ctx context.Context, client *clientv3.Client, optime time.Duration,\n\tmembers []member, service *conf.Service) error {\n\n\t// Check the the member is not already there.\n\tpeerURL := protocol(service.PeerAddress)\n\tfor _, m := range members {\n\t\tif m.peerURL == peerURL {\n\t\t\t// When either the member's name or client URL is not\n\t\t\t// empty, then it must match the desired service's.\n\t\t\tif len(m.name) > 0 && m.name != service.ID ||\n\t\t\t\tlen(m.clientURL) > 0 &&\n\t\t\t\t\tm.clientURL != protocol(service.Address) {\n\n\t\t\t\treturn PeerURLAlreadyInUseError{\n\t\t\t\t\tID:          hexID(m.id),\n\t\t\t\t\tName:        m.name,\n\t\t\t\t\tDescription: _STORAGE_NODE_URI_USED,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Add new member to cluster.\n\tlog.Log(ctx, AddingMember{Name: service.ID, URL: peerURL,\n\t\tDescription: _STORAGE_ADD_NODE})\n\tctx, cancel := context.WithTimeout(ctx, optime)\n\tdefer cancel()\n\tadded, err := client.MemberAdd(ctx, []string{peerURL})\n\tif err != nil {\n\t\treturn AddMemberError{Err: err, Description: _STORAGE_ADD_NODE_FAIL}\n\t}\n\tlog.Log(ctx, AddedMember{ID: hexID(added.Member.ID),\n\t\tDescription: _STORAGE_ADD_NODE_OK})\n\treturn nil\n}\n\n// updateMembers updates the cluster membership using members, pruneMembers and\n// addMember.\nfunc updateMembers(ctx context.Context, conf clientv3.Config, optime time.Duration,\n\tconfigured []*conf.Service, add *conf.Service) error {\n\n\t// If we are adding a member, then remove its endpoint from the client\n\t// configuration: we cannot connect to it before it is added.\n\tif add != nil {\n\t\t// Do not mangle the existing endpoints: create a new one.\n\t\tendpoints := make([]string, 0, len(conf.Endpoints)-1)\n\t\tfor _, endpoint := range conf.Endpoints {\n\t\t\tif endpoint == protocol(add.Address) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tendpoints = append(endpoints, endpoint)\n\t\t}\n\t\tconf.Endpoints = endpoints\n\t}\n\n\t// Initialize an etcd client to use.\n\tlog.Log(ctx, UpdatingClusterMembership{Endpoints: conf.Endpoints,\n\t\tDescription: _STORAGE_UPDATE_CLUSTER})\n\tclient, err := clientv3.New(conf)\n\tif err != nil {\n\t\treturn EtcdClientError{Err: err,\n\t\t\tDescription: _STORAGE_PING_CLIENT_CFG}\n\t}\n\tdefer func() {\n\t\tif err := client.Close(); err != nil {\n\t\t\t// Only log close error, do not return.\n\t\t\tlog.Error(ctx, EtcdClientCloseError{Err: err,\n\t\t\t\tDescription: _STORAGE_ETCD_CONN_CLOSE})\n\t\t}\n\t}()\n\n\t// Get the current list of members configured in the cluster.\n\tmembers, err := members(ctx, client, optime)\n\tif err != nil {\n\t\treturn UpdateListMembersError{Err: err,\n\t\t\tDescription: _STORAGE_ETCD_CLUSTER_UPDATE}\n\t}\n\n\t// Prune members which are no longer configured: must be done before\n\t// adding to put the cluster in a healthy state for membership changes.\n\tif err := pruneMembers(ctx, client, optime, conf, members, configured); err != nil {\n\t\treturn UpdatePruneMembersError{Err: err,\n\t\t\tDescription: _STORAGE_ETCD_CLUSTER_PRUNE}\n\t}\n\n\tif add != nil {\n\t\t// Add this instance to the member list if not already there.\n\t\tif err := addMember(ctx, client, optime, members, add); err != nil {\n\t\t\treturn UpdateAddMemberError{Err: err, Description: _STORAGE_ETCD_CLUSTER_ADD}\n\t\t}\n\t}\n\tlog.Log(ctx, ClusterUpToDate{Description: _STORAGE_ETCD_CLUSTER_OK})\n\treturn nil\n}\n"
  },
  {
    "path": "storage/service/storage/log_desc.go",
    "content": "package main\n\nconst (\n\t_STORAGE_WAL_OPEN            = \"Failed to open wal/ directory\"\n\t_STORAGE_ETCD_OPEN           = \"Failed to list all files/directories in etcd root directory\"\n\t_STORAGE_ETCD_MEM            = \"Prepare to fetch all etcd nodes from etcd cluster\"\n\t_STORAGE_ETCD_MEM_FAIL       = \"Failed to list all etcd nodes\"\n\t_STORAGE_ETCD_MEM_LIST       = \"List all etcd nodes\"\n\t_STORAGE_SAME_ID_CLIENT      = \"Each etcd client should have a single URI\"\n\t_STORAGE_SAME_ID             = \"Each etcd node should have a single URI\"\n\t_STORAGE_PING_CLIENT         = \"Starting to ping an etcd client\"\n\t_STORAGE_PING_CLIENT_CFG     = \"Failed to create etcd client\"\n\t_STORAGE_PING_CLIENT_PRUNED  = \"Removed etcd node is still alive\"\n\t_STORAGE_PING_CLIENT_FAIL    = \"Failed to ping an etcd client\"\n\t_STORAGE_PRUNE_NODE          = \"Starting to prune an etcd node\"\n\t_STORAGE_PRUNE_NODE_FAIL     = \"Failed to prune an etcd node\"\n\t_STORAGE_PRUNE_NODE_OK       = \"Successfully pruned an etcd node\"\n\t_STORAGE_NODE_URI_USED       = \"etcd node URI is already in-use\"\n\t_STORAGE_ADD_NODE            = \"Starting to add an etcd node\"\n\t_STORAGE_ADD_NODE_FAIL       = \"Failed to add an etcd node\"\n\t_STORAGE_ADD_NODE_OK         = \"Successfully added an etcd node\"\n\t_STORAGE_UPDATE_CLUSTER      = \"Update cluster membership\"\n\t_STORAGE_ETCD_CONN_CLOSE     = \"Failed to close etcd connection\"\n\t_STORAGE_ETCD_CLUSTER_UPDATE = \"Failed to update etcd cluster membership\"\n\t_STORAGE_ETCD_CLUSTER_PRUNE  = \"Failed to prune etcd cluster inactive nodes\"\n\t_STORAGE_ETCD_CLUSTER_ADD    = \"Failed to add etcd nodes into a cluster\"\n\t_STORAGE_ETCD_CLUSTER_OK     = \"etcd cluster is successfully updateed and ready to use\"\n\t_STORAGE_ETCD_SYSLOG         = \"Failed to create syslog instance for etcd output\"\n\t_STORAGE_ETCD_SYSLOG_CLOSE   = \"Failed to close etcd syslog instance\"\n\t_STORAGE_ETCD_ESCAPE         = \"Some of etcd native logs are not JSON format\"\n\t_STORAGE_ETCD_FORMAT         = \"etcd native log is JSON format, but doesn't match an expected pattern on IVXV side\"\n\t_STORAGE_ETCD_UNKNOWN_LVL    = \"Unknown log level detected in etcd native log\"\n\t_STORAGE_CFG                 = \"Failed to parse storage configuration\"\n\t_STORAGE_CA                  = \"Failed to parse storage peers CA certificate\"\n\t_STORAGE_CERT                = \"Failed to parse storage service key and certificate\"\n\t_STORAGE_ADDR                = \"Invalid TCP address of storage service clients\"\n\t_STORAGE_ADDR_PEER           = \"Invalid TCP address of storage service peers\"\n\t_STORAGE_ETCD_PARSE          = \"Failed to parse storage peer certificate\"\n\t_STORAGE_ETCD_CA_VERIFY      = \"Failed to verify storage peer certificate against storage peer CA certificate\"\n\t_STORAGE_ETCD_CA_VERIFY_EXT  = \"Verification of peer certificate is only possible is certificate has proper x509 extension\"\n\t_STORAGE_ETCD_CA_WRITE       = \"Unable to write storage peer CA certificate to etcd directory\"\n\t_STORAGE_ETCD_START          = \"Start etcd binary\"\n\t_STORAGE_ETCD_START_FAIL     = \"Failed to start etcd binary\"\n\t_STORAGE_ETCD_WAIT           = \"Wait until systemd brings up etcd process up\"\n\t_STORAGE_ETCD_STOP_FAIL      = \"Failed to stop etcd binary gracefully\"\n\t_STORAGE_ETCD_UNIX           = \"Failed to connect to unix socket\"\n\t_STORAGE_ETCD_UNIX_READ      = \"Failed to read from unix socket\"\n\t_STORAGE_ETCD_UNIX_READY     = \"Expected to receive 'READY=1' on unix socket\"\n\t_STORAGE_ETCD_STOP           = \"Got stop signal from etcd\"\n\t_STORAGE_OS_INTERRUPT        = \"Got SIGINT signal from OS\"\n\t_STORAGE_PROTOCOL            = \"IVXV storage protocol is only allowed to be 'etcd'\"\n\t_STORAGE_CTRL_CFG            = \"Failed to parse controller configuration for storage service\"\n\t_STORAGE_CTRL                = \"Failed to create controller for storage service\"\n\t_STORAGE_CTRL_SERVE          = \"Failed to control a storage service\"\n)\n"
  },
  {
    "path": "storage/service/storage/logger.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log/syslog\"\n\t\"net/url\"\n\n\t\"ivxv.ee/common/collector/log\"\n)\n\ntype etcdLog struct {\n\tLevel     string `json:\"level\"`\n\tTimestamp string `json:\"ts\"`\n\tMessage   string `json:\"msg\"`\n\t// other fields are not important\n}\n\nconst (\n\tError  = \"error\"\n\tWarn   = \"warn\"\n\tNotice = \"notice\"\n\tInfo   = \"info\"\n\tDebug  = \"debug\"\n)\n\ntype logger struct {\n\t*syslog.Writer\n}\n\nfunc newLogger() (logger, error) {\n\t// Same facility as used in ivxv.ee/common/collector/log.\n\tw, err := syslog.New(syslog.LOG_LOCAL0, \"etcd\")\n\tif err != nil {\n\t\treturn logger{nil}, EtcdSyslogError{Err: err, Description: _STORAGE_ETCD_SYSLOG}\n\t}\n\treturn logger{w}, nil\n}\n\nfunc (l logger) log(ctx context.Context, r io.ReadCloser) {\n\tdefer r.Close()\n\tdefer func() {\n\t\tif err := l.Close(); err != nil {\n\t\t\tlog.Error(ctx, EtcdSyslogCloseError{Err: err,\n\t\t\t\tDescription: _STORAGE_ETCD_SYSLOG_CLOSE})\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(r)\n\n\t// All etcd logs should be valid JSON, however there are exceptions\n\t// which were found out during stress testing.\n\t//\n\t// For example etcd could be killed by systemd oomd.service, in that\n\t// case `line = Killed`. But it doesn't mean that ivxv-storage should\n\t// panic due to invalid log.\n\t// ivxv-storage will catch etcd's SIGKILL or any other signal and then\n\t// will try to restart a node.\n\t//\n\t// Another example is etcd warning messages that aren't valid JSON,\n\t// as an example:\n\t// `line = Server.processUnaryRPC failed to write connection error: desc = \"transport is closing\"`\n\t// This message is not harmful for etcd operation and only tells that\n\t// etcd cannot write log messages to the client right now, source:\n\t// https://github.com/etcd-io/etcd/issues/12895\n\tfor scanner.Scan() {\n\t\t// Before: %7B%22level%22%3A%22info%22%2C%22ts%22%3A%\n\t\tline := scanner.Text()\n\n\t\t// After: \"{\"level\": \"info\", \"ts\":\n\t\tunescaped, err := url.QueryUnescape(line)\n\t\tif err != nil {\n\t\t\tlog.Error(ctx, QueryUnescapeLogError{Line: line,\n\t\t\t\tDescription: _STORAGE_ETCD_ESCAPE})\n\t\t\tcontinue\n\t\t}\n\n\t\tvar elog etcdLog\n\t\terr = json.Unmarshal([]byte(unescaped), &elog)\n\t\tif err != nil {\n\t\t\tlog.Error(ctx, EtcdUnexpectedLogError{Line: line,\n\t\t\t\tDescription: _STORAGE_ETCD_FORMAT})\n\t\t\tcontinue\n\t\t}\n\n\t\tlevel := elog.Level\n\t\tmsg := elog.Message\n\n\t\tswitch level {\n\t\tcase Error:\n\t\t\terr = l.Err(msg)\n\t\tcase Warn:\n\t\t\terr = l.Warning(msg)\n\t\tcase Notice:\n\t\t\terr = l.Notice(msg)\n\t\tcase Info:\n\t\t\terr = l.Info(msg)\n\t\tcase Debug:\n\t\t\terr = l.Debug(msg)\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Error(ctx, EtcdLogError{\n\t\t\t\tLevel:       level,\n\t\t\t\tMessage:     msg,\n\t\t\t\tErr:         log.Alert(err),\n\t\t\t\tDescription: _STORAGE_ETCD_UNKNOWN_LVL,\n\t\t\t})\n\t\t}\n\t}\n\t// We are reading from a PipeReader and are not using CloseWithError,\n\t// so only EOF is returned: ignore scanner.Err().\n}\n"
  },
  {
    "path": "storage/service/storage/main.go",
    "content": "/*\nThe storage service controls a locally running etcd instance.\n*/\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"time\"\n\n\tclientv3 \"go.etcd.io/etcd/client/v3\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/storage/etcd\"\n\t\"ivxv.ee/common/collector/yaml\"\n\t//ivxv:modules common/collector/container\n)\n\nconst zero = 0\n\ntype ctrl struct {\n\t// Configuration for the etcd cluster client.\n\tbstrap  bool\n\tcli     clientv3.Config\n\toptime  time.Duration\n\tservice *conf.Service\n\tmembers []*conf.Service\n\n\t// Configuration for starting the server.\n\twd     string\n\tcapath string\n\tcapem  []byte\n\tnsock  string\n\tenv    []string\n\targs   []string\n\n\t// Command for the started server and channel waiting for it to exit.\n\tcmd   *exec.Cmd\n\twaitc chan error\n\n\t// Cleanup function to call after etcd has stopped.\n\tcleanup func()\n}\n\nfunc newCtrl(cfg *conf.Technical, service *conf.Service, election string) (\n\tc *ctrl, code int, err error) {\n\n\tc = &ctrl{service: service, wd: conf.Sensitive(service.ID)}\n\tc.capath = filepath.Join(c.wd, \"ca.pem\")\n\tc.nsock = filepath.Join(c.wd, \"notify.sock\") // See ctrl.start for explanation.\n\n\t// Parse the storage configuration block as etcd configuration.\n\tvar etcdCfg etcd.Conf\n\tif err = yaml.Apply(cfg.Storage.Conf, &etcdCfg); err != nil {\n\t\treturn nil, exit.Config, EtcdConfigurationError{Err: err,\n\t\t\tDescription: _STORAGE_CFG}\n\t}\n\tc.bstrap = bootstrap(service.ID, &etcdCfg)\n\tc.cli.DialTimeout = time.Duration(etcdCfg.ConnTimeout) * time.Second\n\tc.optime = time.Duration(etcdCfg.OpTimeout) * time.Second\n\tc.capem = []byte(etcdCfg.CA)\n\n\t// Parse the CA certificate and member TLS certificate and private key.\n\tc.cli.TLS = &tls.Config{MinVersion: tls.VersionTLS12}\n\tif c.cli.TLS.RootCAs, err = cryptoutil.PEMCertificatePool(etcdCfg.CA); err != nil {\n\t\treturn nil, exit.Config, EtcdCAParseError{Err: err,\n\t\t\tDescription: _STORAGE_CA}\n\t}\n\ttlsPEMPath, tlsKeyPath := conf.TLS(c.wd)\n\tc.cli.TLS.Certificates = make([]tls.Certificate, 1)\n\tif c.cli.TLS.Certificates[0], err = tls.LoadX509KeyPair(tlsPEMPath, tlsKeyPath); err != nil {\n\t\treturn nil, exit.DataErr, LoadTLSCertificateError{Err: err,\n\t\t\tDescription: _STORAGE_CERT}\n\t}\n\n\t// Check that the TLS certificate is issued by the CA and has key usage\n\t// for both server and client auth.\n\tif err = checkTLS(c.cli.TLS.RootCAs, &c.cli.TLS.Certificates[0]); err != nil {\n\t\treturn nil, exit.DataErr, err\n\t}\n\n\t// Collect all storage services in the entire network (not only this\n\t// segment) as client endpoints and cluster members.\n\tvar cluster bytes.Buffer\n\tfor _, segment := range cfg.Network {\n\t\tfor _, member := range segment.Services.Storage {\n\t\t\tc.members = append(c.members, member)\n\t\t\tc.cli.Endpoints = append(c.cli.Endpoints, protocol(member.Address))\n\t\t\tfmt.Fprint(&cluster, \",\", member.ID, \"=\", protocol(member.PeerAddress))\n\t\t}\n\t}\n\tcluster.Next(1) // Skip leading comma.\n\n\t// Listening addresses cannot be hostnames and must be resolved.\n\tcaddr, err := net.ResolveTCPAddr(\"tcp\", service.Address)\n\tif err != nil {\n\t\treturn nil, exit.Config, ResolveClientAddressError{\n\t\t\tAddress:     service.Address,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_ADDR,\n\t\t}\n\t}\n\tcresv := caddr.String()\n\n\tpaddr, err := net.ResolveTCPAddr(\"tcp\", service.PeerAddress)\n\tif err != nil {\n\t\treturn nil, exit.Config, ResolvePeerAddressError{\n\t\t\tAddress:     service.PeerAddress,\n\t\t\tErr:         err,\n\t\t\tDescription: _STORAGE_ADDR_PEER,\n\t\t}\n\t}\n\tpresv := paddr.String()\n\n\tc.env = []string{\"NOTIFY_SOCKET=\" + c.nsock}\n\n\t// Assemble the command-line arguments.\n\tstate := \"new\"\n\tif !c.bstrap {\n\t\tstate = \"existing\"\n\t}\n\tc.args = []string{\n\t\t// Member flags.\n\t\t\"--name\", service.ID,\n\t\t\"--data-dir\", filepath.Join(c.wd, \"etcd\"),\n\t\t\"--wal-dir\", walDir(c.wd),\n\t\t\"--listen-client-urls\", protocol(cresv),\n\t\t\"--listen-peer-urls\", protocol(presv),\n\n\t\t// Clustering flags.\n\t\t\"--advertise-client-urls\", protocol(service.Address),\n\t\t\"--initial-advertise-peer-urls\", protocol(service.PeerAddress),\n\t\t\"--initial-cluster\", cluster.String(),\n\t\t\"--initial-cluster-state\", state,\n\t\t\"--initial-cluster-token\", election,\n\n\t\t// Security flags.\n\t\t\"--cert-file\", tlsPEMPath,\n\t\t\"--key-file\", tlsKeyPath,\n\t\t\"--client-cert-auth\",\n\t\t\"--trusted-ca-file\", c.capath,\n\t\t\"--peer-client-cert-auth\",\n\t\t\"--peer-trusted-ca-file\", c.capath,\n\t\t\"--peer-cert-file\", tlsPEMPath,\n\t\t\"--peer-key-file\", tlsKeyPath,\n\t}\n\n\t// Runtime reconfiguration parameters.\n\t// Parameters that can be changed without affecting cluster consistency.\n\t// Cluster should be brought down and then up with these updated values.\n\tif etcdCfg.Size > zero {\n\t\tsizeParam := strconv.FormatUint(etcdCfg.Size, 10)\n\t\tc.args = append(c.args, \"--quota-backend-bytes\", sizeParam)\n\t}\n\tif etcdCfg.SnapshotCount > zero {\n\t\tsnapshotParam := strconv.FormatUint(etcdCfg.SnapshotCount, 10)\n\t\tc.args = append(c.args, \"--snapshot-count\", snapshotParam)\n\t}\n\tif etcdCfg.HeartbeatTimeout > zero {\n\t\theartbeatParam := strconv.FormatUint(etcdCfg.HeartbeatTimeout, 10)\n\t\tc.args = append(c.args, \"--heartbeat-interval\", heartbeatParam)\n\t}\n\tif etcdCfg.ElectionTimeout > zero {\n\t\telectionParam := strconv.FormatUint(etcdCfg.ElectionTimeout, 10)\n\t\tc.args = append(c.args, \"--election-timeout\", electionParam)\n\t}\n\n\t// Don't use etcd --log-level 'debug' for production nor for testing,\n\t// since this will intercept client interaction with a database and may\n\t// result in `context deadline exceeded` errors on client requests.\n\t// Behaviour is not predictable.\n\t//\n\t// From etcd docs (https://etcd.io/docs/v3.6/op-guide/monitoring/):\n\t// If --log-level=debug is set, the etcd server exports debugging information\n\t// on its client port under the /debug path.\n\t// Take care when setting --log-level=debug, since there will be degraded\n\t// performance and verbose logging.\n\t// if cfg.Debug {\n\t//\tc.args = append(c.args, \"--log-level\", \"debug\")\n\t// }\n\n\tc.waitc = make(chan error, 1)\n\treturn\n}\n\nfunc checkTLS(roots *x509.CertPool, cert *tls.Certificate) error {\n\tvar err error\n\tif cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {\n\t\treturn ParseTLSCertificateError{Err: err,\n\t\t\tDescription: _STORAGE_ETCD_PARSE}\n\t}\n\tif _, err = cert.Leaf.Verify(x509.VerifyOptions{\n\t\tRoots:     roots,\n\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, // Checked manually.\n\t}); err != nil {\n\t\treturn VerifyTLSCertificateError{Err: err,\n\t\t\tDescription: _STORAGE_ETCD_CA_VERIFY}\n\t}\n\n\t// *x509.Certificate.Verify only checks if any of the specified\n\t// extended key usages is allowed. We want them all, so we need to\n\t// check manually.\nrequired:\n\tfor _, r := range []x509.ExtKeyUsage{\n\t\tx509.ExtKeyUsageClientAuth, // For connecting to other peers.\n\t\tx509.ExtKeyUsageServerAuth, // For serving connections.\n\t} {\n\t\tfor _, u := range cert.Leaf.ExtKeyUsage {\n\t\t\tif r == u {\n\t\t\t\tcontinue required\n\t\t\t}\n\t\t}\n\t\treturn TLSCertificateMissingUsageError{Usage: r, Description: _STORAGE_ETCD_CA_VERIFY_EXT}\n\t}\n\treturn nil\n}\n\n/*\n\t Since start contains a lot of conditional behavior based on the state of the\n\t storage instance, here is a small table which hopefully gives an overview\n\t on what happens when:\n\n\t\tfirst boot of      | restart of         | first boot of  | restart of\n\t\tbootstrap instance | bootstrap instance | added instance | added instance\n\t\t-------------------|--------------------|----------------|---------------\n\t\t                   |                    | prune members  |\n\t\t                   |                    | add new member |\n\t\tstart etcd         | start etcd         | start etcd     | start etcd\n\t\t                   | wait for ready     | wait for ready | wait for ready\n\t\t                   | prune members      |                | prune members\n\n\t The first boot of bootstrap instances includes no cluster membership\n\t updates, since there is no cluster to update. It also contains no waiting\n\t for etcd to become ready, because bootstrap instances are started serially\n\t and it would cause a deadlock (the instance being started is waiting for\n\t other cluster members, but other cluster members are not started until the\n\t first one is ready).\n\n\t The first boot of an added (non-bootstrap) instance updates cluster\n\t membership before starting etcd, because the cluster needs to know in\n\t advance about new members. Pruning also has to happen before adding (instead\n\t of after starting etcd), because members cannot be added to an unhealthy\n\t cluster.\n\n\t Restarts of both kinds of instances are identical: the etcd instance is\n\t started first to ensure that the cluster is healthy and members are pruned\n\t after etcd has notified it is ready.\n*/\nfunc (c *ctrl) start(ctx context.Context) error {\n\tfirst, err := firstBoot(c.wd)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif first && !c.bstrap { // First boot of added instance: add member.\n\t\tif err := updateMembers(ctx, c.cli, c.optime, c.members, c.service); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Write the CA to the working directory location added to arguments in\n\t// newCtrl. Do this when starting and not before to avoid overwriting\n\t// the CA file when only checking the configuration.\n\tif err := os.WriteFile(c.capath, c.capem, 0600); err != nil {\n\t\treturn EtcdCAWriteError{Err: err, Description: _STORAGE_ETCD_CA_WRITE}\n\t}\n\n\t// Create notification socket waiting for etcd to become ready. Do this\n\t// even if this is a bootstrap instance and the channel is never read,\n\t// so etcd does not complain about the missing socket.\n\treadyctx, cancelReady := context.WithCancel(context.Background())\n\treadyc, err := listenReady(readyctx, c.nsock)\n\tif err != nil {\n\t\tcancelReady()\n\t\treturn err\n\t}\n\n\t// Do not use CommandContext: we do not want it to be killed, but will\n\t// perform a graceful stop ourselves.\n\tc.cmd = exec.Command(\"/usr/bin/etcd\", c.args...) //nolint:gosec // Args are trusted.\n\tc.cmd.Env = c.env\n\n\t// Forward all etcd output to logger.\n\tr, w := io.Pipe()\n\tc.cmd.Stdout = w\n\tc.cmd.Stderr = w\n\tlogger, err := newLogger()\n\tif err != nil {\n\t\tcancelReady()\n\t\treturn err\n\t}\n\tgo logger.log(ctx, r)\n\n\t// Create cleanup function for after etcd has stopped.\n\tc.cleanup = func() {\n\t\tw.Close()\n\t\tcancelReady() // Start notification socket cleanup.\n\t\t// Wait until readyc is closed, i.e., cleanup is done,\n\t\t// discarding any notification messages along the way.\n\t\t//nolint:revive\n\t\tfor range readyc {\n\t\t}\n\t}\n\n\t// Start etcd.\n\tlog.Log(ctx, StartingEtcd{Args: c.cmd.Args, Env: c.cmd.Env,\n\t\tDescription: _STORAGE_ETCD_START})\n\tif err := c.cmd.Start(); err != nil {\n\t\tc.cleanup()\n\t\treturn StartEtcdError{Err: err, Description: _STORAGE_ETCD_START_FAIL}\n\t}\n\n\t// Start a goroutine which waits on c.cmd and sends the result on waitc.\n\tgo func() { c.waitc <- c.cmd.Wait() }()\n\n\t// Block until etcd is ready, unless this is the first boot of a\n\t// boostrap instance (see the comment for start).\n\t//\n\t// If there is a ready notification error or the context is cancelled\n\t// then manually stop etcd, but only log stopping errors: return the\n\t// original error which caused the stop.\n\tif !(first && c.bstrap) {\n\t\tlog.Log(ctx, WaitingForEtcdReady{Description: _STORAGE_ETCD_WAIT})\n\t\tselect {\n\t\tcase err := <-c.waitc:\n\t\t\tc.cleanup()\n\t\t\treturn EtcdStartupError{Err: err, Description: _STORAGE_ETCD_START_FAIL}\n\t\tcase err := <-readyc:\n\t\t\tif err != nil {\n\t\t\t\tif serr := c.stop(ctx); serr != nil {\n\t\t\t\t\tlog.Error(ctx, EtcdNotifyStopError{Err: serr,\n\t\t\t\t\t\tDescription: _STORAGE_ETCD_STOP_FAIL})\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !first { // Restarting an instance: prune members.\n\t\t\t\tif err := updateMembers(ctx, c.cli, c.optime,\n\t\t\t\t\tc.members, nil); err != nil {\n\n\t\t\t\t\t// Do not stop an already started\n\t\t\t\t\t// healthy instance if pruning fails,\n\t\t\t\t\t// but do log it as an alert to signal\n\t\t\t\t\t// that the configuration was not\n\t\t\t\t\t// applied as expected.\n\t\t\t\t\tlog.Error(ctx, EtcdPruneError{Err: log.Alert(err),\n\t\t\t\t\t\tDescription: _STORAGE_UPDATE_CLUSTER})\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\tif err := c.stop(ctx); err != nil {\n\t\t\t\tlog.Error(ctx, EtcdCanceledStopError{Err: err,\n\t\t\t\t\tDescription: _STORAGE_ETCD_STOP_FAIL})\n\t\t\t}\n\t\t\treturn ctx.Err()\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc listenReady(ctx context.Context, nsock string) (<-chan error, error) {\n\t// etcd expects to be run under systemd with Type=notify. This means\n\t// that we can open our own notification socket at nsock and listen on\n\t// it for etcd to be ready.\n\tconn, err := net.ListenPacket(\"unixgram\", nsock)\n\tif err != nil {\n\t\treturn nil, ListenNotifyError{Address: nsock, Err: err,\n\t\t\tDescription: _STORAGE_ETCD_UNIX}\n\t}\n\n\t// Start a goroutine which listens for the notification and sends the\n\t// result on sockc.\n\tsockc := make(chan error, 1)\n\tgo func() {\n\t\tconst ready = \"READY=1\"\n\t\tsockMsg := make([]byte, len(ready)+1) // Allocate extra byte to detect trailing garbage.\n\t\tn, _, err := conn.ReadFrom(sockMsg)\n\t\tif err != nil {\n\t\t\tsockc <- ReadFromNotifyError{Err: err, Description: _STORAGE_ETCD_UNIX_READ}\n\t\t\treturn\n\t\t}\n\t\tif msg := string(sockMsg[:n]); msg != ready {\n\t\t\tsockc <- UnexpectedNotifyMessageError{Message: msg, Description: _STORAGE_ETCD_UNIX_READY}\n\t\t\treturn\n\t\t}\n\t\tsockc <- nil\n\t}()\n\n\t// Start a goroutine which forwards sockc to readyc and cleans up the\n\t// notification socket after something is received from sockc or the\n\t// context is cancelled.\n\treadyc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(readyc)\n\t\tdefer os.Remove(nsock)\n\t\tdefer conn.Close()\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\tcase err := <-sockc:\n\t\t\treadyc <- err\n\t\t}\n\t}()\n\treturn readyc, nil\n}\n\nfunc (c *ctrl) check(_ context.Context) error {\n\tselect {\n\tcase err := <-c.waitc:\n\t\tc.cleanup()\n\t\treturn EtcdTerminatedError{Err: err, Description: _STORAGE_ETCD_STOP}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (c *ctrl) stop(_ context.Context) error {\n\tif err := c.cmd.Process.Signal(os.Interrupt); err != nil {\n\t\treturn InterruptEtcdError{PID: c.cmd.Process.Pid, Err: err,\n\t\t\tDescription: _STORAGE_OS_INTERRUPT}\n\t}\n\n\terr := <-c.waitc\n\tc.cleanup()\n\tif exit1, ok := err.(*exec.ExitError); ok {\n\t\t// Exiting because of interrupt signal is expected.\n\t\tif exit1.Sys().(syscall.WaitStatus).Signal() == os.Interrupt {\n\t\t\terr = nil\n\t\t}\n\t}\n\treturn err\n}\n\nfunc main() {\n\t// Call storagemain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(storagemain())\n}\n\nfunc storagemain() (code int) {\n\tc := command.NewWithoutStorage(\"ivxv-storage\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tvar s *server.Controller\n\tvar err error\n\tif c.Conf.Technical != nil {\n\t\tprot := c.Conf.Technical.Storage.Protocol\n\t\tif prot != storage.Etcd {\n\t\t\treturn c.Error(exit.Config, StorageProtocolError{Protocol: prot,\n\t\t\t\tDescription: _STORAGE_PROTOCOL},\n\t\t\t\t\"etcd storage protocol must be used for the\",\n\t\t\t\t\"storage service, but protocol is\", prot)\n\t\t}\n\n\t\tvar cmd *ctrl\n\t\tif cmd, code, err = newCtrl(c.Conf.Technical, c.Service,\n\t\t\tc.Conf.Election.Identifier); err != nil {\n\n\t\t\treturn c.Error(code, EtcdControllerError{Err: err,\n\t\t\t\tDescription: _STORAGE_CTRL_CFG},\n\t\t\t\t\"failed to configure etcd options:\", err)\n\t\t}\n\n\t\tif s, err = server.NewController(&c.Conf.Version,\n\t\t\tcmd.start, cmd.check, cmd.stop); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ControllerConfError{Err: err,\n\t\t\t\tDescription: _STORAGE_CTRL},\n\t\t\t\t\"failed to configure controller:\", err)\n\t\t}\n\t}\n\n\t// Control etcd during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.Control(c.Ctx); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ControlError{Err: err,\n\t\t\t\tDescription: _STORAGE_CTRL_SERVE},\n\t\t\t\t\"failed to control storage service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "systemd/.gitignore",
    "content": "*.service\n"
  },
  {
    "path": "systemd/Makefile",
    "content": "SERVICES := choices mid smartid webeid proxy storage voting verification votesorder sessionstatus\nOUTPUT   := $(SERVICES:%=ivxv-%@.service)\n\n.PHONY: all\nall: $(OUTPUT)\n\nivxv-%@.service: ivxv-$$service@.service.in\n\tsed 's/@service@/$*/' < '$^' > $@\n\n\t# ivxv-storage service should have a greater start timeout in case large db (> 8GB)\n\t# nodes will be brought up. Systemd uses TimeoutStartSec=90s by default, which may\n\t# be not enough.\n\tif [ \"$*\" = \"storage\" ]; then \\\n\t\tsed -i \"s/TimeoutStopSec=90s/TimeoutStopSec=90s\\nTimeoutStartSec=600s/g\" ivxv-$*@.service; \\\n\tfi\n\n.PHONY: install\ninstall: all\n\tmkdir -p $(DESTDIR)/usr/lib/systemd/user\n\tcp $(OUTPUT) $(DESTDIR)/usr/lib/systemd/user\n\n.PHONY: clean\nclean:\n\trm -f $(OUTPUT)\n"
  },
  {
    "path": "systemd/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n-----------------------\n Systemd service files\n-----------------------\n\nSystemd service files for controlling collector services.\n"
  },
  {
    "path": "systemd/ivxv-$service@.service.in",
    "content": "[Unit]\nDescription=IVXV @service@ service\n\n# All collector services bind to an explicitly configured address, so we\n# require the network to be online. IP_FREEBIND is not a good option in Go 1.9.\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=notify\nEnvironmentFile=-/etc/default/ivxv\nExecStart=/usr/bin/ivxv-@service@ $EXTRAOPTS -instance %i\nKillMode=mixed\nRestart=on-failure\n# Do not automatically restart if the service failed with:\n#\tUsage (64), DataErr(65), NoInput (66), CantCreate (73), or Config (78).\nRestartPreventExitStatus=64 65 66 73 78\nTimeoutStopSec=90s\n\n[Install]\nWantedBy=default.target\n"
  },
  {
    "path": "verification/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "verification/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "verification/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n----------------------\n Verification service\n----------------------\n\n<Description of verification service.>\n"
  },
  {
    "path": "verification/go.mod",
    "content": "module ivxv.ee/verification\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "verification/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "verification/internal/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value when calling RPC.Verify\n\t// for a first time.\n\tVote = \"RPC.Vote\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Verify\n\t// for a next time.\n\tVerify = \"RPC.Verify\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tverifyTTL int64\n\tclient    client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != exitCodeOK {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:    tlsDialer,\n\t\tverifyTTL: c.Conf.Technical.Status.Session.VerifyTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err, Description: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\t// Send request to session status server and verify response\n\tok, err := r.verifyAndUpdateSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndUpdateSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndUpdateSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will update h.Header.SessionID record in the underlying storage\n// by marking session status Caller as serviceMethod.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndUpdateSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\t// NB! Most important part, that prevents any attack on SessionID\n\tvar ok bool\n\tvar ttl string\n\tok, err = verifyStatusReadResp(&respRead, verifyHandler)\n\tttl = strconv.FormatInt(r.verifyTTL, 10)\n\tif !ok || err != nil {\n\t\treturn ok, VerifyStatusReadRespError{Err: err, Description: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp, h func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// authenticateHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Authenticate request.\nfunc verifyHandler(r *api.StatusReadResp) (bool, error) {\n\t// When voted with ID card\n\tidCardAuth := r.Caller == Vote && r.Auth == client.IDcardAuth ||\n\t\tr.Caller == Verify && r.Auth == client.IDcardAuth\n\n\t// When voted with Mobile-ID\n\tmidAuth := r.Caller == Vote && r.Auth == client.MobileIDAuth ||\n\t\tr.Caller == Verify && r.Auth == client.MobileIDAuth\n\n\t// When voted with Smart-ID\n\tsidAuth := r.Caller == Vote && r.Auth == client.SmartIDAuth ||\n\t\tr.Caller == Verify && r.Auth == client.SmartIDAuth\n\n\t// When voted with Web eID\n\twidAuth := r.Caller == Vote && r.Auth == client.WebeIDAuth ||\n\t\tr.Caller == Verify && r.Auth == client.WebeIDAuth\n\n\tif !(idCardAuth) && !(midAuth) && !(sidAuth) && !(widAuth) {\n\t\treturn false, VerifyInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Verify,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "verification/internal/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ          = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT  = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_TLS_DIAL                       = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                    = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_UPDATE_FAIL                    = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID           = \"SessionID has been attempted to tamper\"\n)\n"
  },
  {
    "path": "verification/service/verification/log_desc.go",
    "content": "package main\n\nconst (\n\t_VERIFICATION_VERIFYREQ          = \"RPC.VerifyReq\"\n\t_VERIFICATION_SESSION_ID         = \"Malformed SessionID\"\n\t_VERIFICATION_SESSION_ID_EXPIRED = \"SessionID has been expired\"\n\t_VERIFICATION_NO_VOTES           = \" Voter hasn't voted yet\"\n\t_VERIFICATION_NO_DB              = \"Cannot fetch verification data for the voter from database\"\n\t_VERIFICATION_LIMIT              = \"Verification limit is exceeded\"\n\t_VERIFICATION_TIMEOUT            = \"Verification attempts timeout\"\n\t_VERIFICATION_OLD_VOTE           = \"Voter has tampered a request and tries to verify old vote\"\n\t_VERIFICATION_RACE               = \"Race condition, where voter tries to verify a vote in one session and gives a vote in another session\"\n\t_VERIFICATION_VOTE_NO_DB         = \"Cannot fetch vote of the voter from database\"\n\t_VERIFICATION_VERIFYRESP         = \"RPC.VerifyResp\"\n\t_VERIFICATION_START              = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_VERIFICATION_STOP               = \"Failed to transform verification stop time from election configuration file to RFC3339 format\"\n\t_VERIFICATION_SERVER             = \"Failed to parse server configuration for verification service\"\n\t_VERIFICATION_SERVER_SERVE       = \"Failed to serve verification service to clients\"\n)\n"
  },
  {
    "path": "verification/service/verification/main.go",
    "content": "/*\nThe verification service serves stored votes and qualifying properties for\nrequested vote identifiers.\n*/\npackage main\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\t\"ivxv.ee/common/collector/storage\"\n\tinternal \"ivxv.ee/verification/internal/sessionstatus/rpc\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\n// RPC is the handler for verification service calls.\ntype RPC struct {\n\tstatus               client.Verifier\n\telection             *conf.Election\n\tqps                  []q11n.Protocol // Qualifying properties expected for each vote.\n\tstorage              *storage.Client\n\tforeignCode          string // Administrative unit code for foreign voters.\n\tpredefinedDistrictID string\n}\n\n// Args are the arguments provided to a call of RPC.Verify.\ntype Args struct {\n\tserver.Header\n\tVoteID []byte `size:\"16\"` // Vote identifier of the vote to return.\n}\n\n// Response is the response returned by RPC.Verify.\ntype Response struct {\n\tserver.Header\n\tType          container.Type  // Type of the stored vote container.\n\tVote          []byte          // The stored vote.\n\tQualification q11n.Properties // Qualifying properties for the vote.\n\tChoicesList   []byte          // Choices list\n}\n\n// Verify is the remote procedure call performed by clients to retrieve votes\n// from the collector for verification.\nfunc (r *RPC) Verify(args Args, resp *Response) error {\n\tlog.Log(args.Ctx, VerifyReq{VoteID: args.VoteID, Description: _VERIFICATION_VERIFYREQ})\n\tnow := time.Now()\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Verify).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, VerifySessionIDError{Err: err, Description: _VERIFICATION_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, VerifyUpdateSessionIDError{Description: _VERIFICATION_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// If the verification count changed between retrieving it and\n\t// attempting to update, then that means that there was another\n\t// concurrent verification session for this ID that got there first. If\n\t// this happens, then try from the beginning, but still use the time\n\t// the request originally came in.\n\tfor {\n\t\t// Get the verification statistics for the vote ID.\n\t\tcount, at, latest, choicesList, err := r.storage.GetVerificationStats(\n\t\t\targs.Ctx, args.VoteID, r.foreignCode, r.predefinedDistrictID)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) != nil {\n\t\t\t\t// Client has passed \"RPC.Verify.VoteID\" argument,\n\t\t\t\t// but this particular vote ID hasn't voted\n\t\t\t\tlog.Error(args.Ctx, BadVoteIDError{Err: err, Description: _VERIFICATION_NO_VOTES})\n\t\t\t\treturn server.ErrBadRequest\n\t\t\t}\n\t\t\t// Backend internal error, database is unreachable\n\t\t\tlog.Error(args.Ctx, GetVerificationStatsError{Err: log.Alert(err),\n\t\t\t\tDescription: _VERIFICATION_NO_DB})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\t// Check that we have not reached the count limit. We want this\n\t\t// to look exactly like a bad vote identifier was submitted, so\n\t\t// return ErrBadRequest instead of a more descriptive error.\n\t\tif limit := r.election.Verification.Count; limit > 0 && count >= limit {\n\t\t\t// election.yml verification:count has been reached, client\n\t\t\t// has no more attempt to verify its vote\n\t\t\tlog.Error(args.Ctx, VerificationCountError{Count: count, Description: _VERIFICATION_LIMIT})\n\t\t\treturn server.ErrBadRequest\n\t\t}\n\n\t\t// Check that we are inside the time limit.\n\t\tif limit := r.election.Verification.Minutes; limit > 0 &&\n\t\t\tnow.After(at.Add(time.Duration(limit)*time.Minute)) { //nolint:gosec\n\t\t\t// election.yml veritication:minutes has ended for a vote verification\n\t\t\tlog.Error(args.Ctx, VerificationTimeError{At: at, Description: _VERIFICATION_TIMEOUT})\n\t\t\treturn server.ErrBadRequest\n\t\t}\n\n\t\t// Check if this is the latest vote.\n\t\tif r.election.Verification.LatestOnly && !latest {\n\t\t\t// Client has passed \"RPC.Verify.VoteID\" argument, but VoteID is an old its vote,\n\t\t\t// which is not yet expired, but client wish to verify it, this is prohibited,\n\t\t\t// only latest vote is verifiable\n\t\t\tlog.Error(args.Ctx, VerificationNotLatestError{Description: _VERIFICATION_OLD_VOTE})\n\t\t\treturn server.ErrBadRequest\n\t\t}\n\n\t\t// Retrieve the stored vote tied to the requested vote\n\t\t// identifier and increase the verification count, as long as\n\t\t// it has not changed.\n\t\tvote, err := r.storage.GetVerification(args.Ctx, args.VoteID, count, r.qps...)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.UnexpectedValueError)) != nil {\n\t\t\t\tlog.Log(args.Ctx, ConcurrentVerificationWarning{Err: err,\n\t\t\t\t\tDescription: _VERIFICATION_RACE})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Backend internal error, database may be unreachable\n\t\t\tlog.Error(args.Ctx, GetVerificationError{Err: log.Alert(err),\n\t\t\t\tDescription: _VERIFICATION_VOTE_NO_DB})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\tresp.Type = vote.VoteType\n\t\tresp.Vote = vote.Vote\n\t\tresp.Qualification = vote.Qualification\n\t\tresp.ChoicesList = choicesList\n\n\t\t// Convert vote.Qualification:map[q11n.Protocol][]byte to\n\t\t// logq11n:map[q11n.Protocol]log.Sensitive for logging.\n\t\tlogq11n := make(map[q11n.Protocol]log.Sensitive)\n\t\tfor p, b := range vote.Qualification {\n\t\t\tlogq11n[p] = b\n\t\t}\n\t\t// Successful verify response, treat vote as sensitive\n\t\tlog.Log(args.Ctx, VerifyResp{\n\t\t\tType:          resp.Type,\n\t\t\tVote:          log.Sensitive(resp.Vote),\n\t\t\tQualification: logq11n,\n\t\t\tDescription:   _VERIFICATION_VERIFYRESP,\n\t\t})\n\t\treturn nil\n\t}\n}\n\nfunc main() {\n\t// Call verifymain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(verifymain())\n}\n\nfunc verifymain() (code int) {\n\tc := command.New(\"ivxv-verification\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := internal.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Create new RPC instance with the election configuration and storage\n\t// client.\n\trpc := &RPC{election: c.Conf.Election, storage: c.Storage, status: statusClient}\n\n\tvar start, stop time.Time\n\tvar err error\n\tif c.Conf.Election != nil {\n\t\t// Check election configuration time values.\n\t\tif start, err = c.Conf.Election.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err, Description: _VERIFICATION_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\tif stop, err = c.Conf.Election.VerificationStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StopTimeError{Err: err, Description: _VERIFICATION_STOP},\n\t\t\t\t\"bad service stop time:\", err)\n\t\t}\n\n\t\t// Initialize the list of qualifying properties which must\n\t\t// exist for each vote.\n\t\tfor _, q := range c.Conf.Election.Qualification {\n\t\t\trpc.qps = append(rpc.qps, q.Protocol)\n\t\t}\n\n\t\t// If not an empty string then ignore voterlist, use predefined district ID for all voters\n\t\trpc.predefinedDistrictID = strings.TrimSpace(c.Conf.Election.IgnoreVoterList)\n\n\t\t// Get foreign code (voterforeignehak) from election config\n\t\trpc.foreignCode = strings.TrimSpace(c.Conf.Election.VoterForeignEHAKDefault())\n\n\t\t// No authentication is used during verification.\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err, Description: _VERIFICATION_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err, Description: _VERIFICATION_SERVER_SERVE},\n\t\t\t\t\"failed to serve verification service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "votesorder/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "votesorder/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "votesorder/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n----------------\n Votes order service\n----------------\n"
  },
  {
    "path": "votesorder/go.mod",
    "content": "module ivxv.ee/votesorder\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n"
  },
  {
    "path": "votesorder/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "votesorder/service/votesorder/log_desc.go",
    "content": "package main\n\nconst (\n\t_VOTESORDER_VOTESSEQREQ  = \"RPC.VotesSeqReq\"\n\t_VOTESORDER_VOTESCOUNT   = \"Cannot fetch from a database an amount of total votes given (duplicate votes included)\"\n\t_VOTESORDER_VOTESSEQRESP = \"RPC.VotesSeqResp\"\n\t_VOTESORDER_VOTESREQ     = \"RPC.VotesReq\"\n\t_VOTESORDER_NO_SEQ       = \"Trying to fetch a vote sequence nr. that doesn't exist\"\n\t_VOTESORDER_ALL_VOTES    = \"Failed to fetch from database (by a batch) all successful votes with range of [from, to)\"\n\t_VOTESORDER_SEQ          = \"Cannot parse sequence nr. to base-10 integer\"\n\t_VOTESORDER_DISTRICT     = \"Cannot parse district nr. to base-10 integer\"\n\t_VOTESORDER_VOTESRESP    = \"RPC.VotesResp\"\n\t_VOTESORDER_STOP         = \"Failed to transform service start stop from election configuration file to RFC3339 format\"\n\t_VOTESORDER_SERVER       = \"Failed to parse server configuration for votesorder service\"\n\t_VOTESORDER_SERVER_SERVE = \"Failed to serve votesorder service to clients\"\n)\n"
  },
  {
    "path": "votesorder/service/votesorder/main.go",
    "content": "/*\nThe votesorder service serves votes lists.\n*/\npackage main\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/auth\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\n// RPC is the handler for votesorder service calls.\ntype RPC struct {\n\tstorage *storage.Client\n}\n\n// VotesNoArgs are the arguments provided to a call of RPC.VoterChoices. There\n// are none, because the identity of the voter will be extracted from the\n// authentication info.\ntype VotesNoArgs struct {\n\tserver.Header\n}\n\n// VotesNoResponse is the response returned by RPC.VotesSeqNo.\ntype VotesNoResponse struct {\n\tserver.Header\n\tSeqNo uint64\n}\n\n// VotesSeqNo is the remote procedure call performed by votesorder service provider to\n// retrieve current votes sequence number.\nfunc (r *RPC) VotesSeqNo(args VotesNoArgs, resp *VotesNoResponse) (err error) {\n\tlog.Log(args.Ctx, VotesNoReq{Description: _VOTESORDER_VOTESSEQREQ})\n\n\tif resp.SeqNo, err = r.storage.GetVotesCount(args.Ctx); err != nil {\n\t\tlog.Error(args.Ctx, GetVotesNoError{Err: log.Alert(err), Description: _VOTESORDER_VOTESCOUNT})\n\t\treturn server.ErrInternal\n\t}\n\n\tlog.Log(args.Ctx, VotesNoResp{SeqNo: resp.SeqNo, Description: _VOTESORDER_VOTESSEQRESP})\n\treturn\n}\n\n// VotesArgs are the arguments provided to a call of RPC.Votes.\ntype VotesArgs struct {\n\tserver.Header\n\tVotesFrom    int\n\tBatchMaxSize int\n}\n\n// VotesResponse is the response returned by RPC.Votes.\ntype VotesResponse struct {\n\tserver.Header\n\tBatchRecords []Vote `json:\"batchRecords\"`\n}\n\ntype Vote struct {\n\tSeqNo               uint64 `json:\"seqNo\"`\n\tIDCode              string `json:\"idCode\"`\n\tVoterName           string `json:\"voterName\"`\n\tKovCode             string `json:\"kovCode\"`\n\tElectoralDistrictNo uint64 `json:\"electoralDistrictNo\"`\n}\n\n// Votes is the remote procedure call to send votes to votesorder.\nfunc (r *RPC) Votes(args VotesArgs, resp *VotesResponse) (err error) {\n\tlog.Log(args.Ctx, VotesReq{VotesFrom: args.VotesFrom, Description: _VOTESORDER_VOTESREQ})\n\tvar votes []Vote\n\tvar seqNo uint64\n\tif seqNo, err = r.storage.GetVotesCount(args.Ctx); err != nil {\n\t\tlog.Error(args.Ctx, GetVotesCountErr{Err: log.Alert(err),\n\t\t\tDescription: _VOTESORDER_VOTESCOUNT})\n\t\treturn server.ErrInternal\n\t}\n\tif seqNo < uint64(args.VotesFrom) { //nolint:gosec\n\t\tlog.Error(args.Ctx, VotesFromErr{SeqNo: seqNo, VotesFrom: args.VotesFrom,\n\t\t\tDescription: _VOTESORDER_NO_SEQ})\n\t\treturn server.ErrBadRequest\n\t}\n\tvotesOrder, err := r.storage.GetVotesOrder(args.Ctx, args.VotesFrom, args.BatchMaxSize)\n\tif err != nil {\n\t\tlog.Error(args.Ctx, GetVotesOrderError{Err: log.Alert(err), Description: _VOTESORDER_ALL_VOTES})\n\t\treturn server.ErrInternal\n\t}\n\tfor _, vote := range votesOrder {\n\t\tno, err := strconv.ParseUint(vote.SeqNo, 0, 64)\n\t\tif err != nil {\n\t\t\tlog.Error(args.Ctx, VotesSeqNoParse{SeqNo: vote.SeqNo, Description: _VOTESORDER_SEQ})\n\t\t}\n\t\td, err := strconv.ParseUint(vote.DistrictNo, 0, 64)\n\t\tif err != nil {\n\t\t\tlog.Error(args.Ctx, VotesDistrictNoParse{DistrictNo: vote.DistrictNo,\n\t\t\t\tDescription: _VOTESORDER_DISTRICT})\n\t\t}\n\n\t\tvotes = append(votes, Vote{\n\t\t\tSeqNo:               no,\n\t\t\tVoterName:           vote.VoterName,\n\t\t\tIDCode:              vote.IDCode,\n\t\t\tKovCode:             vote.KovCode,\n\t\t\tElectoralDistrictNo: d,\n\t\t})\n\t}\n\tlog.Log(args.Ctx, VotesResp{Description: _VOTESORDER_VOTESRESP})\n\tresp.BatchRecords = votes\n\treturn\n}\n\nfunc main() {\n\t// Call votesorder in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(votesordermain())\n}\n\nfunc votesordermain() (code int) {\n\tc := command.New(\"ivxv-votesorder\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Create new RPC instance with the storage client.\n\trpc := &RPC{storage: c.Storage}\n\n\tvar stop time.Time\n\tvar clientCA string\n\tvar err error\n\tif elec := c.Conf.Election; elec != nil {\n\t\tif stop, err = elec.ElectionStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StopTimeError{Err: err, Description: _VOTESORDER_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\t\tclientCA = elec.XRoad.CA\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t\tClientCA: clientCA,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err, Description: _VOTESORDER_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.Serve(c.Ctx); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err, Description: _VOTESORDER_SERVER_SERVE},\n\t\t\t\t\"failed to serve votesorder service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "voting/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "voting/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "voting/README.rst",
    "content": "================================\n IVXV Internet voting framework\n================================\n----------------\n Voting service\n----------------\n\nThe voting service\n\n- verifies eligibility of voters,\n- verifies signatures on submitted votes,\n- generates vote identifiers,\n- qualifies votes as configured, which usually entails\n  - acquiring revocation information for signing certificates, and\n  - registering votes with a registration service,\n- stores votes, identifiers, and qualifying properties in the `storage\n  service`_, and\n- sends voters the vote identifiers and qualifying properties for\n  verification.\n\nThe vote container and signature format, qualifying properties, and storage\nprotocols are all abstracted and can be replaced with new implementations.\n\n.. _`storage service`: https://ivxv.ee/common/collector/storage\n"
  },
  {
    "path": "voting/cmd/voteexp/log_desc.go",
    "content": "package main\n\nconst (\n\t_VOTEEXP_OPEN           = \"Failed to open a file\"\n\t_VOTEEXP_EXP            = \"Failed to export votes\"\n\t_VOTEEXP_CLOSE          = \"Failed to close opened file\"\n\t_VOTEEXP_EXP_CTX_CANCEL = \"Context has been cancelled during votes export\"\n\t_VOTEEXP_ZIP_CLOSE      = \"Failed to close .zip container\"\n\t_VOTEEXP_EXP_INFO       = \"Exporting votes in progress\"\n\t_VOTEEXP_EXP_ERR        = \"Error occurred during votes export\"\n\t_VOTEEXP_EXP_NON_FATAL  = \"Non-fatal error occurred during votes export\"\n\t_VOTEEXP_EXP_PROGRESS   = \"Progress counter of votes export\"\n\t_VOTEEXP_ZIP_VERSION    = \"Failed to add '.version' file into .zip container\"\n\t_VOTEEXP_ZIP_VOTE       = \"Failed to add vote '.bdoc' file into .zip container\"\n\t_VOTEEXP_ZIP_Q          = \"Failed to add vote qualification properties files into .zip container\"\n\t_VOTING_VOTE_COUNT      = \"Total votes exported\"\n\t_VOTING_ZIP_HEADER      = \"Failed to create header for a file to be added into .zip container\"\n\t_VOTING_ZIP_FILE        = \"Failed to add file into .zip container\"\n)\n"
  },
  {
    "path": "voting/cmd/voteexp/main.go",
    "content": "/*\nThe voteexp application is used for exporting votes from the storage service.\n*/\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `voteexp exports votes from the collector's storage service for use by the\nprocessing application.\n\nvoteexp only exports complete votes and votes which are only missing fields\nlisted with the optional flag.\n\nIf there were non-fatal errors, e.g. there were some partial votes in storage,\nthen voteexp exits with code 2.`\n\nvar (\n\toptionalp = flag.String(\"optional\", \"tspreg,ocsp\",\n\t\t// End with newline for printing default value.\n\t\t\"comma-separated `list` of vote fields which are optional\\n\")\n\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call voteexpmain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(voteexpmain())\n}\n\nfunc voteexpmain() (code int) {\n\tc := command.New(\"ivxv-voteexp\", usage, \"output archive\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tvar qps []q11n.Protocol\n\tfor _, q := range c.Conf.Election.Qualification {\n\t\tqps = append(qps, q.Protocol)\n\t}\n\tvar opt []string\n\tif len(*optionalp) > 0 {\n\t\topt = strings.Split(*optionalp, \",\")\n\t}\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\tif c.Until < command.Execute {\n\t\treturn exit.OK\n\t}\n\toutput := c.Args[0]\n\n\t// Create the output container file.\n\tfp, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn c.Error(exit.CantCreate, CreateOutputError{Output: output, Err: err,\n\t\t\tDescription: _VOTEEXP_OPEN},\n\t\t\t\"failed to create output archive:\", err)\n\t}\n\n\t// Export the votes into the opened file descriptor.\n\terr = export(c.Ctx, c.Storage, qps, opt, fp)\n\tcerr := fp.Close()\n\n\tif errors.CausedBy(err, new(NonFatalError)) != nil {\n\t\t// Ad-hoc code to signal that everything got done, but\n\t\t// there were non-fatal errors.\n\t\tcode = 2\n\t\terr = nil\n\t}\n\tif err != nil {\n\t\treturn c.Error(exit.Unavailable, ExportVotesError{Err: err,\n\t\t\tDescription: _VOTEEXP_EXP},\n\t\t\t\"failed to export votes:\", err)\n\t}\n\tif cerr != nil {\n\t\treturn c.Error(exit.IOErr, CloseOutputError{Err: err,\n\t\t\tDescription: _VOTEEXP_CLOSE},\n\t\t\t\"failed to close output file:\", err)\n\t}\n\n\treturn\n}\n\n// export reads the votes and related metadata from the storage service and\n// puts them in a ZIP archive at fp.\nfunc export(ctx context.Context, s *storage.Client, qps []q11n.Protocol,\n\toptional []string, fp io.Writer) (err error) {\n\n\t// Check if the context is canceled before the expensive GetVotes\n\t// operation.\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ExportCanceled{Err: ctx.Err(),\n\t\t\tDescription: _VOTEEXP_EXP_CTX_CANCEL}\n\tdefault:\n\t}\n\n\t// Wrap the file writer into a zip archive writer.\n\tw := zip.NewWriter(fp)\n\tdefer func() {\n\t\tif cerr := w.Close(); cerr != nil && err == nil {\n\t\t\terr = CloseZIPArchiveError{Err: err, Description: _VOTEEXP_ZIP_CLOSE}\n\t\t}\n\t}()\n\n\t// Start exporting votes from the storage service.\n\tlog.Log(ctx, ExportingVotes{Description: _VOTEEXP_EXP_INFO})\n\tprogress.Static(\"Exporting votes:\")\n\taddprogress := progress.Count(0, true)\n\tprogress.Redraw()\n\tdefer progress.Keep()\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel() // Stop GetVotes if we terminate early.\n\tc, errc := s.GetVotes(ctx, qps, optional)\n\n\t// Start a goroutine which reads and logs errors parallel to this one.\n\t// It either sends nil on lerrc if everything went fine, NonFatalError,\n\t// if non-fatal errors occurred, or a fatal error from GetVotes.\n\tgerrc := make(chan error, 1)\n\tgo func() {\n\t\tvar gerr log.ErrorEntry\n\t\tfor err := range errc {\n\t\t\tgerr = GetVotesError{Err: err, Description: _VOTEEXP_EXP_ERR}\n\t\t\tif errors.CausedBy(err, new(storage.GetVotesFatalError)) != nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tgerr = NonFatalError{Err: gerr, Description: _VOTEEXP_EXP_NON_FATAL}\n\t\t\tlog.Error(ctx, gerr)\n\t\t\tprogress.Hide()\n\t\t\tfmt.Fprintln(os.Stderr, \"error: non-fatal error:\", gerr)\n\t\t\tprogress.Show()\n\t\t}\n\t\tgerrc <- gerr\n\t}()\n\tdefer func() {\n\t\tif gerr := <-gerrc; err == nil {\n\t\t\terr = gerr\n\t\t}\n\t}()\n\n\t// Add the entries to the ZIP archive in the following hierarchy:\n\t//\n\t//     votes/\n\t//     └ <voter id>/\n\t//       ├ <timestamp>.version\n\t//       ├ <timestamp>.<vote type>\n\t//       └ <timestamp>.<q11n protocol>*\n\t//\n\tvar count uint64\n\tcountlog := VoteExportProgress{Current: 0, Description: _VOTEEXP_EXP_PROGRESS}\n\tconst logstep = 10000 // Log progress after each logstep.\n\tfor vote := range c {\n\t\tprefix := fmt.Sprintf(\"votes/%s/%s.\", vote.Voter,\n\t\t\tstrings.ReplaceAll(vote.Time.Format(\"20060102150405.000-0700\"), \".\", \"\"))\n\n\t\tif err = addFile(w, vote.Time, prefix+\"version\", []byte(vote.Version)); err != nil {\n\t\t\treturn AddVersionError{VoteID: vote.VoteID, Prefix: prefix, Err: err,\n\t\t\t\tDescription: _VOTEEXP_ZIP_VERSION}\n\t\t}\n\t\tif err = addFile(w, vote.Time, prefix+string(vote.VoteType), vote.Vote); err != nil {\n\t\t\treturn AddVoteError{VoteID: vote.VoteID, Prefix: prefix, Err: err,\n\t\t\t\tDescription: _VOTEEXP_ZIP_VOTE}\n\t\t}\n\t\tfor p, b := range vote.Qualification {\n\t\t\tif err = addFile(w, vote.Time, prefix+string(p), b); err != nil {\n\t\t\t\treturn AddQualifyingPropertyError{\n\t\t\t\t\tVoteID:      vote.VoteID,\n\t\t\t\t\tPrefix:      prefix,\n\t\t\t\t\tProtocol:    p,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tDescription: _VOTEEXP_ZIP_Q,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif count = addprogress(1); count%logstep == 0 {\n\t\t\tcountlog.Current = count\n\t\t\tlog.Log(ctx, countlog)\n\t\t}\n\t}\n\tlog.Log(ctx, VoteCount{Count: count, Description: _VOTING_VOTE_COUNT})\n\treturn\n}\n\nfunc addFile(w *zip.Writer, mod time.Time, name string, value []byte) error {\n\tf, err := w.CreateHeader(&zip.FileHeader{\n\t\tName:     name,\n\t\tMethod:   zip.Deflate,\n\t\tModified: mod,\n\t})\n\tif err != nil {\n\t\treturn AddFileCreateError{Name: name, Err: err, Description: _VOTING_ZIP_HEADER}\n\t}\n\tif _, err := f.Write(value); err != nil {\n\t\treturn AddFileWriteError{Name: name, Err: err, Description: _VOTING_ZIP_FILE}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "voting/cmd/voterstats/log_desc.go",
    "content": "package main\n\nconst (\n\t_VOTERSTATS_START            = \"Failed to transform 'count votes from' time from election configuration file to RFC3339 format\"\n\t_VOTERSTATS_STOP             = \"Failed to transform 'count votes until' time from election configuration file to RFC3339 format\"\n\t_VOTERSTATS_TIME             = \"'count votes from' time > 'count votes until' time\"\n\t_VOTERSTATS_TZ               = \"Failed to load timezone\"\n\t_VOTERSTATS_COUNTIES         = \"Cannot fetch counties from a database\"\n\t_VOTERSTATS_COUNTIES_JSON    = \"Counties are invalid JSON format\"\n\t_VOTERSTATS_COUNTIES_FOREIGN = \"Counties contain 'FOREIGN' county, which is not allowed, since foreign county should have its own ID not a verbose name\"\n\t_VOTERSTATS_COUNTIES_TOTAL   = \"Counties contain 'TOTAL' county, which is unknown county\"\n\t_VOTERSTATS_FILE             = \"Failed to open a file\"\n\t_VOTERSTATS_FILE_CLOSE       = \"Failed to close a file\"\n\t_VOTERSTATS_EXP              = \"Failed to export voters statistics\"\n\t_VOTERSTATS_JSON             = \"Voters statistics are invalid JSON\"\n\t_VOTERSTATS_EXP_INFO         = \"Voters statistics export in progress\"\n\t_VOTERSTATS_EXP_FAIL         = \"Cannot fetch voters statistics from a database\"\n\t_VOTERSTATS_OUTSIDE_TIME     = \"Voter has voted after voters statistics export began\"\n\t_VOTERSTATS_COUNTY_ADMIN     = \"Admin code doesn't have corresponding county\"\n\t_VOTERSTATS_NON_FATAL        = \"Non-fatal error\"\n)\n"
  },
  {
    "path": "voting/cmd/voterstats/main.go",
    "content": "/*\nThe voterstats application is used for exporting voter statistics from the\nstorage service.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/storage\"\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/storage\n)\n\nconst usage = `voterstats exports voter statistics from the collector's storage service for\nuse by election management services.\n\nvoterstats can export two types of voter statistics: a simple running total of\nvoters (the default) and a detailed per county count of voters per day.\n\nThe statistics only contain voters that have cast a complete vote. This might\nbe less than the number of votes exported using voteexp, because the latter\nexports some partial votes needed by the processing application.`\n\nvar (\n\tdetailedp = flag.Bool(\"detailed\", false, \"export detailed statistics\")\n\n\ttimezonep = flag.String(\"timezone\", \"Local\",\n\t\t\"name of IANA Time Zone location to use for partitioning votes by day,\\n\"+\n\t\t\t\"e.g., Europe/Tallinn\")\n\n\tqp = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\t// Call voterstatsmain in a separate function so that it can set up\n\t// defers and have them trigger before returning with a non-zero exit\n\t// code.\n\tos.Exit(voterstatsmain())\n}\n\nconst (\n\tcountyForeign = \"FOREIGN\"\n\tcountyTotal   = \"TOTAL\"\n)\n\nfunc voterstatsmain() (code int) {\n\tc := command.New(\"ivxv-voterstats\", usage, \"output file\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\t// Parse start and stop time for statistics period calculation.\n\tvar start, stop time.Time\n\tvar err error\n\tif start, err = c.Conf.Election.ElectionStartTime(); err != nil {\n\t\treturn c.Error(exit.Config, ElectionStartTimeError{Err: err,\n\t\t\tDescription: _VOTERSTATS_START},\n\t\t\t\"failed to parse election start time:\", err)\n\t}\n\tif stop, err = c.Conf.Election.ServiceStopTime(); err != nil {\n\t\treturn c.Error(exit.Config, ServiceStopTimeError{Err: err,\n\t\t\tDescription: _VOTERSTATS_STOP},\n\t\t\t\"failed to parse service stop time:\", err)\n\t}\n\tif start.After(stop) {\n\t\treturn c.Error(exit.Config,\n\t\t\tElectionStartTimeAfterServiceStopError{Start: start, Stop: stop,\n\t\t\t\tDescription: _VOTERSTATS_TIME},\n\t\t\t\"election start time after service stop:\", start, \">\", stop)\n\t}\n\n\tif c.Until < command.CheckInput {\n\t\treturn exit.OK\n\t}\n\n\t// Load time zone data for the location.\n\tloc, err := time.LoadLocation(*timezonep)\n\tif err != nil {\n\t\treturn c.Error(exit.DataErr, LoadTimeZoneError{Err: err,\n\t\t\tDescription: _VOTERSTATS_TZ},\n\t\t\t\"failed to load time zone:\", err)\n\t}\n\n\t// Load counties list from storage and parse into dists.\n\tdists := new(districtlist)\n\tcounties, err := c.Storage.GetCounties(c.Ctx)\n\tif err != nil {\n\t\tcode = exit.Software\n\t\tif errors.CausedBy(err, new(storage.NotExistError)) != nil {\n\t\t\tcode = exit.NoInput\n\t\t}\n\t\treturn c.Error(code, GetCountiesError{Err: err, Description: _VOTERSTATS_COUNTIES},\n\t\t\t\"failed to get counties list from storage:\", err)\n\t}\n\tif err := json.Unmarshal(counties, &dists.Counties); err != nil {\n\t\treturn c.Error(exit.DataErr, ParseCountiesError{Err: err,\n\t\t\tDescription: _VOTERSTATS_COUNTIES_JSON},\n\t\t\t\"failed to parse counties list:\", err)\n\t}\n\tif _, ok := dists.Counties[countyForeign]; ok {\n\t\treturn c.Error(exit.DataErr, ForeignCountyError{Description: _VOTERSTATS_COUNTIES_FOREIGN},\n\t\t\t\"counties list contains county named\", countyForeign)\n\t}\n\tif _, ok := dists.Counties[countyTotal]; ok {\n\t\treturn c.Error(exit.DataErr, TotalCountyError{Description: _VOTERSTATS_COUNTIES_TOTAL},\n\t\t\t\"counties list contains county named\", countyTotal)\n\t}\n\n\tif c.Until < command.Execute {\n\t\treturn exit.OK\n\t}\n\n\t// Create the output file before exporting. Avoid defaulting to\n\t// standard output, because progress is printed there.\n\toutput := c.Args[0]\n\tfp, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn c.Error(exit.CantCreate, CreateOutputError{Output: output, Err: err,\n\t\t\tDescription: _VOTERSTATS_FILE},\n\t\t\t\"failed to create output file:\", err)\n\t}\n\tdefer func() {\n\t\tif err := fp.Close(); err != nil && code == exit.OK {\n\t\t\tcode = c.Error(exit.IOErr, CloseOutputError{Err: err, Description: _VOTERSTATS_FILE_CLOSE},\n\t\t\t\t\"failed to close output file:\", err)\n\t\t}\n\t}()\n\n\tvar stats interface{}\n\tif *detailedp {\n\t\tperiods := cumulativePeriodsNotAfter(start, time.Now(), stop, loc)\n\t\tstats, err = statsDetailed(c.Ctx, c.Conf.Election.Identifier,\n\t\t\tdists, periods, c.Storage, loc)\n\t} else {\n\t\tstats, err = statsTotal(c.Ctx, c.Conf.Election.Identifier, c.Storage)\n\t}\n\tif err != nil {\n\t\treturn c.Error(exit.Unavailable, ExportStatisticsError{Err: err,\n\t\t\tDescription: _VOTERSTATS_EXP},\n\t\t\t\"failed to export statistics:\", err)\n\t}\n\n\t// Write exported statistics as JSON to output.\n\tencoder := json.NewEncoder(fp)\n\tencoder.SetIndent(\"\", \"    \")\n\terr = encoder.Encode(stats)\n\tif err != nil {\n\t\treturn c.Error(exit.Unavailable, EncodeStatisticsError{Err: err,\n\t\t\tDescription: _VOTERSTATS_JSON},\n\t\t\t\"failed to encode voter statistics:\", err)\n\t}\n\n\treturn\n}\n\ntype votersTotal struct {\n\tTotal votersTotalInner `json:\"TOTAL\"`\n}\n\ntype votersTotalInner struct {\n\tTime     string `json:\"time\"`\n\tElection string `json:\"election\"`\n\tVoted    uint64 `json:\"online-voters\"`\n}\n\nfunc statsTotal(ctx context.Context, election string, s *storage.Client) (*votersTotal, error) {\n\tlog.Log(ctx, ExportingTotalStatistics{Description: _VOTERSTATS_EXP_INFO})\n\tprogress.Static(\"Exporting total statistics:\")\n\taddprogress := progress.Count(0, true)\n\tprogress.Redraw()\n\tdefer progress.Keep()\n\n\tvar voted uint64\n\tstatsc, errc := s.GetVotedStats(ctx)\n\tfor range statsc {\n\t\tvoted++\n\t\taddprogress(1)\n\t}\n\tif err := <-errc; err != nil {\n\t\treturn nil, GetTotalVotedStatsError{Err: err, Description: _VOTERSTATS_EXP_FAIL}\n\t}\n\treturn &votersTotal{\n\t\tTotal: votersTotalInner{\n\t\t\tTime:     time.Now().Format(time.RFC3339),\n\t\t\tElection: election,\n\t\t\tVoted:    voted,\n\t\t},\n\t}, nil\n}\n\ntype votersDetailed struct {\n\tElection string                 `json:\"election\"`\n\tTime     string                 `json:\"time\"`\n\tCounties []votersDetailedCounty `json:\"data\"`\n}\n\ntype votersDetailedCounty struct {\n\tCounty  string                 `json:\"county\"`\n\tPeriods []votersDetailedPeriod `json:\"online-voters\"`\n}\n\ntype votersDetailedPeriod struct {\n\tLabel string `json:\"day-order\"`\n\tVoted uint64 `json:\"voted-count\"`\n}\n\nfunc statsDetailed(ctx context.Context, election string,\n\tdists *districtlist, periods []period, s *storage.Client, loc *time.Location) (\n\t*votersDetailed, error) {\n\n\tlog.Log(ctx, ExportingDetailedStatistics{Description: _VOTERSTATS_EXP_INFO})\n\tprogress.Static(\"Exporting detailed statistics:\")\n\taddprogress := progress.Count(0, true)\n\tprogress.Redraw()\n\tdefer progress.Keep()\n\n\t// Map from county to period label to voter count for fast aggregation.\n\tcounts := make(map[string]map[string]uint64, len(dists.Counties)+2)\n\taddCount := func(county string, labels []string) {\n\t\tperiodMap, ok := counts[county]\n\t\tif !ok {\n\t\t\tperiodMap = make(map[string]uint64, len(periods))\n\t\t\tcounts[county] = periodMap\n\t\t}\n\t\tfor _, label := range labels {\n\t\t\tperiodMap[label]++\n\t\t}\n\t}\n\n\t// Get successful voter stats from storage and add to map.\n\tstatsc, errc := s.GetVotedStats(ctx)\n\tfor stats := range statsc {\n\t\tlabels := findPeriodLabels(periods, stats.Time)\n\t\tif len(labels) == 0 {\n\t\t\tnonfatal(ctx, TimeOutsideStatsPeriodError{\n\t\t\t\tVoter:       stats.Voter,\n\t\t\t\tTime:        stats.Time,\n\t\t\t\tDescription: _VOTERSTATS_OUTSIDE_TIME,\n\t\t\t}, \"submission time outside of statistics period:\", stats.Time)\n\t\t\tcontinue\n\t\t}\n\t\taddCount(countyTotal, labels)\n\t\taddprogress(1)\n\n\t\tif stats.AdminCode != \"\" {\n\t\t\tcounty := dists.findCounty(stats.AdminCode)\n\t\t\tif county == \"\" {\n\t\t\t\tnonfatal(ctx, AdminUnitWithoutCountyError{\n\t\t\t\t\tVoter:       stats.Voter,\n\t\t\t\t\tAdminCode:   stats.AdminCode,\n\t\t\t\t\tDescription: _VOTERSTATS_COUNTY_ADMIN,\n\t\t\t\t}, \"administrative unit without county:\", stats.AdminCode)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\taddCount(county, labels)\n\t\t}\n\t}\n\tif err := <-errc; err != nil {\n\t\treturn nil, GetDetailedVotedStatsError{Err: err, Description: _VOTERSTATS_EXP_FAIL}\n\t}\n\n\t// Convert from map to desired data structure.\n\tconvertCount := func(county string) votersDetailedCounty {\n\t\tperiodMap, ok := counts[county]\n\n\t\t// Range over periods slice instead of map to preserve ordering.\n\t\tconverted := make([]votersDetailedPeriod, len(periods))\n\t\tfor i, period := range periods {\n\t\t\tcount := votersDetailedPeriod{Label: period.label}\n\t\t\tif ok {\n\t\t\t\t// future days voted should be 0\n\t\t\t\tif period.contains(previousMidnight(time.Now(), loc).Add(24 * time.Hour)) {\n\t\t\t\t\tcount.Voted = 0\n\t\t\t\t} else {\n\t\t\t\t\tcount.Voted = periodMap[period.label]\n\t\t\t\t}\n\t\t\t}\n\t\t\tconverted[i] = count\n\t\t}\n\t\treturn votersDetailedCounty{\n\t\t\tCounty:  county,\n\t\t\tPeriods: converted,\n\t\t}\n\t}\n\n\t// The order of counties is lost since they were unmarshaled into a\n\t// map, but at least sort them to ensure consistent output.\n\tcounties := make([]votersDetailedCounty, 0, len(dists.Counties)+2)\n\tfor _, county := range dists.sortedCounties() {\n\t\tcounties = append(counties, convertCount(county))\n\t}\n\tcounties = append(counties, convertCount(countyForeign))\n\tcounties = append(counties, convertCount(countyTotal))\n\n\treturn &votersDetailed{\n\t\tElection: election,\n\t\tTime:     time.Now().Format(time.RFC3339),\n\t\tCounties: counties,\n\t}, nil\n}\n\n// districtlist is a subset of the election districtlist list. It only contains\n// the counties subsection necessary for separating votes by county.\ntype districtlist struct {\n\t// Counties maps from county code to voter administrative unit codes\n\t// that belong to that county.\n\tCounties map[string][]string\n\n\t// countyMap maps from voter administrative unit codes to the county\n\t// they belong to. Used to speed up findCounty.\n\tcountyMap map[string]string\n}\n\nfunc (d *districtlist) findCounty(unitCode string) string {\n\tif unitCode == countyForeign {\n\t\treturn unitCode\n\t}\n\tif d.countyMap == nil {\n\t\td.countyMap = make(map[string]string)\n\t\tfor county, codes := range d.Counties {\n\t\t\tfor _, code := range codes {\n\t\t\t\td.countyMap[code] = county\n\t\t\t}\n\t\t}\n\t}\n\treturn d.countyMap[unitCode]\n}\n\nfunc (d *districtlist) sortedCounties() []string {\n\tcounties := make([]string, 0, len(d.Counties))\n\tfor county := range d.Counties {\n\t\tcounties = append(counties, county)\n\t}\n\tsort.Strings(counties)\n\treturn counties\n}\n\nfunc nonfatal(ctx context.Context, err log.ErrorEntry, a ...interface{}) {\n\tlog.Error(ctx, NonFatalError{Err: err, Description: _VOTERSTATS_NON_FATAL})\n\tprogress.Hide()\n\tfmt.Fprintln(os.Stderr, append([]interface{}{\"error: non-fatal error:\"}, a...)...)\n\tprogress.Show()\n}\n"
  },
  {
    "path": "voting/cmd/voterstats/period.go",
    "content": "package main\n\nimport (\n\t\"strconv\"\n\t\"time\"\n)\n\n// period is a labeled time period [from, to) in which to report statistics.\n// Either time can also be the zero value in which case that end is unbound.\ntype period struct {\n\tlabel string\n\tfrom  time.Time\n\tto    time.Time\n}\n\nfunc (p period) contains(point time.Time) bool {\n\treturn (p.from.IsZero() || !point.Before(p.from)) &&\n\t\t(p.to.IsZero() || point.Before(p.to))\n}\n\n// findPeriodLabels returns the labels of periods which contain point.\nfunc findPeriodLabels(periods []period, point time.Time) []string {\n\tvar labels []string\n\tfor _, period := range periods {\n\t\tif period.contains(point) {\n\t\t\tlabels = append(labels, period.label)\n\t\t}\n\t}\n\treturn labels\n}\n\n// cumulativePeriods returns cumulative periods for days from start to stop.\n//\n// Each day represents the period from start up to the end of that day in\n// location loc. So each period contains the cumulative statistics up to and\n// including that day. The last period ends at stop instead.\n//\n// The days are labeled in reverse order: the last day (stop) gets the label\n// \"1\", the second to last gets \"2\", etc.\nfunc cumulativePeriods(start, stop time.Time, loc *time.Location) []period {\n\t// Always at least one period from start to stop.\n\tlabel := 1\n\tperiods := []period{\n\t\t{label: strconv.Itoa(label), from: start, to: stop},\n\t}\n\n\t// Prepend periods from start to midnight for every day in between.\n\tmidnight := previousMidnight(stop, loc)\n\tfor midnight.After(start) {\n\t\tlabel++\n\t\tperiods = append([]period{\n\t\t\t{label: strconv.Itoa(label), from: start, to: midnight},\n\t\t}, periods...)\n\t\tmidnight = previousMidnight(midnight, loc)\n\t}\n\n\treturn periods\n}\n\n// cumulativePeriodsNotAfter returns periods which start\n// from start and are up until stop, but not after notAfter\nfunc cumulativePeriodsNotAfter(start time.Time, stop time.Time, notAfter time.Time, loc *time.Location) []period {\n\t// label = 1 is the very last day of e-voting, which should include all votes\n\t// from start to notAfter\n\tlabel := 1\n\tperiods := []period{\n\t\t{label: strconv.Itoa(label), from: start, to: notAfter},\n\t}\n\n\t// Prepend periods from start to midnight for every day in between.\n\tmidnight := previousMidnight(notAfter, loc)\n\tfor midnight.After(start) {\n\t\tlabel++\n\n\t\t// If midnight >= next midnight of stop day, i.e\n\t\t// 11.03.2024 00:00:00 >= (09.03.2024 09:47 + 1 day at 00:00:00 is 10.03.2024 00:00:00)\n\t\tupUntilMidnight := nextMidnight(stop, loc)\n\n\t\tif midnight.After(upUntilMidnight) || midnight.Equal(upUntilMidnight) {\n\t\t\t// Remove all periods that has been added so far, it is important to remove\n\t\t\t// periods here in order to get rid of, if needed, very first period that\n\t\t\t// has been added outside the loop\n\t\t\tperiods = periods[len(periods):]\n\t\t}\n\t\tperiods = append([]period{\n\t\t\t{label: strconv.Itoa(label), from: start, to: midnight},\n\t\t}, periods...)\n\n\t\tmidnight = previousMidnight(midnight, loc)\n\t}\n\n\treturn periods\n}\n\nfunc previousMidnight(t time.Time, loc *time.Location) time.Time {\n\tyear, month, day := t.Add(-1).In(loc).Date()\n\treturn time.Date(year, month, day, 0, 0, 0, 0, loc)\n}\n\n// nextMidnight returns t.Day + 1 at 00:00:00\nfunc nextMidnight(t time.Time, loc *time.Location) time.Time {\n\tyear, month, day := t.In(loc).Date()\n\treturn time.Date(year, month, day+1, 0, 0, 0, 0, loc)\n}\n"
  },
  {
    "path": "voting/cmd/voterstats/period_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc mustRFC3339(t *testing.T, value string) time.Time {\n\ttm, err := time.Parse(time.RFC3339, value)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn tm\n}\n\nfunc assertPeriods(t *testing.T, periods []period, expected string) {\n\tvar buf bytes.Buffer\n\tfor _, period := range periods {\n\t\tfmt.Fprintf(&buf, \"%s [%s, %s)\\n\",\n\t\t\tperiod.label,\n\t\t\tperiod.from.Format(time.RFC3339),\n\t\t\tperiod.to.Format(time.RFC3339))\n\t}\n\n\tif got := buf.String(); got != expected {\n\t\tt.Errorf(\"unexpected periods:\\ngot:\\n%swant:\\n%s\", got, expected)\n\t}\n}\n\nfunc TestCumulativePeriods(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2021-03-27T09:00:00+02:00\")\n\tstop := mustRFC3339(t, \"2021-03-29T20:15:00+03:00\")\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriods(start, stop, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"3 [2021-03-27T09:00:00+02:00, 2021-03-28T00:00:00+02:00)\\n\"+\n\t\t\"2 [2021-03-27T09:00:00+02:00, 2021-03-29T00:00:00+03:00)\\n\"+\n\t\t\"1 [2021-03-27T09:00:00+02:00, 2021-03-29T20:15:00+03:00)\\n\")\n}\n\nfunc TestDayBeforeInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-02T13:00:00+03:00\")\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\")\n}\n\nfunc TestFirstDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-03T23:59:59+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\")\n}\n\nfunc TestSecondDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-04T23:59:59+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\")\n}\n\nfunc TestThirdDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-05T23:59:59+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\")\n}\n\nfunc TestFourthDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-06T00:00:00+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\"+\n\t\t\"3 [2024-06-03T09:00:00+02:00, 2024-06-07T00:00:00+03:00)\\n\")\n}\n\nfunc TestFifthDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-07T00:00:00+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\"+\n\t\t\"3 [2024-06-03T09:00:00+02:00, 2024-06-07T00:00:00+03:00)\\n\"+\n\t\t\"2 [2024-06-03T09:00:00+02:00, 2024-06-08T00:00:00+03:00)\\n\")\n}\n\nfunc TestSixthAkaLastDayOfInternetVotingCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-08T00:00:00+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\"+\n\t\t\"3 [2024-06-03T09:00:00+02:00, 2024-06-07T00:00:00+03:00)\\n\"+\n\t\t\"2 [2024-06-03T09:00:00+02:00, 2024-06-08T00:00:00+03:00)\\n\"+\n\t\t\"1 [2024-06-03T09:00:00+02:00, 2024-06-08T20:15:00+02:00)\\n\")\n}\n\nfunc TestSixthAkaLastDayOfInternetVotingLastSecondCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-08T20:14:59+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\"+\n\t\t\"3 [2024-06-03T09:00:00+02:00, 2024-06-07T00:00:00+03:00)\\n\"+\n\t\t\"2 [2024-06-03T09:00:00+02:00, 2024-06-08T00:00:00+03:00)\\n\"+\n\t\t\"1 [2024-06-03T09:00:00+02:00, 2024-06-08T20:15:00+02:00)\\n\")\n}\n\nfunc TestSixthAkaLastDayOfInternetVotingSecondAfterEndCumulativePeriodsNotAfter(t *testing.T) {\n\t// Include DST transition in the period.\n\tstart := mustRFC3339(t, \"2024-06-03T09:00:00+02:00\")\n\tnotAfter := mustRFC3339(t, \"2024-06-08T20:15:00+02:00\")\n\tstop := mustRFC3339(t, \"2024-06-08T20:15:01+03:00\")\n\n\tloc, err := time.LoadLocation(\"Europe/Tallinn\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tperiods := cumulativePeriodsNotAfter(start, stop, notAfter, loc)\n\n\tassertPeriods(t, periods, \"\"+\n\t\t\"6 [2024-06-03T09:00:00+02:00, 2024-06-04T00:00:00+03:00)\\n\"+\n\t\t\"5 [2024-06-03T09:00:00+02:00, 2024-06-05T00:00:00+03:00)\\n\"+\n\t\t\"4 [2024-06-03T09:00:00+02:00, 2024-06-06T00:00:00+03:00)\\n\"+\n\t\t\"3 [2024-06-03T09:00:00+02:00, 2024-06-07T00:00:00+03:00)\\n\"+\n\t\t\"2 [2024-06-03T09:00:00+02:00, 2024-06-08T00:00:00+03:00)\\n\"+\n\t\t\"1 [2024-06-03T09:00:00+02:00, 2024-06-08T20:15:00+02:00)\\n\")\n}\n"
  },
  {
    "path": "voting/cmd/voteunion/main.go",
    "content": "/*\nThe voteunion application is used for finding the union of a set of exported\nballot boxes.\n*/\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"flag\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"os\"\n\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/command/status\"\n)\n\nfunc usage() {\n\tfmt.Fprintln(os.Stderr, \"usage:\", os.Args[0], `[options] <output> <input>...\n\nvoteunion takes a set of ballot boxes exported using voteexp and outputs a\nballot box which contains the union of votes in these inputs.\n\noptions:`)\n\tflag.PrintDefaults()\n}\n\nvar (\n\tqp     = flag.Bool(\"q\", false, \"quiet, do not show progress\")\n\taddedp = flag.Bool(\"added\", true, \"print the origin and name of \"+\n\t\t\"files added to output\")\n\n\tprogress *status.Line\n)\n\nfunc main() {\n\tflag.Usage = usage\n\tflag.Parse()\n\targs := flag.Args()\n\tif len(args) < 1 {\n\t\tusage()\n\t\tos.Exit(exit.Usage)\n\t}\n\n\tif !*qp {\n\t\tprogress = status.New()\n\t}\n\n\tcode, err := zipunion(args[0], args[1:])\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"error:\", err)\n\t}\n\tos.Exit(code)\n}\n\nfunc zipunion(output string, input []string) (code int, err error) {\n\t// Create output archive and setup defers to finalize the ZIP and close\n\t// the file pointer.\n\tfp, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\tif err != nil {\n\t\treturn exit.CantCreate, fmt.Errorf(\"failed to create output: %v\", err)\n\t}\n\tdefer func() {\n\t\tif cerr := fp.Close(); err == nil && cerr != nil {\n\t\t\tcode = exit.IOErr\n\t\t\terr = fmt.Errorf(\"failed to close output: %v\", err)\n\t\t}\n\t}()\n\n\tu := union{\n\t\tw:        zip.NewWriter(fp),\n\t\tcontents: make(map[string][]byte),\n\t\thash:     sha256.New(),\n\t}\n\tu.sum = make([]byte, u.hash.Size())\n\tu.block = make([]byte, 256*u.hash.BlockSize())\n\tdefer func() {\n\t\tif cerr := u.w.Close(); err == nil && cerr != nil {\n\t\t\tcode = exit.Unavailable\n\t\t\terr = fmt.Errorf(\"failed to write central directory: %v\", err)\n\t\t}\n\t}()\n\n\t// Open input archives and add their contents to output if not already\n\t// there.\n\tshowname := progress.Updatable(\"\", false)\n\tdefer progress.Hide()\n\tfor _, in := range input {\n\t\tshowname(in + \":\")\n\t\tcode, err = u.zipadd(in)\n\t\tif err != nil {\n\t\t\treturn code, fmt.Errorf(\"failed to add %q: %v\", in, err)\n\t\t}\n\t}\n\treturn\n}\n\ntype union struct {\n\tw        *zip.Writer\n\tcontents map[string][]byte // file name to SHA-256 hash of the contents.\n\n\t// Reusable scratch variables.\n\thash  hash.Hash\n\tsum   []byte\n\tblock []byte\n}\n\nfunc (u *union) zipadd(path string) (code int, err error) {\n\tr, err := zip.OpenReader(path)\n\tif err != nil {\n\t\treturn exit.NoInput, fmt.Errorf(\"failed to open input: %v\", err)\n\t}\n\tdefer r.Close()\n\n\t// Status line: 100% 100000/100000 filename\n\taddpercent := progress.Percent(uint64(len(r.File)), false)\n\tdefer progress.Pop()\n\taddcount := progress.Count(uint64(len(r.File)), false)\n\tdefer progress.Pop()\n\tshowname := progress.Updatable(\"\", true) // showname triggers a redraw.\n\tdefer progress.Pop()\n\n\tfor _, f := range r.File {\n\t\taddpercent(1)\n\t\taddcount(1)\n\t\tshowname(f.FileHeader.Name)\n\n\t\tcode, err = u.fileadd(f, path)\n\t\tif err != nil {\n\t\t\treturn code, fmt.Errorf(\"failed to copy file %q: %v\",\n\t\t\t\tf.FileHeader.Name, err)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (u *union) fileadd(f *zip.File, path string) (code int, err error) {\n\t// Open the archive file for reading.\n\tsrc, err := f.Open()\n\tif err != nil {\n\t\treturn exit.DataErr, fmt.Errorf(\"failed to open file: %v\", err)\n\t}\n\tdefer src.Close()\n\n\t// Copy the contents of the file to hash.\n\tu.hash.Reset()\n\tdst := u.hash.(io.Writer)\n\n\tname := f.FileHeader.Name\n\texisting, ok := u.contents[name]\n\tif !ok {\n\t\t// This file name is not in the output yet, so also copy the\n\t\t// contents to a new entry there.\n\t\tvar o io.Writer\n\t\to, err = u.w.CreateHeader(&f.FileHeader)\n\t\tif err != nil {\n\t\t\treturn exit.Unavailable, fmt.Errorf(\"failed to create file: %v\", err)\n\t\t}\n\t\tdst = io.MultiWriter(dst, o)\n\t}\n\n\t//nolint:gosec // Only used on trusted Zip archives.\n\tif _, err = io.CopyBuffer(dst, src, u.block); err != nil {\n\t\treturn exit.Unavailable, fmt.Errorf(\"failed to copy contents of file: %v\", err)\n\t}\n\n\t// Check or store the hash of the file contents.\n\tu.sum = u.hash.Sum(u.sum[:0])\n\tif ok {\n\t\t// Compare hash to previous value.\n\t\tif !bytes.Equal(u.sum, existing) {\n\t\t\treturn exit.DataErr, fmt.Errorf(\"file content mismatch: \"+\n\t\t\t\t\"hash is %x, previous was %x\", u.sum, existing)\n\t\t}\n\t} else {\n\t\t// Store the new hash value.\n\t\tstored := make([]byte, len(u.sum))\n\t\tcopy(stored, u.sum)\n\t\tu.contents[name] = stored\n\n\t\tif *addedp {\n\t\t\tprogress.Hide()\n\t\t\tfmt.Printf(\"added %q file %q\\n\", path, name)\n\t\t\tprogress.Show()\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "voting/cmd/voteunion/union_test.go",
    "content": "package main\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/command/exit\"\n)\n\nfunc TestUnion(t *testing.T) {\n\ttests := []struct {\n\t\tinput    []string // paths to input archives\n\t\texpected string   // path to archive containing expected output\n\t\terr      string   // expected error\n\t}{\n\t\t{[]string{\"foo.zip\"}, \"foo.zip\", \"\"},\n\t\t{[]string{\"foo.zip\", \"foo.zip\"}, \"foo.zip\", \"\"},\n\t\t{[]string{\"foo.zip\", \"bar.zip\"}, \"foobar.zip\", \"\"},\n\t\t{[]string{\"foo.zip\", \"foobar.zip\"}, \"foobar.zip\", \"\"},\n\t\t{[]string{\"foobar.zip\", \"bar.zip\"}, \"foobar.zip\", \"\"},\n\n\t\t{[]string{\"dir.zip\"}, \"dir.zip\", \"\"},\n\t\t{[]string{\"dirfoo.zip\", \"bar.zip\"}, \"dirfoobar.zip\", \"\"},\n\n\t\t{[]string{\"foo.zip\", \"foo2.zip\"}, \"mismatch\",\n\t\t\t`failed to add \"testdata/foo2.zip\": failed to copy file \"foo\": ` +\n\t\t\t\t\"file content mismatch: hash is \" +\n\t\t\t\t\"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730, \" +\n\t\t\t\t\"previous was \" +\n\t\t\t\t\"b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c\"},\n\t}\n\n\t*qp = true // Run tests in quiet mode.\n\tfor _, test := range tests {\n\t\tt.Run(strings.Join(test.input, \" \"), func(t *testing.T) {\n\t\t\toutput := filepath.Join(\"testdata\", test.expected+\".tmp\")\n\t\t\tdefer func() {\n\t\t\t\tif err := os.Remove(output); err != nil {\n\t\t\t\t\tt.Error(\"failed to remove output:\", err)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tvar input []string\n\t\t\tfor _, in := range test.input {\n\t\t\t\tinput = append(input, filepath.Join(\"testdata\", in))\n\t\t\t}\n\n\t\t\tcode, err := zipunion(output, input)\n\t\t\tif err != nil {\n\t\t\t\tif code == exit.OK {\n\t\t\t\t\tt.Error(\"exit code OK with non-nil error\")\n\t\t\t\t}\n\t\t\t\tif len(test.err) == 0 {\n\t\t\t\t\tt.Fatal(\"union error:\", err)\n\t\t\t\t}\n\t\t\t\tif err.Error() != test.err {\n\t\t\t\t\tt.Fatalf(\"unexpected error: got %q, want %q\",\n\t\t\t\t\t\terr, test.err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif code != exit.OK {\n\t\t\t\tt.Error(\"exit code\", code, \"with nil error\")\n\t\t\t}\n\t\t\tif len(test.err) > 0 {\n\t\t\t\tt.Fatalf(\"expected error %q, got nil\", test.err)\n\t\t\t}\n\n\t\t\tzipequal(t, filepath.Join(\"testdata\", test.expected), output)\n\t\t})\n\t}\n}\n\nfunc zipequal(t *testing.T, expected, result string) {\n\te, err := zip.OpenReader(expected)\n\tif err != nil {\n\t\tt.Fatal(\"open expected error:\", err)\n\t}\n\tdefer e.Close()\n\n\tr, err := zip.OpenReader(result)\n\tif err != nil {\n\t\tt.Fatal(\"open result error:\", err)\n\t}\n\tdefer r.Close()\n\n\tif len(e.File) != len(r.File) {\n\t\tfor _, f := range r.File {\n\t\t\tt.Log(f.FileHeader.Name)\n\t\t}\n\t\tt.Fatal(\"expected\", len(e.File), \"file(s), got\", len(r.File))\n\t}\n\tfor i, ef := range e.File {\n\t\trf := r.File[i]\n\t\tif ef.FileHeader.Name != rf.FileHeader.Name {\n\t\t\tt.Fatalf(\"expected %q, got %q\",\n\t\t\t\tef.FileHeader.Name, rf.FileHeader.Name)\n\t\t}\n\n\t\tep, err := ef.Open()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"open expected file error:\", err)\n\t\t}\n\t\tdefer ep.Close()\n\t\tec, err := io.ReadAll(ep)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"read expected file error:\", err)\n\t\t}\n\n\t\trp, err := rf.Open()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"open result file error:\", err)\n\t\t}\n\t\tdefer rp.Close()\n\t\trc, err := io.ReadAll(rp)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"read result file error:\", err)\n\t\t}\n\n\t\tif !bytes.Equal(ec, rc) {\n\t\t\tt.Fatalf(\"%q content mismatch: expected %q, got %q\",\n\t\t\t\tef.FileHeader.Name, ec, rc)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "voting/go.mod",
    "content": "module ivxv.ee/voting\n\ngo 1.23\n\nrequire (\n\tgithub.com/gtank/ristretto255 v0.1.2 // indirect\n\tgolang.org/x/crypto v0.27.0 // indirect\n\tivxv.ee/common/collector v1.9.11\n\tivxv.ee/sessionstatus/api v1.9.11\n)\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n\ttivi.io/core v0.2.2-0.20241127235149-149435f9e4f4\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "voting/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=\ngithub.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=\ngolang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ntivi.io/core v0.2.2-0.20241127235149-149435f9e4f4 h1:NgIyqKugpgiIia6wiRM6I9E1XStcitd1x8wIs0z75f8=\ntivi.io/core v0.2.2-0.20241127235149-149435f9e4f4/go.mod h1:iVb6IvZMOrl1JPCwSkeVv7sV7Cd9cDjmyCg4xOvRL+w=\n"
  },
  {
    "path": "voting/internal/ciphertext.go",
    "content": "package internal\n\nimport (\n\t\"ivxv.ee/common/collector/crypto/elgamal\"\n\n\t\"tivi.io/core/math/group\"\n)\n\ntype ASN1CiphertextVerifier interface {\n\t// ASN1CiphertextVerify verifies ASN.1 ciphertext correctness, without\n\t// decrypting it.\n\t// We wish to secure offline applications from malformed ciphertexts.\n\tASN1CiphertextVerify(ciphertext []byte) error\n}\n\ntype ElGamalASN1CiphertextVerifier struct {\n\tg group.Group\n}\n\nfunc NewElGamalASN1CiphertextVerifier(g group.Group) *ElGamalASN1CiphertextVerifier {\n\treturn &ElGamalASN1CiphertextVerifier{g: g}\n}\n\nfunc (e *ElGamalASN1CiphertextVerifier) ASN1CiphertextVerify(data []byte) error {\n\t_, err := elgamal.UnmarshalCiphertext(e.g, data)\n\treturn err\n}\n"
  },
  {
    "path": "voting/internal/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value when calling RPC.Vote\n\t// for ID card/Web eID\n\tVoterChoices = \"RPC.VoterChoices\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Vote\n\t// for Mobile-ID/Smart-ID\n\tSignStatus = \"RPC.SignStatus\"\n\n\t// This will prevent reusing SessionID until it is deleted from a database,\n\t// or expired\n\tVote = \"RPC.Vote\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tverifyTTL int64\n\tclient    client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != 0 {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:    tlsDialer,\n\t\tverifyTTL: c.Conf.Technical.Status.Session.VerifyTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err, Description: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\t// Send request to session status server and verify response\n\tok, err := r.verifyAndDeleteSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndDeleteSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndDeleteSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will delete h.Header.SessionID record from the underlying storage.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndDeleteSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\t// NB! Most important part, that prevents any attack on SessionID\n\tvar ok bool\n\tvar ttl string\n\tok, err = verifyStatusReadResp(&respRead, voteHandler)\n\tif !ok || err != nil {\n\t\treturn ok, VerifyStatusReadRespError{Err: err, Description: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\tttl = strconv.FormatInt(r.verifyTTL, 10)\n\trespRead.Lease = \"\"\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp,\n\th func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// voteHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Vote request.\nfunc voteHandler(r *api.StatusReadResp) (bool, error) {\n\t// When authenticating with ID card, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.VoterChoices method\n\tidCardAuth := r.Caller == VoterChoices && r.Auth == client.IDcardAuth\n\n\t// When authenticating with Mobile-ID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.SignStatus method\n\tmidAuth := r.Caller == SignStatus && r.Auth == client.MobileIDAuth\n\n\t// When authenticating with Smart-ID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.RPCSignStatus method\n\tsidAuth := r.Caller == SignStatus && r.Auth == client.SmartIDAuth\n\n\t// When authenticating with Web eID, then IVXV requires that client\n\t// has previously interacted with IVXV using RPC.RPCVoterChoices method\n\twidAuth := r.Caller == VoterChoices && r.Auth == client.WebeIDAuth\n\n\t// All conditions must satisfy simultaneously!\n\tif !(idCardAuth) && !(midAuth) && !(sidAuth) && !(widAuth) {\n\t\treturn false, VoteInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Vote,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID,\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "voting/internal/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ          = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT  = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_TLS_DIAL                       = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                    = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_UPDATE_FAIL                    = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID           = \"SessionID has been attempted to tamper\"\n)\n"
  },
  {
    "path": "voting/service/voting/log_desc.go",
    "content": "package main\n\nconst (\n\t_VOTING_VOTEREQ            = \"RPC.VoteReq\"\n\t_VOTING_VOTER_NO_AUTH      = \"Voter has not been authenticated\"\n\t_VOTING_SESSION_ID         = \"Malformed SessionID\"\n\t_VOTING_SESSION_ID_EXPIRED = \"SessionID has been expired\"\n\t_VOTING_ID_MISMATCH        = \"Voter that has authenticated is not the same voter that voted\"\n\t_VOTING_VOTEID             = \"Failed to generate random vote ID\"\n\t_VOTING_VOTEID_OK          = \"Vote ID has been successfully generated\"\n\t_VOTING_STORE              = \"Failed to store a vote in a database\"\n\t_VOTING_TXN_INIT           = \"Failed to initialize a database transaction\"\n\t_VOTING_TXN_START          = \"Failed to start a database transaction\"\n\t_VOTING_TXN_STARTED        = \"Database transaction has been successfully started\"\n\t_VOTING_TXN_SET_AUTOCOMMIT = \"Database transaction is set to AUTOCOMMIT mode\"\n\t_VOTING_Q_PROP             = \"Requesting qualification property for a vote from providers\"\n\t_VOTING_Q_PROP_BAD_CERT    = \"Provider has qualified a vote as BAD CERTIFICATE\"\n\t_VOTING_Q_PROP_FAIL        = \"Provider responded with error code\"\n\t_VOTING_Q_PROP_OK          = \"Provider has successfully qualified a vote\"\n\t_VOTING_Q_PROP_STORE       = \"Failed to store qualification property in a database transaction\"\n\t_VOTING_Q_PROP_STORE_INFO  = \"Qualification property has been successfully stored in a database transaction\"\n\t_VOTING_Q_PROP_TIMESTAMP   = \"Unable to parse qualification timestamp from qualification property\"\n\t_VOTING_TIMESTAMP          = \"Set vote timestamp to the timestamp when voter has reached RPC.VoteReq\"\n\t_VOTING_TEST_VOTE          = \"Test vote\"\n\t_VOTING_NO_TIMESTAMP_Q     = \"Election configuration file doesn't have any timestamp qualifiers\"\n\t_VOTING_INVALID_TIMESTAMP  = \"TSA timestamp > OCSP 'producedAt'\"\n\t_VOTING_TXN_END            = \"Database transaction is finished (either committed or rolled back)\"\n\t_VOTING_TXN_ROLLBACK       = \"Database transaction is rolled back\"\n\t_VOTING_TXN_COMMITTED      = \"Database transaction has been successfully committed, however after that there were errors with database availability\"\n\t_VOTING_VOTERESP           = \"RPC.VoteResp\"\n\t_VOTING_RATE               = \"Cannot fetch voting attempts for given voter from a database\"\n\t_VOTING_LIMIT_TIME         = \"Voter has reached its limit for votes per timeframe\"\n\t_VOTING_RACE_VOTE          = \"Voter has many active sessions and all of them are trying to store votes\"\n\t_VOTING_RATE_UPDATE        = \"Failed to update voter's voting attemtps in a database\"\n\t_VOTING_ASICE              = \"Only .bdoc vote format is supported\"\n\t_VOTING_BDOC_OPEN          = \"Failed to open vote .bdoc container\"\n\t_VOTING_BDOC_SIG           = \"Vote .bdoc signature info\"\n\t_VOTING_BDOC_MANY_SIG      = \"Vote .bdoc has more than 1 signature\"\n\t_VOTING_ID_FROM_BDOC       = \"Cannot parse voter ID from voter certificate 'Subject'\"\n\t_VOTING_IDENTITY           = \"Voter identity info\"\n\t_VOTING_NO_IN_VOTERSLIST   = \"Voter is not in a voters list\"\n\t_VOTING_VOTERSLIST         = \"Cannot fetch choices list for a voter from a database\"\n\t_VOTING_DISTRICTS_MISMATCH = \"Voter has provided in RPC.VoteReq districts list from where it has chosen a candidate, but it turns out that latest voters list assigned different district for this voter\"\n\t_VOTING_ELIGIBLE           = \"Voter is eligible for voting\"\n\t_VOTING_BDOC_EXTRA         = \"Unexpected files found in .bdoc vote\"\n\t_VOTING_NO_BALLOT          = \".bdoc vote doesn't have ballot file\"\n\t_VOTING_BALLOT_OK          = \".bdoc vote is correct\"\n\t_VOTING_START              = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_VOTING_START_ACCEPT       = \"Failed to transform election start time from election configuration file to RFC3339 format\"\n\t_VOTING_STOP               = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_VOTING_RATE_LIMITS        = \"Invalid rate limits configuration (rate count > 0, but rate limit timeout is 0)\"\n\t_VOTING_AUTH               = \"Failed to parse authentication configuration for voting service\"\n\t_VOTING_BDOC_VERIFIER      = \"Failed to configure .bdoc container verifier and parser\"\n\t_VOTING_QUALIFIER          = \"Failed to configure vote qualification properties qualifier\"\n\t_VOTING_SERVER             = \"Failed to parse server configuration for voting service\"\n\t_VOTING_SERVER_SERVE       = \"Failed to serve voting service to clients\"\n\t_VOTING_CIP                = \"Invalid ciphertext\"\n\t_VOTING_ENC_PUB            = \"Failed to x509 unmarshal vote encryption public key\"\n\t_VOTING_ENC_G              = \"Failed to fetch a group for ciphertext verifier\"\n\t_VOTING_NO_CIP_PARAMS      = \"No ciphertext verifier parameters set\"\n)\n"
  },
  {
    "path": "voting/service/voting/main.go",
    "content": "/*\nThe voting service verifies votes, checks voter eligibility, and requests\nqualifying properties for the signatures which are all stored in the storage\nservice.\n*/\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/crypto/elgamal\"\n\t\"ivxv.ee/common/collector/errors\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/q11n\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/voting/internal\"\n\tsessstatus \"ivxv.ee/voting/internal/sessionstatus/rpc\"\n\n\t\"tivi.io/core/crypto\"\n\tx_509 \"tivi.io/core/crypto/x509\"\n\t\"tivi.io/core/math/group\"\n\t_ \"tivi.io/core/math/group/all\"\n\t//ivxv:modules common/collector/auth\n\t//ivxv:modules common/collector/container\n\t//ivxv:modules common/collector/q11n\n\t//ivxv:modules common/collector/storage\n)\n\n// identLen is the number of random bytes generated for a unique vote\n// identifier.\nconst identLen = 16\n\nfunc genIdent() (ident []byte, err error) {\n\tident = make([]byte, identLen)\n\t_, err = rand.Read(ident)\n\treturn\n}\n\n// RPC is the handler for voting service calls.\ntype RPC struct {\n\tstatus             client.Verifier\n\telection           *conf.Election\n\tcontainer          container.Opener\n\tidentify           identity.Identifier\n\tq11n               q11n.Qualifiers\n\tstorage            *storage.Client\n\tciphertextVerifier internal.ASN1CiphertextVerifier\n\n\t// Election start time: all votes registered before this are test\n\t// votes and are not counted.\n\tstart time.Time\n\n\tskipEligible bool   // Should we skip checking voter eligibility?\n\tforeignCode  string // Administrative unit code for foreign voters.\n}\n\n// Args are the arguments provided to a call of RPC.Vote.\ntype Args struct {\n\tserver.Header\n\tChoices string         `size:\"10\"` // Identifier of the choice list used.\n\tType    container.Type `size:\"10\"` // The type of container that the ballot is encapsulated in.\n\tVote    []byte         // The signed container of the ballot. Size is limited by codec filter.\n}\n\n// Response is the response returned by RPC.Vote.\ntype Response struct {\n\tserver.Header\n\tVoteID        []byte          // Generated vote identifier.\n\tQualification q11n.Properties // Qualifying properties for the vote.\n\tTestVote      bool            `json:\",omitempty\"` // Is this a test vote?\n}\n\n// Vote is the remote procedure call performed by clients to submit votes to\n// the collector.\nfunc (r *RPC) Vote(args Args, resp *Response) error {\n\tlog.Log(args.Ctx, VoteReq{\n\t\tChoices:     args.Choices,\n\t\tType:        args.Type,\n\t\tVote:        log.Sensitive(args.Vote),\n\t\tDescription: _VOTING_VOTEREQ,\n\t})\n\n\t// Get the voter identifier associated with RPC call.\n\t// If empty, then the request is not authenticated.\n\tauther := server.VoterIdentity(args.Ctx)\n\tif len(auther) == 0 {\n\t\tlog.Error(args.Ctx, UnauthenticatedVoteError{Description: _VOTING_VOTER_NO_AUTH})\n\t\treturn server.ErrUnauthenticated\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(sessstatus.Vote).\n\t\tWithRequest(args.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(args.Ctx, VoteVerifySessionIDError{Err: err, Description: _VOTING_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(args.Ctx, VoteUpdateSessionIDError{Description: _VOTING_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Apply rate limiting to vote submissions if enabled.\n\tsubmitted := time.Now()\n\tif r.election.Voting.RateLimitMinutes > 0 {\n\t\tif err := r.ratelimit(args.Ctx, auther, submitted); err != nil {\n\t\t\t// Errors have already been logged by ratelimit.\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Verify the vote container and get the signer.\n\t//\n\t// Return all voter ballots in order to perform ciphertext validation\n\tvotec, ballots, signer, voterName, version, err := r.verify(args.Ctx, args.Choices, args.Type, args.Vote)\n\tif votec != nil {\n\t\tdefer votec.Close()\n\t}\n\tif err != nil {\n\t\t// Errors have already been logged by verify.\n\t\treturn err\n\t}\n\n\t// For each ballot, perform ciphertext correctness checks\n\tfor _, ballot := range ballots {\n\t\terr := r.ciphertextVerifier.ASN1CiphertextVerify(ballot)\n\t\tif err != nil {\n\t\t\tlog.Error(args.Ctx, VoteInvalidCiphertextError{\n\t\t\t\tErr:         err,\n\t\t\t\tDescription: _VOTING_CIP,\n\t\t\t})\n\t\t\treturn server.ErrBadRequest\n\t\t}\n\t}\n\n\t// Check that the submitter matches the signer.\n\tif auther != signer {\n\t\tlog.Error(args.Ctx, AuthenticatedSignerMismatchError{\n\t\t\tAuthenticated: auther,\n\t\t\tSigner:        signer,\n\t\t\tDescription:   _VOTING_ID_MISMATCH,\n\t\t})\n\t\treturn server.ErrIdentityMismatch\n\t}\n\n\t// Check if an authentication token has already specified the vote\n\t// identifier. If not, generate one. Alert, if it cannot be generated.\n\tresp.VoteID = server.VoteIdentifier(args.Ctx)\n\tif len(resp.VoteID) == 0 {\n\t\tif resp.VoteID, err = genIdent(); err != nil {\n\t\t\tlog.Error(args.Ctx, GenerateVoteIDError{Err: log.Alert(err),\n\t\t\t\tDescription: _VOTING_VOTEID})\n\t\t\treturn server.ErrInternal\n\t\t}\n\t}\n\n\t// VoteID successfully generated\n\tlog.Log(args.Ctx, VoteID{VoteID: resp.VoteID, Description: _VOTING_VOTEID_OK})\n\n\t// Store the vote identifier, submission time, vote container, voter\n\t// identity, and voter list version.\n\tif err = r.storage.StoreVote(args.Ctx, storage.StoredVote{\n\t\tVoteID:   resp.VoteID,\n\t\tTime:     submitted,\n\t\tVoteType: args.Type,\n\t\tVote:     args.Vote,\n\t\tVoter:    signer,\n\t\tVersion:  version,\n\t}); err != nil {\n\t\t// Error in storing the vote, database may be unreachable\n\t\tlog.Error(args.Ctx, StoreVoteError{Err: log.Alert(err), Description: _VOTING_STORE})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Request qualifying properties for the vote and store them.\n\tresp.Qualification = make(q11n.Properties)\n\tlogq11n := make(map[q11n.Protocol]log.Sensitive)\n\n\t// Init Transaction\n\ttransaction := r.storage.Txn()\n\tlog.Debug(args.Ctx, VoteTxnInit{VoteID: resp.VoteID, Description: _VOTING_TXN_INIT})\n\t// BEGIN\n\ttxnOp, err := transaction.Begin(args.Ctx)\n\tif err != nil {\n\t\t// Backend cannot begin a transaction for vote storage. Database unreachable?\n\t\tlog.Error(args.Ctx, VoteTxnSetVotedBeginError{Err: err, Description: _VOTING_TXN_START})\n\t\treturn server.ErrInternal\n\t}\n\t// Transaction successfully initiated\n\tlog.Debug(args.Ctx, VoteTxnBegin{VoteID: resp.VoteID, Description: _VOTING_TXN_STARTED})\n\t// Transaction: SET AUTOCOMMIT ON\n\ttransaction.AutoCommit(args.Ctx, txnOp)\n\tlog.Debug(args.Ctx, VoteTxnSetAutoCommitToOn{VoteID: resp.VoteID, Description: _VOTING_TXN_SET_AUTOCOMMIT})\n\n\tfor _, q := range r.q11n {\n\t\t// Initiate query to request qualifying property\n\t\tlog.Log(args.Ctx, RequestingQualifyingProperty{Protocol: q.Protocol, Description: _VOTING_Q_PROP})\n\t\tprop, err := q.Qualifier.Qualify(args.Ctx, votec)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(q11n.BadCertificateStatusError)) != nil {\n\t\t\t\t// Qualification service - OCSP - has reported bad certificate status\n\t\t\t\tlog.Error(args.Ctx, BadSignerCertificateError{Err: err,\n\t\t\t\t\tDescription: _VOTING_Q_PROP_BAD_CERT})\n\t\t\t\treturn server.ErrCertificate\n\t\t\t}\n\n\t\t\t// Qualification service has returned an error\n\t\t\tlog.Error(args.Ctx, QualifierError{\n\t\t\t\tProtocol:    q.Protocol,\n\t\t\t\tErr:         log.Alert(err),\n\t\t\t\tDescription: _VOTING_Q_PROP_FAIL,\n\t\t\t})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\t// Qualifying property has been successfully retreived, we treat it's value as\n\t\t// sensitive\n\t\tlog.Log(args.Ctx, QualifyingProperty{\n\t\t\tProtocol:    q.Protocol,\n\t\t\tProperty:    log.Sensitive(prop),\n\t\t\tDescription: _VOTING_Q_PROP_OK,\n\t\t})\n\n\t\tswitch q.Protocol {\n\t\t// Don't store TSPREG response in transaction, instead\n\t\t// store it immediately, this is a requirement!\n\t\t// TODO: review this code, since qualifying services are dynamically configured\n\t\t// What we need to achieve, that each response is stored as soon as it is received\n\t\t// and only final statistics is updated in transaction\n\t\tcase q11n.TSPREG, q11n.TSP:\n\t\t\terr = r.storage.StoreQualifyingProperty(\n\t\t\t\targs.Ctx, resp.VoteID, q.Protocol, prop)\n\t\t\tif err != nil {\n\t\t\t\t// Error in storing the qualifying property, database may be unreachable.\n\t\t\t\tlog.Error(args.Ctx, StoreQualifyingPropertyError{\n\t\t\t\t\tErr:         log.Alert(err),\n\t\t\t\t\tDescription: _VOTING_Q_PROP_STORE,\n\t\t\t\t})\n\t\t\t\treturn server.ErrInternal\n\t\t\t}\n\t\tcase q11n.OCSP:\n\t\t\tr.storage.TxnStoreQualifyingProperty(\n\t\t\t\targs.Ctx, resp.VoteID, q.Protocol, prop, txnOp)\n\t\t}\n\n\t\t// Transaction properly finished\n\t\tlog.Log(args.Ctx, VoteTxnStoreQualifyingProperty{Protocol: q.Protocol,\n\t\t\tDescription: _VOTING_Q_PROP_STORE_INFO})\n\t\tresp.Qualification[q.Protocol] = prop\n\t\tlogq11n[q.Protocol] = prop\n\t}\n\n\tctime := submitted\n\tif q11nTime, err := q11n.CanonicalTime(resp.Qualification); err != nil {\n\t\t// Error in retrieving qulification time\n\t\t// Do not return an error here: as fas as the voter is\n\t\t// concerned, they voted successfully.\n\t\t// TODO: this logic needs validation\n\t\tlog.Error(args.Ctx, QualificationTimeError{Err: log.Alert(err),\n\t\t\tDescription: _VOTING_Q_PROP_TIMESTAMP})\n\t} else if !q11nTime.IsZero() {\n\t\t// Qualification time\n\t\tlog.Log(args.Ctx, QualificationTime{Time: q11nTime, Description: _VOTING_TIMESTAMP})\n\t\tctime = q11nTime\n\t}\n\n\t// Check if the vote canonical time was before election start: if so,\n\t// report to the voter that this is a test vote.\n\tif ctime.Before(r.start) {\n\t\tlog.Log(args.Ctx, TestVote{VoteID: resp.VoteID, Description: _VOTING_TEST_VOTE})\n\t\tresp.TestVote = true\n\t}\n\n\t// Check qualifiers' times\n\terr = q11n.CompareQualificationTimes(r.q11n, resp.Qualification)\n\tif errors.CausedBy(err, new(q11n.NoPreconfiguredQualifiersError)) != nil {\n\t\t// Don't return err to client, since NoPreconfiguredQualifiersError means\n\t\t// that administrator didn't set \"qualification:\" in election.yml\n\t\t// and is aware of that\n\t\tlog.Log(args.Ctx, VoteNoPreconfiguredQualifiersError{Err: err, Description: _VOTING_NO_TIMESTAMP_Q})\n\t} else if err != nil {\n\t\t// TSA timestamp is > OCSP producedAt value, this should never happen in production\n\t\t// and will only mean that OCSP authority and TSA authority are out of the sync\n\t\tlog.Error(args.Ctx, VoteCompareQualificationTimesError{Err: log.Alert(err),\n\t\t\tDescription: _VOTING_INVALID_TIMESTAMP})\n\t\treturn server.ErrInternal\n\t}\n\n\terr = r.storage.TxnSetVoted(args.Ctx, txnOp, resp.VoteID, voterName, ctime, resp.TestVote)\n\t// Vote has been successfully stored. TODO - is this logging step in the correct place?\n\tlog.Log(args.Ctx, VoteTxnSetVoted{VoteID: resp.VoteID, Description: _VOTING_TXN_END})\n\n\t// if err is equals or contains nested storage.UnexpectedValueError\n\tif errors.CausedBy(err, new(storage.UnexpectedValueError)) != nil {\n\t\t// Transaction with statistics could not be finished. Is database reachable?\n\t\tlog.Error(args.Ctx, VoteTxnSetVotedAutoCommitError{Err: log.Alert(err),\n\t\t\tDescription: _VOTING_TXN_ROLLBACK})\n\t\treturn server.ErrInternal\n\t} else if err != nil {\n\t\t// Do not return an error here: as fas as the voter is\n\t\t// concerned, they voted successfully. TODO - this needs review.\n\t\tlog.Error(args.Ctx, VoteTxnSetVotedError{Err: log.Alert(err),\n\t\t\tDescription: _VOTING_TXN_COMMITTED})\n\t}\n\n\t// Vote has been successfully stored and reply provided to the voteapp\n\tlog.Log(args.Ctx, VoteResp{\n\t\tVoteID:        resp.VoteID,\n\t\tQualification: logq11n,\n\t\tDescription:   _VOTING_VOTERESP,\n\t})\n\treturn nil\n}\n\n// ratelimit applies rate limiting to vote submissions by the same voter.\nfunc (r *RPC) ratelimit(ctx context.Context, voter string, submitted time.Time) error {\n\tstart := r.election.Voting.RateLimitStart\n\tminutes := time.Duration(r.election.Voting.RateLimitMinutes) * time.Minute //nolint:gosec\n\n\t// If the vote submission statistics changed between retrieving and\n\t// attempting to update, then that means that there was another\n\t// concurrent voting session for this voter that got there first. If\n\t// this happens, then try from the beginning, but still use the same\n\t// submission time.\n\tfor {\n\t\tsubmissions, last, err := r.storage.GetVoterRateStats(ctx, voter)\n\t\tif err != nil {\n\t\t\t// Backend could not fetch voter's voting frequency data from the storage.\n\t\t\t// Database may be unreachable.\n\t\t\tlog.Error(ctx, GetVoterRateStatsError{Err: log.Alert(err),\n\t\t\t\tDescription: _VOTING_RATE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\n\t\t// Check if rate limiting should be applied to this voter.\n\t\tif submissions >= start && submitted.Before(last.Add(minutes)) {\n\t\t\t// Voter has exceeded the maximum amount of voting attemps and is restricted to\n\t\t\t// vote exactly 1 time per ratelimitminutes set in election.yml. Current session\n\t\t\t// is rejected, since it does not respect the limit\n\t\t\tlog.Error(ctx, RateLimitAppliedError{\n\t\t\t\tSubmissions: submissions,\n\t\t\t\tLast:        last,\n\t\t\t\tNow:         submitted,\n\t\t\t\tDescription: _VOTING_LIMIT_TIME,\n\t\t\t})\n\t\t\treturn server.ErrVotingRateLimit\n\t\t}\n\n\t\t// Store the latter time of last and submitted as the\n\t\t// timestamp: this way we do not rewind the timestamp of the\n\t\t// last vote in case a vote submitted before gets processed\n\t\t// later.\n\t\ttimestamp := submitted\n\t\tif last.After(timestamp) {\n\t\t\ttimestamp = last\n\t\t}\n\n\t\t// Only update statistics if the rate limit was not applied: do\n\t\t// not refresh the timeout if a new vote came too early.\n\t\tif err = r.storage.SetVoterRateStats(ctx, voter,\n\t\t\tsubmissions, last, timestamp); err != nil {\n\n\t\t\t// We attempted to update statistics for rate limit and received an\n\t\t\t// unexpected value, which may indicate a parallel voting process for the\n\t\t\t// particular voter\n\t\t\tif errors.CausedBy(err, new(storage.UnexpectedValueError)) != nil {\n\t\t\t\tlog.Log(ctx, ConcurrentVotingWarning{Err: err, Description: _VOTING_RACE_VOTE})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Error updating statistics in the storage, database may be unreachable\n\t\t\tlog.Error(ctx, SetVoterRateStatsError{Err: log.Alert(err), Description: _VOTING_RATE_UPDATE})\n\t\t\treturn server.ErrInternal\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// verify verifies the vote container, verifies voter eligibility and choice\n// list used, and checks that the contents of the container are sane. It\n// returns the vote container, voter identifier, and version of the voter list\n// used for eligibility checks.\nfunc (r *RPC) verify(ctx context.Context, choices string, t container.Type, containerb []byte) (\n\tvotec container.Container, ballots map[string][]byte, identity, voterName, version string, err error) {\n\n\t// ASiCE alias of BDOC is not currently allowed for voting.\n\tif t == container.ASiCE {\n\t\tlog.Error(ctx, ASiCEVoteNotAllowedError{Description: _VOTING_ASICE})\n\t\terr = server.ErrBadRequest\n\t\treturn\n\t}\n\n\t// Open the container.\n\tvotec, err = r.container.Open(t, bytes.NewReader(containerb))\n\tif err != nil {\n\t\t// Malformed vote-container received from the voting application\n\t\tlog.Error(ctx, OpenContainerError{Err: err, Description: _VOTING_BDOC_OPEN})\n\t\tvotec = nil // Ensure we do not have a half-initialized container.\n\t\terr = server.ErrBadRequest\n\t\treturn\n\t}\n\n\tsignatures := votec.Signatures()\n\n\t// Container successfully opened\n\tlog.Log(ctx, ContainerOpened{Signatures: signatures, Description: _VOTING_BDOC_SIG})\n\n\t// Only one signer is allowed to be associated with the vote container\n\tif len(signatures) != 1 {\n\t\tlog.Error(ctx, NoSingleSignatureError{Description: _VOTING_BDOC_MANY_SIG})\n\t\terr = server.ErrBadRequest\n\t\treturn\n\t}\n\tsigner := signatures[0].Signer\n\tif identity, err = r.identify(&signer.Subject); err != nil {\n\t\t// Backend cannot parse personal code from a client's RPC \"Vote\" container\n\t\tlog.Error(ctx, SignerIdentityError{Err: err, Description: _VOTING_ID_FROM_BDOC})\n\t\terr = server.ErrIneligible\n\t\treturn\n\t}\n\n\t// Signer successfully identified\n\tlog.Log(ctx, Signer{Identity: identity, Description: _VOTING_IDENTITY})\n\tfirstName := findName(&signer.Subject, asn1.ObjectIdentifier{2, 5, 4, 42})\n\tlastName := findName(&signer.Subject, asn1.ObjectIdentifier{2, 5, 4, 4})\n\tvoterName = firstName + \" \" + lastName\n\n\t// Verify voter eligibility and choices, unless skipEligible is set.\n\tversion = \"N/A\" // Mock voter list version used when the voter list is ignored.\n\tif !r.skipEligible {\n\t\tvar current string\n\t\tversion, current, err = r.storage.VoterChoices(ctx, identity, r.foreignCode)\n\t\tif err != nil {\n\t\t\tif errors.CausedBy(err, new(storage.NotExistError)) != nil {\n\t\t\t\t// Voter successfully authenticated to the backend but\n\t\t\t\t// was not found in the current voterlist\n\t\t\t\tlog.Error(ctx, IneligibleVoterError{Err: err, Description: _VOTING_NO_IN_VOTERSLIST})\n\t\t\t\terr = server.ErrIneligible\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Backend cannot fetch choices list from storage, database may be unreachable\n\t\t\tlog.Error(ctx, VoterChoicesError{Err: log.Alert(err), Description: _VOTING_VOTERSLIST})\n\t\t\terr = server.ErrInternal\n\t\t\treturn\n\t\t}\n\t\tif choices != current {\n\t\t\t// Backend fetched current choices list and it doesn't match RPC \"Choices\"\n\t\t\t// argument that client has sent, more recent voter list is available\n\t\t\tlog.Error(ctx, OutdatedChoicesError{Choices: choices, Current: current,\n\t\t\t\tDescription: _VOTING_DISTRICTS_MISMATCH})\n\t\t\terr = server.ErrOutdatedChoices\n\t\t\treturn\n\t\t}\n\n\t\t// Voter eligibility has been successfully verified\n\t\tlog.Log(ctx, VoterEligible{Version: version, Description: _VOTING_ELIGIBLE})\n\t}\n\n\t// All ballots must have a key of \"<election>.<question>.ballot\".\n\tballots = make(map[string][]byte)\nballots:\n\tfor key, value := range votec.Data() {\n\t\tfor _, q := range r.election.Questions {\n\t\t\tif key == fmt.Sprintf(\"%s.%s.ballot\", r.election.Identifier, q) {\n\t\t\t\tballots[q] = value\n\t\t\t\tcontinue ballots\n\t\t\t}\n\t\t}\n\n\t\t// More datafiles found from the container than expected.\n\t\t// Their content is treated as sensitive information\n\t\tlog.Error(ctx, ExtraDataError{Key: key, Value: log.Sensitive(value),\n\t\t\tDescription: _VOTING_BDOC_EXTRA})\n\t\terr = server.ErrBadRequest\n\t\treturn\n\t}\n\tif got, want := len(ballots), len(r.election.Questions); got != want {\n\t\tvar gotid []string\n\t\tfor key := range ballots {\n\t\t\tgotid = append(gotid, key)\n\t\t}\n\t\t// If IVXV is configured to have multi-contest election, then we expect that\n\t\t// all contests are answered with single ballot.\n\t\tlog.Error(ctx, MissingBallotsError{Ballots: gotid, Questions: r.election.Questions,\n\t\t\tDescription: _VOTING_NO_BALLOT})\n\t\terr = server.ErrBadRequest\n\t\treturn\n\t}\n\n\t// Ballot has been validated successfully\n\tlog.Log(ctx, BallotsOK{Description: _VOTING_BALLOT_OK})\n\n\treturn\n}\n\n// findName searches name for oid and returns the value for that oid or an\n// empty string. Panics if the value for the oid is not a string.\nfunc findName(name *pkix.Name, oid asn1.ObjectIdentifier) string {\n\tfor _, n := range name.Names {\n\t\tif n.Type.Equal(oid) {\n\t\t\treturn n.Value.(string)\n\t\t}\n\t}\n\treturn \"\"\n}\nfunc main() {\n\t// Call votemain in a separate function so that it can set up defers\n\t// and have them trigger before returning with a non-zero exit code.\n\tos.Exit(votemain())\n}\n\nfunc votemain() (code int) {\n\tc := command.New(\"ivxv-voting\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := sessstatus.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Create new RPC instance with the election configuration and storage\n\t// client.\n\trpc := &RPC{election: c.Conf.Election, storage: c.Storage, status: statusClient}\n\n\tvar start, stop time.Time\n\tvar authConf server.AuthConf\n\tvar err error\n\n\tif elec := c.Conf.Election; elec != nil {\n\t\t// Check election configuration time values.\n\t\tif start, err = elec.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ServiceStartTimeError{Err: err,\n\t\t\t\tDescription: _VOTING_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\tif rpc.start, err = elec.ElectionStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ElectionStartTimeError{Err: err,\n\t\t\t\tDescription: _VOTING_START_ACCEPT},\n\t\t\t\t\"bad election start time:\", err)\n\t\t}\n\n\t\tif stop, err = elec.ServiceStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StopTimeError{Err: err,\n\t\t\t\tDescription: _VOTING_STOP},\n\t\t\t\t\"bad service stop time:\", err)\n\t\t}\n\n\t\t// Skip voter eligibility if we are told to ignore it.\n\t\trpc.skipEligible = len(elec.IgnoreVoterList) > 0\n\n\t\t// Set administrative unit code to use for foreign voters.\n\t\trpc.foreignCode = strings.TrimSpace(elec.VoterForeignEHAKDefault())\n\n\t\t// Check voting rate limit values. Non-zero start indicates\n\t\t// that rate limiting is desired, but zero minutes disables it.\n\t\tif elec.Voting.RateLimitStart > 0 && elec.Voting.RateLimitMinutes == 0 {\n\t\t\treturn c.Error(exit.Config, RateLimitError{Description: _VOTING_RATE_LIMITS},\n\t\t\t\t\"voting rate limit start set, but minutes is 0\")\n\t\t}\n\n\t\t// Create ASN.1 ciphertext correctness verifier\n\t\tvar g group.Group\n\t\t// Prefer public key (optional) over group name (required) from election config\n\t\tif elec.Ballot.EncPkey != \"\" { //nolint:gocritic\n\t\t\t// Get group from a x509 public key\n\t\t\tunmarshaller := x_509.NewUnmarshaller[crypto.EncryptionKey](elgamal.NewX509Unmarshaller()) //nolint:lll\n\t\t\tparams, _, err := unmarshaller.Unmarshal([]byte(elec.Ballot.EncPkey))\n\t\t\tif err != nil {\n\t\t\t\treturn c.Error(exit.Config, X509KeyUnmarshalError{Err: err,\n\t\t\t\t\tDescription: _VOTING_ENC_PUB},\n\t\t\t\t\t\"failed to x509 unmarshal vote encryption public key:\", err)\n\t\t\t}\n\t\t\tg = params.Group()\n\t\t} else if elec.Ballot.EncPkeyGroup != \"\" {\n\t\t\t// Get group straight from a config\n\t\t\tg, err = group.Get(elec.Ballot.EncPkeyGroup)\n\t\t\tif err != nil {\n\t\t\t\treturn c.Error(exit.Config, GroupFetchError{Err: err,\n\t\t\t\t\tDescription: _VOTING_ENC_G},\n\t\t\t\t\t\"failed to fetch a group for ciphertext verifier:\", err)\n\t\t\t}\n\t\t} else {\n\t\t\treturn c.Error(exit.Config, NoCiphertextVerifierParamsError{\n\t\t\t\tDescription: _VOTING_NO_CIP_PARAMS},\n\t\t\t\t\"no ciphertext verifier parameters set\")\n\t\t}\n\n\t\trpc.ciphertextVerifier = internal.NewElGamalASN1CiphertextVerifier(g)\n\n\t\t// Parse client-authentication configuration.\n\t\tif authConf, err = server.NewAuthConf(\n\t\t\telec.Auth, elec.Identity, &elec.Age); err != nil {\n\n\t\t\treturn c.Error(exit.Config, ServerAuthConfError{Err: err,\n\t\t\t\tDescription: _VOTING_AUTH},\n\t\t\t\t\"failed to configure client authentication:\", err)\n\t\t}\n\n\t\t// Configure supported ballot container parsers for this\n\t\t// election.\n\t\tif rpc.container, err = container.Configure(elec.Vote); err != nil {\n\t\t\treturn c.Error(exit.Config, ContainerConfError{Err: err,\n\t\t\t\tDescription: _VOTING_BDOC_VERIFIER},\n\t\t\t\t\"failed to configure container parsers:\", err)\n\t\t}\n\n\t\t// Store the voter identifier for signer identification.\n\t\trpc.identify = authConf.Identity\n\n\t\t// Configure vote qualifiers.\n\t\tif rpc.q11n, err = q11n.Configure(elec.Qualification,\n\t\t\tconf.Sensitive(c.Service.ID)); err != nil {\n\n\t\t\treturn c.Error(exit.Config, QualificationConfError{Err: err,\n\t\t\t\tDescription: _VOTING_QUALIFIER},\n\t\t\t\t\"failed to configure vote qualifiers:\", err)\n\t\t}\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err,\n\t\t\t\tDescription: _VOTING_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.WithAuth(authConf).ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err,\n\t\t\t\tDescription: _VOTING_SERVER_SERVE},\n\t\t\t\t\"failed to serve voting service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "voting/service/voting/testdata/eligible.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBeDCCAR+gAwIBAgIJAPfaxniLI3kkMAoGCCqGSM49BAMCMBkxFzAVBgNVBAMM\nDmVsaWdpYmxlIHZvdGVyMB4XDTE2MTAwNzA4NTMyNloXDTI2MDgxNjA4NTMyNlow\nGTEXMBUGA1UEAwwOZWxpZ2libGUgdm90ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMB\nBwNCAASmEi0PCIXnVphmFeRW9QGzxvOgYlpZVCkRzy0BsRDfjEbBqSjLOiPb0mi+\nn9YaZLeHqjOlYAJMGJGd48hD+isVo1AwTjAdBgNVHQ4EFgQUeM++vOewg45SAynU\nWKyLPFf1MrgwHwYDVR0jBBgwFoAUeM++vOewg45SAynUWKyLPFf1MrgwDAYDVR0T\nBAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiAHPhG4dqxcVce3US9HlJk3l+RY7gxW\nt6fPsNXZckfxkAIgNPZx3w+BqSqBnkeNEaf1XQfIlf32Gnu9nYYjA7+K2og=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "voting/service/voting/testdata/ineligible.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfDCCASOgAwIBAgIJAOGADyc92S38MAoGCCqGSM49BAMCMBsxGTAXBgNVBAMM\nEGluZWxpZ2libGUgdm90ZXIwHhcNMTYxMDA3MDg1NDAzWhcNMjYwODE2MDg1NDAz\nWjAbMRkwFwYDVQQDDBBpbmVsaWdpYmxlIHZvdGVyMFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE6aG3/SWMdzTSOsRarz4eBcZGemDrpLHbslVd+qOGz7GAuoP3pDFs\nt2TSSrNBm6JC+Fh80SvQQGSR4v5D4az+kqNQME4wHQYDVR0OBBYEFMZZ8MUn55qV\nuF9wPe54rMTBcCMzMB8GA1UdIwQYMBaAFMZZ8MUn55qVuF9wPe54rMTBcCMzMAwG\nA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgfVX9/+ybjqemfPDBM7EbgqGd\nE4jN9+7g35IPg7Poe7UCIBxKh2xIPBdFBQGixITnLoVBQBN0kbqnLx3/05P2taeH\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "voting/service/voting/voting_test.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/container\"\n\t\"ivxv.ee/common/collector/identity\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/storage\"\n\t\"ivxv.ee/common/collector/storage/memory\"\n\n\t_ \"ivxv.ee/common/collector/container/dummy\"\n)\n\nfunc TestBallot(t *testing.T) {\n\tvar err error\n\trpc := new(RPC)\n\n\t// Set election and question identifiers.\n\trpc.election = &conf.Election{\n\t\tIdentifier: \"voting\",\n\t\tQuestions:  []string{\"test\"},\n\t}\n\n\t// Setup container parser for dummy containers.\n\trpc.container, err = container.Configure(container.Conf{container.Dummy: nil})\n\tif err != nil {\n\t\tt.Fatal(\"failed to configure container parser:\", err)\n\t}\n\n\trpc.identify, err = identity.Get(identity.CommonName)\n\tif err != nil {\n\t\tt.Fatal(\"failed to get voter identifier:\", err)\n\t}\n\n\t// Setup storage client with eligible voters.\n\tdistrict := string(storage.EncodeAdminDistrict(\"100\", \"1\"))\n\trpc.storage = storage.NewWithProtocol(memory.New(map[string]string{\n\t\t\"/voters/version\":          \"0\",\n\t\t\"/voters/0/eligible voter\": district,\n\t\t\"/districts/\" + district:   \"100.1\",\n\t}))\n\n\t// Helper functions to simplify actual tests.\n\tctx := log.TestContext(context.Background())\n\tparse := func(choices, encoded string) (string, error) {\n\t\t_, _, voter, _, _, err := rpc.verify(ctx, choices, container.Dummy, []byte(encoded))\n\t\treturn voter, err\n\t}\n\n\t// Test certificates.\n\teligible := signer(t, \"testdata/eligible.pem\")\n\tineligible := signer(t, \"testdata/ineligible.pem\")\n\n\t// Subtests.\n\tfor _, test := range []struct {\n\t\tname     string\n\t\texpected error\n\t\tchoices  string\n\t\tencoded  string\n\t}{\n\t\t{\"no signers\", server.ErrBadRequest, \"\", \"\"},\n\t\t{\"multiple signers\", server.ErrBadRequest, \"\", `\nsignatures:\n  - signer: ` + eligible + `\n  - signer: ` + ineligible},\n\n\t\t{\"ineligible signer\", server.ErrIneligible, \"\", `\nsignatures:\n  - signer: ` + ineligible},\n\n\t\t{\"outdated choices\", server.ErrOutdatedChoices, \"200.1\", `\nsignatures:\n  - signer: ` + eligible},\n\n\t\t{\"no ballot\", server.ErrBadRequest, \"100.1\", `\nsignatures:\n  - signer: ` + eligible + `\ndata:`},\n\n\t\t{\"extra data\", server.ErrBadRequest, \"100.1\", `\nsignatures:\n  - signer: ` + eligible + `\ndata:\n  voting.test.ballot: |\n  extra.data: |`},\n\t} {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := parse(test.choices, test.encoded)\n\t\t\tif err.Error() != test.expected.Error() {\n\t\t\t\tt.Errorf(\"unexpected error: %q, want %q\", err, test.expected)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"OK\", func(t *testing.T) {\n\t\tid, err := parse(\"100.1\", `\nsignatures:\n  - signer: `+eligible+`\ndata:\n  voting.test.ballot: choice`)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"unexpected error:\", err)\n\t\t}\n\t\tif id != \"eligible voter\" {\n\t\t\tt.Errorf(\"unexpected voter identity: %s\", id)\n\t\t}\n\t})\n}\n\n// signer loads a test certificate from an external file as a literal value and\n// indents all lines to match what is expected of signers.\nfunc signer(t *testing.T, path string) string {\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(\"failed to load test certificate:\", err)\n\t}\n\treturn strings.ReplaceAll(\"|\\n\"+string(b), \"\\n\", \"\\n      \")\n}\n"
  },
  {
    "path": "webeid/.gitignore",
    "content": "bin/\npkg/\n"
  },
  {
    "path": "webeid/Makefile",
    "content": "include ../common/go/common.mk\n"
  },
  {
    "path": "webeid/go.mod",
    "content": "module ivxv.ee/webeid\n\ngo 1.23\n\nrequire ivxv.ee/common/collector v1.9.11\n\nrequire ivxv.ee/sessionstatus/api v1.9.11\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.3.2 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgo.etcd.io/etcd/api/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect\n\tgo.etcd.io/etcd/client/v3 v3.5.17 // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgo.uber.org/multierr v1.6.0 // indirect\n\tgo.uber.org/zap v1.17.0 // indirect\n\tgolang.org/x/net v0.29.0 // indirect\n\tgolang.org/x/sys v0.25.0 // indirect\n\tgolang.org/x/text v0.18.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect\n\tgoogle.golang.org/grpc v1.68.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.2 // indirect\n)\n\nreplace ivxv.ee/common/collector => ../common/collector\n\nreplace ivxv.ee/sessionstatus/api => ../sessionstatus/api\n"
  },
  {
    "path": "webeid/go.sum",
    "content": "github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=\ngo.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=\ngo.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=\ngo.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=\ngolang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=\ngolang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=\ngolang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=\ngoogle.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=\ngoogle.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "webeid/internal/sessionstatus/rpc/client.go",
    "content": "package rpc\n\nimport (\n\t\"strconv\"\n\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\tapi \"ivxv.ee/sessionstatus/api/rpc\"\n)\n\nconst (\n\t// This should be a StatusReadResp.Caller value when calling RPC.Challenge\n\tEmpty = \"\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.Token\n\tChallenge = \"RPC.Challenge\"\n\n\t// This should be a StatusReadResp.Caller value when calling RPC.VoterChoices\n\tToken = \"RPC.Token\"\n)\n\nconst exitCodeOK = 0\n\ntype RPC struct {\n\tauthTTL int64\n\tclient  client.TLSDialer\n}\n\n// NewClient initializes session status server client.\nfunc NewClient(c *command.C) (client.Verifier, int) {\n\t// Initialize RPC TLS session status client\n\ttlsDialer, errCode := api.NewClient(c)\n\tif errCode != exitCodeOK {\n\t\treturn nil, errCode\n\t}\n\n\treturn &RPC{\n\t\tclient:  tlsDialer,\n\t\tauthTTL: c.Conf.Technical.Status.Session.AuthTTL,\n\t}, exitCodeOK\n}\n\nfunc (r *RPC) Verify(dto interface{}) (bool, error) {\n\t// dto should cast to *status.VerifyReq\n\tverifyReq, err := status.CastAnyToVerifyReq(dto)\n\tif err != nil {\n\t\treturn false, CastAnyToVerifyReqError{Err: err, Description: _SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ}\n\t}\n\n\t// verifyReq.Request should cast to server.Header\n\theader, err := api.CastVerifyRequestToServerHeader(verifyReq)\n\tif err != nil {\n\t\treturn false, CastVerifyRequestToServerHeaderError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER}\n\t}\n\n\t// Send request to session status server and verify response\n\tok, err := r.verifyAndUpdateSessionStatus(verifyReq.ServiceMethod, *header)\n\tif err != nil {\n\t\treturn false, VerifyAndUpdateSessionStatusError{Err: err,\n\t\t\tDescription: _SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT}\n\t}\n\n\treturn ok, nil\n}\n\n// verifyAndUpdateSessionStatus will first check h.Header.SessionID\n// record against the underlying storage, and if everything is correct,\n// then will update h.Header.SessionID record in the underlying storage\n// by marking session status Caller as serviceMethod.\n//\n// Note, that here serviceMethod is the RPC method that calls this function.\nfunc (r *RPC) verifyAndUpdateSessionStatus(serviceMethod string, h server.Header) (bool, error) {\n\t// Create new session read status request\n\treqRead := api.NewSessionStatusReadReqBuilder().\n\t\tWithHeader(h).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqReadRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusRead).\n\t\tWithRequest(reqRead).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespReadRaw, err := r.client.TLSDial(&reqReadRPC)\n\tif err != nil {\n\t\treturn false, SessionReadReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespReadRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respReadRaw).\n\t\tBuild()\n\n\t// Process session read status response\n\trespRead := api.NewSessionStatusReadRespBuilder().\n\t\tWithResponse(respReadRPC.Response).\n\t\tBuild()\n\n\t// NB! Most important part, that prevents any attacks on SessionID\n\tvar ok bool\n\tvar ttl string\n\tswitch serviceMethod {\n\tcase Challenge:\n\t\tok, err = verifyStatusReadResp(&respRead, challengeHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\tcase Token:\n\t\tok, err = verifyStatusReadResp(&respRead, tokenHandler)\n\t\tttl = strconv.FormatInt(r.authTTL, 10)\n\t}\n\tif !ok || err != nil {\n\t\treturn ok, VerifyStatusReadRespError{Err: err, Description: _SESSIONSTATUS_RESP_VERIFY}\n\t}\n\n\t// Create new session update status request\n\treqUpdate := api.NewSessionStatusUpdateReqBuilder().\n\t\tWithHeader(h).\n\t\tWithCaller(serviceMethod).\n\t\tWithAuth(respRead.Auth).\n\t\tWithLease(respRead.Lease).\n\t\tWithTTL(ttl).\n\t\tBuild()\n\n\t// Create new RPC request to status server, embeds session status request\n\treqUpdateRPC := status.NewStatusReqBuilder().\n\t\tWithServiceMethod(api.Endpoint.SessionStatusUpdate).\n\t\tWithRequest(reqUpdate).\n\t\tBuild()\n\n\t// RPC call to .WithServiceMethod(...)\n\trespUpdateRaw, err := r.client.TLSDial(&reqUpdateRPC)\n\tif err != nil {\n\t\treturn false, SessionUpdateReqTLSDialError{Err: err, Description: _SESSIONSTATUS_TLS_DIAL}\n\t}\n\n\t// Process raw RPC response, doesn't care about the embedded status type\n\trespUpdateRPC := status.NewStatusRespBuilder().\n\t\tWithResponse(respUpdateRaw).\n\t\tBuild()\n\n\t// Process session update status response\n\trespUpdate := api.NewSessionStatusUpdateRespBuilder().\n\t\tWithResponse(respUpdateRPC.Response).\n\t\tBuild()\n\n\t// If true, then status has been successfully updated\n\tok = respUpdate.Ok\n\tif !ok {\n\t\treturn false, SessionStatusUpdateError{\n\t\t\tCaller:      reqUpdate.Caller,\n\t\t\tAuth:        respRead.Auth,\n\t\t\tDescription: _SESSIONSTATUS_UPDATE_FAIL,\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// verifyStatusReadResp r by applying an appropriate handler h.\nfunc verifyStatusReadResp(r *api.StatusReadResp,\n\th func(*api.StatusReadResp) (bool, error)) (bool, error) {\n\treturn h(r)\n}\n\n// challengeHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Challenge request.\nfunc challengeHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Challenge is the very first client request to IVXV,\n\t// so IVXV requires no previous interactions\n\tfirstTime := r.Caller == Empty && r.Auth == client.NoAuth\n\n\tif !(firstTime) {\n\t\treturn false, ChallengeInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Challenge,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_CHALLENGE,\n\t\t}\n\t}\n\n\tr.Auth = client.WebeIDAuth\n\treturn true, nil\n}\n\n// tokenHandler performs filter operation on StatusReadResp r to\n// detect invalid SessionID in a client RPC.Token request.\nfunc tokenHandler(r *api.StatusReadResp) (bool, error) {\n\t// RPC.Token is the second client request to IVXV,\n\t// so IVXV requires RPC.Challenge previously interacted\n\tsecondTime := r.Caller == Challenge && r.Auth == client.WebeIDAuth\n\n\tif !(secondTime) {\n\t\treturn false, TokenInvalidCallerOrAuthForSessionID{\n\t\t\tMethod:      Token,\n\t\t\tCaller:      r.Caller,\n\t\t\tAuth:        r.Auth,\n\t\t\tDescription: _SESSIONSTATUS_MALFORMED_SESSION_ID_TOKEN,\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "webeid/internal/sessionstatus/rpc/log_desc.go",
    "content": "package rpc\n\nconst (\n\t_SESSIONSTATUS_CAST_ANY_TO_VERIFYREQ          = \"Cannot cast any to VerifyReq\"\n\t_SESSIONSTATUS_CAST_VERIFYREQ_TO_SERVERHEADER = \"Cannot cast VerifyReq to server.Header\"\n\t_SESSIONSTATUS_SEND_UPDATE_REQ_AND_VERIFY_IT  = \"SessionID verification request that has been sent to sessionstatus service has been failed\"\n\t_SESSIONSTATUS_TLS_DIAL                       = \"TLS dial to sessionstatus service failed\"\n\t_SESSIONSTATUS_RESP_VERIFY                    = \"Sessionstatus service response verification failed\"\n\t_SESSIONSTATUS_UPDATE_FAIL                    = \"Sessionstatus service hasn't updated the Session ID state\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_CHALLENGE = \"SessionID has been attempted to tamper at RPC.Challenge\"\n\t_SESSIONSTATUS_MALFORMED_SESSION_ID_TOKEN     = \"SessionID has been attempted to tamper at RPC.Token\"\n)\n"
  },
  {
    "path": "webeid/service/webeid/handler.go",
    "content": "package main\n\nimport (\n\t\"crypto/x509\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/cryptoutil\"\n\t\"ivxv.ee/common/collector/log\"\n\t\"ivxv.ee/common/collector/server\"\n\tstatus \"ivxv.ee/common/collector/status/client/rpc\"\n\t\"ivxv.ee/common/collector/token\"\n\t\"ivxv.ee/common/collector/token/custom\"\n\t\"ivxv.ee/common/collector/token/webeid\"\n\tinternal \"ivxv.ee/webeid/internal/sessionstatus/rpc\"\n)\n\n// ChallengeReq is a client RPC call to retrieve a cryptographic nonce.\ntype ChallengeReq struct {\n\tserver.Header\n}\n\n// ChallengeResp is a server RPC response to the ChallengeReq.\ntype ChallengeResp struct {\n\tserver.Header\n\tChallenge []byte\n\tBearer    string\n}\n\n// Challenge is an RPC endpoint to provide a cryptographic nonce.\nfunc (r *RPC) Challenge(req ChallengeReq, resp *ChallengeResp) (err error) {\n\tlog.Log(req.Ctx, ChallengeRequest{Description: _WEBEID_CHALLENGEREQ})\n\n\t// Check that election period is not ended\n\tif !time.Now().Before(r.authEnd) {\n\t\tlog.Log(req.Ctx, ChallengeVotingEnded{Description: _WEBEID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Challenge).\n\t\tWithRequest(req.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(req.Ctx, ChallengeVerifySessionIDError{Err: err, Description: _WEBEID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(req.Ctx, ChallengeUpdateSessionIDError{Description: _WEBEID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Generate 44 byte nonce\n\tnonce, err := cryptoutil.Nonce44Bytes()\n\tif err != nil {\n\t\t// Error in nonce generation\n\t\tlog.Error(req.Ctx, ChallengeGenerateNonceError{Err: err, Description: _WEBEID_CHALLENGE})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Generate brand-new bearer token\n\tbearerToken := custom.NewFromEmptyBuilder().\n\t\tWithNonce(nonce).\n\t\tWithTimeStamp(time.Now()).\n\t\tBuild()\n\n\t// Payload is a serialized bearerToken\n\tpayload, err := bearerToken.Payload()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, ChallengeExtractPayloadFromBearerError{Err: err,\n\t\t\tDescription: _WEBEID_PAYLOAD})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Create a signature over a payload using shared secret\n\tsig := r.cookie.Create([]byte(payload))\n\n\t// Create new bearer token with a payload and a signature,\n\t// and serialize it to base64 string\n\trawComplete, err := custom.NewFromExistingBuilder().\n\t\tWithPayload(payload).\n\t\tWithSignature(string(sig)).\n\t\tBuild().\n\t\tMarshal()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, ChallengeMarshalBearerError{Err: err, Description: _WEBEID_BEARER})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Return raw bearer token to client\n\tresp.Bearer = rawComplete\n\n\t// Return nonce to client\n\tresp.Challenge = []byte(nonce)\n\n\t// Successful challenge response\n\tlog.Log(req.Ctx, ChallengeResponse{\n\t\tChallenge:   resp.Challenge,\n\t\tBearer:      resp.Bearer,\n\t\tDescription: _WEBEID_CHALLENGERESP,\n\t})\n\n\treturn nil\n}\n\n// TokenReq is a client RPC call to validate a Web eID authentication token.\ntype TokenReq struct {\n\tserver.Header\n\t// Token has a maximum size of 16000 bytes.\n\t// Token size is not defined by a backend and is completely up to the user.\n\tToken string `size:\"16000\"`\n\t// Bearer has a maximum size of 400 bytes.\n\t// Bearer size is fully defined by a backend.\n\tBearer string `size:\"400\"`\n}\n\n// TokenResp is a server RPC response to the TokenReq.\ntype TokenResp struct {\n\tserver.Header\n\tStatus       string\n\tGivenName    string\n\tSurname      string\n\tPersonalCode string\n\tAuthToken    []byte\n}\n\n// Token is an RPC endpoint to validate a Web eID authentication token.\nfunc (r *RPC) Token(req TokenReq, resp *TokenResp) (err error) {\n\tlog.Log(req.Ctx, TokenRequest{Description: _WEBEID_TOKENREQ})\n\n\t// Check that election period is not ended\n\tif !time.Now().Before(r.authEnd) {\n\t\tlog.Log(req.Ctx, TokenVotingEnded{Description: _WEBEID_EXPIRED})\n\t\treturn server.ErrVotingEnd\n\t}\n\n\t// Build up VerifyReq for session status service\n\tverifyReq := status.NewVerifyReqBuilder().\n\t\tWithServiceMethod(internal.Token).\n\t\tWithRequest(req.Header).\n\t\tBuild()\n\n\t// SessionID security check\n\tok, err := r.status.Verify(&verifyReq)\n\tif err != nil {\n\t\t// Error during SessionID check - database unreachable, service stalled, etc.\n\t\tlog.Error(req.Ctx, TokenVerifySessionIDError{Err: err, Description: _WEBEID_SESSION_ID})\n\t\treturn server.ErrBadRequest\n\t}\n\tif !ok {\n\t\t// SessionID is unknown / has expired, we shall not further process the request\n\t\tlog.Error(req.Ctx, TokenUpdateSessionIDError{Description: _WEBEID_SESSION_ID_EXPIRED})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Generate bearer token from a raw bearer token (base64 string)\n\tbearerToken, err := custom.NewFromRawBuilder().\n\t\tWithBearer(req.Bearer).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenUnmarshalBearerError{Err: err, Description: _WEBEID_BEARER})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Extract bearer token signature\n\tsig, err := bearerToken.Signature()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenExtractSignatureFromBearerError{Err: err,\n\t\t\tDescription: _WEBEID_SIG})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Verify bearer token signature\n\tdata, err := r.cookie.Open(sig)\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenVerifyBearerSignatureError{Err: err,\n\t\t\tDescription: _WEBEID_SIG_VERIFY})\n\t\treturn server.ErrBadRequest\n\t}\n\tif data == nil {\n\t\t// No data in the bearer token\n\t\tlog.Error(req.Ctx, TokenBearerSignatureIsEmptyAfterDecryptError{Err: err,\n\t\t\tDescription: _WEBEID_SIG_DATA})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Payload returns nonce\n\tnonce, err := bearerToken.Payload()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenExtractPayloadFromBearerError{Err: err,\n\t\t\tDescription: _WEBEID_PAYLOAD})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Generate Web eID token from a raw Web eID token (json string)\n\twToken, err := webeid.NewFromRawBuilder().\n\t\tWithToken(req.Token).\n\t\tWithNonce(nonce).\n\t\tWithOrigin(r.origin).\n\t\tBuild().\n\t\tUnmarshal()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenUnmarshalWebeidError{Err: err,\n\t\t\tDescription: _WEBEID_TOKEN})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Parse auth cert from Web eID token\n\tcert := wToken.(token.Certifier).Certify()\n\n\t// Add auth cert to the req.Ctx context, so that auth.tls verifier\n\t// could perform TLS certificate validation\n\treq.Ctx = server.TLSClientKey(req.Ctx, []*x509.Certificate{cert})\n\n\t// Validate auth cert from the req.Ctx and perform OCSP check\n\tauthCert, _, err := r.auther.Auth.Verify(req.Ctx, auth.TLS, nil)\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenVerifyWebeidAuthCertCAnOCSPError{Err: err,\n\t\t\tDescription: _WEBEID_CERT})\n\t\treturn server.ErrCertificate\n\t}\n\n\t// Verify Web eID auth token signature\n\terr = wToken.Verify()\n\tif err != nil {\n\t\tlog.Error(req.Ctx, TokenVerifyWebeidError{Err: err,\n\t\t\tDescription: _WEBEID_TOKEN_VERIFY})\n\t\treturn server.ErrBadRequest\n\t}\n\n\t// Retrieve specific data from client certificate from Web eID auth token\n\tresp.Status = StatusOK\n\tresp.GivenName = findName(authCert, oid[\"givenName\"])\n\tresp.Surname = findName(authCert, oid[\"surname\"])\n\tresp.PersonalCode = personalCode(authCert)\n\n\t// Create client authentication cookie (ticket)\n\tif resp.AuthToken, err = r.ticket.Create(cert.Subject); err != nil {\n\t\tlog.Error(req.Ctx, TokenAuthenticationTicketError{Err: err,\n\t\t\tDescription: _WEBEID_AUTHTOKEN})\n\t\treturn server.ErrInternal\n\t}\n\n\t// Successful token response\n\n\tlog.Log(req.Ctx, TokenResponse{\n\t\tStatus:       resp.Status,\n\t\tGivenName:    resp.GivenName,\n\t\tSurname:      resp.Surname,\n\t\tPersonalCode: resp.PersonalCode,\n\t\tAuthToken:    log.Sensitive(resp.AuthToken),\n\t\tDescription:  _WEBEID_TOKENRESP,\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "webeid/service/webeid/log_desc.go",
    "content": "package main\n\nconst (\n\t_WEBEID_CHALLENGEREQ       = \"RPC.ChallengeReq\"\n\t_WEBEID_EXPIRED            = \"Current time >= 'electionstoptime' from election configuration file\"\n\t_WEBEID_SESSION_ID         = \"Malformed SessionID\"\n\t_WEBEID_SESSION_ID_EXPIRED = \"SessionID has been expired\"\n\t_WEBEID_CHALLENGE          = \"Failed to generate 44 byte random challenge\"\n\t_WEBEID_PAYLOAD            = \"Failed to extract payload from a Bearer token\"\n\t_WEBEID_BEARER             = \"Failed to create new Bearer token\"\n\t_WEBEID_CHALLENGERESP      = \"RPC.ChallengeResp\"\n\t_WEBEID_TOKENREQ           = \"RPC.TokenReq\"\n\t_WEBEID_SIG                = \"Failed to extract signature from a Bearer token\"\n\t_WEBEID_SIG_VERIFY         = \"Unable to verify Bearer token signature\"\n\t_WEBEID_SIG_DATA           = \"Signature is verified, but extracted data from signature is empty\"\n\t_WEBEID_TOKEN              = \"Failed to create new Web-eID token\"\n\t_WEBEID_CERT               = \"Failed to verify voter certificate, that was parsed from certificate\"\n\t_WEBEID_TOKEN_VERIFY       = \"Failed to verify Web-eID token\"\n\t_WEBEID_AUTHTOKEN          = \"Failed to create server.AuthToken (authentication token)\"\n\t_WEBEID_TOKENRESP          = \"RPC.TokenResp\"\n\t_WEBEID_BAD_URL            = \"Incorrect Web-eID URI\"\n\t_WEBEID_START              = \"Failed to transform service start time from election configuration file to RFC3339 format\"\n\t_WEBEID_AUTH_STOP          = \"Failed to transform authentication stop time from election configuration file to RFC3339 format\"\n\t_WEBEID_STOP               = \"Failed to transform service stop time from election configuration file to RFC3339 format\"\n\t_WEBEID_TICKET             = \"Failed to create a Web-eID ticket (AES cipher) that is used for cookie signing\"\n\t_WEBEID_TICKET_AUTH        = \"Web-eID service should be configured for ticket based authentication\"\n\t_WEBEID_AUTH               = \"Failed to parse authentication configuration for webeid service\"\n\t_WEBEID_TLS_AUTH           = \"Web-eID service should be also configured for voter certificate authentication\"\n\t_WEBEID_TLS_AUTH_CFG       = \"Failed to parse TLS configuration for voter certificate authentication\"\n\t_WEBEID_SERVER             = \"Failed to parse server configuration for webeid service\"\n\t_WEBEID_BEARER_COOKIE      = \"Failed to create AES shared secret to encrypt Bearer tokens\"\n\t_WEBEID_SERVER_SERVE       = \"Failed to serve webeid service to clients\"\n)\n"
  },
  {
    "path": "webeid/service/webeid/main.go",
    "content": "// Package main provides a Web eID authentication method against IVXV backend.\npackage main\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"ivxv.ee/common/collector/auth\"\n\t\"ivxv.ee/common/collector/auth/ticket\"\n\t_ \"ivxv.ee/common/collector/auth/tls\"\n\t\"ivxv.ee/common/collector/command\"\n\t\"ivxv.ee/common/collector/command/exit\"\n\t\"ivxv.ee/common/collector/conf\"\n\t\"ivxv.ee/common/collector/cookie\"\n\t\"ivxv.ee/common/collector/server\"\n\t\"ivxv.ee/common/collector/status/client\"\n\tinternal \"ivxv.ee/webeid/internal/sessionstatus/rpc\"\n\t//ivxv:modules common/collector/container\n)\n\n// RPC is a handler for Web eID service calls.\ntype RPC struct {\n\tstatus  client.Verifier\n\tcookie  *cookie.C\n\tauther  *server.AuthConf\n\torigin  []byte\n\tauthEnd time.Time\n\tticket  *ticket.T\n}\n\nfunc main() {\n\tos.Exit(webeidmain())\n}\n\nfunc webeidmain() (code int) {\n\tc := command.NewWithoutStorage(\"ivxv-webeid\", \"\")\n\tdefer func() {\n\t\tcode = c.Cleanup(code)\n\t}()\n\n\t// Configure session status client\n\tstatusClient, errCode := internal.NewClient(c)\n\tif statusClient == nil || errCode != 0 {\n\t\treturn errCode\n\t}\n\n\t// Check that origin is correct HTTPS\n\tif !server.VerifyHTTPSOrigin(c.Service.Origin) {\n\t\treturn c.Error(exit.Config, BadUrlSchema{Description: _WEBEID_BAD_URL},\n\t\t\t\"bad service origin URL:\", c.Service.Origin)\n\t}\n\n\t// Create new RPC instance and start the session cleaner.\n\trpc := &RPC{\n\t\torigin: []byte(c.Service.Origin),\n\t\tstatus: statusClient,\n\t}\n\n\tvar start, stop time.Time\n\tvar authConf server.AuthConf\n\tvar err error\n\n\tif elec := c.Conf.Election; elec != nil {\n\t\t// Check election configuration time values - service start\n\t\tif start, err = c.Conf.Election.ServiceStartTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, StartTimeError{Err: err,\n\t\t\t\tDescription: _WEBEID_START},\n\t\t\t\t\"bad service start time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - election stop\n\t\tif rpc.authEnd, err = c.Conf.Election.ElectionStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ElectionStopTimeError{Err: err,\n\t\t\t\tDescription: _WEBEID_AUTH_STOP},\n\t\t\t\t\"bad election stop time:\", err)\n\t\t}\n\n\t\t// Check election configuration time values - service stop\n\t\tif stop, err = c.Conf.Election.ServiceStopTime(); err != nil {\n\t\t\treturn c.Error(exit.Config, ServiceStopTimeError{Err: err,\n\t\t\t\tDescription: _WEBEID_STOP},\n\t\t\t\t\"bad service stop time:\", err)\n\t\t}\n\n\t\tif rpc.ticket, err = ticket.NewFromSystem(); err != nil {\n\t\t\treturn c.Error(exit.Config, TicketConfError{Err: err,\n\t\t\t\tDescription: _WEBEID_TICKET},\n\t\t\t\t\"failed to configure ticket manager:\", err)\n\t\t}\n\n\t\t// Auther for Web eID client authentication in RPC authFilter,\n\t\t// after server.Header.AuthToken is issued\n\t\tticketConf, ok := c.Conf.Election.Auth[auth.Ticket]\n\t\tif !ok {\n\t\t\treturn c.Error(exit.Config, TicketAuthError{Description: _WEBEID_TICKET_AUTH},\n\t\t\t\t\"ticket authentication is mandatory for webeid\")\n\t\t}\n\t\tif authConf, err = server.NewAuthConf(auth.Conf{auth.Ticket: ticketConf},\n\t\t\telec.Identity, &elec.Age); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerTicketAuthConfError{Err: err,\n\t\t\t\tDescription: _WEBEID_AUTH},\n\t\t\t\t\"failed to configure client ticket authentication:\", err)\n\t\t}\n\n\t\t// Auther for Web eID auth token TLS validation in RPC.Token method\n\t\ttlsConf, ok := c.Conf.Election.Auth[auth.TLS]\n\t\tif !ok {\n\t\t\treturn c.Error(exit.Config, TLSAuthError{Description: _WEBEID_TLS_AUTH},\n\t\t\t\t\"TLS authentication is mandatory for webeid\")\n\t\t}\n\t\tvar auther server.AuthConf\n\t\tif auther, err = server.NewAuthConf(auth.Conf{auth.TLS: tlsConf},\n\t\t\t\"\", nil); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerTLSAuthConfError{Err: err,\n\t\t\t\tDescription: _WEBEID_TLS_AUTH_CFG},\n\t\t\t\t\"failed to configure client TLS authentication:\", err)\n\t\t}\n\t\t// This allows to perform TLS validation inside RPC.Token or any other\n\t\t// RPC method\n\t\trpc.auther = &auther\n\t}\n\n\tvar s *server.S\n\tif c.Conf.Technical != nil {\n\t\t// Configure a new server with the service instance\n\t\t// configuration and the RPC handler instance.\n\t\tcert, key := conf.TLS(conf.Sensitive(c.Service.ID))\n\t\tif s, err = server.New(&server.Conf{\n\t\t\tCertPath: cert,\n\t\t\tKeyPath:  key,\n\t\t\tAddress:  c.Service.Address,\n\t\t\tEnd:      stop,\n\t\t\tFilter:   &c.Conf.Technical.Filter,\n\t\t\tVersion:  &c.Conf.Version,\n\t\t}, rpc); err != nil {\n\t\t\treturn c.Error(exit.Config, ServerConfError{Err: err,\n\t\t\t\tDescription: _WEBEID_SERVER},\n\t\t\t\t\"failed to configure server:\", err)\n\t\t}\n\n\t\t// cookie is used as a shared secret to encrypt Bearer tokens.\n\t\t// Bearer tokens are used to provide stateless nonce verification\n\t\t// for Web eID authentication\n\t\trpc.cookie, err = ticket.NewFromSystemAsCookie()\n\t\tif err != nil {\n\t\t\treturn c.Error(exit.Config, ReadSharedSecretRPCConfError{Err: err,\n\t\t\t\tDescription: _WEBEID_BEARER_COOKIE},\n\t\t\t\t\"failed to read RPC shared secret (cookie):\", err)\n\t\t}\n\t}\n\n\t// Start listening for incoming connections during the voting period.\n\tif c.Until >= command.Execute {\n\t\tif err = s.WithAuth(authConf).ServeAt(c.Ctx, start); err != nil {\n\t\t\treturn c.Error(exit.Unavailable, ServeError{Err: err,\n\t\t\t\tDescription: _WEBEID_SERVER_SERVE},\n\t\t\t\t\"failed to serve webeid service:\", err)\n\t\t}\n\t}\n\treturn exit.OK\n}\n"
  },
  {
    "path": "webeid/service/webeid/util.go",
    "content": "package main\n\nimport (\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"strings\"\n)\n\nconst (\n\tStatusOK = \"OK\"\n\tPNOEE    = \"PNOEE-\"\n)\n\n// oid is a map of asn1.ObjectIdentifier that is used to extract particular\n// data from a x509.Certificate.Subject.Names.\nvar oid = map[string][]int{\n\t\"personalCode\": asn1.ObjectIdentifier{2, 5, 4, 5},\n\t\"givenName\":    asn1.ObjectIdentifier{2, 5, 4, 42},\n\t\"surname\":      asn1.ObjectIdentifier{2, 5, 4, 4},\n}\n\n// findName searches name for oID and returns the value for that oID or an\n// empty string. Panics if the value for the oID is not a string.\nfunc findName(name *pkix.Name, oID asn1.ObjectIdentifier) string {\n\tfor _, n := range name.Names {\n\t\tif n.Type.Equal(oID) {\n\t\t\treturn n.Value.(string)\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// personalCode reads personal code (isikukood) from a cert.\nfunc personalCode(cert *pkix.Name) string {\n\tcode := findName(cert, oid[\"personalCode\"])\n\treturn strings.TrimSuffix(code, PNOEE)\n}\n"
  },
  {
    "path": "xroad-service/.gitignore",
    "content": "dist\nvenv\n.idea\n"
  },
  {
    "path": "xroad-service/.goreleaser.yml",
    "content": "version: 2\nproject_name: xroad-service\nbuilds:\n  - id: xroad-service\n    main: ./cmd\n    binary: xroad-service\n    goos:\n      - linux\n    goarch:\n      - amd64\n    ldflags:\n      - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}\nchecksum:\n  name_template: 'checksums.txt'\nnfpms:\n  - description: |-\n      X-road service is helper service between X-road and EHS systems.\n    maintainer: \"IVXV Developer <info@ivotingcentre.ee>\"\n    formats:\n      - deb\n    dependencies:\n      - musl\n    bindir: /usr/bin\n    release: 1\n    section: default\n    priority: extra\n    contents:\n      - src: pkg/configs/xroad-service.json\n        dst: /etc/xroad-service/xroad-service.json\n        type: config\n        file_info:\n          mode: 0644\n      - src: openapi/ehs-xroad-api.yaml\n        dst: /etc/xroad-service/ehs-xroad-api.yaml\n        type: config\n        file_info:\n          mode: 0644\n      - src: init/systemd/xroad-service.service\n        dst: /lib/systemd/system/xroad-service.service\n        file_info:\n          mode: 0644\n    scripts:\n      postinstall: scripts/postinstall.sh\n      preremove: scripts/preremove.sh\n      postremove: scripts/postremove.sh\n"
  },
  {
    "path": "xroad-service/Makefile",
    "content": "ROOTDIR   := $(abspath ../)/\ninclude ../common/go/govar.mk\n\nhelp:\n\t@echo \"usage: make all          Build service\"\n\t@echo \"usage: make clean          Clean build\"\n\nall:\n\tgoreleaser release --skip publish --clean --snapshot\n\nclean:\n\trm -rf dist\n"
  },
  {
    "path": "xroad-service/README.rst",
    "content": "==============================\nIVXV Internet voting framework\n==============================\n\nService that communicates X-road and IVXV election votesorder service.\n\nMore information:\n\n  https://github.com/e-gov/VIS3-EHS/tree/main/9_e_haaletamiste_jooksev_nimekiri\n\nConfiguration fields are explained in IVXV Documentation.\n\n----------\n Building\n----------\n\n``make all`` - Builds binary and deb package."
  },
  {
    "path": "xroad-service/cmd/main.go",
    "content": "/*\nThe xroad-service communicates with X-road security server and IVXV system.\n*/\npackage main\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"flag\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"xroad/pkg/conf\"\n\t\"xroad/pkg/server\"\n\t\"xroad/pkg/service\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst appName = \"xroad-service\"\n\nconst (\n\tsuccess int = iota\n\targError\n\tconfError\n\tserverShutdownError\n)\n\nfunc xroadmain() int {\n\tflags := flag.NewFlagSet(appName, flag.ContinueOnError)\n\tlog.SetFormatter(&log.JSONFormatter{})\n\tconfLoc := flags.String(\"conf\", \"/etc/xroad-service/xroad-service.json\", \"configuration file\")\n\tif err := flags.Parse(os.Args[1:]); err != nil {\n\t\tlog.Error(\"Parsing args\", err)\n\t\treturn argError\n\t}\n\tconf, err := conf.LoadJSON(*confLoc)\n\tif err != nil {\n\t\tlog.Error(\"Loading conf: \", err)\n\t\treturn confError\n\t}\n\ts := service.NewEHSService(conf.Elections, conf.Server.BatchMaxSize)\n\thandler := server.CreateHandler(s, conf.Server.OpenApiPath)\n\n\tcert, err := conf.Server.TLS.X509Certificate()\n\tif err != nil {\n\t\tlog.Error(\"Loading certs: \", err)\n\t\treturn confError\n\t}\n\txcert, err := os.ReadFile(conf.Server.Xroad.Certificate)\n\tif err != nil {\n\t\tlog.Error(\"Loading xroad cert: \", err)\n\t\treturn confError\n\t}\n\tcertPool := x509.NewCertPool()\n\tif ok := certPool.AppendCertsFromPEM(xcert); !ok {\n\t\tlog.Error(\"unable to parse xroad CA file\")\n\t\treturn confError\n\t}\n\n\ttlsConf := &tls.Config{\n\t\tCertificates: []tls.Certificate{*cert},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tClientCAs:    certPool,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\n\t// TODO: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server\n\tsrv := http.Server{ //nolint:gosec\n\t\tAddr:      conf.Server.Address,\n\t\tHandler:   handler,\n\t\tTLSConfig: tlsConf,\n\t}\n\n\terr = srv.ListenAndServeTLS(\"\", \"\")\n\tif err != http.ErrServerClosed {\n\t\tlog.Error(\"Serving ended:\", err)\n\t\treturn serverShutdownError\n\t}\n\treturn success\n}\n\nfunc main() {\n\tos.Exit(xroadmain())\n\n}\n"
  },
  {
    "path": "xroad-service/go.mod",
    "content": "module xroad\n\ngo 1.23\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.10.0\n\tgithub.com/sirupsen/logrus v1.9.3\n)\n\nrequire (\n\tgithub.com/bytedance/sonic v1.11.6 // indirect\n\tgithub.com/bytedance/sonic/loader v0.1.1 // indirect\n\tgithub.com/cloudwego/base64x v0.1.4 // indirect\n\tgithub.com/cloudwego/iasm v0.2.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.3 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.20.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.7 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.12 // indirect\n\tgolang.org/x/arch v0.8.0 // indirect\n\tgolang.org/x/crypto v0.23.0 // indirect\n\tgolang.org/x/net v0.25.0 // indirect\n\tgolang.org/x/sys v0.20.0 // indirect\n\tgolang.org/x/text v0.15.0 // indirect\n\tgoogle.golang.org/protobuf v1.34.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "xroad-service/go.sum",
    "content": "github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=\ngithub.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=\ngithub.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=\ngithub.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=\ngithub.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=\ngithub.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=\ngithub.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=\ngithub.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=\ngithub.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=\ngithub.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=\ngithub.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=\ngithub.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=\ngithub.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=\ngolang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=\ngolang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nnullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\n"
  },
  {
    "path": "xroad-service/init/systemd/xroad-service.service",
    "content": "[Unit]\nDescription=X-Road Service server\n\n[Service]\nExecStart=/usr/bin/xroad-service\nRestart=on-failure\nUser=xroad-service\nStateDirectory=xroad-service\nConfigurationDirectory=xroad-service\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "xroad-service/openapi/ehs-xroad-api.yaml",
    "content": "openapi: 3.0.1\ninfo:\n  title: E-hääletamiste jooksev nimekiri\n  description: EHS poolt VIS3-le osutatava X-tee teenuse spetsifikatsioon\n  license:\n    name: MIT\n  version: 1.0.0\nservers:\n  - url: https://host/xroad/v1\ntags:\n  - name: ELECTION\n    description: Valimissündmuste info pärimise teenused\n\npaths:\n  /elections:\n    get:\n      summary: Valimissündmuste loetelu\n      description: Otspunkt väljastab aktiivsete valimissündmuste loetelu. Aktiivne valimissündmus teenuse kontekstis on selline, mille kohta EHS on valmis väljastama jooksvat e-hääletanute nimekirja.\n      responses:\n        200:\n          description: Edukas operatsioon\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/Elections\"\n        400:\n          description: Vigane päring\n        500:\n          description: Tehniline viga\n\n  /elections/{electionId}/lastseqno:\n    get:\n      summary: Valimissündmuses viimase e-hääletamise järjenumbri pärimine.\n      description: Valimissündmuses viimase e-hääletamise järjenumbri pärimine.\n      parameters:\n        - name: electionId\n          in: path\n          description: Valimissündmuse ID\n          required: true\n          schema:\n            type: string\n      responses:\n        200:\n          description: Edukas operatsioon\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/LastSeqNoResponse\"\n        400:\n          description: Vigane päring\n        404:\n          description: Objekti ei leitud\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ErrorResponse\"\n            text/plain:\n              schema:\n                $ref: \"#/components/schemas/GenericError\"\n        410:\n          description: Enam ei paku\n        500:\n          description: |\n            \"Tehniline viga\" - üldnimetus olukordadele, kus EHS ei suuda päringut teenindada.\n\n  /elections/{electionId}/evotingsbatchfrom/{fromSeqNo}:\n    get:\n      summary: e-hääletamiste pakk\n      description: VIS3 pärib EHS-le valimissündmuses e-hääletamiste paki, alates järjenumbrist.\n      parameters:\n        - name: electionId\n          in: path\n          description: Valimissündmuse ID\n          required: true\n          schema:\n            type: string\n        - name: fromSeqNo\n          in: path\n          description: Viimase e-hääletamise järjenumber\n          required: true\n          schema:\n            type: number\n      responses:\n        200:\n          description: Edukas operatsioon\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/EVotingsBatchResponse\"\n        400:\n          description: Vigane päring\n        404:\n          description: Päritud järjenumbriga e-hääletamist ei ole.\n          content:\n            application/json:\n              schema:\n                $ref: \"#/components/schemas/ErrorResponse\"\n            text/plain:\n              schema:\n                $ref: \"#/components/schemas/GenericError\"\n        410:\n          description: Enam ei paku\n        500:\n          description: |\n            \"Tehniline viga\" - üldnimetus olukordadele, kus EHS ei suuda päringut teenindada.\n\ncomponents:\n  schemas:\n\n    Elections:\n      type: object\n      properties:\n        elections:\n          type: array\n          items:\n            $ref: \"#/components/schemas/Election\"\n\n    Election:\n      type: object\n      description: Valimissündmus\n      properties:\n        name:\n          type: string\n          description: Valimissündmuse nimi\n          example: \"RK_2023\"\n\n    LastSeqNoResponse:\n      type: object\n      properties:\n        electionName:\n          type: string\n          description: Valimissündmuse identifikaator\n          example: \"RK_2023\"\n        lastSeqNo:\n          type: number\n          description: Viimase e-hääletamise järjenumber; või 0, kui e-hääletamisi veel ei ole.\n          example: 54001\n\n    EVotingsBatchResponse:\n      type: object\n      properties:\n        electionName:\n          type: string\n          description: Valimissündmuse identifikaator\n          example: \"RK_2023\"\n        fromSeqNo:\n          type: number\n          description: E-hääletamise järjenumber, millest alates on pakis kirjed\n          example: 54001\n        batchMaxSize:\n          type: number\n          description: Maksimaalne kirjete arv pakis.\n          example: 100\n        eVotingsBatch:\n          type: object\n          properties:\n            batchRecords:\n              type: array\n              items:\n                $ref: \"#/components/schemas/EVotingFact\"\n\n    EVotingFact:\n      type: object\n      properties:\n        seqNo:\n          type: number\n          description: E-hääletamise järjenumber (konkreetses valimissündmuses)\n          example: 54001\n        idCode:\n          type: string\n          description: Isikukood\n          example: \"38101010020\"\n        voterName:\n          type: string\n          description: E-hääletanu nimi (nii ees- kui perekonnanimi), teabeliseks otstarbeks.\n          example: \"LEO KASS\"\n        kovCode:\n          type: string\n          description: KOV EHAK kood\n          example: \"0068\"\n        electoralDistrictNo:\n          type: number\n          description: Valimisringkonna number\n          example: 4\n\n    ErrorResponse:\n      type: array\n      description: Päringu töötlemise ebaõnnestumise korral tagastatav veasõnum\n      items:\n        type: object\n        properties:\n          code:\n            type: string\n            description: Vea kood\n            example: \"not_found\"\n          field:\n            type: string\n            description: Viide vea põhjustanud atribuudile\n            example: \"id\"\n          value:\n            type: integer\n            description: Vea kood\n            example: 0\n\n    GenericError:\n      type: string\n      description: Ei leidu\n      example: \"404 Not Found\""
  },
  {
    "path": "xroad-service/pkg/conf/conf.go",
    "content": "package conf\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"os\"\n\t\"time\"\n)\n\ntype Conf struct {\n\tServer    Server\n\tElections []EHS\n}\n\ntype Server struct {\n\tAddress      string\n\tTLS          TLS\n\tBatchMaxSize int\n\tOpenApiPath  string //nolint:revive,stylecheck\n\tXroad        XRoad\n}\n\ntype XRoad struct {\n\tCertificate string\n}\n\ntype TLS struct {\n\tCertificate string\n\tKey         string\n\tInsecure    bool\n}\n\ntype EHS struct {\n\tAddress    string\n\tName       string\n\tServerName string\n\tRootCA     string\n\tClientCert string\n\tClientKey  string\n}\n\nfunc (t TLS) insecure() (cert, key []byte, err error) {\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"generate signer: %w\", err)\n\t}\n\ttmpl := x509.Certificate{\n\t\tSubject: pkix.Name{\n\t\t\tCommonName: \"localhost\",\n\t\t},\n\t\tSerialNumber:          big.NewInt(1),\n\t\tIsCA:                  true,\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().Add(24 * time.Hour),\n\t}\n\tselfsigned, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, signer.Public(), signer)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"create self-signed certificate: %w\", err)\n\t}\n\tencsigner, err := x509.MarshalPKCS8PrivateKey(signer)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"marshal private key: %w\", err)\n\t}\n\tcertpem := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: selfsigned,\n\t})\n\tkeypem := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"PRIVATE KEY\",\n\t\tBytes: encsigner,\n\t})\n\treturn certpem, keypem, nil\n}\n\nfunc (t TLS) X509Certificate() (*tls.Certificate, error) {\n\tvar certPEMblock, keyPEMblock []byte\n\tvar err error\n\tif !t.Insecure {\n\t\tcertPEMblock, err = os.ReadFile(t.Certificate)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get certificate: %w\", err)\n\t\t}\n\t\tkeyPEMblock, err = os.ReadFile(t.Key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get key: %w\", err)\n\t\t}\n\t} else {\n\t\tcertPEMblock, keyPEMblock, err = t.insecure()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"generate insecure certificate: %w\", err)\n\t\t}\n\t}\n\tcrt, err := tls.X509KeyPair(certPEMblock, keyPEMblock)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse certificate: %w\", err)\n\t}\n\treturn &crt, nil\n}\n\nfunc LoadJSON(path string) (conf Conf, err error) {\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn conf, fmt.Errorf(\"reading configuration failed: %w\", err)\n\t}\n\tif err = json.Unmarshal(bytes, &conf); err != nil {\n\t\treturn conf, fmt.Errorf(\"configuration unmarshal failed: %w\", err)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "xroad-service/pkg/configs/xroad-service.json",
    "content": "{\n    \"server\": {\n        \"address\": \":443\",\n        \"batchmaxsize\": 1000,\n        \"openapipath\": \"/etc/xroad-service/ehs-xroad-api.yaml\",\n        \"tls\": {\n            \"insecure\": false,\n            \"certificate\": \"/etc/xroad-service/cert.pem\",\n            \"key\": \"/etc/xroad-service/cert.key\"\n        },\n        \"xroad\": {\n            \"certificate\": \"/etc/xroad-service/xroad.pem\"\n        }\n    },\n    \"elections\": [\n        {\n            \"name\": \"RK_2023\",\n            \"address\": \"ivxv1.ivxv.ee:443\",\n            \"servername\": \"votesorder.ivxv.invalid\",\n            \"rootca\": \"/etc/xroad-service/ehs.pem\",\n            \"clientcert\": \"/etc/xroad-service/client.pem\",\n            \"clientkey\": \"/etc/xroad-service/client.key\"\n        }\n    ]\n}"
  },
  {
    "path": "xroad-service/pkg/errors/errors.go",
    "content": "package errors\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n)\n\nvar (\n\tErrNotFound   = errors.New(\"NOT_FOUND\")\n\tErrBadRequest = errors.New(\"BAD_REQUEST\")\n\tErrVotingEnd  = errors.New(\"VOTING_END\")\n)\n\n// EHSError is an any error returned by the EHS backend.\n// It implements Error and Is methods so could be safely used in\n// errors.Is function as well as being used as a regular error interface.\ntype EHSError struct {\n\tErr error\n}\n\n// Error overrides error interface's Error method.\nfunc (v EHSError) Error() string {\n\treturn v.Err.Error()\n}\n\n// Is overrides anonymous interface{ Is(error) bool } interface's method,\n// used in errors.Is.\nfunc (v EHSError) Is(err error) bool {\n\treturn v.Err.Error() == err.Error()\n}\n\ntype FieldError struct {\n\tCode  string      `json:\"code\"`\n\tField string      `json:\"field\"`\n\tValue interface{} `json:\"value\"`\n}\n\nfunc (e FieldError) ToErr() error {\n\tmarshal, err := json.Marshal(e)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn errors.New(string(marshal))\n}\n"
  },
  {
    "path": "xroad-service/pkg/errors/errors_test.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype NestedError struct {\n\tErr error\n}\n\nfunc (n *NestedError) Error() string {\n\treturn n.Err.Error()\n}\n\nfunc methodThatAlwaysReturnsErrNotFound() error   { return errors.New(\"NOT_FOUND\") }\nfunc methodThatAlwaysReturnsErrBadRequest() error { return errors.New(\"BAD_REQUEST\") }\nfunc methodThatAlwaysReturnsErrVotingEnd() error  { return errors.New(\"VOTING_END\") }\n\nfunc TestEHSErrorCanBeUsedInErrorsIsFunc(t *testing.T) {\n\terrNotFound := methodThatAlwaysReturnsErrNotFound()\n\terrBadRequest := methodThatAlwaysReturnsErrBadRequest()\n\terrVotingEnd := methodThatAlwaysReturnsErrVotingEnd()\n\n\twrapErrNotFound := &EHSError{Err: errNotFound}\n\twrapErrBadRequest := &EHSError{Err: errBadRequest}\n\twrapErrVotingEnd := &EHSError{Err: errVotingEnd}\n\n\tdata := []error{wrapErrNotFound, wrapErrBadRequest, wrapErrVotingEnd}\n\texpected := []error{ErrNotFound, ErrBadRequest, ErrVotingEnd}\n\n\tfor i, err := range data {\n\t\tareEqual := errors.Is(err, expected[i])\n\t\tif areEqual != true {\n\t\t\tmsg := \"Expected err to be equal to %v, but got %v\"\n\t\t\tt.Fatal(fmt.Sprintf(msg, expected[i], err))\n\t\t}\n\t}\n}\n\nfunc TestNestedEHSErrorCanBeUsedInErrorsIsFunc(t *testing.T) {\n\terrNotFound := methodThatAlwaysReturnsErrNotFound()\n\terrBadRequest := methodThatAlwaysReturnsErrBadRequest()\n\terrVotingEnd := methodThatAlwaysReturnsErrVotingEnd()\n\n\tthreeLevelsNestedErrNotFound := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errNotFound,\n\t\t\t},\n\t\t},\n\t}\n\tthreeLevelsNestedErrBadRequest := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errBadRequest,\n\t\t\t},\n\t\t},\n\t}\n\tthreeLevelsNestedErrVotingEnd := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errVotingEnd,\n\t\t\t},\n\t\t},\n\t}\n\n\twrapThreeLevelsNestedErrNotFound := &EHSError{Err: threeLevelsNestedErrNotFound}\n\twrapThreeLevelsNestedErrBadRequest := &EHSError{Err: threeLevelsNestedErrBadRequest}\n\twrapThreeLevelsNestedErrVotingEnd := &EHSError{Err: threeLevelsNestedErrVotingEnd}\n\n\tdata := []error{\n\t\twrapThreeLevelsNestedErrNotFound,\n\t\twrapThreeLevelsNestedErrBadRequest,\n\t\twrapThreeLevelsNestedErrVotingEnd,\n\t}\n\n\texpected := []error{ErrNotFound, ErrBadRequest, ErrVotingEnd}\n\n\tfor i, err := range data {\n\t\tareEqual := errors.Is(err, expected[i])\n\t\tif areEqual != true {\n\t\t\tmsg := \"Expected err to be equal to %v, but got %v\"\n\t\t\tt.Fatal(fmt.Sprintf(msg, expected[i], err))\n\t\t}\n\t}\n}\n\nfunc TestRegularErrorCannotBeUsedInErrorsIsFunc(t *testing.T) {\n\terrNotFound := methodThatAlwaysReturnsErrNotFound()\n\terrBadRequest := methodThatAlwaysReturnsErrBadRequest()\n\terrVotingEnd := methodThatAlwaysReturnsErrVotingEnd()\n\n\tdata := []error{errNotFound, errBadRequest, errVotingEnd}\n\texpected := []error{ErrNotFound, ErrBadRequest, ErrVotingEnd}\n\n\tfor i, err := range data {\n\t\tareEqual := errors.Is(err, expected[i])\n\t\tif areEqual == true {\n\t\t\tmsg := \"Expected err not to be equal to %v\"\n\t\t\tt.Fatal(fmt.Sprintf(msg, err))\n\t\t}\n\t}\n}\n\nfunc TestNestedRegularErrorCannotBeUsedInErrorsIsFunc(t *testing.T) {\n\terrNotFound := methodThatAlwaysReturnsErrNotFound()\n\terrBadRequest := methodThatAlwaysReturnsErrBadRequest()\n\terrVotingEnd := methodThatAlwaysReturnsErrVotingEnd()\n\n\tthreeLevelsNestedErrNotFound := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errNotFound,\n\t\t\t},\n\t\t},\n\t}\n\tthreeLevelsNestedErrBadRequest := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errBadRequest,\n\t\t\t},\n\t\t},\n\t}\n\tthreeLevelsNestedErrVotingEnd := &NestedError{\n\t\tErr: &NestedError{\n\t\t\tErr: &NestedError{\n\t\t\t\tErr: errVotingEnd,\n\t\t\t},\n\t\t},\n\t}\n\n\tdata := []error{\n\t\tthreeLevelsNestedErrNotFound,\n\t\tthreeLevelsNestedErrBadRequest,\n\t\tthreeLevelsNestedErrVotingEnd,\n\t}\n\n\texpected := []error{ErrNotFound, ErrBadRequest, ErrVotingEnd}\n\n\tfor i, err := range data {\n\t\tareEqual := errors.Is(err, expected[i])\n\t\tif areEqual == true {\n\t\t\tmsg := \"Expected err not to be equal to %v\"\n\t\t\tt.Fatal(fmt.Sprintf(msg, err))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "xroad-service/pkg/server/handler.go",
    "content": "package server\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"xroad/pkg/errors\"\n\t\"xroad/pkg/service\"\n\n\t\"github.com/gin-gonic/gin\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype Handler struct {\n\tservice.EHSService\n\thttp.Handler\n}\n\nfunc (h Handler) get(c *gin.Context) {\n\tc.IndentedJSON(http.StatusOK, h.EHSService.GetElections())\n}\n\nfunc (h Handler) getSeqNo(c *gin.Context) {\n\ts, ok := requireCode(c)\n\tif !ok {\n\t\treturn\n\t}\n\tres, err := h.GetSeqNo(s)\n\trespond(c, http.StatusOK, res, err)\n}\n\nfunc (h Handler) getBatch(c *gin.Context) {\n\ts, ok := requireCode(c)\n\tif !ok {\n\t\treturn\n\t}\n\tno, ok := requireSeqNo(c)\n\tif !ok {\n\t\treturn\n\t}\n\tres, err := h.GetBatch(s, no)\n\trespond(c, http.StatusOK, res, err)\n\n}\n\nfunc CreateHandler(service service.EHSService, apiPath string) http.Handler {\n\tgin.SetMode(gin.ReleaseMode)\n\trouter := gin.New()\n\trouter.Use(initLogger())\n\th := Handler{service, router}\n\tapi := router.Group(\"xroad/v1\")\n\trouter.StaticFile(\"openapi\", apiPath)\n\tapi.GET(\"/elections\", h.get)\n\tapi.GET(\"/elections/:electionId/lastseqno\", h.getSeqNo)\n\tapi.GET(\"/elections/:electionId/evotingsbatchfrom/:fromSeqNo\", h.getBatch)\n\treturn router\n}\n\nfunc initLogger() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tlog.SetFormatter(&log.JSONFormatter{})\n\n\t\tinfo := struct {\n\t\t\tRemoteAddr    string `json:\"RemoteAddr\"`\n\t\t\tRequestMethod string `json:\"RequestMethod\"`\n\t\t\tRequestUri    string `json:\"RequestUri\"` //nolint:revive,stylecheck\n\t\t}{c.Request.RemoteAddr, c.Request.Method, c.Request.URL.RequestURI()}\n\t\tlog.Info(info)\n\t\tc.Next()\n\t}\n}\n\nfunc requireCode(c *gin.Context) (string, bool) {\n\telectionId := c.Param(\"electionId\") //nolint:revive,stylecheck\n\tif electionId == \"\" {\n\t\tc.JSON(http.StatusBadRequest, errors.FieldError{\n\t\t\tCode:  errors.ErrBadRequest.Error(),\n\t\t\tField: \"electionId\",\n\t\t\tValue: electionId,\n\t\t})\n\t\treturn \"\", false\n\t}\n\treturn electionId, true\n}\n\nfunc requireSeqNo(c *gin.Context) (int, bool) {\n\tid := c.Param(\"fromSeqNo\")\n\tid64, err := strconv.ParseInt(id, 10, 0)\n\tif err != nil || id64 == 0 {\n\t\tc.JSON(http.StatusBadRequest, errors.FieldError{\n\t\t\tCode:  errors.ErrBadRequest.Error(),\n\t\t\tField: \"fromSeqNo\",\n\t\t\tValue: id,\n\t\t})\n\t\treturn 0, false\n\t}\n\treturn int(id64), true\n}\n\n// respond sends appropriate response according to the provided error and value.\nfunc respond(c *gin.Context, status int, value interface{}, err error) {\n\tif err != nil {\n\t\tvar fieldErr errors.FieldError\n\t\tjson.Unmarshal([]byte(err.Error()), &fieldErr) //nolint:errcheck\n\t\tif fieldErr.Code == errors.ErrNotFound.Error() {\n\t\t\tc.JSON(httpErr(errors.ErrNotFound), fieldErr)\n\t\t\treturn\n\t\t}\n\t\tc.JSON(httpErr(err), gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\tc.JSON(status, value)\n}\n\n// httpErr transforms the given error to proper HTTP error response\nfunc httpErr(err error) int {\n\t// Wrap EHS backend err into errors.EHSError, so it could correctly\n\t// be used in stderrors.Is, see errors.EHSError description for details.\n\twrapErr := errors.EHSError{Err: err}\n\tswitch {\n\tcase stderrors.Is(wrapErr, errors.ErrNotFound):\n\t\treturn http.StatusNotFound\n\tcase stderrors.Is(wrapErr, errors.ErrBadRequest):\n\t\treturn http.StatusBadRequest\n\tcase stderrors.Is(wrapErr, errors.ErrVotingEnd):\n\t\treturn http.StatusGone\n\tdefault:\n\t\treturn http.StatusInternalServerError\n\t}\n}\n"
  },
  {
    "path": "xroad-service/pkg/service/service.go",
    "content": "package service\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"net/rpc/jsonrpc\"\n\t\"os\"\n\t\"xroad/pkg/conf\"\n\t\"xroad/pkg/errors\"\n)\n\ntype EHSService struct {\n\tbatchMaxSize int\n\telections    []conf.EHS\n}\n\ntype Elections struct {\n\tElections []Election `json:\"elections\"`\n}\n\ntype Election struct {\n\tName string `json:\"name\"`\n}\n\nfunc NewEHSService(elections []conf.EHS, batchMaxSize int) EHSService {\n\treturn EHSService{batchMaxSize: batchMaxSize, elections: elections}\n}\n\nfunc (e EHSService) GetElections() Elections {\n\telections := []Election{}\n\tfor _, election := range e.elections {\n\t\t// GetSeqNo is the fastest call, that helps determine the backend state\n\t\t_, err := e.GetSeqNo(election.Name)\n\t\tif err != nil {\n\t\t\t// If particular backend responds with err then from xroad-service\n\t\t\t// perspective it is an inactive backend\n\t\t\tcontinue\n\t\t}\n\t\telections = append(elections, Election{Name: election.Name})\n\t}\n\treturn Elections{elections}\n}\n\ntype ElectionSeqNo struct {\n\tName  string `json:\"electionName\"`\n\tSeqNo int    `json:\"lastSeqNo\"`\n}\n\ntype SeqNo struct {\n\tSeqNo     int\n\tSessionID string\n}\n\nfunc (e EHSService) GetSeqNo(electionName string) (ElectionSeqNo, error) {\n\tvar ehs conf.EHS\n\tvar ok bool\n\tif ehs, ok = e.isCorrectName(electionName); !ok {\n\t\treturn ElectionSeqNo{}, errors.FieldError{\n\t\t\tCode:  errors.ErrNotFound.Error(),\n\t\t\tField: \"electionId\",\n\t\t\tValue: electionName}.ToErr()\n\t}\n\tconn, err := e.ehsConn(ehs.Address, ehs.RootCA, ehs.ClientCert, ehs.ClientKey, ehs.ServerName)\n\tif err != nil {\n\t\treturn ElectionSeqNo{}, err\n\t}\n\tdefer conn.Close()\n\tclient := jsonrpc.NewClient(conn)\n\n\tvar resp SeqNo\n\terr = client.Call(\"RPC.VotesSeqNo\", nil, &resp)\n\tif err != nil {\n\t\treturn ElectionSeqNo{}, err\n\t}\n\treturn ElectionSeqNo{electionName, resp.SeqNo}, nil\n}\n\ntype ElectionBatch struct {\n\tName          string       `json:\"electionName\"`\n\tSeqNo         int          `json:\"fromSeqNo\"`\n\tBatchMaxSize  int          `json:\"batchMaxSize\"`\n\tEVotingsBatch BatchRecords `json:\"eVotingsBatch\"`\n}\ntype BatchRecords struct {\n\tBatchRecords []Batch `json:\"batchRecords\"`\n}\n\ntype Batch struct {\n\tSeqNo               int    `json:\"seqNo\"`\n\tIdCode              string `json:\"idCode\"` //nolint:revive,stylecheck\n\tVoterName           string `json:\"voterName\"`\n\tKovCode             string `json:\"kovCode\"`\n\tElectoralDistrictNo int    `json:\"electoralDistrictNo\"`\n}\n\ntype VotesArgs struct {\n\tVotesFrom    int\n\tBatchMaxSize int\n}\n\nfunc (e EHSService) GetBatch(electionName string, seqNo int) (ElectionBatch, error) {\n\tvar ehs conf.EHS\n\tvar ok bool\n\tif ehs, ok = e.isCorrectName(electionName); !ok {\n\t\treturn ElectionBatch{}, errors.FieldError{\n\t\t\tCode:  errors.ErrNotFound.Error(),\n\t\t\tField: \"electionId\",\n\t\t\tValue: electionName}.ToErr()\n\t}\n\tconn, err := e.ehsConn(ehs.Address, ehs.RootCA, ehs.ClientCert, ehs.ClientKey, ehs.ServerName)\n\tif err != nil {\n\t\treturn ElectionBatch{}, err\n\t}\n\tdefer conn.Close()\n\tclient := jsonrpc.NewClient(conn)\n\n\tvar resp BatchRecords\n\terr = client.Call(\"RPC.Votes\", VotesArgs{VotesFrom: seqNo, BatchMaxSize: e.batchMaxSize}, &resp)\n\tif err != nil {\n\t\tswitch { //nolint:gocritic\n\t\tcase stderrors.Is(err, errors.ErrBadRequest):\n\t\t\terr = errors.FieldError{\n\t\t\t\tCode:  errors.ErrNotFound.Error(),\n\t\t\t\tField: \"fromSeqNo\",\n\t\t\t\tValue: seqNo}.ToErr()\n\t\t}\n\t\treturn ElectionBatch{}, err\n\t}\n\n\treturn ElectionBatch{Name: electionName, SeqNo: seqNo, BatchMaxSize: e.batchMaxSize, EVotingsBatch: resp}, nil\n}\n\nfunc (e EHSService) isCorrectName(electionName string) (conf.EHS, bool) {\n\tfor _, election := range e.elections {\n\t\tif election.Name == electionName {\n\t\t\treturn election, true\n\t\t}\n\t}\n\treturn conf.EHS{}, false\n}\n\nfunc (e EHSService) ehsConn(addr string, certLoc string, clientcert string, clientkey string, serverName string) (*tls.Conn, error) { //nolint:lll\n\tcert, err := os.ReadFile(certLoc)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"EHS cert %s: \", err)\n\t}\n\tcertPool := x509.NewCertPool()\n\tif ok := certPool.AppendCertsFromPEM(cert); !ok {\n\t\treturn nil, fmt.Errorf(\"unable to parse EHS cert %s\", cert)\n\t}\n\tclientCert, err := tls.LoadX509KeyPair(clientcert, clientkey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"EHS client cert %s: \", err)\n\t}\n\tconn, err := tls.Dial(\"tcp\", addr, &tls.Config{\n\t\tRootCAs:      certPool,\n\t\tCertificates: []tls.Certificate{clientCert},\n\t\tServerName:   serverName,\n\t\tMinVersion:   tls.VersionTLS12})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"EHS connection error: %s\", err)\n\t}\n\treturn conn, nil\n}\n"
  },
  {
    "path": "xroad-service/scripts/postinstall.sh",
    "content": "#!/bin/sh\n\nset -e\n\ncase \"$1\" in\n    configure)\n        if ! getent passwd xroad-service > /dev/null; then\n            adduser --system --no-create-home xroad-service\n        fi\n        systemctl daemon-reload\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n"
  },
  {
    "path": "xroad-service/scripts/postremove.sh",
    "content": "#!/bin/sh\n\nset -e\n\ncase \"$1\" in\n    purge)\n        if getent passwd xroad-service > /dev/null; then\n            deluser --system xroad-service\n        fi\n    ;;\n\n    remove)\n        systemctl daemon-reload\n    ;;\n\n    upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)\n    ;;\n\n    *)\n        echo \"postrm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n"
  },
  {
    "path": "xroad-service/scripts/preremove.sh",
    "content": "#!/bin/sh\n\nset -e\n\ncase \"$1\" in\n    remove)\n        systemctl unmask xroad-service.service\n        systemctl stop xroad-service.service\n        systemctl disable xroad-service.service\n    ;;\n\n    upgrade|failed-upgrade|deconfigure)\n    ;;\n\n    *)\n        echo \"prerm called with unknown argument \\`$1'\" >&2\n        exit 1\n    ;;\nesac\n"
  }
]