Skip to content

sofus-bech/pbs-tool

Repository files navigation

PBS File Submission Tool

A command-line tool for generating and submitting Danish PBS (Pengeinstitutternes BetalingsSystemer) enrollment files to a bank's SFTP server.

What it does

PBS is the Danish bank payment system. This tool lets you:

  1. Generate a correctly formatted PBS enrollment file by entering customer data
  2. Verify the file meets the PBS format requirement (every line must be exactly 128 characters)
  3. Send the file to the bank via SFTP using SSH key authentication
  4. Archive old files and clean up temp files

Requirements

  • Python 3
  • paramiko — SFTP upload
  • python-dotenv — loading credentials from .env

Install dependencies:

pip install -r requirements.txt

Setup

1. SSH key

Place your private SSH key in the project folder and name it exactly:

id_rsa

No file extension — not id_rsa.pem or id_rsa.txt, just id_rsa.

2. SFTP credentials and creditor settings

Run the tool and choose option 5 (Setup) from the main menu, then option 1 (Configure settings). You will be prompted for:

Variable Description
SFTP_HOST Bank SFTP server hostname
SFTP_PORT Port (usually 22)
SFTP_USERNAME Your SFTP username
SFTP_PASSPHRASE Passphrase for the SSH key (if any)
SFTP_REMOTE_DIR Remote folder to upload into (default /)
CREDITOR_DATA_SUPPLIER Data supplier number (8 digits)
CREDITOR_SUBSYSTEM Subsystem identifier (usually BS1)
CREDITOR_PBS_NUMBER Your PBS / creditor number (8 digits)
CREDITOR_DEBTOR_GROUP Debtor group number (1–5 digits)
CREDITOR_NAME Creditor name in the file (max 15 characters)

The creditor values go into fixed-width fields in the PBS file, so generate.py checks their exact widths and refuses to build a file until they are valid.

3. Bank host key (first connection only)

send.py refuses to connect to a server it does not recognise — that is what protects the upload from a man-in-the-middle attack. Before the first upload, fetch the bank's host key and save it to a known_hosts file in the project folder:

ssh-keyscan -p 22 sftp.bank.dk >> known_hosts

Then verify the key's fingerprint with the bank before uploading. send.py prints these exact instructions if the host is unknown.

This creates a .env file in the project folder (see .env.example for the expected format). Do not commit .env or id_rsa to version control — both are already listed in .gitignore.


Usage

Start the tool:

python3 runme.py

You will see the main menu:

=== Main Menu - PBS Enrollment ===
1. Create enrollment file
2. Verify the file
3. Send file to PBS
4. Reset and archive
5. Setup (SFTP + creditor settings)
6. Exit

Option 1 — Create enrollment file

Walks you through entering customer data:

  • Customer number (3–5 digits)
  • CPR number — validated against the full Danish CPR spec (date check, century table, modulus-11). Dashes and spaces are accepted.
  • Registration number (4 digits)
  • Account number (1–10 digits)

You can add multiple customers in one session. The output is saved as PBS-DDMMYY-HHMMSS.txt.

Option 2 — Verify the file

Checks every line in the generated PBS file is exactly 128 characters, as required by the PBS format.

Option 3 — Send file to PBS

Shows the newest PBS*.txt file and asks for confirmation, then uploads it to the bank's SFTP server using the settings from .env and the id_rsa key (Ed25519, ECDSA and RSA keys are all supported). The server must be listed in known_hosts (Setup step 3) — unknown servers are refused. Failures are reported with a non-zero exit code so the menu shows an error instead of "completed successfully".

Note: tested against a local SFTP server; not yet verified against the bank's production server.

Option 4 — Reset and archive

Copies all PBS*.txt files to an archive/ subfolder, then deletes the archived PBS files from the working directory.

Option 5 — Setup

Opens a submenu to configure or view your SFTP and creditor settings, and shows the status of your SSH key.


File overview

File Purpose
runme.py Entry point — main menu
generate.py Builds the PBS enrollment file
cpr_validator.py Danish CPR number validator
verify.py Validates PBS file line lengths
send.py Uploads the file via SFTP
reset.py Archives and cleans up files
setup.py SFTP + creditor configuration setup
requirements.txt Python dependencies
.env.example Template showing the settings .env needs
CLAUDE.md Context notes for AI-assisted development
docs/ Official Betalingsservice record-layout reference
samples/ Example generated files (fictitious data only)
id_rsa ⚠️ Your SSH private key — never commit this
.env ⚠️ Your credentials — never commit this

CPR Validation

The CPR validator (cpr_validator.py) implements the official Danish CPR spec and reports one of three statuses:

Status Meaning
VALID Passes all checks including modulus-11
⚠️ UNVERIFIABLE Structurally correct but fails modulus-11 — may be a legitimate post-2007 number
INVALID Definitively wrong (bad date, invalid century/sequence combo)

The validator can also be run standalone:

python3 cpr_validator.py 0101901234
python3 cpr_validator.py  # interactive mode

Data safety

Generated PBS files contain CPR numbers (personal data) — treat them as sensitive. The repo's .gitignore blocks everything that must never be committed:

.env              # SFTP credentials
id_rsa            # SSH private key
*.txt             # PBS files (CPR data) — requirements.txt is explicitly re-allowed
archive/          # archived PBS files

Don't weaken the *.txt rule: it's what keeps CPR data out of git. If a secret or a PBS file is ever committed, scrub the git history (git filter-repo) — deleting the file in a later commit is not enough.


Known issues

  1. No automated tests yet. cpr_validator.py and the 128-char templates are good first candidates.
  2. send.py has not been verified against the bank's production SFTP server — only against a local test server.

(Resolved since the July 2026 audit: creditor values moved from hardcoded templates to .env; send.py now uploads the newest file with a confirmation prompt, verifies the bank's host key against known_hosts, supports Ed25519/ECDSA/RSA keys, and gives clear errors when settings are missing; verify.py and runme.py now report failures with real exit codes instead of "completed successfully".)

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages