anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

commit 86a95111dc0c29fcd58c747cec3cd49725c6af12
parent 7358b27a289c4899a3de32852e72f5145a6bcb6b
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 18:57:39 +0200

make the Czech birth number regex and checksum agree

Diffstat:
Mcontrib/redux.cz.json | 2+-
Msrc/reducer/test_validation.c | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/reducer/validation_CZ_BN.c | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 142 insertions(+), 25 deletions(-)

diff --git a/contrib/redux.cz.json b/contrib/redux.cz.json @@ -23,7 +23,7 @@ "tooltip": "rodné číslo", "widget": "anastasis_gtk_ia_birthnumber_cz", "uuid": "03e3a05b-1192-44f1-ac36-7425512eee1a", - "validation-regex": "^[0-9]{2}(((0|2|5|7)[0-9])|10|11|31|32|51|52|81|82)/[0-9]{3}[0-9]?$", + "validation-regex": "^[0-9]{6}/[0-9]{3}[0-9]?$", "validation-logic": "CZ_BN_check" } ] diff --git a/src/reducer/test_validation.c b/src/reducer/test_validation.c @@ -168,6 +168,55 @@ main (int argc, return 1; } + /* Czech birth numbers. The modern form carries a check digit and must be + divisible by 11; the nine-digit pre-1954 form has none and is accepted + on the strength of its date alone. */ + if (! CZ_BN_check ("850615/0004")) + { + GNUNET_break (0); + return 1; + } + if (! CZ_BN_check ("756203/0003")) /* woman: month carries +50 */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("850615/0005")) /* wrong check digit */ + { + GNUNET_break (0); + return 1; + } + if (! CZ_BN_check ("480615/123")) /* born 1948: legacy form */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("850615/123")) /* 1985 cannot use the legacy form */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("851315/0004")) /* month 13 */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("850635/0004")) /* day 35 */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("8506150004")) /* the slash is not optional */ + { + GNUNET_break (0); + return 1; + } + if (CZ_BN_check ("482115/123")) /* +20 months postdate the 9-digit form */ + { + GNUNET_break (0); + return 1; + } + /* AL_NID_check has no implementation yet and deliberately accepts everything; when it is implemented, this expectation must change. */ if (! AL_NID_check ("I05101999I")) diff --git a/src/reducer/validation_CZ_BN.c b/src/reducer/validation_CZ_BN.c @@ -1,6 +1,6 @@ /* This file is part of Anastasis - Copyright (C) 2020, 2021 Anastasis SARL + Copyright (C) 2020, 2021, 2026 Anastasis SARL Anastasis is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -15,17 +15,74 @@ */ /** * @file reducer/validation_CZ_BN.c - * @brief validation of Czeck Birth Numbers + * @brief validation of Czech birth numbers * @author Christian Grothoff */ #include <string.h> #include <stdbool.h> #include <stdio.h> -#include <gcrypt.h> + + +/** + * Check the YYMMDD part of a birth number. + * + * The month carries the sex and, since 2004, an overflow marker: women have + * 50 added, and an exhausted daily serial adds another 20. + * + * @param d the first six characters of the birth number + * @param allow_overflow true if the +20/+70 month variants are acceptable + * @param[out] yy set to the two-digit year + * @return true if the date is well-formed + */ +static bool +check_date (const char *d, + bool allow_overflow, + unsigned int *yy) +{ + unsigned int mm; + unsigned int dd; + + for (unsigned int i = 0; i<6; i++) + if ( ('0' > d[i]) || ('9' < d[i]) ) + return false; + *yy = (d[0] - '0') * 10 + (d[1] - '0'); + mm = (d[2] - '0') * 10 + (d[3] - '0'); + dd = (d[4] - '0') * 10 + (d[5] - '0'); + if ( (mm >= 71) && (mm <= 82) ) + { + if (! allow_overflow) + return false; + mm -= 70; + } + else if ( (mm >= 51) && (mm <= 62) ) + { + mm -= 50; + } + else if ( (mm >= 21) && (mm <= 32) ) + { + if (! allow_overflow) + return false; + mm -= 20; + } + if ( (mm < 1) || (mm > 12) ) + return false; + if ( (dd < 1) || (dd > 31) ) + return false; + return true; +} + /** - * Function to validate a Check birth number. Basically, - * if it has 10 digits, it must be divisible by 11. + * Function to validate a Czech birth number (rodné číslo). + * + * The modern form is YYMMDD/XXXC, where the ten digits must be divisible by + * 11; as a documented exception, a first-nine-digits remainder of 10 is + * written with a check digit of 0. Numbers issued before 1954 have only nine + * digits (YYMMDD/XXX) and carry no check digit at all, so nothing beyond the + * date can be verified for them. Those are accepted only when the year reads + * as 1900-1953; a nine-digit number claiming a later year is a truncated + * modern one. (People born before 1900 also hold nine-digit numbers, which + * this therefore rejects.) * * @param b_number birth number to validate (input) * @return true if b_number is valid @@ -37,27 +94,38 @@ CZ_BN_check (const char *b_number); bool CZ_BN_check (const char *b_number) { - unsigned long long n; - char dummy; - char in[11]; + size_t len = strlen (b_number); + unsigned int yy; - if (10 == strlen (b_number)) - return true; - if (11 != strlen (b_number)) + if ( (10 != len) && (11 != len) ) return false; - if (b_number[6] != '/') + if ('/' != b_number[6]) return false; - memcpy (in, - b_number, - 6); - memcpy (&in[6], - &b_number[7], - 4); - in[10] = '\0'; - if (1 != sscanf (in, - "%llu%c", - &n, - &dummy)) + for (size_t i = 7; i<len; i++) + if ( ('0' > b_number[i]) || ('9' < b_number[i]) ) + return false; + if (! check_date (b_number, + 11 == len, + &yy)) return false; - return 0 == (n % 11); + if (10 == len) + { + /* pre-1954 form: no check digit exists */ + return (yy < 54); + } + { + unsigned long long n = 0; + unsigned long long base; + unsigned int check; + + for (unsigned int i = 0; i<6; i++) + n = n * 10 + (b_number[i] - '0'); + for (unsigned int i = 7; i<11; i++) + n = n * 10 + (b_number[i] - '0'); + base = n / 10; + check = (unsigned int) (n % 10); + if (10 == base % 11) + return (0 == check); + return (0 == n % 11); + } }