en_CA.ssn() can start with 0 or 8, and neither is a personal SIN
en_CA.ssn() can start with 0 or 8, and neither is a personal SIN
The provider reverses the checksum steps to build a SIN, and the code says as much:
# faker/providers/ssn/en_CA/__init__.py:44-48
# This function reverses the checksum steps to create a random
# valid nine-digit Canadian SIN (Social Insurance Number) in the
# format '### ### ###'.
def ssn(self) -> str:
# Create an array of 8 elements initialized randomly.
digits = self.generator.random.sample(range(9), 8)sample(range(9), 8) draws the first digit from {0..8}, so the leading digit comes out
uniform over nine values and 9 never appears at all. Measured over 6000 values:
first digit: {'0': 659, '1': 648, '2': 671, '3': 652, '4': 683,
'5': 672, '6': 675, '7': 657, '8': 683} 9: never
starting with 0 or 8: 22.37% (2/9 = 22.2% if uniform)Neither of those two leading digits belongs to a personal SIN:
8is reserved for the BN (business number) issued to corporations, not individuals.0is CRA-assigned individual tax numbers, temporary tax numbers or adoption tax numbers — again not a SIN.
So 0 and 8 are excluded from the province series 1–7, and the temporary-resident
series is 9. stdnum encodes exactly that:
# stdnum/ca/sin.py
if number[0] in '08':
raise InvalidComponent()
return luhn.validate(number)which puts faker's Canadian SINs at 1524/2000 accepted (76.2%). Every rejection I
looked at begins with 0 or 8:
017 255 647 814 075 669 023 468 259 850 233 776I am confident about 0 — several independent validators treat a leading 0 as
structurally invalid, since those numbers are not issued to individuals. For 8 the
sources agree too (Wikipedia's SIN article states it is used exclusively for business
numbers, and DataVeil's SIN mask treats 0/8 as invalid unless you tick an option),
though a couple of validators only mention 0, so I have flagged the split rather than
overstate it.
Worth noting that faker's own checksum() helper in the same file cites the Wikipedia
article as its source, so the reference that documents the restriction is already the one
this provider points at.
Why the test does not catch it
tests/providers/test_ssn.py:219 checks the length, that the characters are digits, and
the Luhn check:
def test_ssn(self):
for _ in range(100):
sin = self.fake.ssn()
assert len(sin) == 11
assert sin.replace(" ", "").isdigit()
assert ca_checksum(sin) == int(sin[-1])The checksum is right in all of these — the provider is careful about that — so nothing in the test looks at which series the number claims to be from.
A fix
Draw the leading digit from the series that a personal SIN can actually have — 1–7,
plus 9 if you want to cover temporary residents, which range(9) currently excludes
entirely. The rest of the algorithm can stay as it is; the check digit logic looks
correct and I could not fault it.
Separately: at 22% of output affected this is frequent enough to matter for anyone using
en_CA for test fixtures, which is the point of the provider.
Provenance: main at 975dd2a1 and released faker==40.39.0. I searched the open issues
and open PRs and found nothing on this; #763 is a 2018 report about the provider returning
malformed output under Python 3, which is a different symptom and is closed.
Source: joke2k/faker