YubiKey OTP Filter
Validate Yubico OTPs and extract their components without hand-written substring offsets
The yubikey filter validates a Yubico OTP that arrives in a string value and
returns either the full OTP or one of its components. It is intended for the
common deployment patterns described in
YubiKey Authentication, where the
OTP is read out of a PAP password field that may also contain a static
password.
Yubico OTP structure
A Yubico OTP is a fixed 44-character modhex string. The first 12 characters are the public UID — a stable identifier per token — and the trailing 32 characters are the AES-128-encrypted ciphertext that carries the private UID, counters, timestamp, and CRC.
┌──────────────────── 44-character OTP ─────────────────────┐
│ <12 chars: public UID> │ <32 chars: encrypted ciphertext> │
└────────────────────────┴──────────────────────────────────┘
In two-factor deployments the OTP is concatenated with a static password in the same PAP field. The OTP is always 44 characters; the password is whatever sits next to it:
password-otp: <password>vvccccriigjnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
otp-password: vvccccriigjnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX<password>
Each filter <extract> value selects one slice of this layout:
| Extract | Slice returned | Length |
|---|---|---|
otp | The full validated OTP. | 44 chars |
public-uid | Leading 12 modhex characters of the OTP. | 12 chars |
ciphertext | Trailing 32 modhex characters of the OTP. | 32 chars |
password | The static password sitting next to the OTP. | Whatever is left |
public-uid is the most commonly used component. It is a stable per-token
identifier, so it is the natural value to bind a SQL or LDAP user lookup to
when the same user can present more than one YubiKey.
password returns whatever sits next to the OTP in the input value. It is
only valid with placement = password-otp or placement = otp-password;
combining it with placement = otp is rejected at configuration parse time
because there is no password slice to return.
Syntax
The filter is positional and always takes two arguments:
<input> | yubikey(<placement>, <extract>)
<placement>describes where the OTP sits inside the input value.<extract>selects which slice of the structure above to return.
Placement
The placement names describe the layout of the input value.
| Placement | Input layout | Notes |
|---|---|---|
otp | <otp> | The whole input value is the 44-character OTP. |
password-otp | <password><otp> | The OTP is the trailing 44 characters; the leading text is the password. |
otp-password | <otp><password> | The OTP is the leading 44 characters; the trailing text is the password. |
For unusual layouts that none of these shorthands describe, preprocess the
input with substring or some other
filter to isolate the 44-character OTP, then use placement = otp.
Accepted input value types
The filter accepts plain strings, byte strings, and masked passwords. Masked passwords are not revealed in logs or stored output; the filter only inspects the wrapped bytes long enough to validate the OTP and return the requested component.
Validation and error handling
Before extracting a component, the filter checks that the candidate OTP slice:
- is exactly 44 characters long, and
- contains only ASCII characters.
If either check fails, or if the slice is not a well-formed Yubico OTP, the
filter produces a value error. The OTP slice is always validated first, so a
malformed OTP cannot smuggle arbitrary bytes through the password extract.
The filter only performs these cheap structural checks; it does not decrypt
the OTP or verify replay counters. Cryptographic validation still happens in
the yubikey action (offline) or in the
yubikey HTTP backend (cloud).
The filter's job is to extract the right substring so those validators
receive clean input and so user lookups can key on the public UID.
Wrap the call in recover when
the policy needs to treat malformed input as an authentication reject rather
than a backend execution error:
modify vars.presented_yubikey_public_uid =
radius.request.password | yubikey(password-otp, public-uid) | recover(none);
if all {
vars.presented_yubikey_public_uid == none;
} then {
reject "Malformed YubiKey OTP";
}
Examples
Extract public UID before a user lookup
Use yubikey(password-otp, public-uid) to pull the per-token identifier
out of a combined <password><otp> PAP field, then use it as a binding for
the user lookup query.
@execute {
modify vars.presented_yubikey_public_uid =
radius.request.password | yubikey(password-otp, public-uid) | recover(none);
if all {
vars.presented_yubikey_public_uid == none;
} then {
reject "Malformed YubiKey OTP";
}
backend {
name "USERS";
query "FIND_USER";
}
}
Forward the full OTP to a cloud validator
When the OTP is the whole PAP value, yubikey(otp, otp) validates length and
encoding before the cloud backend sees the request.
modify vars.validated_otp = radius.request.password | yubikey(otp, otp);
Extract the static password from a combined PAP field
yubikey(password-otp, password) returns the leading static password from a
<password><otp> PAP field. Use it to feed the password portion into the
pap action or to compare against a stored value
without writing substring() offsets.
modify vars.static_password = radius.request.password | yubikey(password-otp, password);
Related
- Filters - All available filters and the general
<value> | <filter>syntax. yubikeyaction - Offline OTP decryption and replay-counter check.- YubiKey Authentication - End-to-end configuration patterns for cloud and offline validation.
About Radiator software development security
Architecture Overview
Backend Load Balancing
Basic Installation
Built-in Environment Variables
Comparison Operators
Configuration Editor
Configuration Import and Export
Containers
Data Types
Duration Units
Environment Variables
Execution Context
Execution Pipelines
Filters
Getting a Radiator License
Health check /live and /ready
High Availability and Load Balancing
High availability identifiers
HTTP Basic Authentication
Introduction
Linux systemd support
Local AAA Backends
Log storage and formatting
Management API privilege levels
Namespaces
Password Hashing
Probabilistic Sampling
Prometheus scraping
PROXY Protocol Support
Radiator server health and boot up logic
Radiator sizing
Radiator software releases
Rate Limiting
Rate Limiting Algorithms
Reverse Dynamic Authorization
Service Level Objective
TACACS+ Authentication, Authorization, and Accounting
Template Rendering CLI
Tools radiator-client
TOTP/HOTP Authentication
What is Radiator?
YubiKey Authentication
YubiKey Context Variables
About Radiator software development security
Architecture Overview
Backend Load Balancing
Basic Installation
Built-in Environment Variables
Comparison Operators
Configuration Editor
Configuration Import and Export
Containers
Data Types
Duration Units
Environment Variables
Execution Context
Execution Pipelines
Filters
Getting a Radiator License
Health check /live and /ready
High Availability and Load Balancing
High availability identifiers
HTTP Basic Authentication
Introduction
Linux systemd support
Local AAA Backends
Log storage and formatting
Management API privilege levels
Namespaces
Password Hashing
Probabilistic Sampling
Prometheus scraping
PROXY Protocol Support
Radiator server health and boot up logic
Radiator sizing
Radiator software releases
Rate Limiting
Rate Limiting Algorithms
Reverse Dynamic Authorization
Service Level Objective
TACACS+ Authentication, Authorization, and Accounting
Template Rendering CLI
Tools radiator-client
TOTP/HOTP Authentication
What is Radiator?
YubiKey Authentication
YubiKey Context Variables