diff --git a/.devcontainer/.env.development b/.devcontainer/.env.development index aa5763d9ee2..45e9e1c302a 100644 --- a/.devcontainer/.env.development +++ b/.devcontainer/.env.development @@ -44,8 +44,8 @@ AWS_REGION=us-east-1 # the DevContainer development environment. AWS_S3_ROOT_USER=minio_user AWS_S3_ROOT_PASSWORD=minio_pass -AWS_S3_ACCESS_KEY=LZAw7hwBziRjwAhfP6Xi -AWS_S3_ACCESS_SECRET=4x8krlfXgEquxp9KhlCrCdkrECrszGQQlJa5nGct +AWS_S3_ACCESS_KEY= +AWS_S3_ACCESS_SECRET= # Don't set these two for production - we'll just use default AWS URIs instead AWS_S3_PRIVATE_URI=http://minio:9000 @@ -148,7 +148,7 @@ VAULT_SERVER_URL=http://vault:8201 # Only used when SECRETS_BACKEND=HashiCorpVault # Hashicorp Vault Token -VAULT_TOKEN=hvs.s0djsk0LLBI19K0DkW4Fajs7 +VAULT_TOKEN= ################################################################################ # RabbitMQ configuration @@ -188,15 +188,15 @@ KEYCLOAK_ADMIN_PASSWORD=admin # Client Id and secret used for the authentication flow KEYCLOAK_CLIENT_ID=service-account -KEYCLOAK_CLIENT_SECRET=zh0GWEjbynXJDcpF1YipFXUleEKTQiO0 +KEYCLOAK_CLIENT_SECRET= # Client id used for the IVR system-access KEYCLOAK_IVR_SERVICE_CLIENT_ID=ivr-service # Client secrets for the IVR, can be left empty for re-generation -KEYCLOAK_IVR_SERVICE_CLIENT_SECRET=1qBFz0LoRM3JH6lJwHhBTIzGSb7In5Va +KEYCLOAK_IVR_SERVICE_CLIENT_SECRET= # Secret for the client, "ivr-voting" -KEYCLOAK_IVR_VOTING_CLIENT_SECRET=4oMNNlwfjVQYbaUVF9y7O83QISVPZ6wK +KEYCLOAK_IVR_VOTING_CLIENT_SECRET= # Client Id and secret used for administrative tasks like creating a new realm # or similar @@ -365,7 +365,7 @@ SECONDS_TO_SHOW_ALERT=300 # CLI Usage KEYCLOAK_CLI_CLIENT_ID=admin-portal -KEYCLOAK_CLI_CLIENT_SECRET=wBy8rpuKQxPWikQ3rIFv9g42t0WK0Xiu +KEYCLOAK_CLI_CLIENT_SECRET= #CloudFlare + Custom urls CLOUDFLARE_ZONE= @@ -383,7 +383,7 @@ B4_URL=http://b4:50051 # loadero LOADERO_BASE_URL=https://api.loadero.com/v2/projects/13356 -LOADERO_API_KEY=a133481e0cf90882554c73bac3e7b2e82a8d79e19c170ed5 +LOADERO_API_KEY= LOADERO_INTERVAL_POLLING_TIME=30 # -- PDF generation -- @@ -505,3 +505,12 @@ SP_CERT_DATA=MIIDOzCCAiMCBgGaMBMbKzANBgkqhkiG9w0BAQsFADBhMV8wXQYDVQQDDFZ0ZW5hbnQ # Local Development: https://localhost:3000 # Production: https://voting-example.sequent.vote (Sequent will provide your subdomain) VOTING_PORTAL_URL=http://localhost:3000 + +KEYCLOAK_CERTIFICATES_CLIENT_SECRET= + +# Credential defaults are intentionally empty. Initialize local development with +# .devcontainer/scripts/initialize-command.sh; provision deployment credentials separately. + +SSP_SIGNING_CERTIFICATE= + +KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE= diff --git a/.devcontainer/.env.remote-deployment.example b/.devcontainer/.env.remote-deployment.example index 56dd4e6be2a..b228b8399a9 100644 --- a/.devcontainer/.env.remote-deployment.example +++ b/.devcontainer/.env.remote-deployment.example @@ -90,11 +90,11 @@ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_CLIENT_ID=service-account # Note: Client secrets will be generated by configure-environment.sh -KEYCLOAK_CLIENT_SECRET=zh0GWEjbynXJDcpF1YipFXUleEKTQiO0 +KEYCLOAK_CLIENT_SECRET= KEYCLOAK_ADMIN_CLIENT_ID=admin KEYCLOAK_ADMIN_CLIENT_SECRET=admin KEYCLOAK_CLI_CLIENT_ID=admin-portal -KEYCLOAK_CLI_CLIENT_SECRET=wBy8rpuKQxPWikQ3rIFv9g42t0WK0Xiu +KEYCLOAK_CLI_CLIENT_SECRET= SUPER_ADMIN_TENANT_ID=90505c8a-23a9-4cdf-a26b-4e19f6a097d5 KC_OTP_RESEND_INTERVAL=60 KEYCLOAK_VOTER_GROUP_NAME=voter @@ -273,3 +273,16 @@ WINDMILL_PROBE_READY_PATH=ready BEAT_PROBE_ADDR=0.0.0.0:8003 BEAT_PROBE_LIVE_PATH=live BEAT_PROBE_READY_PATH=ready + +KEYCLOAK_IVR_SERVICE_CLIENT_SECRET= + +KEYCLOAK_IVR_VOTING_CLIENT_SECRET= + +KEYCLOAK_CERTIFICATES_CLIENT_SECRET= + +# Credential defaults are intentionally empty. Initialize local development with +# .devcontainer/scripts/initialize-command.sh; provision deployment credentials separately. + +SSP_SIGNING_CERTIFICATE= + +KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE= diff --git a/.devcontainer/certs/README.md b/.devcontainer/certs/README.md new file mode 100644 index 00000000000..1c63388b2ef --- /dev/null +++ b/.devcontainer/certs/README.md @@ -0,0 +1,16 @@ +# Local development certificates + +Run `.devcontainer/scripts/initialize-command.sh` from the repository root before +building the development Compose services. It creates a local self-signed nginx +certificate and a separate SimpleSAML signing certificate with fresh private keys. +It also creates `vp-sso-signing.key`/`.crt` for the local SAML client. Only the public +certificate enters the Keycloak realm; a client that signs requests uses the local +private file. The copied private key and its old public trust entry are removed. +Python 3 and OpenSSL must be installed on the host. Generated files are ignored by Git. + +Reopening the environment preserves existing credentials and certificates. To renew +a certificate, stop the affected local service, retain its existing pair if needed, +remove that pair, rerun initialization and rebuild the affected image. Reimport the +new IdP public certificate into local Keycloak when renewing the SimpleSAML pair. +These certificates are development inputs; use deployment-specific certificates +and controlled key storage for an installation. diff --git a/.devcontainer/certs/nginx-tls.crt b/.devcontainer/certs/nginx-tls.crt deleted file mode 100644 index 5b4372df447..00000000000 --- a/.devcontainer/certs/nginx-tls.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDNTCCAh2gAwIBAgIUBiTu6dK35v1FJCHcj1WeKsGIdDswDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMxNzE0NDk1N1oXDTI3MDMx -NzE0NDk1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAsziC9Zzr1yp3G/c/CtrRjsH56Pr7O22Sc0tr20p/5qvE -QytiDKPx8UI+kfHDwUfapLsJtvKwPv/JY1bS/nKT4/TVAVmnXSB2vDCkhKogpoMo -C7sRu/2h8DRvGzXWBnOCQ4Pz01RNcgycMXQL8UPMn3Wn67t32bV9NbKIIwWPmQyX -vrkUV9In5VzyXlB9o8LNQgbGR0ACh0Vq4lBTK1AgOtMHomvOJPi/C+eAtAbqSY4L -SztUMrmzdahXGUTQTu1LhmZEWJyuczWe2iD9GO5KQIsJ77Fop4T4a2FcDNaEd91+ -I/WaslAjpDlPCNhTidCLm3oPnDOmpB7kQZcP/+ePtwIDAQABo38wfTAdBgNVHQ4E -FgQUk/osja4W09i9T/2YG0yDDn1XbNYwHwYDVR0jBBgwFoAUk/osja4W09i9T/2Y -G0yDDn1XbNYwDwYDVR0TAQH/BAUwAwEB/zAqBgNVHREEIzAhhwR/AAABgglsb2Nh -bGhvc3SCDmtleWNsb2FrLW5naW54MA0GCSqGSIb3DQEBCwUAA4IBAQAHSgjPTOQw -4EJZrcYoe+wbsnxj6i6BlgHl/SeUEDgkjczOoFpV/FdQmluCKa8I158r28rmjGSz -keMDdLKhq226k3VNnQt3JRel6sFRqQXX1KNNNXPxNZ3NHFYf+jWf+ryBD3rzd36c -dIZvYRyv714fwNvZR4tAn7+f/tPnwlf3Ge4NXm1bZz03X6WJK9vf06WNBQ+BnvVG -n/YpfM8SeryJCAdl3eGFzexWOR3uGhtAvq3QlcxBHdjHsz/b7j/Lpgdn5LXw4hmI -b2K5XqZg9qRQ7Ccj0dfSDG/0/fXQp4dCPdM3Xo0q+bNF++jAQIhQjWRpB96G+PZB -HkedHM8GFfAW ------END CERTIFICATE----- diff --git a/.devcontainer/certs/nginx-tls.crt.license b/.devcontainer/certs/nginx-tls.crt.license deleted file mode 100644 index 2c78c6f04bc..00000000000 --- a/.devcontainer/certs/nginx-tls.crt.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 Sequent Tech Inc - -SPDX-License-Identifier: AGPL-3.0-only diff --git a/.devcontainer/certs/nginx-tls.key b/.devcontainer/certs/nginx-tls.key deleted file mode 100644 index 463ccf56ef7..00000000000 --- a/.devcontainer/certs/nginx-tls.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzOIL1nOvXKncb -9z8K2tGOwfno+vs7bZJzS2vbSn/mq8RDK2IMo/HxQj6R8cPBR9qkuwm28rA+/8lj -VtL+cpPj9NUBWaddIHa8MKSEqiCmgygLuxG7/aHwNG8bNdYGc4JDg/PTVE1yDJwx -dAvxQ8yfdafru3fZtX01sogjBY+ZDJe+uRRX0iflXPJeUH2jws1CBsZHQAKHRWri -UFMrUCA60weia84k+L8L54C0BupJjgtLO1QyubN1qFcZRNBO7UuGZkRYnK5zNZ7a -IP0Y7kpAiwnvsWinhPhrYVwM1oR33X4j9ZqyUCOkOU8I2FOJ0Iubeg+cM6akHuRB -lw//54+3AgMBAAECggEAJpVK9E+VIlxdCiM/ajnTlZVDFRo7Tf734WRiUvKF+IYQ -GZnUYP5GjvcA+3B+ATVT59TwcXdr5BQzKfS+NdM67X9+/+xqMI1VoowiTwFFSDPC -QkxCtCMVYt4W8Ve0vOzPJXPZ4SwsBBqc4bSnAYjkG4w7228fjToaO5402r1Ovdbw -9e1J3WhVOszZfIFa1maR9vN8Isfs0Uj1YyXkoX8BeopQT2y9AChHWWOBBjWucNWc -0B4wyODDooHA8fcCaPqjqdwPc6/LaM/69iFq6wtUWh2kmthq0oHSrd0Hc1yhytY+ -qjgQ5NVSy6GXqSsk4t4jVF56o87E14ex8lECtMEcIQKBgQDWrDSKEtZjosaf1XjX -cf0CA9cvlvbVXDY55ILqT37c0BCwP45kMH/U8RBsJV8I1mibF6/YdDqaOy7LHqMh -wJ9ARi5LxWxB1ZhXbhVYMnMlczvx7exP+VgyEdhwDnu6VENqgZ0D6QRJuJtkAtW+ -/bskOrSSExgBxtyDNO+3hIsNpwKBgQDVuR2S+/wZjgMhI5AeGOzsw69AWj5n6oYL -HzzNaHrS22v5+7shnW2n3ZGrmMklUZ1FRsck4qqiy3EenAJUnTNXgL7L9ly7NNdd -DFhEavxFRcB7xjzRyph1bmNbbf71L/UicyOa7NG1poQJ7YTEA0rMogpd1P+wlIbP -sa2fijBPcQKBgQDTZ1R6IKDmD9egcV4GdOeew193Kyi65os0KdfOnSimlXPJzV0R -Rz2r2xW8GnVjQ5pSGVp/Q76uQdgMCYjG5WUuq65NygaZMppKh0sXxt7kI34F+rK7 -8yYqcK5/c5Pe47pcOXYB8sfZPaYxMCWEZ2STAgjuzUTYAxkHUOs9A5bYgwKBgEPu -nh7dJ2l8ATSkWCeo5o3vOSIsTQ+8tLSOTkZAAmJJSk3gHL/3iSZFQAsZ+j15bpLZ -h20K+w/ZuG/xSLBaczpJ+ttbyGfcyOOTi+7bTqo6HI97GFPaoJEa8Evcb6DBiJpV -nKiew52IewmozH19SmY2zzK6BDe9TghwQm6mfuqRAoGATBVr+eXR490dhGaSnWtO -FwFogMMI+t8zcFPRavnrdPrmMNdSRgGHklSG4rMXH/D3fdnI3l98y0WIrVt/FemT -nP0+zgZ/fYaFE0VMCwBCuaduSAMmbgDz5JFkVwXBac868YMy47RdGC0n4/rLVexN -ZCYesO7QN68LkQCCMZoSlbo= ------END PRIVATE KEY----- diff --git a/.devcontainer/certs/nginx-tls.key.license b/.devcontainer/certs/nginx-tls.key.license deleted file mode 100644 index 2c78c6f04bc..00000000000 --- a/.devcontainer/certs/nginx-tls.key.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 Sequent Tech Inc - -SPDX-License-Identifier: AGPL-3.0-only diff --git a/.devcontainer/docker-compose-airgap-preparation.yml b/.devcontainer/docker-compose-airgap-preparation.yml index 7f7e1c04e0e..d16aa8a13d9 100644 --- a/.devcontainer/docker-compose-airgap-preparation.yml +++ b/.devcontainer/docker-compose-airgap-preparation.yml @@ -111,8 +111,8 @@ services: - "8200:8200" environment: VAULT_ADDR: 'http://127.0.0.1:8200' - VAULT_UNSEAL_KEY: ciWE5G/CT7/uo5mfaGeRvSyuGRnbtijzvLDg3ru/jv0= - VAULT_TOKEN: hvs.s0djsk0LLBI19K0DkW4Fajs7 + VAULT_UNSEAL_KEY: ${VAULT_UNSEAL_KEY:?Supply VAULT_UNSEAL_KEY for this deployment} + VAULT_TOKEN: ${VAULT_TOKEN:?Supply VAULT_TOKEN for this deployment} cap_add: - IPC_LOCK healthcheck: diff --git a/.devcontainer/docker-compose-base.yml b/.devcontainer/docker-compose-base.yml index e5c6f316b30..e4f8389a16e 100644 --- a/.devcontainer/docker-compose-base.yml +++ b/.devcontainer/docker-compose-base.yml @@ -181,8 +181,8 @@ services: # environment: # VAULT_API_ADDR: 'http://0.0.0.0:8200' # VAULT_ADDR: 'http://0.0.0.0:8201' - # VAULT_UNSEAL_KEY: ciWE5G/CT7/uo5mfaGeRvSyuGRnbtijzvLDg3ru/jv0= - # VAULT_TOKEN: hvs.s0djsk0LLBI19K0DkW4Fajs7 + # VAULT_UNSEAL_KEY: ${VAULT_UNSEAL_KEY:?Supply VAULT_UNSEAL_KEY for this deployment} + # VAULT_TOKEN: ${VAULT_TOKEN:?Supply VAULT_TOKEN for this deployment} # cap_add: # - IPC_LOCK # volumes: diff --git a/.devcontainer/docker-compose-remote.yml b/.devcontainer/docker-compose-remote.yml index 118b9e4e091..38d4c0425d0 100644 --- a/.devcontainer/docker-compose-remote.yml +++ b/.devcontainer/docker-compose-remote.yml @@ -56,6 +56,11 @@ services: KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} SUPER_ADMIN_TENANT_ID: ${SUPER_ADMIN_TENANT_ID} KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + KEYCLOAK_IVR_SERVICE_CLIENT_SECRET: ${KEYCLOAK_IVR_SERVICE_CLIENT_SECRET} + KEYCLOAK_IVR_VOTING_CLIENT_SECRET: ${KEYCLOAK_IVR_VOTING_CLIENT_SECRET} + KEYCLOAK_CERTIFICATES_CLIENT_SECRET: ${KEYCLOAK_CERTIFICATES_CLIENT_SECRET} + SSP_SIGNING_CERTIFICATE: ${SSP_SIGNING_CERTIFICATE} + KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE: ${KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE} APP_VERSION: ${APP_VERSION} APP_HASH: ${APP_HASH} KC_OTP_RESEND_INTERVAL: ${KC_OTP_RESEND_INTERVAL} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 0bb7a8adedb..45a3ddcf2c6 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -38,6 +38,11 @@ services: KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} SUPER_ADMIN_TENANT_ID: ${SUPER_ADMIN_TENANT_ID} KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + KEYCLOAK_IVR_SERVICE_CLIENT_SECRET: ${KEYCLOAK_IVR_SERVICE_CLIENT_SECRET} + KEYCLOAK_IVR_VOTING_CLIENT_SECRET: ${KEYCLOAK_IVR_VOTING_CLIENT_SECRET} + KEYCLOAK_CERTIFICATES_CLIENT_SECRET: ${KEYCLOAK_CERTIFICATES_CLIENT_SECRET} + SSP_SIGNING_CERTIFICATE: ${SSP_SIGNING_CERTIFICATE} + KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE: ${KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE} APP_VERSION: ${APP_VERSION} APP_HASH: ${APP_HASH} KC_OTP_RESEND_INTERVAL: ${KC_OTP_RESEND_INTERVAL} diff --git a/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json b/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json index 358c468f5a4..02fa8c0b5e6 100644 --- a/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json +++ b/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json @@ -847,7 +847,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "${KEYCLOAK_IVR_SERVICE_CLIENT_SECRET}", "redirectUris": [ "/*" ], @@ -980,7 +980,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "**********", + "secret": "${KEYCLOAK_IVR_SERVICE_CLIENT_SECRET}", "redirectUris": [ "/*" ], @@ -1819,7 +1819,6 @@ "attributes": { "saml.useMetadataDescriptorUrl": "false", "realm_client": "false", - "saml.signing.private.key": "MIIJKQIBAAKCAgEApsx31rogbn/SlP+PrbsZpaOXobssac9VePfb79ZLCTQQEL6kI15EOmW7ZvFERneHcLWkC1m/J7BIxvBcJn2HLAESqkFu+BnwXVdxLBsy67hlP4wZnIxhKj8uuV5VqNyvyDQuHABF8UQr2Mfm1GhBst6AIwb/DycF6pmBpwj8kcprwRXd4/Sdp9ZwSw///GeGvCuzjKfE8SRrpdmFGkpanbb4bPN7jTWXwVr1IpdkVJ6VXgOZsCp0r/vjFlkkJ0+CYyk3s1r+rnNe8pzOkgRZVGfN3RLaO8gTW22T9j34uEcGQw4cifJJ0kDwacW7ufgGkc7uDriiF6gyzlpRVamx2ZZMu9h0UN6dJRPulkM6KjGpCuYVCIMZN9F/WjG09e9tNOgmOJCSc6/gfqDL01FsE4pQBlnDl1tJOS+SoiOEtZNnKfJ0YTkMvYOW6SCDT9YOlsdP9jHTyruuNJdcPnH/6VrvkVWEkqhAAVangI0jtZy5ruZ7h2Cbh99FtPVXECGzMl0OrESCSrZ3TltKoGHNHBM4HmTO9+4S7zllwnc+ntS/8mCP0yJEqx0ockBglMSawe8YNWd8TOUrzRepXlPxHmzwlrf+XdI3msbUpZlqLWZRgH+9diw2DzE6l4yC71FEw15iCLvWtXunpoejJrJYv2NT4DEjhfnnMSEIUUdd/x0CAwEAAQKCAgALJhqSfzFgwWl1bRmQ7rMZp7Qgas7eqa2YXtTUFJpCXLkALYXyGUBtONSMXm8GWMH0TifPXGnTAMz0gyddqUFKNy4ktCfzLGStEIWvgYfnSpV16Grj1FxAQyXf3QbTuSCEvLwiQgBqzngxzrbGlsthZc7hyHtu61hRZH5OiGK9ae7G4qvIq1aIW+R4l/Kvz8mpNTCUo7gduCX72Wy0o6in5w121tV4zE6+7VH6RG0/3LO2K35v0H6pLqhwm3e3qvBXqW8gLWJdjbeYdtO13dWMtz2C29o7abWlhE8CBAVmZ9Tq0Xwl2kyF4bJQF5fjJ8lkoJS35i5rb2Ip+c5679ND0MsZ5UWYqd8sSlrnYYmLxW+BHs1UW0XgxZXJScVp5Ug/46y/igEgL4bP4cN8SuIW9Yxg7lgF37Ml01KhThWxHGHXvbfuAVRHvDIhHx1+QYoqU1YQByopdzuPSYL/UVoune6L4dqKt1tVPBWEPkVOpK/7FeiZJvoOTZmm1/LexS8Pa1R2VqpKGgoGdTYUVqVBfeBEmqVx7oK7CV+/NDOjx45AYImrYAoPO6tbMunxi6G0G0wwycpQ/OR09fYxHEkHg5Rt0YHRGDooB0gDRzabJKtb4LkQVfEFx5co7bk54Zsl2x6H2YtiSxzhqbFrPps6Scg9cEyB+DGTK3z868bvbwKCAQEA1wmmI7v3PiUpOQfs9pkEGt3OyP+6pSM01VQUbZ5V0io5el/i73vUHynKPDF2w80XFfekJYSY0WiUoby6lbYFUSpthjgdDO2t4ji6uPIQnfk6KxWzXf3Okqqou/xtAGaMBp/kmf9om1IFMCO0JWXsf9pZ60QI+fiLJDbUP5I0JS72KuOwgE0SnSsleQCa5DFrYCksYd1jQB5YW1nEcBtxvitDGh1IkorYS8MJmn+apUbvgzdDEcfdRn7qyQbiaz2HoTLYlKHqLUp9EuAdHy+KZgTRuTJQCPdVnym3vfqLq4FOhcaQ1zWoj9JBVlLX/oOq+e8XhWKcHVjd40wHHIasHwKCAQEAxpJvtlPk5KZCa7S9M631YTPAwaBTWr9lZsp5A+TadL0zelxW7e2spao0I+M0aXt2ne90TsfojUQMlJaMFN1CEHLJXSXP3ChZcUdt4t7sHUOCN4LpMfkSMtSWhKkkYUtKGHzNSjU2kBFlDnshTY6SCFo3kBWBwcNRX4NiASUoMKfZSpLpHcpJO2Pqg4ZyQ9z16jYaE+N98tkAo55hyhNta41Lu8s68+0tVFPa4kxdSsN9Cg+E2NcER/PPfnWn25pERWOajSkMDinZrnWYn4VkEtM9bMCtZg0YgEYzS9mdqOrn/PbQKd4o6iDp9z966VYnBkbC+H1Pn9zRtBGl2HKtQwKCAQEAvkL5TvDGx6OVn7JVhbLJoQPULJ+oAujtnDrygDLx+CwIAzDH1+EWRCRznWKelIvn+zXLdK6nj9EagSg9Zq1Qcwo5MeKAQaoiieBrxavX7A5R20EuHOGuXrLE7N9hilMxXcMnPuJvKc4ebQOKJvTRliYaHUFrN9EjO32Pulef765nfHdM+Zg3+fvtAez5HyJiwQhtDoIViOvQfbMlTZPaxcn9ayOxzIcw7Ye5/Ysdx1LGJLFGlHrkW7V8AzYmK4CbepNEce35Bu27UHIfEjK8H5vv/A56JhTBGzyMKHmADtUwJskT6na9l9MOoFRYbAcPxmg3xXsEairw2GWyfRjjPQKCAQAyWSTcHQ93bjR/LLegZ+ZEvCOOtMENzGPAmWvvsdI2MVbhGpKsB0x0QOCxjMVYTYa46uMZqpRrZSqvbjCchbFfjEtEQ/cdQlnTYCIbmRxTGy/kNVw9YILdwotbnatsw/NInDRNMYrhIpf2AQBjsteSKJXYAawB3i0+w4hQ9APpulVdFDzpYcYJuf7XVHqafGcZ+JfKHS9tKNqBogH3g8+FqrDeL9yFxshsjkp8gJJTB5gnueGcPYCHYelljh1+j/HpiiTAz/yPhsr78jM9l0vVfhwf/Aa7Tmy88Y/GFqpkDxO9TzNeukazfp70IvNeQIrqi3yaFuPei9XcC3F6nTETAoIBAQDEWyrEckkRc6hYo2UYuyMvYO7RS6R8RiJN1rYiAzfDmwbELfuOWEtsdKQ7vGKhRLBmTOwVLi0lYpS1+x+y85HjtVem1jl6LcvTKQCZJ3OWMC9kTIwKkzUS708nclNqEY4JhOF6HSxpENbrk4rqR66v8cotktt1Ncot0ergVp2GMDZ4/g3SytGPrGayLwHuPzLXMxBjrbC9hYv4GLnTXcacZ644UUO/I3xNPBZbEi5bl9JW46B7qpqqtwyhVmps9YGRvy/6HLwbslISY1S4u2RJHb2xHevlJVAP4qeG93401h0JhtCalJukzq8qhJOkuh9n01/JfuzBJXrU6DXXmyAL", "dpop.bound.access.tokens": "false", "standard.token.exchange.enabled": "false", "saml_assertion_consumer_url_post": "http://localhost:8090/realms/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559/redirect-provider/redirect", @@ -1830,7 +1829,7 @@ "saml.server.signature.keyinfo.ext": "false", "oauth2.device.authorization.grant.enabled": "false", "display.on.consent.screen": "false", - "saml.signing.certificate": "MIIEmzCCAoMCBgGaJQNFJzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZ2cC1zc28wHhcNMjUxMDI3MDkzMTAyWhcNMjgxMDI3MDkzMjQxWjARMQ8wDQYDVQQDDAZ2cC1zc28wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCmzHfWuiBuf9KU/4+tuxmlo5ehuyxpz1V499vv1ksJNBAQvqQjXkQ6Zbtm8URGd4dwtaQLWb8nsEjG8FwmfYcsARKqQW74GfBdV3EsGzLruGU/jBmcjGEqPy65XlWo3K/INC4cAEXxRCvYx+bUaEGy3oAjBv8PJwXqmYGnCPyRymvBFd3j9J2n1nBLD//8Z4a8K7OMp8TxJGul2YUaSlqdtvhs83uNNZfBWvUil2RUnpVeA5mwKnSv++MWWSQnT4JjKTezWv6uc17ynM6SBFlUZ83dEto7yBNbbZP2Pfi4RwZDDhyJ8knSQPBpxbu5+AaRzu4OuKIXqDLOWlFVqbHZlky72HRQ3p0lE+6WQzoqMakK5hUIgxk30X9aMbT172006CY4kJJzr+B+oMvTUWwTilAGWcOXW0k5L5KiI4S1k2cp8nRhOQy9g5bpIINP1g6Wx0/2MdPKu640l1w+cf/pWu+RVYSSqEABVqeAjSO1nLmu5nuHYJuH30W09VcQIbMyXQ6sRIJKtndOW0qgYc0cEzgeZM737hLvOWXCdz6e1L/yYI/TIkSrHShyQGCUxJrB7xg1Z3xM5SvNF6leU/EebPCWt/5d0jeaxtSlmWotZlGAf712LDYPMTqXjILvUUTDXmIIu9a1e6emh6Mmsli/Y1PgMSOF+ecxIQhRR13/HQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQBG+bZ7YQmMbHOrF6gkD9/AyNg8OkBjdj/WPjTSZBetFRL7GHIrSbEGmr32F+6so0WanRavIWJ9Gn7TYqy7USCWLLFl2r2ona59ibgJEQ85MCis/uWiSSy4u1NOGXUbSjnafQNCHj6iaixL0It9IBdSjBU7nDNp7nhwzPk+Qu3+kGNFA7wREAqJ4yFIX5oRLRu6FOhfY+Kd8XIEfzOT2gOn7yIK+S2tCMC8kSwK65scnbJO9VklKwIqn2EfMpw3F9sIaSF+xBhB88g9lc59MTu8HSGowYpxzCTVeOdGn7YEm9/w420BaHwmysx6DVqj0yG7zOSJcBTlIv1RUTasNuVWfZXJljIaFNEUwGwa4Q/l4yGTOa/tQsuZNC36SE1WSJoBifdqKtAG7ScsjBTMXE4iXKsyEHRPtqDHDvgRtJ9RbAZod4nK57dsWoggpKe7+JkwYvvgIv5tIbCW+uXBiRJmwapnyTQNewsneh7oILeAEQWnTEbHOUL8vGDYdsl6jNTLWQkxz96EZx4MxW6y3uFmRrvvyZmQP4R/E9HlIZWA0GTZSXRC1+n5f4wA5e7B6s4/Wkq84IV78DxIBhp7y43QaJHTGbWnIq9j5vxuA1cxqSWosIg7I32Euguzh08AD6q5przm4iJ4WWi7zt+cO8SmbM9nMrkrmZKh2uG5k8AqLg==", + "saml.signing.certificate": "${KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE}", "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#", "saml.server.signature": "true", "saml.assertion.signature": "false", @@ -1948,7 +1947,7 @@ "https://127.0.0.1:8443/*" ], "rootUrl": "http://localhost:3000/", - "secret": "**********", + "secret": "${KEYCLOAK_IVR_SERVICE_CLIENT_SECRET}", "serviceAccountsEnabled": false, "standardFlowEnabled": true, "surrogateAuthRequired": false, @@ -2532,11 +2531,11 @@ "singleSignOnServiceUrl": "http://127.0.0.1:8083/simplesaml/saml2/idp/SSOService.php", "wantAuthnRequestsSigned": "true", "allowedClockSkew": "0", - "encryptionPublicKey": "MIICmjCCAYICCQDX5sKPsYV3+jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTE5MTIyMzA5MDI1MVoXDTIwMDEyMjA5MDI1MVowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdtDJ278DQTp84O5Nq5F8s5YOR34GFOGI2Swb/3pU7X7918lVljiKv7WVM65S59nJSyXV+fa15qoXLfsdRnq3yw0hTSTs2YDX+jl98kK3ksk3rROfYh1LIgByj4/4NeNpExgeB6rQk5Ay7YS+ARmMzEjXa0favHxu5BOdB2y6WvRQyjPS2lirT/PKWBZc04QZepsZ56+W7bd557tdedcYdY/nKI1qmSQClG2qgslzgqFOv1KCOw43a3mcK/TiiD8IXyLMJNC6OFW3xTL/BG6SOZ3dQ9rjQOBga+6GIaQsDjC4Xp7Kx+FkSvgaw0sJV8gt1mlZy+27Sza6d+hHD2pWECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAm2fk1+gd08FQxK7TL04O8EK1f0bzaGGUxWzlh98a3Dm8+OPhVQRi/KLsFHliLC86lsZQKunYdDB+qd0KUk2oqDG6tstG/htmRYD/S/jNmt8gyPAVi11dHUqW3IvQgJLwxZtoAv6PNs188hvT1WK3VWJ4YgFKYi5XQYnR5sv69Vsr91lYAxyrIlMKahjSW1jTD3ByRfAQghsSLk6fV0OyJHyhuF1TxOVBVf8XOdaqfmvD90JGIPGtfMLPUX4m35qaGAU48PwCL7L3cRHYs9wZWc0ifXZcBENLtHYCLi5txR8c5lyHB9d3AQHzKHMFNjLswn5HsckKg83RH7+eVqHqGw==", + "encryptionPublicKey": "${SSP_SIGNING_CERTIFICATE}", "guiOrder": "0", "hideOnLoginPage": "false", "validateSignature": "true", - "signingCertificate": "MIICmjCCAYICCQDX5sKPsYV3+jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTE5MTIyMzA5MDI1MVoXDTIwMDEyMjA5MDI1MVowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdtDJ278DQTp84O5Nq5F8s5YOR34GFOGI2Swb/3pU7X7918lVljiKv7WVM65S59nJSyXV+fa15qoXLfsdRnq3yw0hTSTs2YDX+jl98kK3ksk3rROfYh1LIgByj4/4NeNpExgeB6rQk5Ay7YS+ARmMzEjXa0favHxu5BOdB2y6WvRQyjPS2lirT/PKWBZc04QZepsZ56+W7bd557tdedcYdY/nKI1qmSQClG2qgslzgqFOv1KCOw43a3mcK/TiiD8IXyLMJNC6OFW3xTL/BG6SOZ3dQ9rjQOBga+6GIaQsDjC4Xp7Kx+FkSvgaw0sJV8gt1mlZy+27Sza6d+hHD2pWECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAm2fk1+gd08FQxK7TL04O8EK1f0bzaGGUxWzlh98a3Dm8+OPhVQRi/KLsFHliLC86lsZQKunYdDB+qd0KUk2oqDG6tstG/htmRYD/S/jNmt8gyPAVi11dHUqW3IvQgJLwxZtoAv6PNs188hvT1WK3VWJ4YgFKYi5XQYnR5sv69Vsr91lYAxyrIlMKahjSW1jTD3ByRfAQghsSLk6fV0OyJHyhuF1TxOVBVf8XOdaqfmvD90JGIPGtfMLPUX4m35qaGAU48PwCL7L3cRHYs9wZWc0ifXZcBENLtHYCLi5txR8c5lyHB9d3AQHzKHMFNjLswn5HsckKg83RH7+eVqHqGw==", + "signingCertificate": "${SSP_SIGNING_CERTIFICATE}", "principalAttribute": "email", "nameIDPolicyFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "entityId": "tenant-49d8de09-df08-401e-8dad-a31002a5a01e-event-379c316c-afaa-4280-b4d6-d186a967176f", @@ -2580,7 +2579,7 @@ "loginHint": "false", "logoutUrl": "http://127.0.0.1:8090/realms/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559/protocol/openid-connect/logout", "sendIdTokenOnLogout": "true", - "clientSecret": "**********", + "clientSecret": "${KEYCLOAK_CERTIFICATES_CLIENT_SECRET}", "uiLocales": "false", "acceptsPromptNoneForwardFromClient": "false", "clientId": "voting-portal-certs", @@ -3914,7 +3913,7 @@ "config": { "recaptchaActionName": "forgotpassword", "recaptchaSiteKey": "6LfJ_RspAAAAAAX_gikmCVVs1MP8DPWV71WYUKUK", - "siteSecret": "6LfJ_RspAAAAAOAA1TlNmGRgzUANakib7MSQe1ae", + "siteSecret": "", "recaptchaEnabled": "false", "recaptchaMinScore": "0.5" } @@ -4021,7 +4020,7 @@ "doc-id-type": "sequent.read-only.id-card-type", "attributes-to-store": "{ \t\"philSysID\": [ \t\t{ \t\t\t\"UserAttribute\": \"firstName\", \t\t\t\"inetumAttributePath\": \"/response/ocr/given_names\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"lastName\", \t\t\t\"inetumAttributePath\": \"/response/ocr/surname\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"sequent.read-only.id-card-number\", \t\t\t\"inetumAttributePath\": \"/response/ocr/personal_number\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"birthDate\", \t\t\t\"inetumAttributePath\": \"/response/ocr/date_of_birth\", \t\t\t\"type\": \"date\", \t\t\t\"storeDateFormat\": \"yyyy-MM-dd\", \t\t\t\"inetumDateFormat\": \"dd/MM/yyyy\" \t\t} \t], \t\"seamanBook\": [ \t\t{ \t\t\t\"UserAttribute\": \"firstName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/given_names\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"lastName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/surname\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"sequent.read-only.id-card-number\", \t\t\t\"inetumAttributePath\": \"/response/mrz/personal_number\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"birthDate\", \t\t\t\"inetumAttributePath\": \"/response/mrz/date_of_birth\", \t\t\t\"type\": \"date\", \t\t\t\"storeDateFormat\": \"yyyy-MM-dd\", \t\t\t\"inetumDateFormat\": \"dd/MM/yyyy\" \t\t} \t], \t\"Philippine Passport\": [ \t\t{ \t\t\t\"UserAttribute\": \"firstName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/given_names\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"lastName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/surname\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"sequent.read-only.id-card-number\", \t\t\t\"inetumAttributePath\": \"/response/mrz/document_number\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"birthDate\", \t\t\t\"inetumAttributePath\": \"/response/mrz/date_of_birth\", \t\t\t\"type\": \"date\", \t\t\t\"storeDateFormat\": \"yyyy-MM-dd\", \t\t\t\"inetumDateFormat\": \"dd/MM/yyyy\" \t\t} \t], \t\"Drivers License\": [ \t\t{ \t\t\t\"UserAttribute\": \"firstName\", \t\t\t\"inetumAttributePath\": \"/response/ocr/given_names\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"lastName\", \t\t\t\"inetumAttributePath\": \"/response/ocr/surname\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"sequent.read-only.id-card-number\", \t\t\t\"inetumAttributePath\": \"/response/ocr/personal_number\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"birthDate\", \t\t\t\"inetumAttributePath\": \"/response/ocr/date_of_birth\", \t\t\t\"type\": \"date\", \t\t\t\"storeDateFormat\": \"yyyy-MM-dd\", \t\t\t\"inetumDateFormat\": \"dd/MM/yyyy\" \t\t} \t], \t\"iBP\": [ \t\t{ \t\t\t\"UserAttribute\": \"firstName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/given_names\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"lastName\", \t\t\t\"inetumAttributePath\": \"/response/mrz/surname\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"sequent.read-only.id-card-number\", \t\t\t\"inetumAttributePath\": \"/response/mrz/personal_number\", \t\t\t\"type\": \"text\" \t\t}, \t\t{ \t\t\t\"UserAttribute\": \"birthDate\", \t\t\t\"inetumAttributePath\": \"/response/mrz/date_of_birth\", \t\t\t\"type\": \"date\", \t\t\t\"storeDateFormat\": \"yyyy-MM-dd\", \t\t\t\"inetumDateFormat\": \"dd/MM/yyyy\" \t\t} \t] }", "attributes-to-validate": "{ \t\"philSysID\": [ \t\t{ \t\t\t\"type\": \"equalValue\", \t\t\t\"equalValue\": \"Identity Card\", \t\t\t\"inetumAttributePath\": \"/response/docVerification/documentIdentification/0/type\", \t\t\t\"errorMsg\": \"attributesInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreDocumental\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreFacial\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t} \t], \t\"seamanBook\": [ \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreValCamposCriticos\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreFacial\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t} \t], \t\"Philippine Passport\": [ \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreValCamposCriticos\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreFacial\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t} \t], \t\"Drivers License\": [ \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreValCamposCriticos\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreFacial\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t} \t], \t\"iBP\": [ \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreValCamposCriticos\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t}, \t\t{ \t\t\t\"type\": \"intMinValue\", \t\t\t\"intMinValue\": \"20\", \t\t\t\"inetumAttributePath\": \"/response/resultData/scoreFacial\", \t\t\t\"errorMsg\": \"scoringInetumError\" \t\t} \t] }", - "api-key": "196c9b53-9fab-48c6-980e-1e68d6685240", + "api-key": "", "user-status": "sequent.read-only.id-card-number-validated", "sdk": "{}", "client-id": "demosequentech", @@ -4222,7 +4221,7 @@ "config": { "recaptchaSiteKey": "6LfJ_RspAAAAAAX_gikmCVVs1MP8DPWV71WYUKUK", "recaptchaActionName": "login", - "siteSecret": "6LfJ_RspAAAAAOAA1TlNmGRgzUANakib7MSQe1ae", + "siteSecret": "", "passwordExpirationUserAttribute": "sequent.read-only.expirationDate", "recaptchaEnabled": "false", "recaptchaMinScore": "0.5" diff --git a/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json b/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json index 9244892665a..7e034b22fb8 100644 --- a/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json +++ b/.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json @@ -3355,7 +3355,6 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "4lzmxNgZHjfzS5BwDVlyrRUDqwvFLUvL", "redirectUris": [ "*" ], @@ -3631,7 +3630,6 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "wBy8rpuKQxPWikQ3rIFv9g42t0WK0Xiu", "redirectUris": [ "*" ], @@ -3878,7 +3876,7 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "secret": "zh0GWEjbynXJDcpF1YipFXUleEKTQiO0", + "secret": "${KEYCLOAK_CLIENT_SECRET}", "redirectUris": [ "/*" ], diff --git a/.devcontainer/scripts/build-sequent-core.sh b/.devcontainer/scripts/build-sequent-core.sh index e960bba3d2d..dc71cd495a9 100755 --- a/.devcontainer/scripts/build-sequent-core.sh +++ b/.devcontainer/scripts/build-sequent-core.sh @@ -20,29 +20,14 @@ which wasm-bindgen wasm-bindgen --version wasm-pack build --mode no-install --out-name index --release --target web --features=wasmtest,default_features -wasm-pack -v pack . 2>&1 | tee output.log +# The package is a dependency of the workspace packages as a directory (file:./rust/pkg), +# not as a committed tarball, so it is copied into place and yarn.lock never changes. cd .. -hash=$(grep "shasum:" sequent-core/output.log | awk '{printf $4}') -awk -v hash="${hash}" ' - /^"sequent-core@file:/ { in_sequent = 1 } - /^"[^"]+":$/ && !/^"sequent-core@file:/ { in_sequent = 0 } - /sequent-core-0.1.0.tgz#/ { - sub(/#.*/, "#"hash"\"") - } - /^ uid "/ && in_sequent { - sub(/"[^"]*"$/, "\""hash"\"") - } - { print } -' yarn.lock > yarn.lock.tmp - -mv yarn.lock.tmp yarn.lock - -rm sequent-core/output.log -rm ./ui-core/rust/sequent-core-0.1.0.tgz ./admin-portal/rust/sequent-core-0.1.0.tgz ./voting-portal/rust/sequent-core-0.1.0.tgz ./ballot-verifier/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./ui-core/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./admin-portal/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./voting-portal/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./ballot-verifier/rust/sequent-core-0.1.0.tgz +for dir in ui-core admin-portal voting-portal ballot-verifier; do + rm -rf "./${dir}/rust/pkg" + mkdir -p "./${dir}/rust" + cp -a sequent-core/pkg "./${dir}/rust/pkg" +done rm -rf node_modules ui-core/node_modules voting-portal/node_modules ballot-verifier/node_modules admin-portal/node_modules diff --git a/.devcontainer/scripts/initialize-command.sh b/.devcontainer/scripts/initialize-command.sh index fc610e09e9d..3736989a3b3 100755 --- a/.devcontainer/scripts/initialize-command.sh +++ b/.devcontainer/scripts/initialize-command.sh @@ -3,19 +3,23 @@ # # SPDX-License-Identifier: AGPL-3.0-only -set -ex -o pipefail +set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# Create .devcontainer/.env if it does not already exists -[ -e .devcontainer/.env ] || touch .devcontainer/.env -cp .devcontainer/.env.development .devcontainer/.env -# Load .devcontainer/.env environment variables -source .devcontainer/.env +# Generate new local credentials only when missing. Reopening the development +# container must preserve credentials already used by its persistent databases. +python3 "$SCRIPT_DIR/initialize-local-secrets.py" # Record the host workspace path for Docker bind mounts. The Dev Containers CLI # provides localWorkspaceFolder; direct invocations fall back to the repository # root derived from this script's location. workspace_folder="${LOCAL_WORKSPACE_FOLDER:-${localWorkspaceFolder:-$(cd "${SCRIPT_DIR}/../.." && pwd)}}" -printf "\nLOCAL_WORKSPACE_FOLDER=%s\n" "$workspace_folder" >> .devcontainer/.env +python3 - "$SCRIPT_DIR/../.env" "$workspace_folder" <<'PY' +from pathlib import Path +import re, shlex, sys +path = Path(sys.argv[1]) +text = re.sub(r'^LOCAL_WORKSPACE_FOLDER=.*\n?', '', path.read_text(), flags=re.MULTILINE) +path.write_text(text + '\nLOCAL_WORKSPACE_FOLDER=' + shlex.quote(sys.argv[2]) + '\n') +PY echo "$(pwd)/.devcontainer/.env file initialized successfully" diff --git a/.devcontainer/scripts/initialize-local-secrets.py b/.devcontainer/scripts/initialize-local-secrets.py new file mode 100644 index 00000000000..6d4c7bc832c --- /dev/null +++ b/.devcontainer/scripts/initialize-local-secrets.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Sequent Tech Inc +# SPDX-License-Identifier: AGPL-3.0-only +"""Initialize untracked development credentials; preserve existing installations.""" +import argparse +import os +from pathlib import Path +import re +import secrets +import subprocess +import tempfile + + +GENERATED = ( + "AWS_S3_ACCESS_KEY", "AWS_S3_ACCESS_SECRET", "KEYCLOAK_CLIENT_SECRET", + "KEYCLOAK_IVR_SERVICE_CLIENT_SECRET", "KEYCLOAK_IVR_VOTING_CLIENT_SECRET", + "KEYCLOAK_CLI_CLIENT_SECRET", "KEYCLOAK_CERTIFICATES_CLIENT_SECRET", +) + + +def certificate(directory, key_name, cert_name, common_name, sans): + directory.mkdir(parents=True, exist_ok=True) + key, cert = directory / key_name, directory / cert_name + if key.is_symlink() or cert.is_symlink(): + raise ValueError("development certificate files must not be symbolic links") + if key.exists() != cert.exists(): + raise ValueError(f"incomplete certificate pair in {directory}; restore the matching pair") + if key.exists(): + return + with tempfile.TemporaryDirectory(dir=directory) as temporary: + private, public = Path(temporary) / key_name, Path(temporary) / cert_name + subprocess.run([ + "openssl", "req", "-x509", "-newkey", "rsa:3072", "-nodes", "-sha256", + "-days", "365", "-subj", f"/CN={common_name}", "-addext", f"subjectAltName={sans}", + "-keyout", str(private), "-out", str(public), + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + private.chmod(0o600) + public.chmod(0o644) + # Exclusive creation avoids replacing another initializer's key material. + with key.open("xb") as target: + target.write(private.read_bytes()) + with cert.open("xb") as target: + target.write(public.read_bytes()) + cert.chmod(0o644) + + +def initialize(root): + os.umask(0o077) + destination = root / ".env" + if destination.is_symlink(): + raise ValueError("development environment file must not be a symbolic link") + text = (destination if destination.exists() else root / ".env.development").read_text() + certificate(root / "certs", "nginx-tls.key", "nginx-tls.crt", "localhost", + "DNS:localhost,DNS:keycloak-nginx,IP:127.0.0.1") + certificate(root / "simplesamlphp/cert", "server.pem", "server.crt", "localhost development IdP", + "DNS:localhost,DNS:simplesamlphp,IP:127.0.0.1") + certificate(root / "certs", "vp-sso-signing.key", "vp-sso-signing.crt", "local vp-sso client", + "DNS:localhost") + for name in GENERATED: + pattern = re.compile(r"^" + name + r"=(.*)$", re.MULTILINE) + matches = list(pattern.finditer(text)) + if len(matches) > 1: + raise ValueError(f"duplicate environment assignment: {name}") + if matches and matches[0].group(1).strip() not in ("", "''", '""'): + continue + value = secrets.token_hex(10 if name == "AWS_S3_ACCESS_KEY" else 24) + text = pattern.sub(name + "=" + value, text) if matches else text + "\n" + name + "=" + value + "\n" + # Keycloak verifies the local IdP with the public half of the generated pair. + # This public value deliberately follows certificate renewal; private key bytes + # never enter the environment file or the Keycloak realm template. + for variable, cert in [("SSP_SIGNING_CERTIFICATE", root / "simplesamlphp/cert/server.crt"), + ("KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE", root / "certs/vp-sso-signing.crt")]: + public = "".join(line.strip() for line in cert.read_text().splitlines() if not line.startswith("---")) + text = re.sub(r"^" + variable + r"=.*\n?", "", text, flags=re.MULTILINE) + text = text.rstrip() + "\n" + variable + "=" + public + "\n" + with tempfile.NamedTemporaryFile(mode="w", dir=root, delete=False) as temporary: + temporary.write(text) + name = temporary.name + os.replace(name, destination) + destination.chmod(0o600) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + args = parser.parse_args() + initialize(args.root.resolve()) + print("Local development environment and certificates are initialized; existing values were preserved.") diff --git a/.devcontainer/scripts/rebuild-sequent-core-full.sh b/.devcontainer/scripts/rebuild-sequent-core-full.sh index 1db545069f2..0cef7d8976a 100755 --- a/.devcontainer/scripts/rebuild-sequent-core-full.sh +++ b/.devcontainer/scripts/rebuild-sequent-core-full.sh @@ -21,32 +21,14 @@ echo "==> Building sequent-core WASM..." wasm-pack build --mode no-install --out-name index --release --target web \ --features=wasmtest,default_features -echo "==> Packing..." -wasm-pack -v pack . 2>&1 | tee output.log - -echo "==> Updating yarn.lock..." +echo "==> Placing the package where the workspace packages expect it..." cd "$PACKAGES_DIR" -hash=$(grep "shasum:" sequent-core/output.log | awk '{printf $4}') -hash="${hash}\\\"" -awk -v hash="${hash}" ' - /sequent-core-0.1.0.tgz#/ { - sub(/#.*/, "#"hash"") - } - { print } -' yarn.lock > yarn.lock.tmp -mv yarn.lock.tmp yarn.lock -rm sequent-core/output.log - -echo "==> Clearing yarn cache and replacing tgz files..." rm -Rf /home/vscode/.cache/yarn/ -rm -f ./ui-core/rust/sequent-core-0.1.0.tgz \ - ./admin-portal/rust/sequent-core-0.1.0.tgz \ - ./voting-portal/rust/sequent-core-0.1.0.tgz \ - ./ballot-verifier/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./ui-core/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./admin-portal/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./voting-portal/rust/sequent-core-0.1.0.tgz -cp sequent-core/pkg/sequent-core-0.1.0.tgz ./ballot-verifier/rust/sequent-core-0.1.0.tgz +for dir in ui-core admin-portal voting-portal ballot-verifier; do + rm -rf "./${dir}/rust/pkg" + mkdir -p "./${dir}/rust" + cp -a sequent-core/pkg "./${dir}/rust/pkg" +done echo "==> Cleaning node_modules and dist..." rm -rf node_modules */node_modules dist */dist diff --git a/.devcontainer/scripts/tests/test_initialize_local_secrets.py b/.devcontainer/scripts/tests/test_initialize_local_secrets.py new file mode 100644 index 00000000000..3b777050d42 --- /dev/null +++ b/.devcontainer/scripts/tests/test_initialize_local_secrets.py @@ -0,0 +1,60 @@ +import importlib.util +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +SCRIPT = Path(__file__).resolve().parents[1] / "initialize-local-secrets.py" +spec = importlib.util.spec_from_file_location("local_secrets", SCRIPT) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +@unittest.skipUnless(shutil.which("openssl"), "OpenSSL is required for development certificates") +class LocalCredentialsTests(unittest.TestCase): + def test_fresh_installations_differ_and_reinitialization_preserves_state(self): + with tempfile.TemporaryDirectory() as temporary: + roots = [Path(temporary) / name for name in ("first", "second")] + identities = [] + for root in roots: + root.mkdir() + (root / ".env.development").write_text("AWS_S3_ACCESS_KEY=\nCUSTOM=value\n") + module.initialize(root) + paths = [root / ".env", root / "certs/nginx-tls.key", root / "certs/nginx-tls.crt", + root / "simplesamlphp/cert/server.pem", root / "simplesamlphp/cert/server.crt", + root / "certs/vp-sso-signing.key", root / "certs/vp-sso-signing.crt"] + before = [p.read_bytes() for p in paths] + self.assertEqual(paths[0].stat().st_mode & 0o777, 0o600) + self.assertEqual(paths[1].stat().st_mode & 0o777, 0o600) + module.initialize(root) + self.assertEqual([p.read_bytes() for p in paths], before) + env = dict(line.split("=", 1) for line in paths[0].read_text().splitlines() if "=" in line) + self.assertEqual(env["CUSTOM"], "value") + self.assertEqual(len({env[k] for k in module.GENERATED}), len(module.GENERATED)) + for name in module.GENERATED: + self.assertRegex(env[name], "^[0-9a-f]{20}$" if name == "AWS_S3_ACCESS_KEY" else "^[0-9a-f]{48}$") + public = "".join(line for line in paths[4].read_text().splitlines() if not line.startswith("---")) + self.assertEqual(env["SSP_SIGNING_CERTIFICATE"], public) + client_public = "".join(line for line in paths[6].read_text().splitlines() if not line.startswith("---")) + self.assertEqual(env["KEYCLOAK_SAML_CLIENT_SIGNING_CERTIFICATE"], client_public) + for key, cert in [(paths[1], paths[2]), (paths[3], paths[4]), (paths[5], paths[6])]: + actual = subprocess.check_output(["openssl", "pkey", "-in", str(key), "-pubout"]) + expected = subprocess.check_output(["openssl", "x509", "-in", str(cert), "-pubkey", "-noout"]) + self.assertTrue(actual == expected, "certificate does not match generated key") + identities.append(before) + self.assertTrue(all(a != b for a, b in zip(*identities)), "installations share generated material") + + def test_incomplete_existing_pair_is_preserved_and_refused(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + key = root / "nginx.key" + key.write_text("existing-key-must-survive") + with self.assertRaisesRegex(ValueError, "incomplete certificate pair"): + module.certificate(root, key.name, "nginx.crt", "localhost", "DNS:localhost") + self.assertEqual(key.read_text(), "existing-key-must-survive") + self.assertFalse((root / "nginx.crt").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/.devcontainer/simplesamlphp/Dockerfile b/.devcontainer/simplesamlphp/Dockerfile index 21810d21440..6e2acbe0dfd 100644 --- a/.devcontainer/simplesamlphp/Dockerfile +++ b/.devcontainer/simplesamlphp/Dockerfile @@ -82,8 +82,11 @@ RUN mkdir $SSP_DIR/data RUN chmod -R 775 $SSP_DIR/data USER root +# Development signing material is generated locally before this image is built. +# Apache's worker must read the private key; keep it unavailable to other users. +RUN chown www-data:www-data $SSP_DIR/cert/server.pem && chmod 600 $SSP_DIR/cert/server.pem COPY scripts/entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] -CMD ["apache2-foreground"] \ No newline at end of file +CMD ["apache2-foreground"] diff --git a/.devcontainer/simplesamlphp/README.md b/.devcontainer/simplesamlphp/README.md index 0a5274e4b7d..497f55f2b4e 100644 --- a/.devcontainer/simplesamlphp/README.md +++ b/.devcontainer/simplesamlphp/README.md @@ -58,7 +58,7 @@ This guide includes: ### Certificate Management -- **Development:** Self-signed certificates in `cert/` directory +- **Development:** Run `.devcontainer/scripts/initialize-command.sh` from the repository root before building. It generates a fresh local key/certificate pair in `cert/`, excluded from Git, and preserves an existing pair on subsequent starts. Reimport the new public certificate in local Keycloak when renewing the pair. See [local certificates](../certs/README.md). - **Production:** Third-parties must use valid certificates from a trusted CA - **Never commit private keys** to version control diff --git a/.devcontainer/simplesamlphp/cert/private.key b/.devcontainer/simplesamlphp/cert/private.key deleted file mode 100644 index 031c654478c..00000000000 --- a/.devcontainer/simplesamlphp/cert/private.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCz+GJ281TB9R/f -Lrro1HnIXC6by5NlZWnU1CqTAXGeWyXBmTDM2MDGKvhYLvJbNAkxJkkc3yh4brWg -LtweVmr/tZ3iW6UzI6TeVP2GkjR5retB73xoj4ctfZoJcydm+CVvNoC7WPyj/fRg -D2g3TYbgqaG3DZzJ37kJNxsGYTeueF0MMWMBUOGaRYC1uLLLRVNSrU52zfW8tc7d -Z4YmuRF8S9P7vC8jx6uo5tujiwzW8xuoLVfuHndWonLk2Bd6EclQv+aejh9h5quG -0J+XP2PYCweyMbJbSEPc9f3vT3qwgZPIs2CUr2NxzWyskhsyTVFFoXa9WMxiWanr -duAMAI6ZAgMBAAECggEAA2C/B7YHVND4bUHiiA0NkHKV0l6cEIwP1DWQ61ZHLxPN -YzNFofaoiZ+pWSMKcVFKTItIKzFPaxFC5J6J4EzlcGa/hPiM4tHsCIXxBetXw6KS -zEJF5Bgw5JbwraA6cK7pfvX3gmyyvejomGakTAjEZmebWC+lI3neQ9DDt6yFSnJC -SFJjEkgydmskCGa03btBkEA7tBUwXvBV1B1U3TCGvftU0iA5fOkmBXT9KvPRw/GM -VORAH9CdzhEz6eJKFGwFRUrjJTiww7SGmPIYnnK3wzP4NYnz2l314BIm/DLoOoSp -vQEcljcm9vVLzGjUIJm6mOObopMFhNaoMV4+U7VDTQKBgQDy4WCIYWLnkmYBLA6I -+54G2xlcyDEm8EIRLNnyhEWo1c2QvQ/GoaGwmUmTxYUSmyMrD4zvE1UwzabOf3ef -QQehmSSsDTKpBao6T55GydjI1Kf3uSGdbS4oV3QCyylP2SMIH7eRLGex4R0OBgQB -RdzRHvGUJ4taR9vlC6TTQ/VDNQKBgQC9sRFA4Ojyxk/118wQth3UKEI3TOyqJMWU -NzSYHz055FckrZKPkkQr8kAj5IwHOInebuq9BiDZnhZ5zQdDyR8rYXubqnyblS6g -UzwUh/Vu7SyfqGs94Thj1dWzi6ytaje7Bg9fGGr3asrRAJLPlsKhbNTa9+WIeGtG -XoY83cYGVQKBgQDEsypOyTAkgzgBMwECh2TQAJllvuuvsVyQNFa+Rcl0ltLIqZUh -NL2SHFKuToUbMHEm6o0lAe84exTgn/LtEulvQmuku3uPz3qxhtGUWtwqqa7uvQww -Rr/fBBR8PrTSafQpHCa6d8/zC3FQbibIMSBIbFsRtYTyNMgtk2SFhMAZEQKBgQCX -P3rxTyOOh98zt4gHfx5TjnIVo+lEVKORZV8zOB1abp2pKhilD0v5+mN3+wBHtaYA -TFIwu5UvzyQ+o+L6AoAu18B2Qz6t0wplXViXSjzA4K1m9CPmKBENUW/ehCz/7KRZ -JCDXgyGu+2CRuzJUAPJZQeyKV4SWBuYKKs4/89i0LQKBgQDNz6Ndov2PyEy3Auap -IChYvDbnf5iFMGnkD8K1HCU4GCb2Tg9LRcpIAAvwRnJn2P6pRcRzW+15Amk+F7NA -DqL7hqocsnRXsjhOaz5ipr7A2daCH8Eo2/S1941vVmFU9z2DArjcd54b/DeNM+Ip -bfG1l4zV8hmevnd5vVESiHs+1A== ------END PRIVATE KEY----- diff --git a/.devcontainer/simplesamlphp/cert/public.crt b/.devcontainer/simplesamlphp/cert/public.crt deleted file mode 100644 index 6a39591f3eb..00000000000 --- a/.devcontainer/simplesamlphp/cert/public.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDajCCAlKgAwIBAgITIjATCp6HX1/9FGqghSHzEHVf7jANBgkqhkiG9w0BAQsF -ADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY -SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI1MTAxNzA5MTU1M1oXDTM1MTAx -NTA5MTU1M1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAf -BgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBALP4YnbzVMH1H98uuujUechcLpvLk2VladTUKpMBcZ5b -JcGZMMzYwMYq+Fgu8ls0CTEmSRzfKHhutaAu3B5Wav+1neJbpTMjpN5U/YaSNHmt -60HvfGiPhy19mglzJ2b4JW82gLtY/KP99GAPaDdNhuCpobcNnMnfuQk3GwZhN654 -XQwxYwFQ4ZpFgLW4sstFU1KtTnbN9by1zt1nhia5EXxL0/u8LyPHq6jm26OLDNbz -G6gtV+4ed1aicuTYF3oRyVC/5p6OH2Hmq4bQn5c/Y9gLB7IxsltIQ9z1/e9PerCB -k8izYJSvY3HNbKySGzJNUUWhdr1YzGJZqet24AwAjpkCAwEAAaNTMFEwHQYDVR0O -BBYEFDYN4kZyTbtK94hawCtLvoO0L/gWMB8GA1UdIwQYMBaAFDYN4kZyTbtK94ha -wCtLvoO0L/gWMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJbN -FhO6ROHyY8W3p3H/w1iDDwPg5fx+vKlDupb+67yFOU+GayzpRdnHR/yXFEsdh23a -jQWSwlNnPfbW6i65Cx/fw4WcWd0AY1WJakRNnTa1SKwAu55+H++JhVvl6MX5gfQC -R3DoBCCv38ftZoTUvuE11GXlKFJZPSdZvurqRnl7mW/wp3eYXrM0dra6PIjNPEyN -u/Iw/18sJOf5Q6axZLypc/X9XU5/wYbyXRxTyyRbOf/8Kk96e2uobPaV2xJuXlQj -YfE2QiIAFXCDMf9dS0x8xVwRRBn/Zx6EZCU19MSrcivxrz8lXMk6V0DAKpdMk2cm -t+VTxjgM8OtVA/Z6jBo= ------END CERTIFICATE----- diff --git a/.devcontainer/simplesamlphp/cert/server.crt b/.devcontainer/simplesamlphp/cert/server.crt deleted file mode 100644 index 6a39591f3eb..00000000000 --- a/.devcontainer/simplesamlphp/cert/server.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDajCCAlKgAwIBAgITIjATCp6HX1/9FGqghSHzEHVf7jANBgkqhkiG9w0BAQsF -ADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY -SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI1MTAxNzA5MTU1M1oXDTM1MTAx -NTA5MTU1M1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAf -BgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBALP4YnbzVMH1H98uuujUechcLpvLk2VladTUKpMBcZ5b -JcGZMMzYwMYq+Fgu8ls0CTEmSRzfKHhutaAu3B5Wav+1neJbpTMjpN5U/YaSNHmt -60HvfGiPhy19mglzJ2b4JW82gLtY/KP99GAPaDdNhuCpobcNnMnfuQk3GwZhN654 -XQwxYwFQ4ZpFgLW4sstFU1KtTnbN9by1zt1nhia5EXxL0/u8LyPHq6jm26OLDNbz -G6gtV+4ed1aicuTYF3oRyVC/5p6OH2Hmq4bQn5c/Y9gLB7IxsltIQ9z1/e9PerCB -k8izYJSvY3HNbKySGzJNUUWhdr1YzGJZqet24AwAjpkCAwEAAaNTMFEwHQYDVR0O -BBYEFDYN4kZyTbtK94hawCtLvoO0L/gWMB8GA1UdIwQYMBaAFDYN4kZyTbtK94ha -wCtLvoO0L/gWMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJbN -FhO6ROHyY8W3p3H/w1iDDwPg5fx+vKlDupb+67yFOU+GayzpRdnHR/yXFEsdh23a -jQWSwlNnPfbW6i65Cx/fw4WcWd0AY1WJakRNnTa1SKwAu55+H++JhVvl6MX5gfQC -R3DoBCCv38ftZoTUvuE11GXlKFJZPSdZvurqRnl7mW/wp3eYXrM0dra6PIjNPEyN -u/Iw/18sJOf5Q6axZLypc/X9XU5/wYbyXRxTyyRbOf/8Kk96e2uobPaV2xJuXlQj -YfE2QiIAFXCDMf9dS0x8xVwRRBn/Zx6EZCU19MSrcivxrz8lXMk6V0DAKpdMk2cm -t+VTxjgM8OtVA/Z6jBo= ------END CERTIFICATE----- diff --git a/.devcontainer/simplesamlphp/cert/server.pem b/.devcontainer/simplesamlphp/cert/server.pem deleted file mode 100644 index 031c654478c..00000000000 --- a/.devcontainer/simplesamlphp/cert/server.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCz+GJ281TB9R/f -Lrro1HnIXC6by5NlZWnU1CqTAXGeWyXBmTDM2MDGKvhYLvJbNAkxJkkc3yh4brWg -LtweVmr/tZ3iW6UzI6TeVP2GkjR5retB73xoj4ctfZoJcydm+CVvNoC7WPyj/fRg -D2g3TYbgqaG3DZzJ37kJNxsGYTeueF0MMWMBUOGaRYC1uLLLRVNSrU52zfW8tc7d -Z4YmuRF8S9P7vC8jx6uo5tujiwzW8xuoLVfuHndWonLk2Bd6EclQv+aejh9h5quG -0J+XP2PYCweyMbJbSEPc9f3vT3qwgZPIs2CUr2NxzWyskhsyTVFFoXa9WMxiWanr -duAMAI6ZAgMBAAECggEAA2C/B7YHVND4bUHiiA0NkHKV0l6cEIwP1DWQ61ZHLxPN -YzNFofaoiZ+pWSMKcVFKTItIKzFPaxFC5J6J4EzlcGa/hPiM4tHsCIXxBetXw6KS -zEJF5Bgw5JbwraA6cK7pfvX3gmyyvejomGakTAjEZmebWC+lI3neQ9DDt6yFSnJC -SFJjEkgydmskCGa03btBkEA7tBUwXvBV1B1U3TCGvftU0iA5fOkmBXT9KvPRw/GM -VORAH9CdzhEz6eJKFGwFRUrjJTiww7SGmPIYnnK3wzP4NYnz2l314BIm/DLoOoSp -vQEcljcm9vVLzGjUIJm6mOObopMFhNaoMV4+U7VDTQKBgQDy4WCIYWLnkmYBLA6I -+54G2xlcyDEm8EIRLNnyhEWo1c2QvQ/GoaGwmUmTxYUSmyMrD4zvE1UwzabOf3ef -QQehmSSsDTKpBao6T55GydjI1Kf3uSGdbS4oV3QCyylP2SMIH7eRLGex4R0OBgQB -RdzRHvGUJ4taR9vlC6TTQ/VDNQKBgQC9sRFA4Ojyxk/118wQth3UKEI3TOyqJMWU -NzSYHz055FckrZKPkkQr8kAj5IwHOInebuq9BiDZnhZ5zQdDyR8rYXubqnyblS6g -UzwUh/Vu7SyfqGs94Thj1dWzi6ytaje7Bg9fGGr3asrRAJLPlsKhbNTa9+WIeGtG -XoY83cYGVQKBgQDEsypOyTAkgzgBMwECh2TQAJllvuuvsVyQNFa+Rcl0ltLIqZUh -NL2SHFKuToUbMHEm6o0lAe84exTgn/LtEulvQmuku3uPz3qxhtGUWtwqqa7uvQww -Rr/fBBR8PrTSafQpHCa6d8/zC3FQbibIMSBIbFsRtYTyNMgtk2SFhMAZEQKBgQCX -P3rxTyOOh98zt4gHfx5TjnIVo+lEVKORZV8zOB1abp2pKhilD0v5+mN3+wBHtaYA -TFIwu5UvzyQ+o+L6AoAu18B2Qz6t0wplXViXSjzA4K1m9CPmKBENUW/ehCz/7KRZ -JCDXgyGu+2CRuzJUAPJZQeyKV4SWBuYKKs4/89i0LQKBgQDNz6Ndov2PyEy3Auap -IChYvDbnf5iFMGnkD8K1HCU4GCb2Tg9LRcpIAAvwRnJn2P6pRcRzW+15Amk+F7NA -DqL7hqocsnRXsjhOaz5ipr7A2daCH8Eo2/S1941vVmFU9z2DArjcd54b/DeNM+Ip -bfG1l4zV8hmevnd5vVESiHs+1A== ------END PRIVATE KEY----- diff --git a/.devcontainer/trustees-data/README.md b/.devcontainer/trustees-data/README.md new file mode 100644 index 00000000000..b6adce4c55e --- /dev/null +++ b/.devcontainer/trustees-data/README.md @@ -0,0 +1,19 @@ +# Local trustee configurations + +Trustee secrets are generated per environment and kept outside Git. Before using +the legacy development air-gap packaging script, provision each trustee with the +`gen_trustee_config` binary from the selected build: + +```bash +umask 077 +for trustee in trustee1 trustee2 trustee3; do + mkdir -p ".devcontainer/trustees-data/$trustee" + # Refuse to replace keys belonging to an existing board or election. + (set -o noclobber; gen_trustee_config > ".devcontainer/trustees-data/$trustee/$trustee.toml") || exit 1 +done +``` + +Run from the repository root. Preserve the resulting files with the associated +environment and register their public keys before use. Do not regenerate them to +recover an existing election; restore its matching keys. The evaluated CM delivery +procedure has its own protected package and custody records. diff --git a/.devcontainer/trustees-data/trustee1/trustee1.toml b/.devcontainer/trustees-data/trustee1/trustee1.toml deleted file mode 100644 index 6b308ebf9b5..00000000000 --- a/.devcontainer/trustees-data/trustee1/trustee1.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEIJAtmrHtGFYiS5tUQepIlrFtCCcKHeSzzuJ2pZqH4bat" -signing_key_pk = "MCowBQYDK2VwAyEAy1vJM4P85hJ1WAPZpRX3/QsOT2usIAuVy4/+t5VHHDs=" -encryption_key = "lQr2vrVuZJ5PAoOkVSfLfuIG7mxt8exlgAnRMBi+4rg" \ No newline at end of file diff --git a/.devcontainer/trustees-data/trustee2/trustee2.toml b/.devcontainer/trustees-data/trustee2/trustee2.toml deleted file mode 100644 index 193815790b3..00000000000 --- a/.devcontainer/trustees-data/trustee2/trustee2.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEICxvn7aLhsYzpGzVadzlqA4UZe/4wuul0fI1xznrpqCd" -signing_key_pk = "MCowBQYDK2VwAyEA50mtZzCBnubUwMhRkKyGomrUCBGgvEsbu79D3Cckjbc=" -encryption_key = "G5dB4N4i5KPgog6HHchw1BUF+2ulppFux2nQ7J2Wfq8" \ No newline at end of file diff --git a/.devcontainer/trustees-data/trustee3/trustee3.toml b/.devcontainer/trustees-data/trustee3/trustee3.toml deleted file mode 100644 index 0b157d467a0..00000000000 --- a/.devcontainer/trustees-data/trustee3/trustee3.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEIAiyrmvYxg0u4pSDcMk3Y3CaPdHXdASHbh8hzBgDaBbB" -signing_key_pk = "MCowBQYDK2VwAyEAfV2aRBpnR8Bm2MnorCwcR9ywjudNlFCqSZu6SGmuMcY=" -encryption_key = "eITqKrdoHUxj1uiKulqrAU01NyLN4Nkq5W8ibjorpJ4" \ No newline at end of file diff --git a/.github/actions/build-sequent-core-wasm/action.yml b/.github/actions/build-sequent-core-wasm/action.yml new file mode 100644 index 00000000000..5741f330993 --- /dev/null +++ b/.github/actions/build-sequent-core-wasm/action.yml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2025 Sequent Tech Inc +# +# SPDX-License-Identifier: AGPL-3.0-only + +name: Build the sequent-core WebAssembly package +description: > + Build packages/sequent-core to WebAssembly with the pinned toolchain and place the resulting + package in the rust/pkg directory of every workspace package that depends on it. The package + is not committed, so this runs before any yarn install in packages/. + +inputs: + features: + description: Cargo features passed to wasm-pack + required: false + default: wasmtest,default_features + +runs: + using: composite + steps: + - name: Set up Nix + uses: cachix/install-nix-action@v25 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Cache the Cargo registry and the WebAssembly target directory + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + packages/sequent-core/target + key: ${{ runner.os }}-sequent-core-wasm-${{ hashFiles('packages/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-sequent-core-wasm- + + - name: Build the WebAssembly package + shell: bash + working-directory: packages/sequent-core + run: | + nix develop --command bash -c ' + set -euo pipefail + wasm-pack --version + wasm-bindgen --version + wasm-pack build --mode no-install --release --target web --out-name index \ + --features=${{ inputs.features }} + ' + + - name: Place it where the workspace packages expect it + shell: bash + working-directory: packages + run: | + set -euo pipefail + for dir in ui-core admin-portal voting-portal ballot-verifier; do + rm -rf "${dir}/rust/pkg" + mkdir -p "${dir}/rust" + cp -a sequent-core/pkg "${dir}/rust/pkg" + done diff --git a/.gitignore b/.gitignore index b0752a66761..4fe6699bb31 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,14 @@ packages/voting-portal/logs packages/ballot-verifier/logs # ignore artifacts for airgapped environments -airgapped-artifacts \ No newline at end of file +airgapped-artifacts +# Locally generated development credentials; never commit private material. +.devcontainer/certs/nginx-tls.key +.devcontainer/certs/nginx-tls.crt +.devcontainer/simplesamlphp/cert/*.key +.devcontainer/simplesamlphp/cert/*.pem +.devcontainer/simplesamlphp/cert/*.crt +.devcontainer/trustees-data/**/*.toml +packages/braid/scripts/trustee[123].toml +.devcontainer/certs/vp-sso-signing.key +.devcontainer/certs/vp-sso-signing.crt diff --git a/README.md b/README.md index a34abb6cc8a..9a3770e5227 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ docker exec -it trustee1 cat /opt/braid/trustee.toml | grep pk Which will give a result similar to: ```bash -signing_key_pk = "YqYrRVXmPhBsWwwCgsOfw15RwUqZP9EhwmxuHKU5E8k" +signing_key_pk = "" ``` Then add the trustee in the admin portal with the key, in this case `YqYrRVXmPhBsWwwCgsOfw15RwUqZP9EhwmxuHKU5E8k`. diff --git a/beyond b/beyond index c5cfbe4df4c..35366ec2b9c 160000 --- a/beyond +++ b/beyond @@ -1 +1 @@ -Subproject commit c5cfbe4df4cf5bc75afcdc3d0ea8267f7842e089 +Subproject commit 35366ec2b9c11d2faa9732934642bab02fa1097e diff --git a/docs/docusaurus/docs/05-reference/03-product_lifecycle_and_release_cadence.md b/docs/docusaurus/docs/05-reference/03-product_lifecycle_and_release_cadence.md index ee56b1f0954..12dc861ad89 100644 --- a/docs/docusaurus/docs/05-reference/03-product_lifecycle_and_release_cadence.md +++ b/docs/docusaurus/docs/05-reference/03-product_lifecycle_and_release_cadence.md @@ -267,6 +267,12 @@ Each release follows this general schedule: - **Emergency patches**: Released within 24-48 hours for critical security issues +### Security Vulnerability Reporting + +If you discover a security vulnerability in the Sequent Voting Platform, please +report it responsibly by following the guidelines outlined in our +[Vulnerability Disclosure Policy](../06-technology/06-vulnerability-disclosure-policy.md). + ## Version Release Lifecycle Each major and minor version follows a structured release process that includes diff --git a/docs/docusaurus/docs/05-reference/third_party_deps/third_party_deps.md b/docs/docusaurus/docs/05-reference/third_party_deps/third_party_deps.md index 51453e09b07..99ab6664675 100644 --- a/docs/docusaurus/docs/05-reference/third_party_deps/third_party_deps.md +++ b/docs/docusaurus/docs/05-reference/third_party_deps/third_party_deps.md @@ -90,9 +90,9 @@ The admin portal is a React-based web application for administrative functions. | uuid | 9.0.0 | MIT | RFC9562 UUIDs | | web-vitals | ^5.1.0 | Apache-2.0 | Easily measure performance metrics in JavaScript | -## B3 +## B4 -B3 is a Rust-based component providing cryptographic utilities and core functionality. +B4 is the Rust bulletin board: the server binary and the client library the other components use to publish to and read from the board. | Dependency | Version | License | Description | |------------|---------|---------|-------------| diff --git a/docs/docusaurus/docs/06-technology/04-cyber-security.md b/docs/docusaurus/docs/06-technology/04-cyber-security.md deleted file mode 100644 index 4b4f6bb3387..00000000000 --- a/docs/docusaurus/docs/06-technology/04-cyber-security.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: cyber-security -title: Cyber Security ---- - - - -Content will be added here soon. \ No newline at end of file diff --git a/docs/docusaurus/docs/06-technology/06-vulnerability-disclosure-policy.md b/docs/docusaurus/docs/06-technology/06-vulnerability-disclosure-policy.md new file mode 100644 index 00000000000..b26c4e0dbb9 --- /dev/null +++ b/docs/docusaurus/docs/06-technology/06-vulnerability-disclosure-policy.md @@ -0,0 +1,185 @@ +--- +id: vulnerability-disclosure-policy +title: Vulnerability Disclosure Policy +--- + + + +import CodeBlock from '@theme/CodeBlock'; +import SecurityTxt from '!!raw-loader!@site/static/.well-known/security.txt'; +import PgpKey from '!!raw-loader!@site/static/security/pgp-key.txt'; + +Sequent Tech welcomes reports from security researchers, consumers, election +auditors, and the general public. The security of the Sequent Voting +Platform, including the `Sequent Voting Platform - uniWAHL Version`, depends +on independent scrutiny, and we are committed to working with anyone who +reports a vulnerability in good faith. + +This policy explains what is in scope, how to report, what you can expect from +us, and what we ask of you. It also publishes our OpenPGP key for +confidential reports and the machine-readable [security.txt](#securitytxt) +pointer (RFC 9116). + +**Version 1.0, 2026-08-03.** The authoritative version of this policy is the +one published at +[docs.sequentech.io/docusaurus/main/docs/technology/vulnerability-disclosure-policy](https://docs.sequentech.io/docusaurus/main/docs/technology/vulnerability-disclosure-policy); +a printed or forwarded copy may be out of date. Changes are announced in the +release notes. + +## Scope + +We accept security reports affecting: + +- Sequent Voting Platform (SVP) components documented on this site. +- The `Sequent Voting Platform - uniWAHL Version`. Reports are accepted for + the product as delivered and documented; source access is not required in + order to report a vulnerability. +- Deployment configurations and operational guidance described in these docs. + +We generally do not accept reports for: + +- Vulnerabilities in third-party infrastructure not operated by Sequent. +- Issues requiring physical access to consumer-managed environments (unless + explicitly in scope for your deployment). +- Social engineering, phishing, and generic denial-of-service testing against + production systems. +- Findings from automated scanners without a demonstrated security impact. + +## How to report + +Send your report by email to +[security@sequentech.io](mailto:security@sequentech.io). For sensitive +details, encrypt to our +[OpenPGP key](#confidential-reporting-with-openpgp). +Encryption is recommended but never required; unencrypted reports are +accepted and processed identically. + +Reports arriving through any other channel are forwarded to the security team +and processed identically. + +Please include, where possible: + +- A clear description of the issue and its potential impact. +- Affected component(s) and version(s). +- Reproduction steps or a proof of concept (safe and non-destructive). +- Any relevant logs or screenshots, with sensitive data removed. +- Your preferred contact details for follow-up, and whether you wish to be + publicly credited. + +Reports should be submitted in English (`Preferred-Languages: en`). + +### Confidential reporting with OpenPGP + +| | | +|---|---| +| User ID | `Sequent Security Team ` | +| Fingerprint | `DB6B 035E 872A B5DA 90C7 194A 60D3 CBE3 0BAC 91ED` | +| Expiry | `2028-07-21` (rotated before expiry) | +| Download | [https://keys.openpgp.org/vks/v1/by-fingerprint/DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED](https://keys.openpgp.org/vks/v1/by-fingerprint/DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED) | +| Keyserver | [keys.openpgp.org](https://keys.openpgp.org/search?q=DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED) | + +{PgpKey} + +To import and verify the key: + +```bash +curl -O https://keys.openpgp.org/vks/v1/by-fingerprint/DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED +gpg --show-keys pgp-key.txt # compare the fingerprint with this page +gpg --import pgp-key.txt +``` + +Verify the fingerprint against a second source before use: it is also stated +in the operational user guidance and in the delivery documentation provided to +consumers. To encrypt a report: + +```bash +gpg --encrypt --armor --recipient security@sequentech.io report.txt +``` + +The key is a role key for the Sequent security team, carries an expiry date, +and is rotated before expiry (immediately, on suspicion of compromise). Key +rotations are announced in the release notes, and the old key remains +available for decrypting reports in transit. + +## What you can expect from us + +- **Acknowledgement** of your report within **5 business days**. +- A point of contact and a tracking reference for follow-up and status + enquiries. +- An assessment of the report (severity is scored with CVSS v3.1) and + regular status updates until resolution. +- Remediation according to our published targets: **30 calendar days** for + critical and **60 calendar days** for high severity issues, with other + severities corrected in the next scheduled release. Where a target cannot be + met, we communicate an interim mitigation and a revised plan. +- A security advisory sent directly to every affected consumer, and a public + advisory published in the release notes of the release containing the fix. +- Credit for your finding, if you want it (see [Recognition](#recognition)). + +## What we ask of you + +To protect voters and running elections, we ask reporters to: + +- Act in good faith: avoid privacy violations, data destruction, and + degradation of the service. +- **Never test against real elections or production environments.** Use local + or dedicated test deployments; the + [Try It Out](../10-try_it_out/01-full-standalone-deployment/01-full-standalone-deployment.md) + section explains how to run the platform yourself. +- Never access, alter, or exfiltrate real voter data or real ballots. If you + encounter personal data accidentally, stop, do not copy it, and report + immediately. +- Do not perform denial-of-service, social-engineering, or physical attacks. +- Give us a reasonable opportunity to investigate and remediate before any + public disclosure. We aim to agree on a coordinated disclosure date with + you; by default we publish coordinated advisories 7-14 days after a fix + release has been made available to affected consumers. + +## Safe harbor + +We consider security research and vulnerability reporting conducted in +accordance with this policy to be authorized and conducted in good faith. We +will not initiate legal action or law-enforcement complaints against you for +such research, and we waive any claims based on the circumvention of +technology controls to the extent that your research complies with this +policy. If a third party initiates legal action against you for research +conducted in accordance with this policy, we will make it known that your +actions were authorized by us. + +This safe harbor does not apply to testing against production election systems +or real voter data, which is never authorized by this policy. + +## Recognition + +We are happy to publicly credit reporters who wish to be named. With your +permission, your name or handle is listed in the public advisory for the +finding. If you prefer to remain anonymous, we respect that. + +## Security advisories and updates + +Every security fix results in a new release version. Consumers affected by the +vulnerability are notified directly, at the contact registered for them when +their release was delivered. + +The public advisory for a fixed vulnerability is published in the release notes +of the release that contains the fix, in the Releases section of this +documentation. It describes the nature and effect of the vulnerability, the +affected versions, and the corrective action, and it identifies no consumer. Its +publication date follows the coordinated disclosure terms above. + +## security.txt + +This policy and our reporting channels are published in machine-readable form +following [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116): + +{SecurityTxt} + +The version-controlled, digitally signed master copy lives in this repository +(`docs/docusaurus/static/.well-known/security.txt`), is embedded above +verbatim, and is published with this documentation site at +[https://docs.sequentech.io/docusaurus/main/.well-known/security.txt](https://docs.sequentech.io/docusaurus/main/.well-known/security.txt). +The file is clear-signed with the security team OpenPGP key, and its `Expires` +value is renewed together with the periodic key review, before it lapses. diff --git a/docs/docusaurus/docs/07-developers/02-cli/02-tutorials/03-cli-tutorials-running-a-keys-ceremony.md b/docs/docusaurus/docs/07-developers/02-cli/02-tutorials/03-cli-tutorials-running-a-keys-ceremony.md index 8144893858f..43cb3d493f1 100644 --- a/docs/docusaurus/docs/07-developers/02-cli/02-tutorials/03-cli-tutorials-running-a-keys-ceremony.md +++ b/docs/docusaurus/docs/07-developers/02-cli/02-tutorials/03-cli-tutorials-running-a-keys-ceremony.md @@ -62,10 +62,10 @@ If the command succeeds, you will see output similar to: ```bash -Success! Successfully started key ceremony. ID: d9792af0-71b8-4952-8aac-94bc0fead5f7 +Success! Successfully started key ceremony. ID: ``` 📌 Important: -Save the Key Ceremony ID (d9792af0-71b8-4952-8aac-94bc0fead5f7 in this example). +Save the Key Ceremony ID returned by the command. You will need it in Step 2. --- @@ -140,4 +140,4 @@ cli step update-event-voting-status \ ℹ️ Reminder: Values like ac037831-66bd-451b-bdf7-e0a30eb2bfa0 and d9792af0-71b8-4952-8aac-94bc0fead5f7 are examples only. -Your actual IDs will differ depending on your system configuration. \ No newline at end of file +Your actual IDs will differ depending on your system configuration. diff --git a/docs/docusaurus/docs/07-developers/09-braid/braid_trustees_configuration.md b/docs/docusaurus/docs/07-developers/09-braid/braid_trustees_configuration.md index bb5194acdfe..8eb65567901 100644 --- a/docs/docusaurus/docs/07-developers/09-braid/braid_trustees_configuration.md +++ b/docs/docusaurus/docs/07-developers/09-braid/braid_trustees_configuration.md @@ -106,9 +106,9 @@ If you prefer to use a configuration file instead of secrets management, you can Example `trustee.toml`: ```toml -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEIJAtmrHtGFYiS5tUQepIlrFtCCcKHeSzzuJ2pZqH4bat" -signing_key_pk = "MCowBQYDK2VwAyEAy1vJM4P85hJ1WAPZpRX3/QsOT2usIAuVy4/+t5VHHDs=" -encryption_key = "lQr2vrVuZJ5PAoOkVSfLfuIG7mxt8exlgAnRMBi+4rg" +signing_key_sk = "" +signing_key_pk = "" +encryption_key = "" ``` ## Configuration Generation diff --git a/docs/docusaurus/docs/07-developers/10-tutorials/05-api-authentication.md b/docs/docusaurus/docs/07-developers/10-tutorials/05-api-authentication.md index d5ce13e5007..86032abb53b 100644 --- a/docs/docusaurus/docs/07-developers/10-tutorials/05-api-authentication.md +++ b/docs/docusaurus/docs/07-developers/10-tutorials/05-api-authentication.md @@ -164,18 +164,18 @@ print(f"Access token obtained, expires in {expires_in} seconds") ### Sample Response -A successful response will look like this: +A successful response has this structure. Token and session values below are placeholders; use the values returned by your own authentication request: ```json { - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJC...", + "access_token": "", "expires_in": 300, "refresh_expires_in": 1800, - "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6IC...", + "refresh_token": "", "token_type": "Bearer", - "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJC...", + "id_token": "", "not-before-policy": 1712715173, - "session_state": "0986ebd3-b08b-9981-191a-5a418381d0bd", + "session_state": "", "scope": "openid profile email" } ``` @@ -214,7 +214,7 @@ if time.time() - token_acquired_at > expires_in - 300: ```bash # Save the refresh token from the initial authentication -REFRESH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +REFRESH_TOKEN="" # Use it to get a new access token curl -X POST "${KEYCLOAK_URL}/realms/tenant-${TENANT_ID}/protocol/openid-connect/token" \ @@ -274,7 +274,7 @@ Once you have an access token, include it in the `Authorization` header of your ```bash # Save the access token from authentication -ACCESS_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." +ACCESS_TOKEN="" # Use it in API requests curl -X POST "https://api.example.sequent.vote/graphql" \ diff --git a/docs/docusaurus/docusaurus.config.js b/docs/docusaurus/docusaurus.config.js index e62aa7fede2..440bf0549f9 100644 --- a/docs/docusaurus/docusaurus.config.js +++ b/docs/docusaurus/docusaurus.config.js @@ -22,6 +22,8 @@ const config = { onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', + plugins: [require.resolve('./scripts/publication-build.cjs')], + // i18n, if you ever need it: i18n: { defaultLocale: 'en', diff --git a/docs/docusaurus/package.json b/docs/docusaurus/package.json index 406f38249f6..3cc816679a7 100644 --- a/docs/docusaurus/package.json +++ b/docs/docusaurus/package.json @@ -20,12 +20,14 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", + "raw-loader": "^4.0.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.8.1", - "@docusaurus/types": "^3.8.1" + "@docusaurus/types": "^3.8.1", + "cheerio": "1.0.0-rc.12" }, "browserslist": { "production": [ diff --git a/docs/docusaurus/scripts/export-guidance.cjs b/docs/docusaurus/scripts/export-guidance.cjs new file mode 100644 index 00000000000..e98c82e165f --- /dev/null +++ b/docs/docusaurus/scripts/export-guidance.cjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +// Select the rendered consumer articles and their local images, without site code/search. +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const {execFileSync} = require('node:child_process'); +const cheerio = require('cheerio'); +const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); +const escape = text => String(text).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +function inside(root, name) { + if (typeof name !== 'string' || !name || path.isAbsolute(name) || name.includes('\\') || name.split('/').some(p => !p || p === '.' || p === '..')) + throw Error('Invalid relative export path: '+name); + const file = path.join(root, name); + if (!fs.realpathSync(file).startsWith(fs.realpathSync(root)+path.sep) || fs.lstatSync(file).isSymbolicLink()) + throw Error('Export path leaves its source: '+name); + return file; +} +function validateSelection(selection) { + if (selection.schema !== 1 || !/^[0-9a-f]{40}$/.test(selection.commit) || !selection.tag || !selection.toe_reference) + throw Error('Guidance selection needs the actual source commit, tag and TOE reference'); + const docs = selection.documents; + if (!Array.isArray(docs) || docs.length !== 4 || docs.map(d => d.id).sort().join(',') !== 'AGD-01,AGD-02,AGD-03,AGD-04') + throw Error('Select the four distinct AGD handbooks'); + const sets = new Set(); + for (const doc of docs) { + if (!doc.title?.trim() || !doc.version?.trim() || !Array.isArray(doc.sources) || !doc.sources.length || new Set(doc.sources).size !== doc.sources.length) + throw Error(doc.id+': supply the reviewed version and explicit nonempty source list'); + const key = [...doc.sources].sort().join('\n'); + if (sets.has(key)) throw Error('Two handbooks cannot have identical page selections'); + sets.add(key); + for (const name of doc.sources) + if (typeof name !== 'string' || !/^docs\/docusaurus\/docs\/(?:02-election_managers|03-voters|05-reference)\/.+\.mdx?$/.test(name) || name.split('/').includes('..')) + throw Error('Guidance source is outside the consumer roots: '+name); + } + return docs; +} +function checkStaticSource(source, name) { + // Fence bodies are examples, not MDX; no client-rendered component may disappear silently. + let fence; const text = []; + for (const line of source.split('\n')) { + const start = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); + if (!fence && start) { + if (start[2].trim() === 'mermaid') throw Error(name+': Mermaid needs an explicit offline rendering adapter'); + fence = start[1]; + } else if (fence && new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(line)) fence = null; + else if (!fence) text.push(line); + } + if (fence) throw Error(name+': unclosed source fence'); + const body = text.join('\n').replace(//g, ''); + if (/<[A-Z][A-Za-z0-9_.]*(?:\s|\/?>)/.test(body)) throw Error(name+': MDX component needs an explicit offline rendering adapter'); + if (/Content will be added here soon|\bTODO\b|\bTBD\b/.test(body)) throw Error(name+': unfinished guidance placeholder'); +} +function exportGuidance({site, selection, out}) { + site = path.resolve(site); out = path.resolve(out); + if (fs.existsSync(out)) throw Error('Output already exists; preserve it and choose a fresh destination'); + const docs = validateSelection(selection); + const git = (...args) => execFileSync('git',['-C',site,...args],{encoding:'utf8'}).trim(); + const repo = git('rev-parse','--show-toplevel'); + const built = JSON.parse(fs.readFileSync(path.join(site,'.docusaurus/publication-build.json'))); + if (built.schema !== 1 || built.source_dirty || built.source_commit !== selection.commit || git('rev-parse','HEAD') !== selection.commit || + git('status','--porcelain','--','.',':(exclude).yarnrc')) throw Error('Guidance build requires the clean selected source revision'); + for (const [name, digest] of Object.entries(built.files)) { + if (sha(fs.readFileSync(inside(repo,name))) !== digest || execFileSync('git',['-C',repo,'hash-object','--no-filters',inside(repo,name)],{encoding:'utf8'}).trim() !== git('rev-parse','--verify',`${selection.commit}:${name}`)) + throw Error('Source or renderer changed since the documentation build: '+name); + } + const build = path.join(site,'build'); + const prepared = []; const routeTargets = new Map(); const output = new Map(); const images = new Map(); + for (const doc of docs) for (const [index, source] of doc.sources.entries()) { + const matches = (built.pages || []).filter(p => p.source === source); + if (matches.length !== 1) throw Error('Selected source has no unique built route: '+source); + const page = matches[0]; + const sourceText = fs.readFileSync(inside(repo,source),'utf8'); + checkStaticSource(sourceText,source); + const bytes = fs.readFileSync(inside(build,page.output)); + if (sha(bytes) !== page.sha256) throw Error('Built article changed: '+source); + const filename = `${doc.id}/${String(index+1).padStart(3,'0')}.html`; + const item = {doc, page, filename, bytes, source_sha256:sha(sourceText)}; + prepared.push(item); + if (!routeTargets.has(page.permalink.replace(/\/$/,''))) routeTargets.set(page.permalink.replace(/\/$/,''),[]); + routeTargets.get(page.permalink.replace(/\/$/,'')).push(item); + } + const links = []; + const wrap = (title, content) => ''+escape(title)+''+content+''; + for (const item of prepared) { + const {doc, page, filename} = item; + const $ = cheerio.load(item.bytes); + const article = $('article .markdown'); + if (article.length !== 1 || article.find('h1').first().text() !== page.title) throw Error('Rendered article identity differs: '+page.source); + if (article.find('script, iframe, object, embed, video, audio, canvas, link, form, input, select, textarea, .katex, [role="tablist"], .docusaurus-mermaid-container').length) + throw Error(page.source+': interactive or embedded content needs an explicit offline rendering adapter'); + article.find('button').remove(); + article.find('*').each((_, element) => { + const css = ($(element).attr('style') || '') + (element.tagName === 'style' ? $(element).text() : ''); + if (/@import|expression\s*\(/i.test(css) || [...css.matchAll(/url\(\s*['"]?([^)'"\s]+)/gi)].some(m => !m[1].startsWith('#'))) + throw Error('Unsupported style resource in '+page.source); + for (const [name,value] of Object.entries(element.attribs || {})) { + if (/^on/i.test(name) || /^(?:srcdoc|srcset|action|formaction|poster)$/i.test(name) || + /^(?:href|xlink:href|src)$/i.test(name) && /^\s*(?:javascript|data|vbscript):/i.test(value)) throw Error('Active or unresolved content in '+page.source); + if (name === 'src' && element.tagName !== 'img' || name === 'xlink:href' && !value.startsWith('#')) + throw Error('Unsupported resource in '+page.source); + if (name === 'href' && element.tagName !== 'a' && !value.startsWith('#')) throw Error('Unsupported SVG resource in '+page.source); + } + }); + article.find('img').each((_, image) => { + const url = new URL($(image).attr('src'), 'https://guidance.invalid'+page.permalink); + if (url.origin !== 'https://guidance.invalid' || !url.pathname.startsWith(built.base_url) || url.search || url.hash) + throw Error('Image must be a local built asset: '+url.href); + const name = decodeURIComponent(url.pathname.slice(built.base_url.length)); + const bytes = fs.readFileSync(inside(build,name)); const digest = sha(bytes); + if (!built.assets?.[name] || built.assets[name] !== digest) throw Error('Image is unrecorded or changed: '+name); + const ext = path.extname(name).toLowerCase(); + const valid = ext === '.png' ? bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])) : + ['.jpg','.jpeg'].includes(ext) ? bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255 : + ext === '.gif' ? /^GIF8[79]a$/.test(bytes.subarray(0,6).toString()) : + ext === '.webp' ? bytes.subarray(0,4).toString() === 'RIFF' && bytes.subarray(8,12).toString() === 'WEBP' : + ext === '.avif' && bytes.subarray(4,8).toString() === 'ftyp' && /avif|avis/.test(bytes.subarray(8,32).toString()); + if (!valid) throw Error('Unsupported or malformed image type: '+name); + const target = 'assets/'+digest+ext; + images.set(target,bytes); $(image).attr('src',path.posix.relative(path.posix.dirname(filename),target)); + $(image).removeAttr('loading'); + }); + article.find('a[href]').each((_, anchor) => { + const href = $(anchor).attr('href'); + const url = new URL(href,'https://guidance.invalid'+page.permalink); + if (url.origin !== 'https://guidance.invalid') { + if (!['https:','http:','mailto:'].includes(url.protocol)) throw Error('Unsupported link protocol: '+href); + links.push({page:filename,href}); return; + } + const targets = routeTargets.get(url.pathname.replace(/\/$/,'')); + const target = targets?.find(t => t.doc.id === doc.id) || targets?.[0]; + if (!target || url.search) throw Error('Internal link leaves the selected guidance: '+href+' in '+page.source); + if (url.hash) { + const linked = cheerio.load(target.bytes); + if (!linked('[id]').toArray().some(el => linked(el).attr('id') === decodeURIComponent(url.hash.slice(1)))) + throw Error('Selected guidance link has a missing fragment: '+href); + } + $(anchor).attr('href',path.posix.relative(path.posix.dirname(filename),target.filename)+url.hash); + }); + const identity = `

Guidance index — ${escape(doc.id)}, version ${escape(doc.version)}. ${escape(selection.toe_reference)}; release ${escape(selection.tag)}.

`; + output.set(filename,Buffer.from(wrap(doc.title,identity+article.html()))); + } + for (const doc of docs) { + const pages = prepared.filter(p => p.doc.id === doc.id); + const list = pages.map(p => `
  • ${escape(p.page.title)}
  • `).join(''); + output.set(doc.id+'/index.html',Buffer.from(wrap(doc.title,`

    ${escape(doc.id)}: ${escape(doc.title)}

    Version ${escape(doc.version)}. ${escape(selection.toe_reference)}; release ${escape(selection.tag)}.

      ${list}
    `))); + } + const index = docs.map(d => `
  • ${escape(d.id)}: ${escape(d.title)} — ${escape(d.version)}
  • `).join(''); + output.set('index.html',Buffer.from(wrap('Consumer guidance',`

    Consumer guidance

    ${escape(selection.toe_reference)}; release ${escape(selection.tag)}.

      ${index}
    `))); + for (const [name,bytes] of images) output.set(name,bytes); + const manifest = {schema:1,tag:selection.tag,commit:selection.commit,toe_reference:selection.toe_reference, + documents:docs.map(doc => ({...doc,pages:prepared.filter(p => p.doc.id === doc.id).map(p => ({source:p.page.source,source_sha256:p.source_sha256,html:p.filename,title:p.page.title}))})), + external_references:links, files:Object.fromEntries([...output].sort().map(([name,bytes]) => [name,sha(bytes)]))}; + // Validate everything before creating any output. The caller's run retains failed-build logs. + fs.mkdirSync(out,{recursive:true}); + for (const [name,bytes] of output) {fs.mkdirSync(path.dirname(path.join(out,name)),{recursive:true});fs.writeFileSync(path.join(out,name),bytes);} + fs.writeFileSync(path.join(out,'manifest.json'),JSON.stringify(manifest,null,2)+'\n'); + return manifest; +} +if (require.main === module) { + const opts = {}; + for (let i=2;in+d.pages.length,0),files:Object.keys(result.files).length})); +} +module.exports = {exportGuidance, validateSelection}; diff --git a/docs/docusaurus/scripts/publication-build.cjs b/docs/docusaurus/scripts/publication-build.cjs new file mode 100644 index 00000000000..dd8f49fdb17 --- /dev/null +++ b/docs/docusaurus/scripts/publication-build.cjs @@ -0,0 +1,71 @@ +// Identify the inputs and outputs used for the served/public-master comparison. +// This record stays in .docusaurus; it is not an additional public website page. +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const {execFileSync} = require('node:child_process'); +const sha = data => crypto.createHash('sha256').update(data).digest('hex'); +module.exports = function publicationBuild(context) { + const site = context.siteDir; + const git = (...args) => execFileSync('git', ['-C', site, ...args], {encoding:'utf8'}).trim(); + const repo = git('rev-parse', '--show-toplevel'); + const prefix = path.relative(repo, site); + const snapshot = () => ({ + source_commit:git('rev-parse', 'HEAD'), + source_dirty:!!git('status', '--porcelain', '--', '.', ':(exclude).yarnrc'), + local_yarnrc_sha256:fs.existsSync(path.join(site, '.yarnrc')) ? sha(fs.readFileSync(path.join(site, '.yarnrc'))) : null, + files:Object.fromEntries(git('ls-files', '-z').split('\0').filter(Boolean).sort().map(name => [ + `${prefix}/${name}`, sha(fs.readFileSync(path.join(site, name))), + ])), + }); + const before = snapshot(); + let versions = []; + return { + name:'publication-build', + async allContentLoaded({allContent}) { + versions = allContent['docusaurus-plugin-content-docs']?.default?.loadedVersions || []; + }, + async postBuild({outDir}) { + if (JSON.stringify(before) !== JSON.stringify(snapshot())) + throw new Error('Public documentation inputs changed during the build'); + const paths = { + 'vulnerability-disclosure-policy': ['docs/technology/vulnerability-disclosure-policy.html', 'docs/06-technology/06-vulnerability-disclosure-policy.md'], + 'pgp-key': ['security/pgp-key.txt', 'static/security/pgp-key.txt'], + 'security.txt': ['.well-known/security.txt', 'static/.well-known/security.txt'], + 'csaf-provider-metadata': ['.well-known/csaf/provider-metadata.json', 'static/.well-known/csaf/provider-metadata.json'], + }; + const artifacts = {}; + for (const [name, [output, source]] of Object.entries(paths)) { + let file = output; + if (!fs.existsSync(path.join(outDir, file)) && file.endsWith('.html')) + file = file.slice(0, -5) + '/index.html'; + artifacts[name] = {source:`${prefix}/${source}`, output:file, + sha256:sha(fs.readFileSync(path.join(outDir, file)))}; + } + const pages = []; + for (const version of versions) { + for (const doc of version.docs || []) { + if (doc.draft || !doc.source.startsWith('@site/')) continue; + const relative = doc.permalink.slice(context.siteConfig.baseUrl.length).replace(/\/$/, ''); + let output = relative + '.html'; + if (!fs.existsSync(path.join(outDir, output))) output = relative + '/index.html'; + pages.push({source:`${prefix}/${doc.source.slice(6)}`, title:doc.title, + permalink:doc.permalink, output, sha256:sha(fs.readFileSync(path.join(outDir, output)))}); + } + } + const assets = {}; + function images(dir) { + for (const item of fs.readdirSync(dir, {withFileTypes:true})) { + const file = path.join(dir, item.name); + if (item.isDirectory()) images(file); + else if (item.isFile() && /\.(?:png|jpe?g|gif|webp|avif)$/i.test(item.name)) + assets[path.relative(outDir, file).split(path.sep).join('/')] = sha(fs.readFileSync(file)); + } + } + images(outDir); + fs.writeFileSync(path.join(site, '.docusaurus/publication-build.json'), JSON.stringify({ + schema:1, base_url:context.siteConfig.baseUrl, ...before, artifacts, pages, assets, + }, null, 2) + '\n'); + }, + }; +}; diff --git a/docs/docusaurus/scripts/test-export-guidance.cjs b/docs/docusaurus/scripts/test-export-guidance.cjs new file mode 100644 index 00000000000..c172cde2af3 --- /dev/null +++ b/docs/docusaurus/scripts/test-export-guidance.cjs @@ -0,0 +1,92 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const crypto = require('node:crypto'); +const {execFileSync} = require('node:child_process'); +const {exportGuidance} = require('./export-guidance.cjs'); +const sha = b => crypto.createHash('sha256').update(b).digest('hex'); +function fixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(),'guidance-test-')); + const site = path.join(root,'docs/docusaurus'); const build = path.join(site,'build'); + const write = (name, bytes) => {fs.mkdirSync(path.dirname(path.join(root,name)),{recursive:true});fs.writeFileSync(path.join(root,name),bytes);}; + write('.gitignore','docs/docusaurus/build/\ndocs/docusaurus/.docusaurus/\n'); + const docs = [1,2,3,4].map(n => ({id:`AGD-0${n}`,title:`Synthetic handbook ${n}`,version:'test-only',sources:[`docs/docusaurus/docs/03-voters/page${n}.md`]})); + for (const [i,doc] of docs.entries()) { + write(doc.sources[0],`# Page ${i+1}\n\nSynthetic fixture only.`); + write(`docs/docusaurus/build/docs/page${i+1}.html`,``); + } + write('docs/docusaurus/build/assets/image.png',Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl6tXsAAAAASUVORK5CYII=','base64')); + write('docs/docusaurus/build/internal.html','NEVER INCLUDE'); + const git = (...args) => execFileSync('git',['-C',root,...args],{encoding:'utf8'}).trim(); + git('init','-q');git('add','.');git('-c','user.name=Test','-c','user.email=test@example.invalid','-c','commit.gpgSign=false','commit','-qm','Synthetic selection'); + const commit = git('rev-parse','HEAD'); + const manifest = {schema:1,base_url:'/manual/',source_dirty:false,source_commit:commit, + files:Object.fromEntries(docs.map(d => [d.sources[0],sha(fs.readFileSync(path.join(root,d.sources[0])))])), + pages:docs.map((d,i)=>({source:d.sources[0],title:`Page ${i+1}`,permalink:`/manual/docs/page${i+1}`,output:`docs/page${i+1}.html`,sha256:sha(fs.readFileSync(path.join(build,`docs/page${i+1}.html`)))})), + assets:{'assets/image.png':sha(fs.readFileSync(path.join(build,'assets/image.png')))}}; + const record = () => write('docs/docusaurus/.docusaurus/publication-build.json',JSON.stringify(manifest));record(); + const selection={schema:1,tag:'synthetic-v0',toe_reference:'Synthetic TOE',commit,documents:docs}; + const out=path.join(root,'export'); + return {root,site,build,write,git,manifest,record,selection,out,run:()=>exportGuidance({site,selection,out}),clean:()=>fs.rmSync(root,{recursive:true,force:true})}; +} +const using = fn => {const f=fixture();try {return fn(f);} finally {f.clean();}}; +test('exports exactly selected articles and images, with offline links and identities',()=>using(f=>{ + const m=f.run();assert.equal(m.documents.length,4);assert.equal(Object.keys(m.files).length,10); + const html=fs.readFileSync(path.join(f.out,'AGD-01/001.html'),'utf8'); + assert.match(html,/\.\.\/AGD-02\/001.html#detail/);assert.match(html,/test-only/);assert.match(html,/synthetic-v0/); + assert.doesNotMatch(html,/PRIVATE|SECRET|using(f=>{ + f.selection.documents[0].sources=[];assert.throws(f.run,/nonempty/); + f.selection.documents[0].sources=['evidence/bsi-certification/2_alc/9_Internal_To_Do.md'];assert.throws(f.run,/outside/); + f.selection.documents[0].sources=f.selection.documents[1].sources;assert.throws(f.run,/identical/); + assert.equal(fs.existsSync(f.out),false); +})); +test('refuses changed rendered articles and changed image bytes',()=>using(f=>{ + const p=path.join(f.build,'docs/page1.html');const old=fs.readFileSync(p);fs.appendFileSync(p,'changed');assert.throws(f.run,/article changed/);fs.writeFileSync(p,old); + fs.appendFileSync(path.join(f.build,'assets/image.png'),'changed');assert.throws(f.run,/Image is unrecorded or changed/); +})); +test('refuses dirty source even when timestamps are restored',()=>using(f=>{ + const p=path.join(f.root,f.selection.documents[0].sources[0]);const stat=fs.statSync(p);fs.appendFileSync(p,'changed');fs.utimesSync(p,stat.atime,stat.mtime);assert.throws(f.run,/clean selected source/); +})); +function changeArticle(f, content) { + const p=path.join(f.build,'docs/page1.html');fs.writeFileSync(p,`

    Page 1

    ${content}
    `); + f.manifest.pages[0].sha256=sha(fs.readFileSync(p));f.record(); +} +test('refuses unselected local links, absent fragments and remote images',()=>using(f=>{ + changeArticle(f,'Unselected');assert.throws(f.run,/leaves the selected/); + changeArticle(f,'Missing');assert.throws(f.run,/missing fragment/); + changeArticle(f,'');assert.throws(f.run,/local built asset/); +})); +test('refuses embedded, client-only and active content without writing a package',()=>using(f=>{ + for (const html of ['','
    ','

    Text

    ','','']) { + changeArticle(f,html);assert.throws(f.run,/adapter|content|style resource/);assert.equal(fs.existsSync(f.out),false); + } +})); +test('refuses path traversal or symlink substitution in recorded sources',()=>using(f=>{ + f.manifest.files['../outside']='0'.repeat(64);f.record();assert.throws(f.run,/Invalid relative/);delete f.manifest.files['../outside'];f.record(); + const p=path.join(f.root,f.selection.documents[0].sources[0]);const text=fs.readFileSync(p);fs.unlinkSync(p);fs.writeFileSync(path.join(f.root,'outside.md'),text);fs.symlinkSync(path.join(f.root,'outside.md'),p);assert.throws(f.run,/clean selected|leaves its source/); +})); + +test('refuses unfinished source, unrendered diagrams and MDX components',()=>using(f=>{ + const source=f.selection.documents[0].sources[0]; + for (const body of ['Content will be added here soon.','```mermaid\ngraph LR; A-->B\n```','']) { + f.write(source,'# Page 1\n\n'+body); + f.git('add',source);f.git('-c','user.name=Test','-c','user.email=test@example.invalid','-c','commit.gpgSign=false','commit','-qm','Synthetic unsupported source'); + f.selection.commit=f.git('rev-parse','HEAD');f.manifest.source_commit=f.selection.commit; + f.manifest.files[source]=sha(fs.readFileSync(path.join(f.root,source)));f.record(); + assert.throws(f.run,/placeholder|rendering adapter/);assert.equal(fs.existsSync(f.out),false); + } +})); + +test('checks committed media larger than a subprocess output buffer',()=>using(f=>{ + const name='docs/docusaurus/static/large.bin';f.write(name,Buffer.alloc(2*1024*1024,7)); + f.git('add',name);f.git('-c','user.name=Test','-c','user.email=test@example.invalid','-c','commit.gpgSign=false','commit','-qm','Synthetic large media'); + f.selection.commit=f.git('rev-parse','HEAD');f.manifest.source_commit=f.selection.commit; + f.manifest.files[name]=sha(fs.readFileSync(path.join(f.root,name)));f.record(); + assert.equal(f.run().documents.length,4);assert.equal(fs.existsSync(path.join(f.out,'large.bin')),false); +})); diff --git a/docs/docusaurus/scripts/test-publication-build.cjs b/docs/docusaurus/scripts/test-publication-build.cjs new file mode 100644 index 00000000000..978a73ac56d --- /dev/null +++ b/docs/docusaurus/scripts/test-publication-build.cjs @@ -0,0 +1,74 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const {execFileSync} = require('node:child_process'); +const plugin = require('./publication-build.cjs'); +function fixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'publication-build-test-')); + const site = path.join(root, 'docs/docusaurus'); + const outDir = path.join(site, 'build'); + fs.mkdirSync(path.join(site, '.docusaurus'), {recursive:true}); + fs.writeFileSync(path.join(site, '.gitignore'), 'build/\n.docusaurus/\n'); + const pairs = [ + ['docs/06-technology/06-vulnerability-disclosure-policy.md','docs/technology/vulnerability-disclosure-policy.html'], + ['static/security/pgp-key.txt','security/pgp-key.txt'], + ['static/.well-known/security.txt','.well-known/security.txt'], + ['static/.well-known/csaf/provider-metadata.json','.well-known/csaf/provider-metadata.json'], + ]; + for (const [source, output] of pairs) { + fs.mkdirSync(path.dirname(path.join(site, source)), {recursive:true}); + fs.writeFileSync(path.join(site, source), 'Synthetic source: '+source); + fs.mkdirSync(path.dirname(path.join(outDir, output)), {recursive:true}); + fs.writeFileSync(path.join(outDir, output), 'Synthetic output: '+output); + } + const git = (...args) => execFileSync('git', ['-C', root, ...args], {encoding:'utf8'}).trim(); + git('init','-q'); git('add','.'); + git('-c','user.name=Test','-c','user.email=test@example.invalid','-c','commit.gpgSign=false','commit','-qm','Synthetic publication build'); + return {root, site, outDir, git, context:{siteDir:site,siteConfig:{baseUrl:'/docusaurus/main/'}}, + clean:() => fs.rmSync(root,{recursive:true,force:true})}; +} +async function using(fn) {const f=fixture(); try {await fn(f);} finally {f.clean();}} +test('records four outputs and exact committed inputs outside the public build', () => using(async f => { + await plugin(f.context).postBuild({outDir:f.outDir}); + const record=JSON.parse(fs.readFileSync(path.join(f.site,'.docusaurus/publication-build.json'))); + assert.equal(record.source_commit,f.git('rev-parse','HEAD')); + assert.equal(record.source_dirty,false); + assert.equal(Object.keys(record.artifacts).length,4); + assert.equal(Object.keys(record.files).length,5); + assert.equal(fs.existsSync(path.join(f.outDir,'publication-build.json')),false); +})); +test('refuses source changes while rendering', () => using(async f => { + const p=plugin(f.context); + fs.appendFileSync(path.join(f.site,'static/security/pgp-key.txt'),'changed'); + await assert.rejects(() => p.postBuild({outDir:f.outDir}), /inputs changed/); +})); +test('identifies a dirty public source without claiming committed output', () => using(async f => { + fs.appendFileSync(path.join(f.site,'static/security/pgp-key.txt'),'changed'); + await plugin(f.context).postBuild({outDir:f.outDir}); + const record=JSON.parse(fs.readFileSync(path.join(f.site,'.docusaurus/publication-build.json'))); + assert.equal(record.source_dirty,true); +})); +test('hashes the generated offline-mirror routing separately from Git sources', () => using(async f => { + fs.writeFileSync(path.join(f.site,'.yarnrc'),'yarn-offline-mirror "/accepted/vendor/yarn-offline-mirror"\n'); + const p=plugin(f.context); + await p.postBuild({outDir:f.outDir}); + const record=JSON.parse(fs.readFileSync(path.join(f.site,'.docusaurus/publication-build.json'))); + assert.equal(record.source_dirty,false); + assert.match(record.local_yarnrc_sha256,/^[0-9a-f]{64}$/); + fs.appendFileSync(path.join(f.site,'.yarnrc'),'changed'); + await assert.rejects(() => p.postBuild({outDir:f.outDir}), /inputs changed/); +})); +test('binds selected-doc route identity and local raster bytes to the actual build', () => using(async f => { + const p=plugin(f.context); + const file=path.join(f.outDir,'assets/example.png');fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,'synthetic image bytes'); + await p.allContentLoaded({allContent:{'docusaurus-plugin-content-docs':{default:{loadedVersions:[{docs:[{ + source:'@site/docs/06-technology/06-vulnerability-disclosure-policy.md',title:'Synthetic policy',permalink:'/docusaurus/main/docs/technology/vulnerability-disclosure-policy',draft:false, + }]}]}}}}); + await p.postBuild({outDir:f.outDir}); + const r=JSON.parse(fs.readFileSync(path.join(f.site,'.docusaurus/publication-build.json'))); + assert.equal(r.pages.length,1);assert.equal(r.pages[0].source,'docs/docusaurus/docs/06-technology/06-vulnerability-disclosure-policy.md'); + assert.equal(r.pages[0].sha256,r.artifacts['vulnerability-disclosure-policy'].sha256); + assert.match(r.assets['assets/example.png'],/^[0-9a-f]{64}$/); +})); diff --git a/docs/docusaurus/src/css/custom.css b/docs/docusaurus/src/css/custom.css index f3a940eeb04..3fa5ec2baa9 100644 --- a/docs/docusaurus/src/css/custom.css +++ b/docs/docusaurus/src/css/custom.css @@ -164,3 +164,10 @@ body.home .navbar__brand .navbar__title { width: 190px; height: 190px; } + + +@media print { + .theme-edit-this-page { + display: none; + } +} \ No newline at end of file diff --git a/docs/docusaurus/static/.well-known/csaf/changes.csv b/docs/docusaurus/static/.well-known/csaf/changes.csv new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/docusaurus/static/.well-known/csaf/index.txt b/docs/docusaurus/static/.well-known/csaf/index.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/docusaurus/static/.well-known/csaf/provider-metadata.json b/docs/docusaurus/static/.well-known/csaf/provider-metadata.json new file mode 100644 index 00000000000..6d8590c1988 --- /dev/null +++ b/docs/docusaurus/static/.well-known/csaf/provider-metadata.json @@ -0,0 +1,26 @@ +{ + "canonical_url": "https://docs.sequentech.io/docusaurus/main/.well-known/csaf/provider-metadata.json", + "distributions": [ + { + "directory_url": "https://docs.sequentech.io/docusaurus/main/.well-known/csaf/" + } + ], + "last_updated": "2026-09-02T00:00:00.000Z", + "list_on_CSAF_aggregators": false, + "metadata_version": "2.0", + "mirror_on_CSAF_aggregators": false, + "public_openpgp_keys": [ + { + "fingerprint": "DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED", + "url": "https://docs.sequentech.io/docusaurus/main/security/pgp-key.txt" + } + ], + "publisher": { + "category": "vendor", + "name": "Sequent Tech Inc", + "namespace": "https://sequentech.io", + "contact_details": "security@sequentech.io", + "issuing_authority": "Sequent Tech Inc issues security advisories for the Sequent Voting Platform. Advisories are clear-signed with the OpenPGP key listed in public_openpgp_keys." + }, + "role": "csaf_publisher" +} diff --git a/docs/docusaurus/static/.well-known/security.txt b/docs/docusaurus/static/.well-known/security.txt new file mode 100644 index 00000000000..f06a9ed7ef4 --- /dev/null +++ b/docs/docusaurus/static/.well-known/security.txt @@ -0,0 +1,20 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +# Sequent Tech - coordinated vulnerability disclosure (RFC 9116) + +Canonical: https://docs.sequentech.io/docusaurus/main/.well-known/security.txt +Contact: mailto:security@sequentech.io +Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/DB6B035E872AB5DA90C7194A60D3CBE30BAC91ED +Policy: https://docs.sequentech.io/docusaurus/main/docs/technology/vulnerability-disclosure-policy +Acknowledgments: https://docs.sequentech.io/docusaurus/main/docs/technology/vulnerability-disclosure-policy#recognition +Preferred-Languages: en +Expires: 2028-07-01T00:00:00.000Z +-----BEGIN PGP SIGNATURE----- + +iJEEARYKADkWIQTbawNehyq12pDHGUpg08vjC6yR7QUCanBJbBsUgAAAAAAEAA5t +YW51MiwyLjUrMS4xMiwwLDMACgkQYNPL4wuske1G6wD+NziZBacawZPZkYPb3FW8 +RJn48xotxfMRfZsuhaH3zbUBAMxdPqZE5a9ZLpN0gHUvRpUOzKVLeo3lT6y+P1aS +6zwB +=H7TJ +-----END PGP SIGNATURE----- diff --git a/docs/docusaurus/static/security/pgp-key.txt b/docs/docusaurus/static/security/pgp-key.txt new file mode 100644 index 00000000000..a2999eef73a --- /dev/null +++ b/docs/docusaurus/static/security/pgp-key.txt @@ -0,0 +1,15 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEamDIwRYJKwYBBAHaRw8BAQdAlmcwBCPSg9C+HAkQYIj1+MN1ywdtIuRHTo/O +adTSCsq0LlNlcXVlbnQgU2VjdXJpdHkgVGVhbSA8c2VjdXJpdHlAc2VxdWVudGVj +aC5pbz6ItQQTFgoAXRYhBNtrA16HKrXakMcZSmDTy+MLrJHtBQJqYMjBGxSAAAAA +AAQADm1hbnUyLDIuNSsxLjEyLDAsMwIbAwUJA8JnAAULCQgHAgIiAgYVCgkICwIE +FgIDAQIeBwIXgAAKCRBg08vjC6yR7bLtAQDTo0Eo0b0UohwfK8U2cTjhLewapan6 +Gefdsok3rON3jwD/Yl8r9xJGF84E9gNwjZ/IOISYOenCWuFVv8Nwi6/HUgC4OARq +YMkDEgorBgEEAZdVAQUBAQdAV4Fg4HBTjN/E+YWaOB6t8m3iK2wOpRmxkHtQFhXL +exMDAQgHiJoEGBYKAEIWIQTbawNehyq12pDHGUpg08vjC6yR7QUCamDJAxsUgAAA +AAAEAA5tYW51MiwyLjUrMS4xMiwwLDMCGwwFCQPCZwAACgkQYNPL4wuske3dpgD/ +fxo+XtUAH+K1CllAL+kK5vAI1/hL3+CHKZYbCuLBR0MA/1atMNNWmjMN3700SC1V +V58EcMyOzioqPKHhFQQoFXYC +=sf9K +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/docusaurus/yarn.lock b/docs/docusaurus/yarn.lock index 33565c55d65..f25005fda2e 100644 --- a/docs/docusaurus/yarn.lock +++ b/docs/docusaurus/yarn.lock @@ -8396,6 +8396,14 @@ raw-body@~2.5.3: iconv-lite "~0.4.24" unpipe "~1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc@1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" diff --git a/docs/hasura-auth.md b/docs/hasura-auth.md index 9d036fa9b6f..e4642bd8b3f 100644 --- a/docs/hasura-auth.md +++ b/docs/hasura-auth.md @@ -143,21 +143,18 @@ You can obtain the `api-key-client` secret by login into Keycloak, selecting the realm, then going to Clients > api-key-client > Credentials and looking into the `Client Secret` section. -Then, as an example this would be an http request for authenticating with such client using -[CURL]: +Set `CLIENT_SECRET`, `USERNAME` and `PASSWORD` for your own account and client before running this example authentication request with [CURL]: ``` -curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=api-key-client&scope=openid&client_secret=OkT3TE5LgwLkKv0b8iGycz8ocpK8V7hD&grant_type=password&username=admin&password=admin" http://127.0.0.1:8090/realms/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5/protocol/openid-connect/token +curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=api-key-client&scope=openid&client_secret=${CLIENT_SECRET}&grant_type=password&username=${USERNAME}&password=${PASSWORD}" http://127.0.0.1:8090/realms/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5/protocol/openid-connect/token ``` -Note that in Production you'll need to replace the `client_secret` with the real value, as -well as the `username`, `password`, and you'll need to replace `http://127.0.0.1:8090/` with -the actual keycloak address, for example `https://keycloak-staging/`. +Use the Keycloak address and tenant realm for your environment; the URL above is a local development example. Keep client secrets, passwords and returned tokens out of shared command transcripts and documentation. -If successful, this call will return something like this: +A successful response has the following structure. The token and session values are placeholders: ``` -{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJYXRJRWFNd004REEyVGFWa3ZEQklxbjcxQ2plRjhLeEx5VU9MbnBjRXlJIn0.eyJleHAiOjE3MjEyNTY4NDEsImlhdCI6MTcyMTI1NjU0MSwianRpIjoiODJhYTE5YjMtZmYzZS00YjZhLWIyZDQtODc0NGJmODc2MTM4IiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy90ZW5hbnQtOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImEyMDM3NmQ2LWU1M2EtNGVkZi1hZmMzLWNjMThiYjhlMjNlNSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaS1rZXktY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImZmNjMxYWM3LTZhNTMtNDc4Yy05MjRlLWE0MDNmMTVjZWU1NSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsic3VwcG9ydC1tYXRlcmlhbC13cml0ZSIsImVsZWN0aW9uLXR5cGUtY3JlYXRlIiwicm9sZS13cml0ZSIsInRhbGx5LXJlc3VsdHMtcmVhZCIsImVsZWN0aW9uLWV2ZW50LWNyZWF0ZSIsInRhbGx5LXNoZWV0LXZpZXciLCJ2b3RpbmctY2hhbm5lbC1yZWFkIiwidGFsbHktd3JpdGUiLCJ1c2VyLWNyZWF0ZSIsInVtYV9hdXRob3JpemF0aW9uIiwidGFsbHktc2hlZXQtZGVsZXRlIiwiYWRtaW4tZGFzaGJvYXJkLXZpZXciLCJ1c2VyLXJlYWQiLCJub3RpZmljYXRpb24td3JpdGUiLCJ0ZW5hbnQtY3JlYXRlIiwiZG9jdW1lbnQtZG93bmxvYWQiLCJub3RpZmljYXRpb24tc2VuZCIsInRhbGx5LXJlYWQiLCJ0YWxseS1zaGVldC1wdWJsaXNoIiwidGVuYW50LXdyaXRlIiwidHJ1c3RlZS1jcmVhdGUiLCJzdXBwb3J0LW1hdGVyaWFsLXJlYWQiLCJlbGVjdGlvbi10eXBlLXdyaXRlIiwiYWRtaW4tY2VyZW1vbnkiLCJ2b3Rlci13cml0ZSIsIm5vdGlmaWNhdGlvbi1yZWFkIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS13cml0ZSIsInRhbGx5LXN0YXJ0IiwidGVuYW50LXJlYWQiLCJ1c2VyLXBlcm1pc3Npb24tcmVhZCIsImRvY3VtZW50LXdyaXRlIiwicHVibGlzaC1yZWFkIiwidHJ1c3RlZS1yZWFkIiwiY2FzdC12b3RlLXJlYWQiLCJjcmVhdGUtZWxlY3Rpb24tZXZlbnQiLCJlbGVjdGlvbi1ldmVudC1yZWFkIiwidm90aW5nLWNoYW5uZWwtd3JpdGUiLCJyb2xlLWFzc2lnbiIsImRvY3VtZW50LXJlYWQiLCJyb2xlLXJlYWQiLCJhZG1pbi11c2VyIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS1yZWFkIiwiZWxlY3Rpb24tZXZlbnQtd3JpdGUiLCJlbGVjdGlvbi10eXBlLXJlYWQiLCJvZmZsaW5lX2FjY2VzcyIsInZvdGVyLWNyZWF0ZSIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtY3JlYXRlIiwidXNlci1wZXJtaXNzaW9uLWNyZWF0ZSIsImtleXMtcmVhZCIsInZvdGVyLXJlYWQiLCJ1c2VyLXdyaXRlIiwiYXJlYS1yZWFkIiwidGFsbHktc2hlZXQtY3JlYXRlIiwicm9sZS1jcmVhdGUiLCJkZWZhdWx0LXJvbGVzLWVsZWN0b3JhbC1wcm9jZXNzIiwiZWxlY3Rpb24tc3RhdGUtd3JpdGUiLCJkb2N1bWVudC11cGxvYWQiLCJ0cnVzdGVlLXdyaXRlIiwiYXJlYS13cml0ZSIsInVzZXItcGVybWlzc2lvbi13cml0ZSIsInB1Ymxpc2gtd3JpdGUiLCJsb2dzLXJlYWQiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhcGkta2V5LWNsaWVudCI6eyJyb2xlcyI6WyJhcGkta2V5LXJvbGUiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJmZjYzMWFjNy02YTUzLTQ3OGMtOTI0ZS1hNDAzZjE1Y2VlNTUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9oYXN1cmEuaW8vand0L2NsYWltcyI6eyJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJhZG1pbi11c2VyIiwieC1oYXN1cmEtdGVuYW50LWlkIjoiOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1IiwieC1oYXN1cmEtdXNlci1pZCI6ImEyMDM3NmQ2LWU1M2EtNGVkZi1hZmMzLWNjMThiYjhlMjNlNSIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsic3VwcG9ydC1tYXRlcmlhbC13cml0ZSIsImVsZWN0aW9uLXR5cGUtY3JlYXRlIiwicm9sZS13cml0ZSIsInRhbGx5LXJlc3VsdHMtcmVhZCIsImVsZWN0aW9uLWV2ZW50LWNyZWF0ZSIsInRhbGx5LXNoZWV0LXZpZXciLCJ2b3RpbmctY2hhbm5lbC1yZWFkIiwidGFsbHktd3JpdGUiLCJ1c2VyLWNyZWF0ZSIsInVtYV9hdXRob3JpemF0aW9uIiwidGFsbHktc2hlZXQtZGVsZXRlIiwiYWRtaW4tZGFzaGJvYXJkLXZpZXciLCJ1c2VyLXJlYWQiLCJub3RpZmljYXRpb24td3JpdGUiLCJ0ZW5hbnQtY3JlYXRlIiwiZG9jdW1lbnQtZG93bmxvYWQiLCJub3RpZmljYXRpb24tc2VuZCIsInRhbGx5LXJlYWQiLCJ0YWxseS1zaGVldC1wdWJsaXNoIiwidGVuYW50LXdyaXRlIiwidHJ1c3RlZS1jcmVhdGUiLCJzdXBwb3J0LW1hdGVyaWFsLXJlYWQiLCJlbGVjdGlvbi10eXBlLXdyaXRlIiwiYWRtaW4tY2VyZW1vbnkiLCJ2b3Rlci13cml0ZSIsIm5vdGlmaWNhdGlvbi1yZWFkIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS13cml0ZSIsInRhbGx5LXN0YXJ0IiwidGVuYW50LXJlYWQiLCJ1c2VyLXBlcm1pc3Npb24tcmVhZCIsImRvY3VtZW50LXdyaXRlIiwicHVibGlzaC1yZWFkIiwidHJ1c3RlZS1yZWFkIiwiY2FzdC12b3RlLXJlYWQiLCJjcmVhdGUtZWxlY3Rpb24tZXZlbnQiLCJlbGVjdGlvbi1ldmVudC1yZWFkIiwidm90aW5nLWNoYW5uZWwtd3JpdGUiLCJyb2xlLWFzc2lnbiIsImRvY3VtZW50LXJlYWQiLCJyb2xlLXJlYWQiLCJhZG1pbi11c2VyIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS1yZWFkIiwiZWxlY3Rpb24tZXZlbnQtd3JpdGUiLCJlbGVjdGlvbi10eXBlLXJlYWQiLCJvZmZsaW5lX2FjY2VzcyIsInZvdGVyLWNyZWF0ZSIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtY3JlYXRlIiwidXNlci1wZXJtaXNzaW9uLWNyZWF0ZSIsImtleXMtcmVhZCIsInZvdGVyLXJlYWQiLCJ1c2VyLXdyaXRlIiwiYXJlYS1yZWFkIiwidGFsbHktc2hlZXQtY3JlYXRlIiwicm9sZS1jcmVhdGUiLCJkZWZhdWx0LXJvbGVzLWVsZWN0b3JhbC1wcm9jZXNzIiwiZWxlY3Rpb24tc3RhdGUtd3JpdGUiLCJkb2N1bWVudC11cGxvYWQiLCJ0cnVzdGVlLXdyaXRlIiwiYXJlYS13cml0ZSIsInVzZXItcGVybWlzc2lvbi13cml0ZSIsInB1Ymxpc2gtd3JpdGUiLCJsb2dzLXJlYWQiXX0sIm5hbWUiOiJFZHVhcmRvIFJvYmxlcyIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6IkVkdWFyZG8iLCJmYW1pbHlfbmFtZSI6IlJvYmxlcyIsImVtYWlsIjoiZWR1QHNlcXVlbnRlY2guaW8ifQ.LM9m5RBpdTv2QDR-kDDjQPRUlAYU90aUPgvA_BfRlv8YhSp5aDaUrtQSYbOtVvJMSie0nWWlyFDMVmCJYv8bIgc-AOtcBuytDDhEcvD49oW88RBRHaVmthmWlWRkUsSx_eWLp8ltiwCRfrmfWVGSwao1tnHbP3Herv-T-jqRP3RbtWoZDjA8fAXWUuvPLJGFNtV8Nxiq2eXPgfV-ZrOYiPa8I2AIw_zi04AfVA6nL01reZZ7Qn4m_XBn-iYd3mqjTIR_ilgWFMIyTQRnhNNsrInyZ2AKZ4lCqhKvYasa5ipYiTO_iaWoEkvBYO_Qhq_jFbCV3Q-_OPSNEqWUPFVhdA","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyNGRkNThkMC0wZTJlLTQzZTUtODFiMS02NzY0ODhmYjgwZTMifQ.eyJleHAiOjE3MjEyNTgzNDEsImlhdCI6MTcyMTI1NjU0MSwianRpIjoiOTk3NzBiZDgtMDA4ZC00Njg1LWFiMDYtOWI4NGU4ZTlkYTVjIiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy90ZW5hbnQtOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1IiwiYXVkIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy90ZW5hbnQtOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1Iiwic3ViIjoiYTIwMzc2ZDYtZTUzYS00ZWRmLWFmYzMtY2MxOGJiOGUyM2U1IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImFwaS1rZXktY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImZmNjMxYWM3LTZhNTMtNDc4Yy05MjRlLWE0MDNmMTVjZWU1NSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJmZjYzMWFjNy02YTUzLTQ3OGMtOTI0ZS1hNDAzZjE1Y2VlNTUifQ.kMHVSk9skaGnOBVmxGp4mixh0h3omNbMycF6GsbXrlJlh5RCNzrlnf7Tq2_erIovEzu4tyxnt4gvh9Jvhj5Log","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJYXRJRWFNd004REEyVGFWa3ZEQklxbjcxQ2plRjhLeEx5VU9MbnBjRXlJIn0.eyJleHAiOjE3MjEyNTY4NDEsImlhdCI6MTcyMTI1NjU0MSwiYXV0aF90aW1lIjowLCJqdGkiOiJjZTJjYzgyZS0zZTAxLTQzY2ItODA3YS00OTRmZmMxMDk5OGIiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwOTAvcmVhbG1zL3RlbmFudC05MDUwNWM4YS0yM2E5LTRjZGYtYTI2Yi00ZTE5ZjZhMDk3ZDUiLCJhdWQiOiJhcGkta2V5LWNsaWVudCIsInN1YiI6ImEyMDM3NmQ2LWU1M2EtNGVkZi1hZmMzLWNjMThiYjhlMjNlNSIsInR5cCI6IklEIiwiYXpwIjoiYXBpLWtleS1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiZmY2MzFhYzctNmE1My00NzhjLTkyNGUtYTQwM2YxNWNlZTU1IiwiYXRfaGFzaCI6IkhEWWg4SFV1Qy0xeGNCZl9SaWktWlEiLCJhY3IiOiIxIiwic2lkIjoiZmY2MzFhYzctNmE1My00NzhjLTkyNGUtYTQwM2YxNWNlZTU1IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiYWRtaW4tdXNlciIsIngtaGFzdXJhLXRlbmFudC1pZCI6IjkwNTA1YzhhLTIzYTktNGNkZi1hMjZiLTRlMTlmNmEwOTdkNSIsIngtaGFzdXJhLXVzZXItaWQiOiJhMjAzNzZkNi1lNTNhLTRlZGYtYWZjMy1jYzE4YmI4ZTIzZTUiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInN1cHBvcnQtbWF0ZXJpYWwtd3JpdGUiLCJlbGVjdGlvbi10eXBlLWNyZWF0ZSIsInJvbGUtd3JpdGUiLCJ0YWxseS1yZXN1bHRzLXJlYWQiLCJlbGVjdGlvbi1ldmVudC1jcmVhdGUiLCJ0YWxseS1zaGVldC12aWV3Iiwidm90aW5nLWNoYW5uZWwtcmVhZCIsInRhbGx5LXdyaXRlIiwidXNlci1jcmVhdGUiLCJ1bWFfYXV0aG9yaXphdGlvbiIsInRhbGx5LXNoZWV0LWRlbGV0ZSIsImFkbWluLWRhc2hib2FyZC12aWV3IiwidXNlci1yZWFkIiwibm90aWZpY2F0aW9uLXdyaXRlIiwidGVuYW50LWNyZWF0ZSIsImRvY3VtZW50LWRvd25sb2FkIiwibm90aWZpY2F0aW9uLXNlbmQiLCJ0YWxseS1yZWFkIiwidGFsbHktc2hlZXQtcHVibGlzaCIsInRlbmFudC13cml0ZSIsInRydXN0ZWUtY3JlYXRlIiwic3VwcG9ydC1tYXRlcmlhbC1yZWFkIiwiZWxlY3Rpb24tdHlwZS13cml0ZSIsImFkbWluLWNlcmVtb255Iiwidm90ZXItd3JpdGUiLCJub3RpZmljYXRpb24tcmVhZCIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtd3JpdGUiLCJ0YWxseS1zdGFydCIsInRlbmFudC1yZWFkIiwidXNlci1wZXJtaXNzaW9uLXJlYWQiLCJkb2N1bWVudC13cml0ZSIsInB1Ymxpc2gtcmVhZCIsInRydXN0ZWUtcmVhZCIsImNhc3Qtdm90ZS1yZWFkIiwiY3JlYXRlLWVsZWN0aW9uLWV2ZW50IiwiZWxlY3Rpb24tZXZlbnQtcmVhZCIsInZvdGluZy1jaGFubmVsLXdyaXRlIiwicm9sZS1hc3NpZ24iLCJkb2N1bWVudC1yZWFkIiwicm9sZS1yZWFkIiwiYWRtaW4tdXNlciIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtcmVhZCIsImVsZWN0aW9uLWV2ZW50LXdyaXRlIiwiZWxlY3Rpb24tdHlwZS1yZWFkIiwib2ZmbGluZV9hY2Nlc3MiLCJ2b3Rlci1jcmVhdGUiLCJjb21tdW5pY2F0aW9uLXRlbXBsYXRlLWNyZWF0ZSIsInVzZXItcGVybWlzc2lvbi1jcmVhdGUiLCJrZXlzLXJlYWQiLCJ2b3Rlci1yZWFkIiwidXNlci13cml0ZSIsImFyZWEtcmVhZCIsInRhbGx5LXNoZWV0LWNyZWF0ZSIsInJvbGUtY3JlYXRlIiwiZGVmYXVsdC1yb2xlcy1lbGVjdG9yYWwtcHJvY2VzcyIsImVsZWN0aW9uLXN0YXRlLXdyaXRlIiwiZG9jdW1lbnQtdXBsb2FkIiwidHJ1c3RlZS13cml0ZSIsImFyZWEtd3JpdGUiLCJ1c2VyLXBlcm1pc3Npb24td3JpdGUiLCJwdWJsaXNoLXdyaXRlIiwibG9ncy1yZWFkIl19LCJuYW1lIjoiRWR1YXJkbyBSb2JsZXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJFZHVhcmRvIiwiZmFtaWx5X25hbWUiOiJSb2JsZXMiLCJlbWFpbCI6ImVkdUBzZXF1ZW50ZWNoLmlvIn0.lUqvJJajokrluJY_mE0NjVaFRoT5kqK1_oKTRQ0WxYPKNOvBTTp198P82qKcWWgtomLoViWsi8KY9bbPlgcO_9kYr-XH_kV9tZN300wMG4RKh3drAPKTO-s44ItSwXXFVPbSmRKW-AW12oWb_kjsLN29tU0TWk4uWmy_t7f8bSPVqSfxBjjP0InhzEwiUQCDZ45zmNmq9oREj6Esx5nuTNt7KLgTDHtkpfnEu-Xr2xbHauQ3PduHZb0Khd5b0J5qH981DS2d-nfQO7sfWjgSGjg-8yUIpDc5m69Bz8UnGml29h-F80VH4z1PiSqdN0258t9jRzAiwAa-IoopMTO2hA","not-before-policy":1712715173,"session_state":"ff631ac7-6a53-478c-924e-a403f15cee55","scope":"openid profile email"} +{"access_token":"","expires_in":300,"refresh_expires_in":1800,"refresh_token":"","token_type":"Bearer","id_token":"","not-before-policy":1712715173,"session_state":"","scope":"openid profile email"} ``` This json includes an `Access Token`, a `Refresh Token`, `ID Token` as well as the @@ -167,10 +164,10 @@ user has access to, and you can use the `Refresh Token` to refresh the `Access T if it expires. For example, the following is an API call to create an Election Event programmatically -using the previously obtained `Access Token`: +after setting `ACCESS_TOKEN` to the value returned by your authentication request: ``` -curl 'http://localhost:8080/v1/graphql' -X POST -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'content-type: application/json' -H 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJJYXRJRWFNd004REEyVGFWa3ZEQklxbjcxQ2plRjhLeEx5VU9MbnBjRXlJIn0.eyJleHAiOjE3MjEyNTY4NDEsImlhdCI6MTcyMTI1NjU0MSwianRpIjoiODJhYTE5YjMtZmYzZS00YjZhLWIyZDQtODc0NGJmODc2MTM4IiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDkwL3JlYWxtcy90ZW5hbnQtOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1IiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImEyMDM3NmQ2LWU1M2EtNGVkZi1hZmMzLWNjMThiYjhlMjNlNSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwaS1rZXktY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImZmNjMxYWM3LTZhNTMtNDc4Yy05MjRlLWE0MDNmMTVjZWU1NSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsic3VwcG9ydC1tYXRlcmlhbC13cml0ZSIsImVsZWN0aW9uLXR5cGUtY3JlYXRlIiwicm9sZS13cml0ZSIsInRhbGx5LXJlc3VsdHMtcmVhZCIsImVsZWN0aW9uLWV2ZW50LWNyZWF0ZSIsInRhbGx5LXNoZWV0LXZpZXciLCJ2b3RpbmctY2hhbm5lbC1yZWFkIiwidGFsbHktd3JpdGUiLCJ1c2VyLWNyZWF0ZSIsInVtYV9hdXRob3JpemF0aW9uIiwidGFsbHktc2hlZXQtZGVsZXRlIiwiYWRtaW4tZGFzaGJvYXJkLXZpZXciLCJ1c2VyLXJlYWQiLCJub3RpZmljYXRpb24td3JpdGUiLCJ0ZW5hbnQtY3JlYXRlIiwiZG9jdW1lbnQtZG93bmxvYWQiLCJub3RpZmljYXRpb24tc2VuZCIsInRhbGx5LXJlYWQiLCJ0YWxseS1zaGVldC1wdWJsaXNoIiwidGVuYW50LXdyaXRlIiwidHJ1c3RlZS1jcmVhdGUiLCJzdXBwb3J0LW1hdGVyaWFsLXJlYWQiLCJlbGVjdGlvbi10eXBlLXdyaXRlIiwiYWRtaW4tY2VyZW1vbnkiLCJ2b3Rlci13cml0ZSIsIm5vdGlmaWNhdGlvbi1yZWFkIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS13cml0ZSIsInRhbGx5LXN0YXJ0IiwidGVuYW50LXJlYWQiLCJ1c2VyLXBlcm1pc3Npb24tcmVhZCIsImRvY3VtZW50LXdyaXRlIiwicHVibGlzaC1yZWFkIiwidHJ1c3RlZS1yZWFkIiwiY2FzdC12b3RlLXJlYWQiLCJjcmVhdGUtZWxlY3Rpb24tZXZlbnQiLCJlbGVjdGlvbi1ldmVudC1yZWFkIiwidm90aW5nLWNoYW5uZWwtd3JpdGUiLCJyb2xlLWFzc2lnbiIsImRvY3VtZW50LXJlYWQiLCJyb2xlLXJlYWQiLCJhZG1pbi11c2VyIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS1yZWFkIiwiZWxlY3Rpb24tZXZlbnQtd3JpdGUiLCJlbGVjdGlvbi10eXBlLXJlYWQiLCJvZmZsaW5lX2FjY2VzcyIsInZvdGVyLWNyZWF0ZSIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtY3JlYXRlIiwidXNlci1wZXJtaXNzaW9uLWNyZWF0ZSIsImtleXMtcmVhZCIsInZvdGVyLXJlYWQiLCJ1c2VyLXdyaXRlIiwiYXJlYS1yZWFkIiwidGFsbHktc2hlZXQtY3JlYXRlIiwicm9sZS1jcmVhdGUiLCJkZWZhdWx0LXJvbGVzLWVsZWN0b3JhbC1wcm9jZXNzIiwiZWxlY3Rpb24tc3RhdGUtd3JpdGUiLCJkb2N1bWVudC11cGxvYWQiLCJ0cnVzdGVlLXdyaXRlIiwiYXJlYS13cml0ZSIsInVzZXItcGVybWlzc2lvbi13cml0ZSIsInB1Ymxpc2gtd3JpdGUiLCJsb2dzLXJlYWQiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhcGkta2V5LWNsaWVudCI6eyJyb2xlcyI6WyJhcGkta2V5LXJvbGUiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiJmZjYzMWFjNy02YTUzLTQ3OGMtOTI0ZS1hNDAzZjE1Y2VlNTUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9oYXN1cmEuaW8vand0L2NsYWltcyI6eyJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJhZG1pbi11c2VyIiwieC1oYXN1cmEtdGVuYW50LWlkIjoiOTA1MDVjOGEtMjNhOS00Y2RmLWEyNmItNGUxOWY2YTA5N2Q1IiwieC1oYXN1cmEtdXNlci1pZCI6ImEyMDM3NmQ2LWU1M2EtNGVkZi1hZmMzLWNjMThiYjhlMjNlNSIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsic3VwcG9ydC1tYXRlcmlhbC13cml0ZSIsImVsZWN0aW9uLXR5cGUtY3JlYXRlIiwicm9sZS13cml0ZSIsInRhbGx5LXJlc3VsdHMtcmVhZCIsImVsZWN0aW9uLWV2ZW50LWNyZWF0ZSIsInRhbGx5LXNoZWV0LXZpZXciLCJ2b3RpbmctY2hhbm5lbC1yZWFkIiwidGFsbHktd3JpdGUiLCJ1c2VyLWNyZWF0ZSIsInVtYV9hdXRob3JpemF0aW9uIiwidGFsbHktc2hlZXQtZGVsZXRlIiwiYWRtaW4tZGFzaGJvYXJkLXZpZXciLCJ1c2VyLXJlYWQiLCJub3RpZmljYXRpb24td3JpdGUiLCJ0ZW5hbnQtY3JlYXRlIiwiZG9jdW1lbnQtZG93bmxvYWQiLCJub3RpZmljYXRpb24tc2VuZCIsInRhbGx5LXJlYWQiLCJ0YWxseS1zaGVldC1wdWJsaXNoIiwidGVuYW50LXdyaXRlIiwidHJ1c3RlZS1jcmVhdGUiLCJzdXBwb3J0LW1hdGVyaWFsLXJlYWQiLCJlbGVjdGlvbi10eXBlLXdyaXRlIiwiYWRtaW4tY2VyZW1vbnkiLCJ2b3Rlci13cml0ZSIsIm5vdGlmaWNhdGlvbi1yZWFkIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS13cml0ZSIsInRhbGx5LXN0YXJ0IiwidGVuYW50LXJlYWQiLCJ1c2VyLXBlcm1pc3Npb24tcmVhZCIsImRvY3VtZW50LXdyaXRlIiwicHVibGlzaC1yZWFkIiwidHJ1c3RlZS1yZWFkIiwiY2FzdC12b3RlLXJlYWQiLCJjcmVhdGUtZWxlY3Rpb24tZXZlbnQiLCJlbGVjdGlvbi1ldmVudC1yZWFkIiwidm90aW5nLWNoYW5uZWwtd3JpdGUiLCJyb2xlLWFzc2lnbiIsImRvY3VtZW50LXJlYWQiLCJyb2xlLXJlYWQiLCJhZG1pbi11c2VyIiwiY29tbXVuaWNhdGlvbi10ZW1wbGF0ZS1yZWFkIiwiZWxlY3Rpb24tZXZlbnQtd3JpdGUiLCJlbGVjdGlvbi10eXBlLXJlYWQiLCJvZmZsaW5lX2FjY2VzcyIsInZvdGVyLWNyZWF0ZSIsImNvbW11bmljYXRpb24tdGVtcGxhdGUtY3JlYXRlIiwidXNlci1wZXJtaXNzaW9uLWNyZWF0ZSIsImtleXMtcmVhZCIsInZvdGVyLXJlYWQiLCJ1c2VyLXdyaXRlIiwiYXJlYS1yZWFkIiwidGFsbHktc2hlZXQtY3JlYXRlIiwicm9sZS1jcmVhdGUiLCJkZWZhdWx0LXJvbGVzLWVsZWN0b3JhbC1wcm9jZXNzIiwiZWxlY3Rpb24tc3RhdGUtd3JpdGUiLCJkb2N1bWVudC11cGxvYWQiLCJ0cnVzdGVlLXdyaXRlIiwiYXJlYS13cml0ZSIsInVzZXItcGVybWlzc2lvbi13cml0ZSIsInB1Ymxpc2gtd3JpdGUiLCJsb2dzLXJlYWQiXX0sIm5hbWUiOiJFZHVhcmRvIFJvYmxlcyIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6IkVkdWFyZG8iLCJmYW1pbHlfbmFtZSI6IlJvYmxlcyIsImVtYWlsIjoiZWR1QHNlcXVlbnRlY2guaW8ifQ.LM9m5RBpdTv2QDR-kDDjQPRUlAYU90aUPgvA_BfRlv8YhSp5aDaUrtQSYbOtVvJMSie0nWWlyFDMVmCJYv8bIgc-AOtcBuytDDhEcvD49oW88RBRHaVmthmWlWRkUsSx_eWLp8ltiwCRfrmfWVGSwao1tnHbP3Herv-T-jqRP3RbtWoZDjA8fAXWUuvPLJGFNtV8Nxiq2eXPgfV-ZrOYiPa8I2AIw_zi04AfVA6nL01reZZ7Qn4m_XBn-iYd3mqjTIR_ilgWFMIyTQRnhNNsrInyZ2AKZ4lCqhKvYasa5ipYiTO_iaWoEkvBYO_Qhq_jFbCV3Q-_OPSNEqWUPFVhdA' -H 'x-hasura-role: admin-user' -H 'Origin: http://127.0.0.1:3002' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: cross-site' -H 'Priority: u=0' --data-raw '{"operationName":"CreateElectionEvent","variables":{"electionEvent":{"id":"dc65960e-5e64-4d6c-8d66-fc6f841b386b","name":"Create Event 1","encryption_protocol":"RSA256","tenant_id":"90505c8a-23a9-4cdf-a26b-4e19f6a097d5","is_archived":false,"presentation":{"i18n":{"en":{"name":"Create Event 1"},"es":{"name":"Create Event 1"},"cat":{"name":"Create Event 1"},"fr":{"name":"Create Event 1"}},"language_conf":{"enabled_language_codes":["en"],"default_language_code":"en"}}}},"query":"mutation CreateElectionEvent($electionEvent: CreateElectionEventInput!) {\n insertElectionEvent(object: $electionEvent) {\n id\n __typename\n }\n}"}' +curl 'http://localhost:8080/v1/graphql' -X POST -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br, zstd' -H 'content-type: application/json' -H "authorization: Bearer ${ACCESS_TOKEN}" -H 'x-hasura-role: admin-user' -H 'Origin: http://127.0.0.1:3002' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: cross-site' -H 'Priority: u=0' --data-raw '{"operationName":"CreateElectionEvent","variables":{"electionEvent":{"id":"dc65960e-5e64-4d6c-8d66-fc6f841b386b","name":"Create Event 1","encryption_protocol":"RSA256","tenant_id":"90505c8a-23a9-4cdf-a26b-4e19f6a097d5","is_archived":false,"presentation":{"i18n":{"en":{"name":"Create Event 1"},"es":{"name":"Create Event 1"},"cat":{"name":"Create Event 1"},"fr":{"name":"Create Event 1"}},"language_conf":{"enabled_language_codes":["en"],"default_language_code":"en"}}}},"query":"mutation CreateElectionEvent($electionEvent: CreateElectionEventInput!) {\n insertElectionEvent(object: $electionEvent) {\n id\n __typename\n }\n}"}' ``` Note that in Production you'll have to replace `http://localhost:8080/` with the actual value, for example `https://hasura-staging.sequent.vote/`. If successful, this API call will return something like: diff --git a/packages/.cargo/config.toml b/packages/.cargo/config.toml index 2a5f89df159..d5182c76fa7 100644 --- a/packages/.cargo/config.toml +++ b/packages/.cargo/config.toml @@ -1,5 +1,8 @@ -[patch.crates-io] -# FIXME (ereslibre); remove when bumping headless_chrome when everything is aligned -# Due to https://github.com/rust-headless-chrome/rust-headless-chrome/issues/535. -# Use the exact version 0.4.4 as published to crates.io (https://github.com/mdrokz/auto_generate_cdp/commit/634e636fd8edbe032d7191326527c721a2b51106) -auto_generate_cdp = { git = 'https://github.com/mdrokz/auto_generate_cdp', rev = '634e636fd8edbe032d7191326527c721a2b51106' } \ No newline at end of file +# Offline production build: the crate sources of Cargo.lock are vendored into +# packages/vendor by the controlled import of the external inputs, and the build +# resolves nothing over a network. The source replacement that activates the +# directory is the [source] configuration `cargo vendor` prints, appended to this +# file with the vendor directory; declaring the source here without a +# replace-with leaves a checkout that has no vendor directory untouched. +[source.vendored-sources] +directory = "vendor" diff --git a/packages/.gitignore b/packages/.gitignore index 9dd46098c7f..00a3e4cb7e3 100644 --- a/packages/.gitignore +++ b/packages/.gitignore @@ -1,3 +1,10 @@ package-lock.json yarn-error.log -b4/b4.db \ No newline at end of file +b4/b4.db + +# vendored crate sources of the offline production build (packages/.cargo/config.toml) +vendor/ + +# the WebAssembly package of sequent-core, built by the build definitions and the +# devcontainer script rather than committed +*/rust/pkg/ diff --git a/packages/Cargo.lock b/packages/Cargo.lock index e922864aace..bed060709ee 100644 --- a/packages/Cargo.lock +++ b/packages/Cargo.lock @@ -1335,6 +1335,7 @@ dependencies = [ "sequent-core", "serde", "serde_json", + "serial_test", "strand", "strum 0.27.2", "tokio", @@ -1907,7 +1908,6 @@ dependencies = [ [[package]] name = "celery-codegen" version = "0.5.5" -source = "git+https://github.com/Findeton/rusty-celery.git?rev=b4145925aaaf742b7ba54499fff45313bcadfca1#b4145925aaaf742b7ba54499fff45313bcadfca1" dependencies = [ "proc-macro2", "quote", @@ -2585,12 +2585,6 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - [[package]] name = "croner" version = "2.2.0" @@ -3262,15 +3256,6 @@ dependencies = [ "thousands", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" @@ -3467,23 +3452,6 @@ dependencies = [ "signature 1.6.4", ] -[[package]] -name = "ecies" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011318cc6f4f1906c1dae015013fd381e92deac290a29ddcd9f2e0dd14786037" -dependencies = [ - "getrandom 0.2.16", - "hkdf", - "libsecp256k1", - "once_cell", - "openssl", - "parking_lot 0.12.5", - "rand_core 0.6.4", - "sha2 0.10.9", - "wasm-bindgen", -] - [[package]] name = "ed25519" version = "3.0.0-rc.2" @@ -4643,15 +4611,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1489f81ead4b71a09ddeab6850c0356c0932587637d753f21ee1010ab875b013" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -5162,10 +5121,14 @@ name = "immudb-rs" version = "0.1.0" dependencies = [ "anyhow", + "bytes", + "h2 0.4.12", + "http 1.3.1", "log", "prost", "prost-types", "serde", + "tokio", "tonic", "tonic-build", "tracing", @@ -5691,51 +5654,6 @@ dependencies = [ "redox_syscall 0.5.18", ] -[[package]] -name = "libsecp256k1" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" -dependencies = [ - "arrayref", - "base64 0.22.1", - "digest 0.9.0", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.8.5", - "serde", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" -dependencies = [ - "libsecp256k1-core", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -6547,10 +6465,6 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "once_cell_polyfill" @@ -11942,7 +11856,6 @@ dependencies = [ "deadpool-postgres", "dhat", "dotenv", - "ecies", "electoral-log", "encoding_rs", "encoding_rs_io", @@ -12821,8 +12734,3 @@ checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] - -[[patch.unused]] -name = "auto_generate_cdp" -version = "0.4.4" -source = "git+https://github.com/mdrokz/auto_generate_cdp?rev=634e636fd8edbe032d7191326527c721a2b51106#634e636fd8edbe032d7191326527c721a2b51106" diff --git a/packages/Cargo.toml b/packages/Cargo.toml index 32b8d781fb6..fa395d301c2 100644 --- a/packages/Cargo.toml +++ b/packages/Cargo.toml @@ -4,6 +4,7 @@ [workspace] resolver = "2" +exclude = ["third_party/celery-codegen"] members = [ "braid", @@ -35,8 +36,5 @@ anyhow = "1.0" [patch.crates-io] celery = { git = "https://github.com/Findeton/rusty-celery.git", rev = "b4145925aaaf742b7ba54499fff45313bcadfca1" } -[source.crates-io] -replace-with = "vendored-sources" - -[source.vendored-sources] -directory = "vendor" +[patch."https://github.com/Findeton/rusty-celery.git"] +celery-codegen = { path = "third_party/celery-codegen" } diff --git a/packages/Dockerfile.cargo-packages b/packages/Dockerfile.cargo-packages index 7036a7d4313..88bb8169ac6 100644 --- a/packages/Dockerfile.cargo-packages +++ b/packages/Dockerfile.cargo-packages @@ -7,7 +7,7 @@ # for example packages/windmill/Dockerfile, packages/harvest/Dockerfile, etc. -FROM rust:1.96.0-slim-bookworm +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 WORKDIR /app diff --git a/packages/Dockerfile.keycloak b/packages/Dockerfile.keycloak index 20f297b0fb7..f13bf10eaab 100644 --- a/packages/Dockerfile.keycloak +++ b/packages/Dockerfile.keycloak @@ -1,4 +1,5 @@ -# syntax=docker/dockerfile:1.7 +# Use the accepted Docker Engine's bundled BuildKit frontend; no frontend image +# is downloaded when the controlled offline build reads this definition. # SPDX-FileCopyrightText: 2025 Sequent Tech Inc # @@ -17,8 +18,15 @@ ARG OID4VP_VERSION=0.6.2 ARG OID4VP_SHA256=f6cd968bc77fec4f8e1417d3caeefe4920941e01b119f152f1c9186bbd2b2e15 # Start with a Maven base image to compile the Keycloak provider extensions. -FROM maven:3-amazoncorretto-17 AS spis-build -RUN dnf install -y findutils && dnf clean all +# The Maven local repository. The controlled production build supplies it from the accepted +# external inputs with `--build-context maven-repository=`, which `prepare vendor` +# fills with the artifacts these poms resolve; without that override the stage is empty and +# Maven resolves from its remote repositories, which is what a build outside the production +# workflow does. +FROM scratch AS maven-repository + +FROM maven:3-amazoncorretto-17@sha256:771f7b6dbf09ad8b9a0ca910b8b852650d1cff3e6c5d29c81e8f0a0c3d631642 AS spis-build +COPY --from=maven-repository / /root/.m2/repository/ COPY ./keycloak-extensions/ /build/context/packages/keycloak-extensions/ WORKDIR /build/context RUN --mount=from=beyond,source=.,target=/build/beyond-context,readonly \ @@ -29,36 +37,51 @@ RUN --mount=from=beyond,source=.,target=/build/beyond-context,readonly \ mkdir -p beyond/packages; \ cp -a /build/beyond-context/packages/keycloak-extensions "${beyond_keycloak_extensions_dir}"; \ fi; \ - find "${keycloak_extensions_dir}" -name target -type d -prune -exec rm -rf {} +; \ - if [ -d "${beyond_keycloak_extensions_dir}" ]; then \ - find "${beyond_keycloak_extensions_dir}" -name target -type d -prune -exec rm -rf {} +; \ - fi; \ - mvn -B -f "${keycloak_extensions_dir}/pom.xml" clean package; \ + rm -rf "${keycloak_extensions_dir}"/target "${keycloak_extensions_dir}"/*/target; \ + rm -rf "${beyond_keycloak_extensions_dir}"/target "${beyond_keycloak_extensions_dir}"/*/target; \ + mvn --offline -B -f "${keycloak_extensions_dir}/pom.xml" clean package; \ mkdir -p /build/keycloak-provider-jars /build/keycloak-provider-conf; \ cp "${keycloak_extensions_dir}/quarkus.properties" /build/keycloak-provider-conf/quarkus.properties; \ provider_dirs="${keycloak_extensions_dir}"; \ if [ -d "${beyond_keycloak_extensions_dir}" ]; then \ provider_dirs="${provider_dirs} ${beyond_keycloak_extensions_dir}"; \ fi; \ - find ${provider_dirs} \ - -path '*/target/*.jar' \ - ! -name 'original-*.jar' \ - ! -name '*-tests.jar' \ - -exec cp -t /build/keycloak-provider-jars/ {} + - -# Build the oid4vp extension from source (requires Java 21) -FROM maven:3-amazoncorretto-21 AS oid4vp-build + for dir in ${provider_dirs}; do \ + for jar in "${dir}"/*/target/*.jar "${dir}"/target/*.jar; do \ + [ -f "${jar}" ] || continue; \ + case "${jar##*/}" in original-*.jar|*-tests.jar) continue ;; esac; \ + cp "${jar}" /build/keycloak-provider-jars/; \ + done; \ + done + +# The source archive of the oid4vp extension. The controlled production build supplies it +# from the accepted external inputs with `--build-context oid4vp-src=`, which replaces this stage and leaves the download below unbuilt. Without +# that override the archive is downloaded from its publisher and checked against +# OID4VP_SHA256, which is what a build outside the production workflow does. +FROM maven:3-amazoncorretto-21@sha256:81de222c1f34ac467bf968e1800b73fb41e714427c0212ba004296ea972e808a AS oid4vp-download ARG OID4VP_VERSION ARG OID4VP_SHA256 RUN curl -fsSL -o /tmp/oid4vp.tar.gz \ https://github.com/ba-itsys/keycloak-extension-oid4vp/archive/refs/tags/v${OID4VP_VERSION}.tar.gz \ - && echo "${OID4VP_SHA256} /tmp/oid4vp.tar.gz" | sha256sum -c - \ + && echo "${OID4VP_SHA256} /tmp/oid4vp.tar.gz" | sha256sum -c - + +FROM scratch AS oid4vp-src +COPY --from=oid4vp-download /tmp/oid4vp.tar.gz /oid4vp.tar.gz + +# Build the oid4vp extension from source (requires Java 21) +FROM maven:3-amazoncorretto-21@sha256:81de222c1f34ac467bf968e1800b73fb41e714427c0212ba004296ea972e808a AS oid4vp-build +ARG OID4VP_VERSION +ARG OID4VP_SHA256 +COPY --from=maven-repository / /root/.m2/repository/ +COPY --from=oid4vp-src /oid4vp.tar.gz /tmp/oid4vp.tar.gz +RUN echo "${OID4VP_SHA256} /tmp/oid4vp.tar.gz" | sha256sum -c - \ && mkdir -p /build \ && tar -xzf /tmp/oid4vp.tar.gz -C /build \ && cd /build/keycloak-extension-oid4vp-${OID4VP_VERSION} \ - && mvn -B clean package -DskipTests + && mvn --offline -B clean package -DskipTests -FROM quay.io/keycloak/keycloak:26.6.1 AS builder +FROM quay.io/keycloak/keycloak:26.6.1@sha256:dea26401d06341095cc4ea9d66896200b55de5ca1daa1d2fcbe58493afa6e0ad AS builder ARG OID4VP_VERSION # The extension OID4VP_VERSION v0.6.2 requires Keycloak 26.6.1 or later, as @@ -99,7 +122,7 @@ COPY --chown=keycloak:keycloak \ RUN /opt/keycloak/bin/kc.sh build -FROM quay.io/keycloak/keycloak:26.6.1 +FROM quay.io/keycloak/keycloak:26.6.1@sha256:dea26401d06341095cc4ea9d66896200b55de5ca1daa1d2fcbe58493afa6e0ad COPY --from=builder /opt/keycloak/ /opt/keycloak/ COPY --from=spis-build /build/keycloak-provider-conf/quarkus.properties /opt/keycloak/conf/quarkus.properties diff --git a/packages/Dockerfile.prod b/packages/Dockerfile.prod index e69f17cce52..7b2d82daae0 100644 --- a/packages/Dockerfile.prod +++ b/packages/Dockerfile.prod @@ -2,7 +2,114 @@ # # SPDX-License-Identifier: AGPL-3.0-only -FROM node:20-alpine as builder +# Every input the stages below obtain from a network has a stage of its own that the controlled +# production build replaces with the accepted external input, `--build-context =`, +# leaving the download unbuilt; a build outside that workflow downloads it from its publisher. +# `wasm-toolchain` holds the four archives of the WebAssembly toolchain (ALC-06, Section 2.2), +# whose digests are recorded in the CM system's inputs configuration and verified at import; +# `debs-wasm-build` the Debian packages of the wasm-build stage, authenticated by apt against the +# keyring of the base image when downloaded; `cargo-vendor` the crate sources of Cargo.lock, +# which cargo reads through the [source] configuration the production job appends to +# .cargo/config.toml; `yarn-offline-mirror` the npm packages of yarn.lock; `apks-nginx` the +# Alpine packages of the runtime stage, whose signatures apk verifies when it installs them. +ARG WASM_PACK_VERSION=0.15.0 +ARG WASM_BINDGEN_VERSION=0.2.104 +ARG BINARYEN_VERSION=123 +ARG RUST_VERSION=1.96.0 + +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS wasm-toolchain-download +ARG WASM_PACK_VERSION +ARG WASM_BINDGEN_VERSION +ARG BINARYEN_VERSION +ARG RUST_VERSION +ARG TARGETARCH +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN set -eux; \ + case "${TARGETARCH:-amd64}" in \ + amd64) arch=x86_64 ;; \ + arm64) arch=aarch64 ;; \ + *) echo "unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + mkdir -p /archives; \ + curl -fsSL -o /archives/wasm-pack.tar.gz \ + "https://github.com/rustwasm/wasm-pack/releases/download/v${WASM_PACK_VERSION}/wasm-pack-v${WASM_PACK_VERSION}-${arch}-unknown-linux-musl.tar.gz"; \ + curl -fsSL -o /archives/wasm-bindgen.tar.gz \ + "https://github.com/wasm-bindgen/wasm-bindgen/releases/download/${WASM_BINDGEN_VERSION}/wasm-bindgen-${WASM_BINDGEN_VERSION}-${arch}-unknown-linux-musl.tar.gz"; \ + curl -fsSL -o /archives/binaryen.tar.gz \ + "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-${arch}-linux.tar.gz"; \ + curl -fsSL -o /archives/rust-std-wasm32.tar.gz \ + "https://static.rust-lang.org/dist/rust-std-${RUST_VERSION}-wasm32-unknown-unknown.tar.gz" + +FROM scratch AS wasm-toolchain +COPY --from=wasm-toolchain-download /archives/ / + +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS debs-wasm-build-download +RUN mkdir -p /debs && apt-get update && apt-get install -y --no-install-recommends --download-only \ + -o Dir::Cache::archives=/debs \ + clang \ + m4 \ + make + +FROM scratch AS debs-wasm-build +COPY --from=debs-wasm-build-download /debs/*.deb / + +FROM scratch AS cargo-vendor + +FROM scratch AS yarn-offline-mirror + +FROM nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c127ffaeeb4a6d4b95a26fead017d5913 AS apks-nginx-download +RUN mkdir -p /apks && apk fetch --no-cache --recursive --output /apks jq + +FROM scratch AS apks-nginx +COPY --from=apks-nginx-download /apks/*.apk / + +# The WebAssembly module of sequent-core that every single-page application consumes. +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS wasm-build +ARG RUST_VERSION +COPY --from=debs-wasm-build / /tmp/debs/ +RUN apt-get -o Dir::Cache::archives=/tmp/debs --no-download install -y --no-install-recommends /tmp/debs/*.deb \ + && rm -rf /tmp/debs +COPY --from=wasm-toolchain / /archives/ +RUN set -eux; \ + tar -xzf /archives/wasm-pack.tar.gz -C /usr/local/bin --strip-components=1 --wildcards '*/wasm-pack'; \ + tar -xzf /archives/wasm-bindgen.tar.gz -C /usr/local/bin --strip-components=1 --wildcards '*/wasm-bindgen*'; \ + tar -xzf /archives/binaryen.tar.gz -C /usr/local --strip-components=1 --wildcards '*/bin/wasm-opt' '*/lib/*'; \ + mkdir -p /tmp/rust-std; \ + tar -xzf /archives/rust-std-wasm32.tar.gz -C /tmp/rust-std --strip-components=1; \ + /tmp/rust-std/install.sh --prefix="$(rustc --print sysroot)" --disable-ldconfig; \ + rm -rf /tmp/rust-std /archives; \ + wasm-pack --version; wasm-bindgen --version; wasm-opt --version + +WORKDIR /app +COPY ./braid ./braid +COPY ./b4 ./b4 +COPY ./electoral-log ./electoral-log +COPY ./harvest ./harvest +COPY ./strand ./strand +COPY ./immu-board ./immu-board +COPY ./immudb-rs ./immudb-rs +COPY ./sequent-core ./sequent-core +COPY ./velvet ./velvet +COPY ./windmill ./windmill +COPY ./e2e ./e2e +COPY ./step-cli ./step-cli +COPY ./orare ./orare +COPY ./wrap-map-err ./wrap-map-err +COPY ./plugins ./plugins +COPY ./third_party ./third_party +COPY ./Cargo.toml ./Cargo.toml +COPY ./Cargo.lock ./Cargo.lock +COPY ./.cargo ./.cargo +COPY --from=cargo-vendor / ./vendor/ + +WORKDIR /app/sequent-core +RUN CARGO_NET_OFFLINE=true wasm-pack build --mode no-install --release --target web --out-name index \ + --features=wasm,default_features --locked --offline + +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 as builder WORKDIR /usr/src/app/ @@ -16,8 +123,16 @@ COPY ./results-portal ./results-portal COPY ./package.json ./package.json COPY ./yarn.lock ./yarn.lock +# The WebAssembly module each of them depends on, built above rather than committed +COPY --from=wasm-build /app/sequent-core/pkg ./ui-core/rust/pkg +COPY --from=wasm-build /app/sequent-core/pkg ./voting-portal/rust/pkg +COPY --from=wasm-build /app/sequent-core/pkg ./admin-portal/rust/pkg +COPY --from=wasm-build /app/sequent-core/pkg ./ballot-verifier/rust/pkg + # Install dependencies -RUN yarn install --frozen-lockfile --non-interactive +COPY --from=yarn-offline-mirror / /usr/src/yarn-offline-mirror/ +RUN printf 'yarn-offline-mirror "/usr/src/yarn-offline-mirror"\n' > .yarnrc +RUN yarn install --offline --frozen-lockfile --non-interactive ARG SPA_NAME @@ -28,9 +143,10 @@ RUN yarn build:${SPA_NAME} -FROM nginx:alpine +FROM nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c127ffaeeb4a6d4b95a26fead017d5913 # Install dependencies -RUN apk add --no-cache jq +COPY --from=apks-nginx / /tmp/apks/ +RUN apk add --no-network /tmp/apks/*.apk && rm -rf /tmp/apks # Remove default nginx static assets RUN rm -rf /usr/share/nginx/html/* # Copy custom NGINX config diff --git a/packages/Dockerfile.prod-vstl-dependencies b/packages/Dockerfile.prod-vstl-dependencies index 8a3655aaa3c..63b00b29a44 100644 --- a/packages/Dockerfile.prod-vstl-dependencies +++ b/packages/Dockerfile.prod-vstl-dependencies @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-only -FROM node:20-alpine AS dependencies +FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS dependencies WORKDIR /usr/src/app/ diff --git a/packages/Dockerfile.prod-vstl-dependencies-build b/packages/Dockerfile.prod-vstl-dependencies-build index d76484c0e2b..9f9f8dea3ca 100644 --- a/packages/Dockerfile.prod-vstl-dependencies-build +++ b/packages/Dockerfile.prod-vstl-dependencies-build @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-only -FROM nginx:alpine +FROM nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c127ffaeeb4a6d4b95a26fead017d5913 # Install dependencies RUN apk add --no-cache jq diff --git a/packages/ECIESEncryption/src/main/java/com/example/ECIESEncryptionTool.java b/packages/ECIESEncryption/src/main/java/com/example/ECIESEncryptionTool.java index 1ef6f258138..70e06d0d8c7 100644 --- a/packages/ECIESEncryption/src/main/java/com/example/ECIESEncryptionTool.java +++ b/packages/ECIESEncryption/src/main/java/com/example/ECIESEncryptionTool.java @@ -484,7 +484,7 @@ private static String getPrivateKeyPEM(PrivateKey privateKey) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); StringWriter stringWriter = new StringWriter(); try (PrintWriter writer = new PrintWriter(stringWriter)) { - writer.println("-----BEGIN PRIVATE KEY-----"); + writer.println("-----BEGIN PRIVATE KEY-----"); // gitleaks:allow -- delimiter; key bytes come from the runtime PrivateKey. writer.println(Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(pkcs8EncodedKeySpec.getEncoded())); writer.println("-----END PRIVATE KEY-----"); } @@ -502,7 +502,7 @@ private static PublicKey loadPublicKeyFromPEM(String pem) throws Exception { } private static PrivateKey loadPrivateKeyFromPEM(String pem) throws Exception { - String privateKeyPEM = pem.replace("-----BEGIN PRIVATE KEY-----", "") + String privateKeyPEM = pem.replace("-----BEGIN PRIVATE KEY-----", "") // gitleaks:allow -- delimiter; payload is supplied by the caller. .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); byte[] decoded = Base64.getDecoder().decode(privateKeyPEM); diff --git a/packages/admin-portal/package.json b/packages/admin-portal/package.json index d1d74397436..34435ba2941 100644 --- a/packages/admin-portal/package.json +++ b/packages/admin-portal/package.json @@ -53,7 +53,7 @@ "react-js-cron": "^5.0.1", "react-redux": "8.1.2", "react-router": "7.12.0", - "sequent-core": "file:./rust/sequent-core-0.1.0.tgz", + "sequent-core": "file:./rust/pkg", "braid-wasm": "file:./rust/braid-wasm-0.1.0.tgz", "sql.js": "^1.13.0", "stream-browserify": "^3.0.0", @@ -169,7 +169,7 @@ "ts-loader": "9.5.4", "ts-node": "^10.9.2", "typescript": "^5.9.2", - "webpack": "5.94.0", + "webpack": "^5.94.0", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.2" }, diff --git a/packages/admin-portal/rust/sequent-core-0.1.0.tgz b/packages/admin-portal/rust/sequent-core-0.1.0.tgz deleted file mode 100644 index c76d030b6eb..00000000000 Binary files a/packages/admin-portal/rust/sequent-core-0.1.0.tgz and /dev/null differ diff --git a/packages/admin-portal/rust/sequent-core-0.1.0.tgz.license b/packages/admin-portal/rust/sequent-core-0.1.0.tgz.license deleted file mode 100644 index a75f1d02e29..00000000000 --- a/packages/admin-portal/rust/sequent-core-0.1.0.tgz.license +++ /dev/null @@ -1,3 +0,0 @@ - SPDX-FileCopyrightText: 2025 Sequent Tech Inc - -SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/b4/Cargo.toml b/packages/b4/Cargo.toml index 96b30382ac7..fb78aa7cb10 100644 --- a/packages/b4/Cargo.toml +++ b/packages/b4/Cargo.toml @@ -62,3 +62,9 @@ clap = { version = "4.5", features = ["derive", "env"], optional = true } # WASM dependencies js-sys = { version = "0.3", optional = true } + +[dev-dependencies] +serial_test = "3.2" + +[lints.rust] +unsafe_code = "forbid" diff --git a/packages/b4/Dockerfile.prod b/packages/b4/Dockerfile.prod index 39b08913763..80d6b013746 100644 --- a/packages/b4/Dockerfile.prod +++ b/packages/b4/Dockerfile.prod @@ -2,23 +2,46 @@ # # SPDX-License-Identifier: AGPL-3.0-only -FROM rust:1.96.0-slim-bookworm AS builder - -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ +# The Debian packages the two stages below install, and the vendored crates the build reads. +# The controlled production build supplies each from the accepted external inputs with +# `--build-context =`, which replaces the stage of that name and leaves the +# download unbuilt: `debs-builder` and `debs-runtime` hold the .deb files of the packages +# their download stages name, `cargo-vendor` the crate sources of Cargo.lock, which cargo +# reads through the [source] configuration the production job appends to .cargo/config.toml. +# A build outside that workflow downloads the packages from the Debian archive, authenticated +# by apt against the keyring of the base image, and resolves the crates from crates.io. +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS debs-builder-download +RUN mkdir -p /debs && apt-get update && apt-get install -y --no-install-recommends --download-only \ + -o Dir::Cache::archives=/debs \ protobuf-compiler \ libprotobuf-dev \ m4 \ - make \ - && rm -rf /var/lib/apt/lists/* + make + +FROM scratch AS debs-builder +COPY --from=debs-builder-download /debs/*.deb / + +FROM debian:bookworm@sha256:6ebd97fa83deb272194a2cf015b3d26a4d538e9ad3a7a79d544c8af5b0a01443 AS debs-runtime-download +RUN mkdir -p /debs && apt-get update && apt-get install -y --no-install-recommends --download-only \ + -o Dir::Cache::archives=/debs \ + libssl3 \ + ca-certificates +FROM scratch AS debs-runtime +COPY --from=debs-runtime-download /debs/*.deb / + +FROM scratch AS cargo-vendor + +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS builder -# copy projects -#TODO: Copy only necessary files WORKDIR /app + +COPY --from=debs-builder / /tmp/debs/ +RUN apt-get -o Dir::Cache::archives=/tmp/debs --no-download install -y --no-install-recommends /tmp/debs/*.deb \ + && rm -rf /tmp/debs + COPY ./braid ./braid -COPY ./b3 ./b3 +COPY ./b4 ./b4 COPY ./electoral-log ./electoral-log COPY ./harvest ./harvest COPY ./strand ./strand @@ -33,42 +56,34 @@ COPY ./orare ./orare COPY ./wrap-map-err ./wrap-map-err COPY ./braid/scripts ./braid/scripts COPY ./plugins ./plugins +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo +COPY --from=cargo-vendor / ./vendor/ -# build projects -WORKDIR /app/b3 -RUN cargo build --features=server --release --locked +WORKDIR /app/b4 +# The CM helper supplies a fresh namespace per build invocation. Component builds +# share Cargo's dependency artifacts only inside that invocation and architecture. +# Copy selected binaries outside the cache mount into the immutable builder layer. +ARG TEMENOS_CARGO_CACHE_ID=standalone +ARG TARGETARCH +RUN --mount=type=cache,id=${TEMENOS_CARGO_CACHE_ID}-${TARGETARCH},target=/app/target,sharing=locked cargo build --features=native --release --locked --offline \ + && mkdir -p /opt/temenos-build \ + && cp /app/target/release/b4 /opt/temenos-build/ +FROM debian:bookworm@sha256:6ebd97fa83deb272194a2cf015b3d26a4d538e9ad3a7a79d544c8af5b0a01443 +COPY --from=debs-runtime / /tmp/debs/ +RUN apt-get -o Dir::Cache::archives=/tmp/debs --no-download install -y --no-install-recommends /tmp/debs/*.deb \ + && rm -rf /tmp/debs -FROM debian:bookworm -# Some shared libraries (extra-runtime-dependencies) may need to be installed: -# Installing also AWS CLI and Vault CLI for trustee configuration -# Set the default Vault version (put 'latest' for automatic substitution) -ARG VAULT_VERSION=1.15.5 -RUN apt-get update && apt-get install -y --no-install-recommends \ - libssl3 \ - ca-certificates \ - curl \ - unzip \ - && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ - && unzip -o awscliv2.zip \ - && ./aws/install \ - && rm -f awscliv2.zip \ - && if [ "${VAULT_VERSION}" = "latest" ]; then \ - VAULT_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -oP 'vault/\K[0-9.]+(?=/")' | head -n 1); \ - fi \ - && curl -o vault.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip \ - && unzip vault.zip -d /usr/local/bin/ \ - && rm -f vault.zip \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/server /usr/bin/b3 +# Cloud secret CLIs are optional deployment inputs, not downloaded by the build. +# uniWAHL uses a mounted trustee configuration / EnvVarMasterSecret and S3-compatible storage. + +COPY --from=builder /opt/temenos-build/b4 /usr/bin/b4 # init index db RUN mkdir /tmp/cache -ENTRYPOINT ["/usr/bin/b3"] +ENTRYPOINT ["/usr/bin/b4"] diff --git a/packages/b4/Dockerfile.prod-vstl-compile b/packages/b4/Dockerfile.prod-vstl-compile index 8c4a043c23f..aba78c7ed76 100644 --- a/packages/b4/Dockerfile.prod-vstl-compile +++ b/packages/b4/Dockerfile.prod-vstl-compile @@ -21,6 +21,7 @@ COPY ./e2e ./e2e COPY ./step-cli ./step-cli COPY ./wrap-map-err ./wrap-map-err COPY ./braid/scripts ./braid/scripts +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo diff --git a/packages/b4/Dockerfile.prod-vstl-dependencies b/packages/b4/Dockerfile.prod-vstl-dependencies index b78624625f4..71bf136c1b8 100644 --- a/packages/b4/Dockerfile.prod-vstl-dependencies +++ b/packages/b4/Dockerfile.prod-vstl-dependencies @@ -35,6 +35,7 @@ COPY ./wrap-map-err/Cargo.toml ./wrap-map-err/Cargo.toml COPY ./braid/scripts/Cargo.toml ./braid/scripts/Cargo.toml COPY ./orare/Cargo.toml ./orare/Cargo.toml COPY ./orare/doc_renderer/Cargo.toml ./orare/doc_renderer/Cargo.toml +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo diff --git a/packages/b4/src/client/pgsql.rs b/packages/b4/src/client/pgsql.rs index 42ed34ebc26..4a50fb4507c 100644 --- a/packages/b4/src/client/pgsql.rs +++ b/packages/b4/src/client/pgsql.rs @@ -22,8 +22,8 @@ use crate::messages::statement::StatementType; use strand::context::Ctx; use strand::serialization::{StrandDeserialize, StrandSerialize}; -const BOARDS_TABLE: &'static str = "boards"; -const MESSAGES_TABLE: &'static str = "messages"; +const BOARDS_TABLE: &str = "boards"; +const MESSAGES_TABLE: &str = "messages"; const PG_DEFAULT_ENTRIES_TX_LIMIT: usize = 50; const PG_DEFAULT_OFFSET: usize = 0; const PG_DEFAULT_LIMIT: usize = 2500; @@ -310,7 +310,7 @@ impl<'a> PooledPgsqlB3Client<'a> { pub async fn insert_messages( &mut self, board_name: &str, - messages: &Vec, + messages: &[B3MessageRow], ) -> Result<()> { insert_messages(self.client.deref_mut(), board_name, messages).await } @@ -389,7 +389,7 @@ impl PgsqlB3Client { pub async fn insert_messages( &mut self, board_name: &str, - messages: &Vec, + messages: &[B3MessageRow], ) -> Result<()> { insert_messages(self.client.borrow_mut(), board_name, messages).await } @@ -427,7 +427,7 @@ impl PgsqlB3Client { board_name: &str, ballots: Message, ) -> Result<()> { - insert_ballots::(self.client.borrow_mut(), board_name, ballots).await + insert_ballots(self.client.borrow_mut(), board_name, ballots).await } } @@ -745,7 +745,7 @@ async fn get_board(client: &Client, board_name: &str) -> Result>>()?; - if boards.len() > 0 { + if !boards.is_empty() { Ok(Some(boards[0].clone())) } else { Ok(None) @@ -830,11 +830,7 @@ async fn insert_configuration( /// Inserts the ballots into the requested board table. #[instrument(err, skip(client))] -async fn insert_ballots( - client: &mut Client, - board_name: &str, - ballots: Message, -) -> Result<()> { +async fn insert_ballots(client: &mut Client, board_name: &str, ballots: Message) -> Result<()> { if ballots.statement.get_kind() != StatementType::Ballots { return Err(anyhow!("Expected message to be Ballots")); } @@ -869,15 +865,10 @@ async fn insert_ballots( async fn insert_messages( client: &mut Client, board_name: &str, - messages: &Vec, + messages: &[B3MessageRow], ) -> Result<()> { for chunk in messages.chunks(PG_DEFAULT_ENTRIES_TX_LIMIT) { - cfg_if::cfg_if! { if #[cfg(feature = "sqlcopy")] { - insert_copy(client, board_name, chunk).await?; - } - else { - insert(client, board_name, chunk).await?; - }} + insert(client, board_name, chunk).await?; } Ok(()) } @@ -975,13 +966,13 @@ async fn insert(client: &mut Client, board_name: &str, messages: &[B3MessageRow] let transaction = client.transaction().await?; // http://disq.us/p/2ficy6c // https://stackoverflow.com/questions/52432459/postgresql-serialized-inserts-interleaving-sequence-numbers - let lock = format!("select pg_advisory_xact_lock(hashtext($1))"); + let lock = "select pg_advisory_xact_lock(hashtext($1))".to_string(); transaction.execute(&lock, &[&board_name]).await?; let mut batches: i32 = 0; for message in messages { if message.statement_kind == StatementType::Ballots.to_string() { - batches = batches + 1; + batches += 1; } let message_sql = format!( @@ -1096,127 +1087,13 @@ async fn get_one(client: &Client, board_name: &str, id: i64) -> Result 0 { + if !rows.is_empty() { Ok(Some(B3MessageRow::try_from(&rows[0])?)) } else { Ok(None) } } -cfg_if::cfg_if! { if #[cfg(feature = "sqlcopy")] { - - use futures::pin_mut; - use tokio_postgres::binary_copy::BinaryCopyInWriter; - use tokio_postgres::types::{ToSql, Type}; - - // Uses the COPY postgresql command - // Note: For partitioned tables, COPY goes to the partition directly via messages_{board_name} - async fn insert_copy( - client: &mut Client, - board_name: &str, - messages: &[B3MessageRow], - ) -> Result<()> { - // Start a new transaction - let transaction = client.transaction().await?; - let types: Vec = vec![ - Type::VARCHAR, // board_name - Type::TIMESTAMP, // created - Type::INT8, // timestamp (b4 compat) - Type::VARCHAR, - Type::TIMESTAMP, - Type::VARCHAR, - Type::INT4, - Type::INT4, - Type::BYTEA, - Type::VARCHAR, - ]; - let stmt = format!("COPY {} (board_name, created, timestamp, sender_pk, statement_timestamp, statement_kind, batch, mix_number, message, version) FROM STDIN BINARY", MESSAGES_TABLE); - - // http://disq.us/p/2ficy6c - // https://stackoverflow.com/questions/52432459/postgresql-serialized-inserts-interleaving-sequence-numbers - let lock = format!("select pg_advisory_xact_lock(hashtext($1))"); - transaction.execute(&lock, &[&board_name]).await?; - let sink = transaction.copy_in(&stmt).await?; - let writer = BinaryCopyInWriter::new(sink, &types); - let batches = _write(writer, board_name, &messages).await?; - transaction.commit().await?; - - // We do not care if any of these operations fail, they are statistics - if let Some(last) = messages.last() { - let Ok(transaction) = client.transaction().await else { - return Ok(()); - }; - - let message_sql = format!( - r#" - UPDATE {} - SET - last_message_kind = $1, - message_count = (SELECT COUNT(*) FROM {} WHERE board_name = $3), - batch_count = batch_count + $2, - last_updated = localtimestamp - WHERE board_name = $3 - "#, - BOARDS_TABLE, MESSAGES_TABLE, - ); - - let Ok(_) = transaction - .execute(&message_sql, &[&last.statement_kind, &batches, &board_name]) - .await - else { - return Ok(()); - }; - - let _ = transaction.commit().await; - } - - Ok(()) - } - - async fn _write(writer: BinaryCopyInWriter, board_name: &str, messages: &[B3MessageRow]) -> Result { - pin_mut!(writer); - - let mut row: Vec<&'_ (dyn ToSql + Sync)> = vec![]; - let mut ts: Vec<(SystemTime, SystemTime)> = vec![]; - let mut batches = 0; - - for message in messages { - if message.statement_kind == StatementType::Ballots.to_string() { - batches = batches + 1; - } - - let created = crate::system_time_from_timestamp(message.created).ok_or(anyhow!( - "Could not extract system time from 'created' value" - ))?; - let statement_timestamp = crate::system_time_from_timestamp(message.created).ok_or( - anyhow!("Could not extract system time from 'statement_timestamp' value"), - )?; - - ts.push((created, statement_timestamp)); - } - for (i, message) in messages.iter().enumerate() { - let timestamp_i64 = message.created as i64; - row.clear(); - row.push(&board_name); // board_name first - row.push(&ts[i].0); // created - row.push(×tamp_i64); // timestamp - row.push(&message.sender_pk); - row.push(&ts[i].1); // statement_timestamp - row.push(&message.statement_kind); - row.push(&message.batch); - row.push(&message.mix_number); - row.push(&message.message); - row.push(&message.version); - - writer.as_mut().write(&row).await?; - } - - writer.finish().await?; - - Ok(batches) - } -}} - // Run ignored tests with // cargo test -- --include-ignored #[cfg(test)] @@ -1224,12 +1101,12 @@ pub(crate) mod tests { use super::*; use serial_test::serial; - const PG_DATABASE: &'static str = "protocoldb"; - const PG_HOST: &'static str = "localhost"; - const PG_USER: &'static str = "postgres"; - const PG_PASSW: &'static str = "postgrespw"; + const PG_DATABASE: &str = "protocoldb"; + const PG_HOST: &str = "localhost"; + const PG_USER: &str = "postgres"; + const PG_PASSW: &str = "postgrespw"; const PG_PORT: u32 = 49153; - const TEST_BOARD: &'static str = "testboard"; + const TEST_BOARD: &str = "testboard"; async fn set_up() -> PgsqlB3Client { let c = PgsqlConnectionParams::new(PG_HOST, PG_PORT, PG_USER, PG_PASSW); @@ -1284,7 +1161,7 @@ pub(crate) mod tests { let ret = client.get_messages(TEST_BOARD, 0).await.unwrap(); assert_eq!(messages.len(), 1); - let msg = ret.get(0).unwrap(); + let msg = ret.first().unwrap(); // id is autogenerated by postgres // timestamps will not match due to less precision on postgres side assert_eq!(msg.sender_pk, board_message.sender_pk); diff --git a/packages/b4/src/db.rs b/packages/b4/src/db.rs index 15eef84f007..40995848922 100644 --- a/packages/b4/src/db.rs +++ b/packages/b4/src/db.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only use crate::api_types::Message; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use bb8_postgres::{bb8::Pool, PostgresConnectionManager}; use std::env; use tokio_postgres::NoTls; @@ -251,6 +251,10 @@ pub async fn list_boards(pool: &DbPool) -> Result> { .collect()) } +#[expect( + clippy::too_many_arguments, + reason = "Preserve the existing message storage API with explicit payload, storage location and signed statement metadata" +)] pub async fn insert_message( pool: &DbPool, board_name: &str, diff --git a/packages/b4/src/lib.rs b/packages/b4/src/lib.rs index 4dd0dbc2f1c..44914feaf44 100644 --- a/packages/b4/src/lib.rs +++ b/packages/b4/src/lib.rs @@ -18,7 +18,9 @@ pub mod s3; pub mod state; use crate::messages::newtypes::Timestamp; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(feature = "client")] +use std::time::Duration; +use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(not(target_arch = "wasm32"))] pub fn timestamp() -> Timestamp { @@ -36,11 +38,13 @@ pub fn timestamp() -> Timestamp { (js_sys::Date::now() / 1000.0) as u64 } +#[cfg(feature = "client")] pub(crate) fn system_time_from_timestamp(seconds: Timestamp) -> Option { let duration = Duration::from_secs(seconds); UNIX_EPOCH.checked_add(duration) } +#[cfg(feature = "client")] pub(crate) fn timestamp_from_system_time(system_time: &SystemTime) -> Timestamp { let since_the_epoch = system_time .duration_since(UNIX_EPOCH) diff --git a/packages/b4/src/main.rs b/packages/b4/src/main.rs index 22a1b03f594..8eb0e7c40e4 100644 --- a/packages/b4/src/main.rs +++ b/packages/b4/src/main.rs @@ -11,7 +11,6 @@ use dotenv::dotenv; use sequent_core::util::init_log::init_log; use std::env; use tower_http::cors::{Any, CorsLayer}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use b4::{db, handlers, s3, state::AppState}; diff --git a/packages/b4/src/messages/artifact.rs b/packages/b4/src/messages/artifact.rs index 9150aff085f..bef609e8681 100644 --- a/packages/b4/src/messages/artifact.rs +++ b/packages/b4/src/messages/artifact.rs @@ -59,7 +59,7 @@ impl Configuration { pub fn get_trustee_position(&self, trustee_pk: &StrandSignaturePk) -> Option { if trustee_pk == &self.protocol_manager { - Some(PROTOCOL_MANAGER_INDEX as usize) + Some(PROTOCOL_MANAGER_INDEX) } else { self.trustees.iter().position(|t| t == trustee_pk) } diff --git a/packages/b4/src/messages/message.rs b/packages/b4/src/messages/message.rs index 58fa9ca961a..65390486067 100644 --- a/packages/b4/src/messages/message.rs +++ b/packages/b4/src/messages/message.rs @@ -345,11 +345,11 @@ impl Message { // Artifact present - let artifact_hash = strand::hash::hash_to_array(&artifact)?; + let artifact_hash = strand::hash::hash_to_array(artifact)?; // If the cfg_h field matches the artifact, the artifact must be Configuration if st_cfg_h == artifact_hash { assert!(kind == StatementType::Configuration); - if trustee != PROTOCOL_MANAGER_INDEX as usize { + if trustee != PROTOCOL_MANAGER_INDEX { return Err(anyhow!("Configuration must be signed by protocol manager")); } @@ -364,13 +364,11 @@ impl Message { // If the statement type were configuration, cfg_hash should have matched the artifact above assert!(kind != StatementType::Configuration); - if kind == StatementType::Ballots { - if trustee != PROTOCOL_MANAGER_INDEX as usize { - return Err(anyhow!("Ballots must be signed by protocol manager")); - } + if kind == StatementType::Ballots && trustee != PROTOCOL_MANAGER_INDEX { + return Err(anyhow!("Ballots must be signed by protocol manager")); } - let _ = verify_artifact(&configuration, &kind, &artifact)?; + verify_artifact(configuration, &kind, artifact)?; // FIXME remove this potentially expensive clone // See above line: let artifact = self.artifact.take().unwrap(); Ok(VerifiedMessage::new( diff --git a/packages/b4/src/messages/statement.rs b/packages/b4/src/messages/statement.rs index ddc648c9df1..e5c8c2f42a0 100644 --- a/packages/b4/src/messages/statement.rs +++ b/packages/b4/src/messages/statement.rs @@ -321,38 +321,38 @@ impl Statement { ts = *ts_; kind = StatementType::Ballots; cfg = cfg_h.0; - batch = bch.clone(); + batch = *bch; } Self::Mix(ts_, cfg_h, bch, _, _, _) => { ts = *ts_; kind = StatementType::Mix; cfg = cfg_h.0; - batch = bch.clone(); + batch = *bch; } Self::MixSigned(ts_, cfg_h, bch, mix_no, _, _) => { ts = *ts_; kind = StatementType::MixSigned; cfg = cfg_h.0; - batch = bch.clone(); - mix_number = mix_no.clone(); + batch = *bch; + mix_number = *mix_no; } Self::DecryptionFactors(ts_, cfg_h, bch, _, _, _) => { ts = *ts_; kind = StatementType::DecryptionFactors; cfg = cfg_h.0; - batch = bch.clone(); + batch = *bch; } Self::Plaintexts(ts_, cfg_h, bch, _, _, _, _) => { ts = *ts_; kind = StatementType::Plaintexts; cfg = cfg_h.0; - batch = bch.clone(); + batch = *bch; } Self::PlaintextsSigned(ts_, cfg_h, bch, _, _, _, _) => { ts = *ts_; kind = StatementType::PlaintextsSigned; cfg = cfg_h.0; - batch = bch.clone(); + batch = *bch; } } @@ -393,8 +393,7 @@ impl BorshSerialize for ChannelsHashes { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { let vector = &self.0; - let vecs: Result>, std::io::Error> = - vector.iter().map(|t| borsh::to_vec(t)).collect(); + let vecs: Result>, std::io::Error> = vector.iter().map(borsh::to_vec).collect(); let inside = vecs?; inside.serialize(writer) @@ -421,8 +420,7 @@ impl BorshSerialize for SharesHashes { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { let vector = &self.0; - let vecs: Result>, std::io::Error> = - vector.iter().map(|t| borsh::to_vec(t)).collect(); + let vecs: Result>, std::io::Error> = vector.iter().map(borsh::to_vec).collect(); let inside = vecs?; inside.serialize(writer) @@ -449,8 +447,7 @@ impl BorshSerialize for DecryptionFactorsHashes { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { let vector = &self.0; - let vecs: Result>, std::io::Error> = - vector.iter().map(|t| borsh::to_vec(t)).collect(); + let vecs: Result>, std::io::Error> = vector.iter().map(borsh::to_vec).collect(); let inside = vecs?; inside.serialize(writer) diff --git a/packages/ballot-verifier/package.json b/packages/ballot-verifier/package.json index 6d9a49dfb5c..a379c9d0307 100644 --- a/packages/ballot-verifier/package.json +++ b/packages/ballot-verifier/package.json @@ -21,7 +21,7 @@ "@reduxjs/toolkit": "1.9.5", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", - "sequent-core": "file:./rust/sequent-core-0.1.0.tgz", + "sequent-core": "file:./rust/pkg", "@sequentech/ui-essentials": "*", "@sequentech/ui-core": "*", "web-vitals": "^5.1.0" @@ -99,7 +99,7 @@ "ts-loader": "9.5.4", "ts-jest": "26.4.4", "typescript": "^5.9.2", - "webpack": "5.94.0", + "webpack": "^5.94.0", "webpack-cli": "6.0.1", "webpack-dev-server": "5.2.2" } diff --git a/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz b/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz deleted file mode 100644 index c76d030b6eb..00000000000 Binary files a/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz and /dev/null differ diff --git a/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz.license b/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz.license deleted file mode 100644 index a75f1d02e29..00000000000 --- a/packages/ballot-verifier/rust/sequent-core-0.1.0.tgz.license +++ /dev/null @@ -1,3 +0,0 @@ - SPDX-FileCopyrightText: 2025 Sequent Tech Inc - -SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/braid/Cargo.toml b/packages/braid/Cargo.toml index f73d35c976a..6cd00bfe78e 100644 --- a/packages/braid/Cargo.toml +++ b/packages/braid/Cargo.toml @@ -166,3 +166,6 @@ wasm = ["wasm-core"] # Memory profiling (native only) jemalloc = ["dep:tikv-jemalloc-ctl", "dep:tikv-jemallocator"] + +[lints.rust] +unsafe_code = "deny" diff --git a/packages/braid/Dockerfile.prod b/packages/braid/Dockerfile.prod index 88d8f30f445..bdb63562226 100644 --- a/packages/braid/Dockerfile.prod +++ b/packages/braid/Dockerfile.prod @@ -2,26 +2,49 @@ # # SPDX-License-Identifier: AGPL-3.0-only -FROM rust:1.96.0-slim-bookworm as builder +# The Debian packages the two stages below install, and the vendored crates the build reads. +# The controlled production build supplies each from the accepted external inputs with +# `--build-context =`, which replaces the stage of that name and leaves the +# download unbuilt: `debs-builder` and `debs-runtime` hold the .deb files of the packages +# their download stages name, `cargo-vendor` the crate sources of Cargo.lock, which cargo +# reads through the [source] configuration the production job appends to .cargo/config.toml. +# A build outside that workflow downloads the packages from the Debian archive, authenticated +# by apt against the keyring of the base image, and resolves the crates from crates.io. +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS debs-builder-download +RUN mkdir -p /debs && apt-get update && apt-get install -y --no-install-recommends --download-only \ + -o Dir::Cache::archives=/debs \ + protobuf-compiler \ + libprotobuf-dev \ + m4 \ + make -WORKDIR /app +FROM scratch AS debs-builder +COPY --from=debs-builder-download /debs/*.deb / + +FROM debian:bookworm@sha256:6ebd97fa83deb272194a2cf015b3d26a4d538e9ad3a7a79d544c8af5b0a01443 AS debs-runtime-download +RUN mkdir -p /debs && apt-get update && apt-get install -y --no-install-recommends --download-only \ + -o Dir::Cache::archives=/debs \ + libssl3 \ + ca-certificates + +FROM scratch AS debs-runtime +COPY --from=debs-runtime-download /debs/*.deb / + +# Optional external build context for installations using AWS Secrets Manager or +# a Vault-compatible service. Supply reviewed binaries under bin/ with +# --build-context cloud-secret-tools=; the uniWAHL context is empty. +FROM scratch AS cloud-secret-tools + +FROM scratch AS cargo-vendor + +FROM rust:1.96.0-slim-bookworm@sha256:4732ca96fd086cb9be682050c3f0176288eebaac2b80aa2bcefccfaf198e1950 AS builder -RUN apt-get update \ - && apt-get install -y --no-install-recommends debian-archive-keyring \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - protobuf-compiler \ - libprotobuf-dev \ - m4 \ - make \ - unzip \ - curl \ - && rm -rf /var/lib/apt/lists/* - - -# copy projects -#TODO: Copy only necessary files WORKDIR /app + +COPY --from=debs-builder / /tmp/debs/ +RUN apt-get -o Dir::Cache::archives=/tmp/debs --no-download install -y --no-install-recommends /tmp/debs/*.deb \ + && rm -rf /tmp/debs + COPY ./braid ./braid COPY ./b4 ./b4 COPY ./electoral-log ./electoral-log @@ -38,43 +61,38 @@ COPY ./orare ./orare COPY ./wrap-map-err ./wrap-map-err COPY ./braid/scripts ./braid/scripts COPY ./plugins ./plugins +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo +COPY --from=cargo-vendor / ./vendor/ -# build projects WORKDIR /app/braid -RUN cargo build --release --locked +# The CM helper supplies a fresh namespace per build invocation. Component builds +# share Cargo's dependency artifacts only inside that invocation and architecture. +# Copy selected binaries outside the cache mount into the immutable builder layer. +ARG TEMENOS_CARGO_CACHE_ID=standalone +ARG TARGETARCH +RUN --mount=type=cache,id=${TEMENOS_CARGO_CACHE_ID}-${TARGETARCH},target=/app/target,sharing=locked cargo build --release --locked --offline \ + && mkdir -p /opt/temenos-build \ + && cp /app/target/release/main /app/target/release/verify /app/target/release/gen_trustee_config /opt/temenos-build/ +FROM debian:bookworm@sha256:6ebd97fa83deb272194a2cf015b3d26a4d538e9ad3a7a79d544c8af5b0a01443 +COPY --from=debs-runtime / /tmp/debs/ +RUN apt-get -o Dir::Cache::archives=/tmp/debs --no-download install -y --no-install-recommends /tmp/debs/*.deb \ + && rm -rf /tmp/debs -FROM debian:bookworm -# Some shared libraries (extra-runtime-dependencies) may need to be installed: -# Installing also AWS CLI and Vault CLI for trustee configuration -# Set the default Vault version (put 'latest' for automatic substitution) -ARG VAULT_VERSION=1.15.5 -RUN apt-get update && apt-get install -y --no-install-recommends \ - libssl3 \ - ca-certificates \ - curl \ - unzip \ - && curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \ - && unzip -o awscliv2.zip \ - && ./aws/install \ - && rm -f awscliv2.zip \ - && if [ "${VAULT_VERSION}" = "latest" ]; then \ - VAULT_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -oP 'vault/\K[0-9.]+(?=/")' | head -n 1); \ - fi \ - && curl -o vault.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip \ - && unzip vault.zip -d /usr/local/bin/ \ - && rm -f vault.zip \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/main /usr/bin/trustee -COPY --from=builder /app/target/release/verify /usr/bin/verify -COPY --from=builder /app/target/release/gen_trustee_config /usr/bin/ -COPY --from=builder /app/target/release/main /usr/bin/trustee +# Cloud secret CLIs are optional deployment inputs, not downloaded by the build. +# uniWAHL uses a mounted trustee configuration / EnvVarMasterSecret and S3-compatible storage. +COPY --from=cloud-secret-tools / /usr/local/ +ARG CLOUD_SECRET_BACKENDS=disabled +ENV CLOUD_SECRET_BACKENDS=${CLOUD_SECRET_BACKENDS} +ENV SECRETS_BACKEND=EnvVarMasterSecret + +COPY --from=builder /opt/temenos-build/main /usr/bin/trustee +COPY --from=builder /opt/temenos-build/verify /usr/bin/verify +COPY --from=builder /opt/temenos-build/gen_trustee_config /usr/bin/ COPY --from=builder /app/braid/scripts/*.sh /usr/bin/ RUN mkdir -p /opt/braid/ diff --git a/packages/braid/Dockerfile.prod-vstl-compile b/packages/braid/Dockerfile.prod-vstl-compile index c0f09e0900b..4ec40180a07 100644 --- a/packages/braid/Dockerfile.prod-vstl-compile +++ b/packages/braid/Dockerfile.prod-vstl-compile @@ -21,6 +21,7 @@ COPY ./e2e ./e2e COPY ./step-cli ./step-cli COPY ./wrap-map-err ./wrap-map-err COPY ./braid/scripts ./braid/scripts +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo diff --git a/packages/braid/Dockerfile.prod-vstl-dependencies b/packages/braid/Dockerfile.prod-vstl-dependencies index e605949de81..bb7b27e4ef0 100644 --- a/packages/braid/Dockerfile.prod-vstl-dependencies +++ b/packages/braid/Dockerfile.prod-vstl-dependencies @@ -34,6 +34,7 @@ COPY ./wrap-map-err/Cargo.toml ./wrap-map-err/Cargo.toml COPY ./braid/scripts/Cargo.toml ./braid/scripts/Cargo.toml COPY ./orare/Cargo.toml ./orare/Cargo.toml COPY ./orare/doc_renderer/Cargo.toml ./orare/doc_renderer/Cargo.toml +COPY ./third_party ./third_party COPY ./Cargo.toml ./Cargo.toml COPY ./Cargo.lock ./Cargo.lock COPY ./.cargo ./.cargo diff --git a/packages/braid/scripts/README.md b/packages/braid/scripts/README.md new file mode 100644 index 00000000000..c784f2283d6 --- /dev/null +++ b/packages/braid/scripts/README.md @@ -0,0 +1,13 @@ +# Trustee configuration + +Generate each local trustee configuration using the `gen_trustee_config` binary +from the selected build. For example, from this directory: + +```bash +umask 077 +(set -o noclobber; gen_trustee_config > trustee1.toml) +``` + +Repeat for each distinct trustee before registering its public key. The files are +ignored by Git. Keep existing keys for the board/election that uses them; restore +the original configuration for recovery. Never replace it with a newly generated key. diff --git a/packages/braid/scripts/trustee.sh b/packages/braid/scripts/trustee.sh index e1c04909c88..11d378b242c 100755 --- a/packages/braid/scripts/trustee.sh +++ b/packages/braid/scripts/trustee.sh @@ -5,13 +5,13 @@ # SPDX-License-Identifier: AGPL-3.0-only set -e -set -x +umask 077 # Set default values cd /opt/braid #bb_helper --cache-dir /tmp/cache -s "$IMMUDB_URL" -b defaultboard -u "$IMMUDB_USER" -p "$IMMUDB_PASSWORD" upsert-board-db -l debug TRUSTEE_CONFIG_PATH=${TRUSTEE_CONFIG_PATH:-"/opt/braid/trustee.toml"} # Skipping secretsService if TRUSTEE_CONFIG_PATH is set -SECRETS_BACKEND=${SECRETS_BACKEND:-"Awssecretsmanager"} # Default to Awssecretsmanager if not set +SECRETS_BACKEND=${SECRETS_BACKEND:-"EnvVarMasterSecret"} SECRETS_BACKEND_LOWER=$(echo "$SECRETS_BACKEND" | tr '[:upper:]' '[:lower:]') if [ -z "$TRUSTEE_NAME" ] && [ ! -f "$TRUSTEE_CONFIG_PATH" ]; then echo "Error: TRUSTEE_NAME must be set." #Avoid secrets overwriting @@ -39,9 +39,29 @@ if [ "$SECRETS_BACKEND_LOWER" = "awssecretsmanager" ]; then SECRET_KEY_NAME="${AWS_SM_KEY_PREFIX}${SECRET_KEY_NAME}" fi -# Export Vault environment variables (Consumed internally by vault binary) -export VAULT_ADDR="${VAULT_SERVER_URL}" -export VAULT_TOKEN="${VAULT_TOKEN}" +# Fail before generating or changing a trustee key when an optional backend is unavailable. +# An explicitly supplied configuration file does not need a cloud secret service. +if [ ! -f "$TRUSTEE_CONFIG_PATH" ]; then + case "$SECRETS_BACKEND_LOWER" in + awssecretsmanager|hashicorpvault) + if [ "${CLOUD_SECRET_BACKENDS:-disabled}" != "enabled" ]; then + echo "Error: cloud secret backends are disabled in this image configuration." >&2 + exit 1 + fi + if [ "$SECRETS_BACKEND_LOWER" = "awssecretsmanager" ]; then + command -v aws >/dev/null || { echo "Error: reviewed AWS CLI input missing." >&2; exit 1; } + else + # OpenBao implements the Vault KV interface used below. + VAULT_CLI=${VAULT_CLI:-bao} + command -v "$VAULT_CLI" >/dev/null || { echo "Error: reviewed Vault-compatible CLI input missing." >&2; exit 1; } + fi + ;; + esac +fi + +# Export Vault-compatible client environment variables +export VAULT_ADDR="${VAULT_SERVER_URL:-${VAULT_ADDR:-}}" +export VAULT_TOKEN="${VAULT_TOKEN:-}" # Function to log messages log() { @@ -55,7 +75,7 @@ fetch_secret_aws() { # Fetch secret from HashiCorp Vault fetch_secret_vault() { - vault kv get -field=value "$1" + "$VAULT_CLI" kv get -field=value "$1" } # Store secret in AWS Secrets Manager @@ -65,7 +85,7 @@ store_secret_aws() { # Store secret in HashiCorp Vault store_secret_vault() { - vault kv put "$1" value="$2" + "$VAULT_CLI" kv put "$1" value="$2" } # Main function to handle the config @@ -80,22 +100,26 @@ handle_trustee_config() { case "$SECRETS_BACKEND_LOWER" in "envvarmastersecret") if [ -z "$TRUSTEE_CONFIG" ]; then - log "TRUSTEE_CONFIG empty, generating ephemeral config" + if [ "${TRUSTEE_ALLOW_EPHEMERAL:-false}" != "true" ]; then + echo "Error: provide a persistent trustee configuration file or TRUSTEE_CONFIG." >&2 + exit 1 + fi + log "Explicit test mode: generating ephemeral config" config_content=$(gen_trustee_config) else - config_content=$(echo -e "$TRUSTEE_CONFIG") + config_content=$TRUSTEE_CONFIG fi ;; "awssecretsmanager") config_content=$(fetch_secret_aws "$SECRET_KEY_NAME" 2>/dev/null) || { - log "Failed to fetch from AWS Secrets Manager" - config_content="" + log "Failed to fetch from AWS Secrets Manager; no key created or replaced" + exit 1 } ;; "hashicorpvault") config_content=$(fetch_secret_vault "$SECRET_KEY_NAME" 2>/dev/null) || { - log "Failed to fetch from HashiCorp Vault" - config_content="" + log "Failed to fetch from Vault-compatible service; no key created or replaced" + exit 1 } ;; *) @@ -105,18 +129,13 @@ handle_trustee_config() { esac if [ -z "$config_content" ]; then - log "Config does not exist, generating..." - config_content=$(gen_trustee_config) - if [ "$SECRETS_BACKEND_LOWER" = "awssecretsmanager" ]; then - store_secret_aws "$SECRET_KEY_NAME" "$config_content" - elif [ "$SECRETS_BACKEND_LOWER" = "hashicorpvault" ]; then - store_secret_vault "$SECRET_KEY_NAME" "$config_content" - fi + echo "Error: retrieved trustee configuration is empty; provision it explicitly before startup." >&2 + exit 1 fi fi if [ ! -f "$TRUSTEE_CONFIG_PATH" ] || [ "$(cat "$TRUSTEE_CONFIG_PATH")" != "$config_content" ]; then - printf "%b" "$config_content" > "$TRUSTEE_CONFIG_PATH" + printf "%s\n" "$config_content" > "$TRUSTEE_CONFIG_PATH" log "Wrote config to $TRUSTEE_CONFIG_PATH" fi grep key_pk "$TRUSTEE_CONFIG_PATH" diff --git a/packages/braid/scripts/trustee1.toml b/packages/braid/scripts/trustee1.toml deleted file mode 100644 index 6b308ebf9b5..00000000000 --- a/packages/braid/scripts/trustee1.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEIJAtmrHtGFYiS5tUQepIlrFtCCcKHeSzzuJ2pZqH4bat" -signing_key_pk = "MCowBQYDK2VwAyEAy1vJM4P85hJ1WAPZpRX3/QsOT2usIAuVy4/+t5VHHDs=" -encryption_key = "lQr2vrVuZJ5PAoOkVSfLfuIG7mxt8exlgAnRMBi+4rg" \ No newline at end of file diff --git a/packages/braid/scripts/trustee2.toml b/packages/braid/scripts/trustee2.toml deleted file mode 100644 index 193815790b3..00000000000 --- a/packages/braid/scripts/trustee2.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEICxvn7aLhsYzpGzVadzlqA4UZe/4wuul0fI1xznrpqCd" -signing_key_pk = "MCowBQYDK2VwAyEA50mtZzCBnubUwMhRkKyGomrUCBGgvEsbu79D3Cckjbc=" -encryption_key = "G5dB4N4i5KPgog6HHchw1BUF+2ulppFux2nQ7J2Wfq8" \ No newline at end of file diff --git a/packages/braid/scripts/trustee3.toml b/packages/braid/scripts/trustee3.toml deleted file mode 100644 index 0b157d467a0..00000000000 --- a/packages/braid/scripts/trustee3.toml +++ /dev/null @@ -1,7 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Sequent Tech Inc -# -# SPDX-License-Identifier: AGPL-3.0-only - -signing_key_sk = "MC4CAQAwBQYDK2VwBCIEIAiyrmvYxg0u4pSDcMk3Y3CaPdHXdASHbh8hzBgDaBbB" -signing_key_pk = "MCowBQYDK2VwAyEAfV2aRBpnR8Bm2MnorCwcR9ywjudNlFCqSZu6SGmuMcY=" -encryption_key = "eITqKrdoHUxj1uiKulqrAU01NyLN4Nkq5W8ibjorpJ4" \ No newline at end of file diff --git a/packages/braid/src/bin/demo_election_config.rs b/packages/braid/src/bin/demo_election_config.rs index 5d6e33b7eef..b034fb44416 100644 --- a/packages/braid/src/bin/demo_election_config.rs +++ b/packages/braid/src/bin/demo_election_config.rs @@ -27,7 +27,7 @@ const PROTOCOL_MANAGER: &str = "pm.toml"; /// * signing_key_pk: base64 encoding of a der encoded spki /// * encryption_key: base64 encoding of a sign::SymmetricKey /// * Generate .toml config for the protocol manager: -/// signing_key: base64 encoding of a der encoded pkcs#8 v1 +/// signing_key: base64 encoding of a der encoded pkcs#8 v1 /// * Generate a .bin config for a session, a serialized Configuration artifact /// /// FIXME: made obsolete by demo_tool. diff --git a/packages/braid/src/bin/demo_tool.rs b/packages/braid/src/bin/demo_tool.rs index 05b57bfa363..bbcba902c09 100644 --- a/packages/braid/src/bin/demo_tool.rs +++ b/packages/braid/src/bin/demo_tool.rs @@ -11,7 +11,6 @@ use std::fs::File; use std::io::Write; use std::marker::PhantomData; use std::path::Path; -use std::path::PathBuf; use tokio_postgres::NoTls; use tracing::{info, instrument}; @@ -36,7 +35,7 @@ use strand::symm; type DbPool = Pool>; /// The default board if none specified. -const TEST_BOARD: &'static str = "test"; +const TEST_BOARD: &str = "test"; /// The root directory from which the demo directories will be created. const DEMO_DIR: &str = "./demo"; const PROTOCOL_MANAGER: &str = "pm.toml"; @@ -209,10 +208,12 @@ async fn main() -> Result<()> { } Command::InitProtocol => { let path = Path::new(DEMO_DIR).join(CONFIG); - let cfg_bytes = fs::read(&path).expect(&format!( - "Should have been able to read session configuration file at '{:?}'", - path - )); + let cfg_bytes = fs::read(&path).unwrap_or_else(|_| { + panic!( + "Should have been able to read session configuration file at '{:?}'", + path + ) + }); let configuration = Configuration::::strand_deserialize(&cfg_bytes) .map_err(|e| anyhow!("Could not deserialize configuration {}", e))?; @@ -283,10 +284,10 @@ async fn main() -> Result<()> { /// * signing_key_pk: base64 encoding of a der encoded spki /// * encryption_key: base64 encoding of a sign::SymmetricKey /// * Generate .toml config for the protocol manager: -/// signing_key: base64 encoding of a der encoded pkcs#8 v1 +/// signing_key: base64 encoding of a der encoded pkcs#8 v1 /// * Generate a .bin config for a session, a serialized Configuration artifact -/// This configuration artifact includes the protocol manager and trustee information -/// of the previous items. +/// This configuration artifact includes the protocol manager and trustee information +/// of the previous items. /// * Generates default a run script for each trustee. /// /// These files are created in a demo directory with the following layout, @@ -362,10 +363,10 @@ fn gen_configs(n_trustees: usize, threshold: usize) -> Result<()> { /// Initializes the bulletin board with the necessary information to start a protocol run. /// /// This information will be taken from the demo directory created in the gen-config step. -#[instrument(skip(pool, s3_client))] +#[instrument(skip(pool, _s3_client))] async fn init( pool: &DbPool, - s3_client: &S3Client, + _s3_client: &S3Client, bucket: &str, board_name: &str, configuration: Configuration, @@ -431,6 +432,10 @@ async fn init( /// present on the board, an error will be returned. A protocol run can always be reset /// with the init-protocol command. #[instrument(skip(pool, s3_client))] +#[expect( + clippy::too_many_arguments, + reason = "Keep the demo ballot-posting helper aligned with its database, object storage and protocol configuration inputs" +)] async fn post_ballots( pool: &DbPool, s3_client: &S3Client, @@ -468,7 +473,7 @@ async fn post_ballots( let configuration = Configuration::::strand_deserialize(&contents) .map_err(|e| anyhow!("Could not read configuration {e:?}"))?; - let trustee_pk = configuration.trustees.get(0).unwrap(); + let trustee_pk = configuration.trustees.first().unwrap(); let trustee_pk_b64 = trustee_pk.to_der_b64_string()?; info!("Looking for PublicKey from trustee: {}", trustee_pk_b64); @@ -817,32 +822,3 @@ async fn create_board(pool: &DbPool, name: &str) -> Result<()> { info!("Created board: {}", name); Ok(()) } - -/// Drops the entire database file. -#[instrument()] -async fn drop_database(database_url: &Option) -> Result<()> { - let db_path = database_url - .clone() - .or_else(|| env::var("DATABASE_URL").ok()) - .unwrap_or_else(|| { - let mut path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - path.push("b4.db"); - path.display().to_string() - }); - - // Remove sqlite: prefix if present - let file_path = db_path - .trim_start_matches("sqlite:") - .split('?') - .next() - .unwrap(); - - if Path::new(file_path).exists() { - fs::remove_file(file_path)?; - info!("Dropped database: {}", file_path); - } else { - info!("Database file not found: {}", file_path); - } - - Ok(()) -} diff --git a/packages/braid/src/bin/gen_trustee_config.rs b/packages/braid/src/bin/gen_trustee_config.rs index f6b285acdeb..4ffa2160844 100644 --- a/packages/braid/src/bin/gen_trustee_config.rs +++ b/packages/braid/src/bin/gen_trustee_config.rs @@ -45,7 +45,7 @@ fn main() { let args = Cli::parse(); match &args.command { - Command::Trustee => gen_trustee_config::(), + Command::Trustee => gen_trustee_config(), Command::ProtocolManager => gen_protocol_manager_config::(), } } @@ -53,7 +53,7 @@ fn main() { /// Generates a trustee configuration with cryptographic secrets. /// /// Prints configuration to standard out. -fn gen_trustee_config() { +fn gen_trustee_config() { let sk = StrandSignatureSk::generate().unwrap(); let pk = StrandSignaturePk::from_sk(&sk).unwrap(); let encryption_key: symm::SymmetricKey = symm::gen_key(); diff --git a/packages/braid/src/bin/main.rs b/packages/braid/src/bin/main.rs index 432b1103eb2..458c2efab79 100644 --- a/packages/braid/src/bin/main.rs +++ b/packages/braid/src/bin/main.rs @@ -119,7 +119,7 @@ async fn main() -> Result<()> { let mut step_error = false; for board_name in &boards { - if ignored_boards.contains(&board_name) { + if ignored_boards.contains(board_name) { info!("Ignoring board '{}'..", board_name); continue; } @@ -138,13 +138,13 @@ async fn main() -> Result<()> { std::env::var("TRUSTEE_NAME").unwrap_or_else(|_| "Self".to_string()), board_name.to_string(), sk.clone(), - ek.clone(), + ek, storage, None, ); let board = HttpB3BoardParams::new(&args.b4_url).await; - let session = Session::new(&board_name, trustee, board); + let session = Session::new(board_name, trustee, board); session_map.insert(board_name.clone(), session); } @@ -193,7 +193,7 @@ async fn main() -> Result<()> { } loop_count = (loop_count + 1) % i64::MAX; - println!(""); + println!(); sleep(Duration::from_millis(1000)).await; } diff --git a/packages/braid/src/bin/main_concurrent.rs b/packages/braid/src/bin/main_concurrent.rs index 0c20eb1d1b6..bb3e7851d30 100644 --- a/packages/braid/src/bin/main_concurrent.rs +++ b/packages/braid/src/bin/main_concurrent.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only -use anyhow::{anyhow, Result}; +use anyhow::Result; use braid::native::board::HttpB3Index; use clap::Parser; diff --git a/packages/braid/src/native/board/http.rs b/packages/braid/src/native/board/http.rs index 8e2d04b9683..97b3b675fc5 100644 --- a/packages/braid/src/native/board/http.rs +++ b/packages/braid/src/native/board/http.rs @@ -38,11 +38,6 @@ struct ConfirmMessageRequest { mix_number: i32, } -#[derive(Debug, Deserialize)] -struct ConfirmMessageResponse { - success: bool, -} - #[derive(Debug, Deserialize)] struct GetMessagesResponse { messages: Vec, @@ -79,17 +74,18 @@ enum ContentTypeDto { pub struct HttpB3 { client: reqwest::Client, base_url: String, - s3_client: aws_sdk_s3::Client, - bucket_name: String, + // Retain the constructor context; HTTP uploads use server-issued URLs. + _s3_client: aws_sdk_s3::Client, + _bucket_name: String, } impl HttpB3 { - pub async fn new(base_url: &str, s3_client: aws_sdk_s3::Client, bucket_name: &str) -> HttpB3 { + pub async fn new(base_url: &str, _s3_client: aws_sdk_s3::Client, _bucket_name: &str) -> HttpB3 { HttpB3 { client: reqwest::Client::new(), base_url: base_url.to_string(), - s3_client, - bucket_name: bucket_name.to_string(), + _s3_client, + _bucket_name: _bucket_name.to_string(), } } @@ -328,8 +324,8 @@ impl HttpB3BoardParams { HttpB3 { client: reqwest::Client::new(), base_url: self.base_url.clone(), - s3_client: self.s3_client.clone(), - bucket_name: self.bucket_name.clone(), + _s3_client: self.s3_client.clone(), + _bucket_name: self.bucket_name.clone(), } } } @@ -340,8 +336,8 @@ impl BoardFactory for HttpB3BoardParams { HttpB3 { client: reqwest::Client::new(), base_url: self.base_url.clone(), - s3_client: self.s3_client.clone(), - bucket_name: self.bucket_name.clone(), + _s3_client: self.s3_client.clone(), + _bucket_name: self.bucket_name.clone(), } } } @@ -351,8 +347,8 @@ impl BoardFactoryMulti for HttpB3BoardParams { HttpB3 { client: reqwest::Client::new(), base_url: self.base_url.clone(), - s3_client: self.s3_client.clone(), - bucket_name: self.bucket_name.clone(), + _s3_client: self.s3_client.clone(), + _bucket_name: self.bucket_name.clone(), } } } @@ -362,7 +358,7 @@ impl BoardMulti for HttpB3 { async fn get_messages_multi( &self, - requests: &Vec<(String, i64)>, + requests: &[(String, i64)], ) -> Result<(Vec, bool)> { use b4::api_types::{BoardMessageRequest, GetMessagesMultiRequest}; diff --git a/packages/braid/src/native/board/storage_noop.rs b/packages/braid/src/native/board/storage_noop.rs index 3490130a860..910049925a1 100644 --- a/packages/braid/src/native/board/storage_noop.rs +++ b/packages/braid/src/native/board/storage_noop.rs @@ -43,6 +43,12 @@ pub struct NoOpStorage { transient: Mutex>, } +impl Default for NoOpStorage { + fn default() -> Self { + Self::new() + } +} + impl NoOpStorage { pub fn new() -> Self { NoOpStorage { diff --git a/packages/braid/src/native/session/session_m.rs b/packages/braid/src/native/session/session_m.rs index 076ee498d9d..527d28bd100 100644 --- a/packages/braid/src/native/session/session_m.rs +++ b/packages/braid/src/native/session/session_m.rs @@ -56,7 +56,7 @@ impl SessionM { /// call is still required because there may be messages in the /// message_store whose required Actions have not yet executed, /// leading to a possible protocol hang. - pub fn step(&mut self, messages: &Vec) -> Result, ProtocolError> { + pub fn step(&mut self, messages: &[HttpB3Message]) -> Result, ProtocolError> { // NOTE: we must call step even if there are no new remote messages // because there may be actions pending in the trustees LocalBoard. let step_result = self.trustee.step(messages)?; @@ -77,7 +77,7 @@ impl SessionM { /// Used when the remote bulletin board returns a truncated response /// indicating that a further request must be made before inferring any /// new Actions. - pub(crate) fn update_store(&self, messages: &Vec) -> Result<(), ProtocolError> { + pub(crate) fn update_store(&self, messages: &[HttpB3Message]) -> Result<(), ProtocolError> { self.trustee.update_store(messages) } } @@ -122,7 +122,7 @@ impl SessionFactory { ) -> Result> { info!("* Creating new session for board '{}'..", board_name); - let storage = SqliteStorage::new(self.store_root.join(&board_name), None); + let storage = SqliteStorage::new(self.store_root.join(board_name), None); let trustee = Trustee::new( self.trustee_name.clone(), board_name.to_string(), diff --git a/packages/braid/src/native/session/session_master.rs b/packages/braid/src/native/session/session_master.rs index 776a50b877f..ea10c253d0d 100644 --- a/packages/braid/src/native/session/session_master.rs +++ b/packages/braid/src/native/session/session_master.rs @@ -100,7 +100,7 @@ impl SessionMaster { } for (i, h) in self.session_sets.iter_mut().enumerate() { - let boards = std::mem::replace(&mut h.boards, vec![]); + let boards = std::mem::take(&mut h.boards); if h.sender.is_closed() { warn!("Sender was closed, rebuilding set.."); @@ -163,7 +163,7 @@ impl SessionSet { /// 2) Make the requests from the bulletin board, with chunking. /// 3) Receive the responses from the server. /// 4) Distribute the messages to their handling SessionM's - /// and run one trustee step. + /// and run one trustee step. /// 5) Gather all messages produced by the SessionM's /// 6) Post the messages to the bulletin board. /// @@ -208,7 +208,7 @@ impl SessionSet { } } - let boards_set: HashSet = HashSet::from_iter(boards.into_iter()); + let boards_set: HashSet = HashSet::from_iter(boards); session_map.retain(|k, _v| { let ret = boards_set.contains(k); @@ -238,7 +238,7 @@ impl SessionSet { )>, > = session_map .keys() - .map(|k| Ok((k.clone(), self.session_factory.create_session(&k)?))) + .map(|k| Ok((k.clone(), self.session_factory.create_session(k)?))) .collect(); if let Ok(new_sessions) = new_sessions { @@ -335,7 +335,7 @@ impl SessionSet { continue; }; - if messages.len() > 0 { + if !messages.is_empty() { let next_bytes: usize = messages .iter() .map(|m| m.artifact.as_ref().map(|v| v.len()).unwrap_or(0)) @@ -345,7 +345,7 @@ impl SessionSet { } } - if post_messages.len() > 0 { + if !post_messages.is_empty() { info!( "Set {}: posting messages for {} boards with {:.3} MB", self.name, diff --git a/packages/braid/src/native/test/dbg.rs b/packages/braid/src/native/test/dbg.rs index dd92130a4e0..9e2c7493bf9 100644 --- a/packages/braid/src/native/test/dbg.rs +++ b/packages/braid/src/native/test/dbg.rs @@ -168,7 +168,7 @@ impl Status { } } /// Shows status information using ascii tables. - fn to_string(&self) -> String { + fn render_tables(&self) -> String { let mut boards = vec![]; boards.push("Trustees".to_string()); @@ -177,12 +177,12 @@ impl Status { ascii_table.set_max_width(205); ascii_table .column(0) - .set_header(format!("trustee {}", i.to_string())) + .set_header(format!("trustee {}", i)) .set_align(Align::Left); let data1: Vec = self.statement_keys[i] .iter() - .map(|k| format!("{}-{}", k.kind.to_string(), k.signer_position)) + .map(|k| format!("{}-{}", k.kind, k.signer_position)) .collect(); let data2: Vec = self.artifact_keys[i] .iter() @@ -226,7 +226,7 @@ impl Status { ]) } boards.push("Last step messages".to_string()); - if data.len() > 0 { + if !data.is_empty() { boards.push(ascii_table.format(data)); } else { boards.push("-".to_string()); @@ -266,7 +266,7 @@ impl Status { boards.push(ascii_table.format(data)); boards.push("Last actions".to_string()); - if self.last_actions.len() > 0 { + if !self.last_actions.is_empty() { boards.push(format!("{:?}", self.last_actions)); } else { boards.push("-".to_string()); @@ -279,7 +279,7 @@ impl Status { /// Constructs the repl context used to interact with the protocol. fn mk_context(ctx: C, n_trustees: u8, threshold: &[usize]) -> ReplContext { let mut selected = [NULL_TRUSTEE; MAX_TRUSTEES]; - selected[0..threshold.len()].copy_from_slice(&threshold); + selected[0..threshold.len()].copy_from_slice(threshold); let pmkey: StrandSignatureSk = StrandSignatureSk::generate().unwrap(); let pm: ProtocolManager = ProtocolManager { @@ -288,7 +288,6 @@ fn mk_context(ctx: C, n_trustees: u8, threshold: &[usize]) -> ReplContex }; let trustees: Vec> = (0..n_trustees) - .into_iter() .map(|i| { let kp = StrandSignatureSk::generate().unwrap(); // let encryption_key = ChaCha20Poly1305::generate_key(&mut csprng); @@ -372,10 +371,7 @@ fn log(args: ArgMatches, context: &mut ReplContext) -> Result(_args: ArgMatches, context: &mut ReplContext) -> Result(args: ArgMatches, context: &mut ReplContext) -> Result> = ps .iter() .map(|p| { - let encoded = ctx.encode(&p).unwrap(); + let encoded = ctx.encode(p).unwrap(); pk.encrypt(&encoded) }) .collect(); @@ -491,10 +487,10 @@ fn plaintexts(_args: ArgMatches, context: &mut ReplContext) -> Result .iter() .map(|p| context.ctx.encode(p).unwrap()) .collect(); - if encoded.len() > 0 { + if !encoded.is_empty() { Ok(Some(format!("Plaintexts {:?}", encoded))) } else { - Ok(Some(format!("No plaintexts found"))) + Ok(Some("No plaintexts found".to_string())) } } /// Shows and checks the validity of decryptions. @@ -522,7 +518,7 @@ fn decrypted(_args: ArgMatches, context: &mut ReplContext) -> Result< set1 == set2 ))) } else { - Ok(Some(format!("No decrypted plaintexts found"))) + Ok(Some("No decrypted plaintexts found".to_string())) } } @@ -606,7 +602,7 @@ fn step(args: ArgMatches, context: &mut ReplContext) -> Result