Got

ykmangoath Ykman OATH TOTP with Go

Yet another ykman Go lib for requesting OATH TOTP Multi-Factor Authentication Codes from Yubikey Devices.


🚧 Work-in-Progress

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:[email protected]",
]

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:

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:
    1. there are no OATH accounts configured (i.e. the device OATH application is empty/unused)
    2. 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!

GitHub

View Github