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:
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);
+ }
}