Punycode and internationalised domain names

How IDNA turns a Unicode label into ASCII, the difference between the display name and the wire form, and why homograph domains are a security problem rather than a curiosity.

Two forms of the same name

DNS carries only ASCII. An internationalised domain name is therefore converted with IDNA into an ASCII-compatible form beginning with xn--, called the A-label; the Unicode version is the U-label.

import idna

u_label = "münchen"
a_label = idna.encode(u_label).decode()
print(a_label)                  # 'xn--mnchen-3ya'

print(idna.decode(a_label))     # 'münchen'

# a full domain, label by label
print(idna.encode("café.example").decode())   # 'xn--caf-dma.example'
TermExampleWhere it appears
U-labelmünchen.deUser interface, certificates (with SANs in A-label form)
A-labelxn--mnchen-3ya.deDNS queries, wire format, HTTP Host header
Display formMixed scripts, direction handled by the browserAddress bar after IDNA processing
RegistrationNormalised and lowercased firstRegistry rules, per-label validity checks

What IDNA actually normalises away

  • Case is folded, so MÜNCHEN and münchen are the same name.
  • Under IDNA2008, purely decorative characters are disallowed and the label is rejected rather than mapped.
  • Width variants and other compatibility forms are normalised before encoding, which is why NFKC matters here.
  • A label with a hyphen in the third and fourth positions is reserved for the ACE prefix and must not be registered directly.
  • Browsers apply their own display rules on top of IDNA, so the address bar may show the Unicode form even though the wire form is ASCII.
# see what the wire actually carries
python -c "import idna; print(idna.encode('münchen.de').decode())"
curl -sv https://xn--mnchen-3ya.de/ 2>&1 | grep -i '^> Host'

# resolve the ASCII form directly
dig +short xn--mnchen-3ya.de

Homographs and why browsers hide some names

Different scripts contain characters that look identical. An attacker can register a name that renders exactly like a trusted one using a different script, which is a homograph attack.

# two different strings, identical on screen in many fonts
a = "apple"                  # Latin small a ... (all Latin)
b = "\u0430pple"             # Cyrillic small a, then Latin pple

a == b                       # False
print(a, b)                  # they look the same
DefenceLayerLimitation
Single-script check per labelRegistry and browser policySome legitimate names mix scripts
Show the A-label for mixed scriptsBrowser address barUsers rarely read it
Certificate transparencyPKI ecosystemDetects issuance, not the domain itself
Blocklist known confusablesClient-side protectionNever complete
Display in a fixed font, punycode visibleApplicationsBreaks the intended UX
⚠️
Validate the A-label form, not the displayed text, in any allowlist or blocklist. Comparing user-visible Unicode names invites a bypass through a character that looks the same but normalises differently.

FAQ

Do I need to encode domains myself?
Usually not; HTTP clients, browsers and TLS stacks handle IDNA. You do need to do it when you build a DNS query, validate a name, or compare names for equality.
Why does my certificate show xn-- in the name?
X.509 stores the A-label form. The client displays the Unicode form, but the certificate bytes are ASCII by design.

Unicode in depth: planes, properties and normalisation Character sets: ASCII, Latin-1 and Windows-1252

Last refreshed 2026-09-18.