FiloSottile/age · error
invalid HRP
Error message
invalid HRP: %q
What it means
bech32.Encode validates the human-readable part (HRP) before encoding. A bech32 HRP must be at least one character long; an empty string cannot form the required "hrp1" separator structure, so Encode rejects it with this error. It is used by age when serializing SSH identities and recipients.
Solutions
- Pass the intended non-empty HRP (age uses "AGE-SECRET-KEY-" for identities and "age" for recipients)
- Check where the HRP value originates and add a guard: if hrp == "" return a descriptive error before calling Encode
- If the HRP comes from configuration, provide a sane default and validate at load time
Example fix
// before
hrp := cfg.KeyPrefix // "" when config key missing
s, err := bech32.Encode(hrp, data) // "invalid HRP: \"\""
// after
hrp := cfg.KeyPrefix
if hrp == "" {
hrp = "age"
}
s, err := bech32.Encode(hrp, data) Defensive patterns
Strategy: validation
Validate before calling
func validHRP(hrp string) bool {
return len(hrp) >= 1
} Prevention
- Use age's constants "age" and "AGE-SECRET-KEY-" rather than dynamic HRPs
- Fail fast on missing config values that feed the HRP; never let empty strings reach Encode
- Add a unit test covering the empty-HRP path of any custom serialization code
- Default empty prefixes to the age standard values at configuration load
When it happens
Trigger: Calling bech32.Encode (directly, or via agessh encoders like EncodeIdentity/EncodeRecipient) with hrp == "" — e.g. an uninitialized constant, a variable that failed lookup, or code that passes an empty prefix when serializing key bytes.
Common situations: Refactored code where an HRP constant was deleted or set to empty; config-driven HRP selection where the config key is missing and yields ""; tests constructing bech32 strings with an omitted prefix.
Related errors
- invalid HRP character: hrp
- mixed case HRP
- illegal zero padding
- invalid data range: data
- non-zero padding
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/65232ffaa546821e.
Report an issue: GitHub.
Appendix: source
Thrown at internal/bech32/bech32.go:115
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
}
} else if bits >= frombits {
return nil, fmt.Errorf("illegal zero padding")
} else if byte(acc<<(tobits-bits))&maxv != 0 {
return nil, fmt.Errorf("non-zero padding")
}
return ret, nil
}
// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
// the output will be uppercase.
func Encode(hrp string, data []byte) (string, error) {
values, err := convertBits(data, 8, 5, true)
if err != nil {
return "", err
}
if len(hrp) < 1 {
return "", fmt.Errorf("invalid HRP: %q", hrp)
}
for p, c := range hrp {
if c < 33 || c > 126 {
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
}
}
if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {
return "", fmt.Errorf("mixed case HRP: %q", hrp)
}
lower := strings.ToLower(hrp) == hrp
hrp = strings.ToLower(hrp)
var ret strings.Builder
ret.WriteString(hrp)
ret.WriteString("1")
for _, p := range values {
ret.WriteByte(charset[p])
}
for _, p := range createChecksum(hrp, values) {View on GitHub (pinned to b74dce4cdb)