ykmangoath
Ykman OATH TOTP with Go
Yet another ykman
Go lib for requesting OATH TOTP Multi-Factor Authentication Codes from Yubikey Devices.
There are already some packages out there which already wrap Yubikey CLI for Go to manage OATH TOTP, but they lack all or some of the following features:
- Go Context support (handy for timeouts/cancellation etc)
- Multiple Yubikeys (identified by Device Serial Number)
- Password protected Yubikey OATH applications
Hence, this package, which covers those features! Big thanks to joshdk/ykmango
and 99designs/aws-vault
as they heavily influenced this library. Also this library is somewhat based on the previous implementation of Yubikey support in aripalo/vegas-credentials
(which this partly replaces in near future).
This library supports only a small subset of features of Yubikeys & OATH account management, this is by design.
Installation
Requirements
- Yubikey Series 5 device (or newer with a
OATH TOTP
support) ykman
Yubikey Manager CLI as a runtime dependency- Go
1.18
or newer (for development)
Get it
go get -u github.com/aripalo/ykmangoath
Getting Started
This ykmangoath
library provides a struct OathAccounts
which represents a the main functionality of Yubikey OATH accounts (via ykman
CLI). You can “create an instance” of the struct with ykmangoath.New
and provide the following:
- Context (type of
context.Context
) which allows you to implement things like cancellations and timeouts - Device Serial Number which is the 8+ digit serial number of your Yubikey device which you can find:
- printed in the back of your physical Yubikey device
- by running command
ykman info
in your terminal
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/aripalo/ykmangoath"
)
func main() {
myTimeout := 20*time.Second
ctx, cancel := context.WithTimeout(context.Background(), myTimeout)
defer cancel()
deviceSerial := "12345678" // can be empty string if you only use one Yubikey device
oathAccounts, err := ykmangoath.New(ctx, deviceSerial)
// handler err
// after this you can perform various operations on oathAccounts...
}
Once initialized, you may perform operations on oathAccounts
such as List
or Code
methods. See Managing Password if you wish to support password protected Yubikey OATH applications (you really should).
Check if Device Available
To verify if the configured Yubikey device is connected & available:
IsAvailable := oathAccounts.IsAvailable()
fmt.Println(IsAvailable)
Example Go output:
true
List Accounts
List OATH accounts configured in the Yubikey device:
accounts, err := oathAccounts.List()
if err != nil {
log.Fatal(err)
}
fmt.Println(accounts)
The above is the same as running the following in your terminal:
ykman --device 12345678 oath accounts list
Example Go output:
[
"Amazon Web Services:john.doe@example",
]
Request Code
Requests a Time-based One-Time Password (TOTP) 6-digit code for given account (such as ":") from Yubikey OATH application.
account := "<issuer>:<name>"
code, err := oathAccounts.Code(account)
if err != nil {
log.Fatal(err)
}
fmt.Println(code)
The above is the same as running the following in your terminal:
ykman --device 12345678 oath accounts code --single '<issuer>:<name>'
Example Go output:
"123456"
Managing Password
An end-user with Yubikey device may wish to password protect the Yubikey's OATH application. Generally speaking this is a good idea as it adds some protection from theft: A bad actor with someone else's Yubikey device can't actually use the device to generate TOTP MFA codes unless they somehow also know the device password.
The password protection for the Yubikey device's OATH application can be set either via the Yubico Authenticator GUI application or via command-line with ykman
by running:
ykman oath access change
But, if the device is configured with a password protected OATH application, it means that there needs to be a way to provide the password for ykmangoath
: Luckily this is one of the benefits of this specific library as it supports just that by either:
- Directly Assigning the password ahead-of-time
- Prompt Function which you can use to ask the password from end-user
There's also functionality to retrieve the prompted password given by end-user, so you may implement caching mechanisms to provide a smoother user experience where the end-user doesn't have to type in the password for every Yubikey operation; Remember there are of course tradeoffs with security vs. user experience with caching the password!
Check if Password Protected
To check if the Yubikey OATH application is password protected you can use:
isProtected := oathAccounts.IsPasswordProtected()
fmt.Println(isProtected)
Example Go output:
true
Direct Assign
err := oathAccounts.SetPassword("p4ssword")
// handle err
account := "<issuer>:<name>"
code, err := oathAccounts.Code(account)
// handle err
The above is the same as running the following in your terminal:
ykman --device 12345678 oath accounts code --single '<issuer>:<name>' --password 'p4ssword'
Prompt Function
Instead of assigning the password directly ahead-of-time, you may provide a password prompt function that will be executed only if password is required. Often you'll use this to actually ask the password from end-user – either via terminal stdin
or by showing a GUI dialog with tools such as ncruces/zenity
.
It must return a password string
(which can be empty) and an error
(which of course could be nil
on success). The password prompt function will also receive the context.Context
given in ykmangoath.New
initialization, therefore your password prompt function can be cancelled (for example due to timeout).
func myPasswordPrompt(ctx context.Context) (string, error) {
password := "p4ssword" // in real life, you'd resolve this value by asking the end-user
return password, nil
}
err := oathAccounts.SetPasswordPrompt(myPasswordPrompt)
Retrieve the prompted Password
func myPasswordPrompt(ctx context.Context) (string, error) {
password := "p4ssword" // in real life, you'd resolve this value by asking the end-user
return password, nil
}
err := oathAccounts.SetPasswordPrompt(myPasswordPrompt)
// handle err
code, err := oathAccounts.Code("<issuer>:<name>")
// handle err
// do something with code
password, err := oathAccounts.GetPassword()
// handle err
// do something with password (e.g. cache it somewhere):
myCacheSolution.Set(password) // ... just an example
This can be useful if you wish to cache the Yubikey OATH application password for short periods of time in your own application, so that the user doesn't have to type in the password everytime (remember: the physical touch of the Yubikey device should be the actual second factor). How you cache it (hopefully somewhat securely) is up to you.
Design
This tool is designed only for retrieval of specific information from a Yubikey device:
- List of configured OATH accounts
- Time-based One-Time Password (TOTP) code for given OATH account
- ... with a support for password protected Yubikey OATH applications
By design, this tool does NOT support:
- Setting or changing the Yubikey OATH application password: Configuring the password should be an explicit end-user operation (which they can do via Yubico Authenticator GUI or
ykman
CLI) – We do not want to enable situations where this library renders end-user's Yubikey OATH accounts useless by setting a password unknown to the end-user. - Removing or renaming OATH accounts from the Yubikey device: This is another area where – either by accident or on purpose – one could harm the end-user.
If you need some of the above-mentioned unsupported features, feel free to implement them yourself – for example by forking this library but we will never accept a pull request back into this project which implements those features.
This tool may implement following features in the future:
- #1 Adding new OATH acccounts
- #2 Password protecting a Yubikey device's OATH application given that:
- there are no OATH accounts configured (i.e. the device OATH application is empty/unused)
- the device OATH application does not yet have a password protection enabled
But this is not a promise to implement them. If you feel like it, you can create a Pull Request!