Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New code #15

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
# SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
# SPDX-License-Identifier: Apache-2.0

# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json
Expand All @@ -14,9 +14,9 @@ on:
jobs:
build:
name: Build

runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
Expand Down
4 changes: 2 additions & 2 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
# SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
# SPDX-License-Identifier: Apache-2.0

linters-settings:
Expand Down Expand Up @@ -27,7 +27,7 @@ linters-settings:
sections:
- standard
- default
- prefix(cunicu.li/skeleton)
- prefix(cunicu.li/go-trussed-secrets)
- blank
- dot

Expand Down
4 changes: 2 additions & 2 deletions .reuse/dep5
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: skeleton
Upstream-Name: go-trussed-secrets
Upstream-Contact: Steffen Vogel <post@steffenvogel.de>
Source: https://github.com/stv0g/skeleton
Source: https://github.com/stv0g/go-trussed-secrets

Files: go.sum .renovaterc.json flake.lock
Copyright: 2023 Steffen Vogel <post@steffenvogel.de>
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
# SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
# SPDX-License-Identifier: Apache-2.0

* @stv0g
67 changes: 55 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
<!--
SPDX-FileCopyrightText: 2023 Steffen Vogel <post@steffenvogel.de>
SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
SPDX-License-Identifier: Apache-2.0
-->

# skeleton: Template repository for Go packages in the cunicu organization
# go-trussed-secrets

[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/cunicu/skeleton/test.yaml?style=flat-square)](https://github.com/cunicu/skeleton/actions)
[![goreportcard](https://goreportcard.com/badge/github.com/cunicu/skeleton?style=flat-square)](https://goreportcard.com/report/github.com/cunicu/skeleton)
[![Codecov branch](https://img.shields.io/codecov/c/github/cunicu/skeleton/main?style=flat-square&token=6XoWouQg6K)](https://app.codecov.io/gh/cunicu/skeleton/tree/main)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/cunicu/skeleton/blob/main/LICENSES/Apache-2.0.txt)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cunicu/skeleton?style=flat-square)
[![Go Reference](https://pkg.go.dev/badge/github.com/cunicu/skeleton.svg)](https://pkg.go.dev/github.com/cunicu/skeleton)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/cunicu/go-trussed-secrets/test.yaml?style=flat-square)](https://github.com/cunicu/go-trussed-secrets/actions)
[![goreportcard](https://goreportcard.com/badge/github.com/cunicu/go-trussed-secrets?style=flat-square)](https://goreportcard.com/report/github.com/cunicu/go-trussed-secrets)
[![Codecov branch](https://img.shields.io/codecov/c/github/cunicu/go-trussed-secrets/main?style=flat-square&token=6XoWouQg6K)](https://app.codecov.io/gh/cunicu/go-trussed-secrets/tree/main)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/cunicu/go-trussed-secrets/blob/main/LICENSES/Apache-2.0.txt)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/cunicu/go-trussed-secrets?style=flat-square)
[![Go Reference](https://pkg.go.dev/badge/github.com/cunicu/go-trussed-secrets.svg)](https://pkg.go.dev/github.com/cunicu/go-trussed-secrets)

This `skeleton` repo a template for new Go packages belonging to the cunicu organization.
The package `go-trussed-secrets` implements the protocol of the [_Trussed Secrets App_](https://github.com/Nitrokey/trussed-secrets-app).
It is used by the Nitrokey 3 tokens to provide HOTP, TOTP, reverse HOTP and challenge/response credentials.

## Install
**Note:** This package uses the CCID smart-card interface rather than the [CTAPHID](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md) interface as used by [pynitrokey](https://github.com/Nitrokey/pynitrokey)

## Usage
## Features

- Calculation of
- Time-based One-time Passwords (TOTP)
- Hash-based One-time Passwords (HOTP)
- Reverse Hash-based One-time Passwords (HOTP)
- Static password safe entries
- PIN based authentication
- Factory reset of applet
- Credential management
- Add
- Get
- List
- Remove
- Update
- Rename

### Unimplemented

The following features have not been implemented as they have been deprecated in [Nitrokey/trussed-secrets-app](https://github.com/Nitrokey/trussed-secrets-app):

- [YubiKey challenge/response slots](https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html) (as used by KeePassXC)
- Also includes challenge/response-based PIN authentication
- `CalculateAll` instruction
- Is disabled by default on Nitrokey 3's

## Roadmap

- [Untruncated responses](https://github.com/Nitrokey/trussed-secrets-app/issues/116)
- [CTAPHID interface](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md)

## Tested devices

- Nitrokey 3
- FW version v1.7.0

## References

- [**RFC 4226:** HOTP: An HMAC-Based One-Time Password Algorithm](https://datatracker.ietf.org/doc/html/rfc4226)
- [**RFC 6238:** TOTP: Time-Based One-Time Password Algorithm](https://datatracker.ietf.org/doc/html/rfc6238)
- [CTAPHID Protocol specification](https://github.com/Nitrokey/trussed-secrets-app/blob/main/docs/ctaphid.md)
- Token App: [Nitrokey/trussed-secrets-app](https://github.com/Nitrokey/trussed-secrets-app)
- Client CLI: [Nitrokey/pynitrokey (`pynitrokey/nk3/secrets_app.py`)](https://github.com/Nitrokey/pynitrokey/blob/master/pynitrokey/nk3/secrets_app.py)

## Authors

- Steffen Vogel ([@stv0g](https://github.com/stv0g))

## License

skeleton is licensed under the [Apache 2.0](./LICENSE) license.
go-trussed-secrets is licensed under the [Apache 2.0](./LICENSE) license.
62 changes: 62 additions & 0 deletions algorithm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0

package secrets

import (
"crypto/sha1" //nolint:gosec
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
)

// Algorithm denotes the HMAC algorithm used for deriving the one-time passwords
type Algorithm byte

const (
// HMACSHA1 describes a HMAC with SHA-1.
HMACSHA1 Algorithm = 0x01

// HMACSHA256 describes a HMAC with SHA-2 (256-bit).
HMACSHA256 Algorithm = 0x02

// HMACSHA512 describes a HMAC with SHA-2 (512-bit).
// TODO: Not yet supported by firmware.
HMACSHA512 Algorithm = 0x03
)

// String returns a string representation of the algorithm.
func (a Algorithm) String() string {
switch a {
case HMACSHA1:
return "HMAC-SHA1"

case HMACSHA256:
return "HMAC-SHA256"

case HMACSHA512:
return "HMAC-SHA512"

default:
return fmt.Sprintf("unknown %x", byte(a))
}
}

// Hash returns a constructor to create a new hash.Hash object
// for the given algorithm.
func (a Algorithm) Hash() func() hash.Hash {
switch a {
case HMACSHA1:
return sha1.New

case HMACSHA256:
return sha256.New

case HMACSHA512:
return sha512.New

default:
return nil
}
}
56 changes: 56 additions & 0 deletions calculate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0

package secrets

import (
"encoding/binary"
"errors"
"fmt"
"time"

"cunicu.li/go-iso7816/encoding/tlv"
)

var ErrNoValuesFound = errors.New("no values found in response")

// Calculate calculates a TOTP, HOTP with using the clock or current counter value.
func (c *Card) Calculate(id string) (Code, error) {
return c.CalculateWithChallenge(id, ChallengeTOTP(c.Clock(), c.Timestep))
}

// Calculate a TOTP or HMAC by providing a challenge.
func (c *Card) CalculateWithChallenge(id string, challenge []byte) (Code, error) {
// Unlike in YKOATH, the Trussed secrets app only returns truncated
// codes for HOTP and TOTP credentials.
tvs, err := c.send(insCalculate, 0x00, 0x01,
tlv.New(tagCredentialID, []byte(id)),
tlv.New(tagChallenge, challenge),
)
if err != nil {
return Code{}, err
}

for _, tv := range tvs {
switch tv.Tag {
case tagResponse, tagTruncated:
digits := int(tv.Value[0])
hash := tv.Value[1:]
return Code{
Digest: hash,
Digits: digits,
Truncated: tv.Tag == tagTruncated,
}, nil

default:
return Code{}, fmt.Errorf("%w: %x", errUnknownTag, tv.Tag)
}
}

return Code{}, ErrNoValuesFound
}

func ChallengeTOTP(t time.Time, ts time.Duration) []byte {
counter := t.Unix() / int64(ts.Seconds())
return binary.BigEndian.AppendUint64(nil, uint64(counter))
}
127 changes: 127 additions & 0 deletions calculate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0

package secrets_test

import (
"testing"

"github.com/stretchr/testify/require"

secrets "cunicu.li/go-trussed-secrets"
)

func TestCalculate(t *testing.T) {
vs := vectorsTOTP
vs = append(vs, vectorsHOTP...)

withCard(t, vs, true, func(t *testing.T, card *secrets.Card) {
require := require.New(t)

for _, v := range vs {
chal := secrets.ChallengeTOTP(v.Time, secrets.DefaultTimeStep)
code, err := card.CalculateWithChallenge(v.ID, chal)
require.NoError(err)
require.Equal(v.Code, code.OTP())
}
})
}

// func TestCalculateRequireTouch(t *testing.T) {
// withCard(t, []vector{
// {
// ID: "touch-required",
// Algorithm: secrets.HMACSHA256,
// Kind: secrets.TOTP,
// Digits: 6,
// Secret: fromHex("12341234"),
// Properties: secrets.TouchRequired,
// },
// }, true, func(t *testing.T, card *secrets.Card) {
// require := require.New(t)

// // Callback missing
// _, err := card.Calculate("touch-required")
// require.ErrorIs(err, secrets.ErrTouchCallbackRequired)

// // Error raised in callback
// _, err = card.Calculate("touch-required", func(s string) error {
// return errors.New("my error") //nolint:goerr113
// })
// require.ErrorContains(err, "my error")

// // Callback called but button not pressed
// touchRequested := false
// _, err = card.Calculate("touch-required", func(s string) error {
// require.Equal(s, "touch-required")
// touchRequested = true
// return nil
// })
// require.NoError(err)
// require.True(touchRequested)
// })
// }

func TestCalculateTOTP(t *testing.T) {
v := vectorsTOTP[0]
withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) {
require := require.New(t)

code, err := card.Calculate(v.ID)
require.NoError(err)
require.Equal(v.Code, code)
})
}

func TestCalculateHOTPCounterIncrement(t *testing.T) {
v := vectorsHOTP[0]
withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) {
require := require.New(t)

for _, ev := range vectorsHOTP[:10] {
code, err := card.Calculate(v.ID)
require.NoError(err)
require.Equal(ev.Code, code.OTP())
require.Equal(v.Digits, code.Digits)
require.True(code.Truncated)
}
})
}

func TestCalculateHOTPCounterInit(t *testing.T) {
withCard(t, vectorsHOTP, true, func(t *testing.T, card *secrets.Card) {
require := require.New(t)

for _, v := range vectorsHOTP {
code, err := card.Calculate(v.ID)
require.NoError(err)
require.Equal(v.Code, code.OTP())
require.Equal(v.Digits, code.Digits)
require.True(code.Truncated)
}
})
}

// func TestCalculateHMAC(t *testing.T) {
// expResp := fromHex("28c6d33a03e7c67940c30d06253f8980f8ef54bd")

// v := vector{
// ID: "hmac-test-01",
// Algorithm: secrets.HMACSHA1,
// Kind: secrets.HMAC,
// Secret: testSecretSHA1,
// }

// withCard(t, []vector{v}, true, func(t *testing.T, card *secrets.Card) {
// require := require.New(t)

// code, err := card.CalculateWithChallenge(v.ID, fromString("hallo"))
// require.NoError(err)
// require.Equal(expResp, code.Hash)
// require.False(code.Truncated)
// require.Zero(code.Digits)

// hash := v.Algorithm.Hash()()
// require.Len(code.Hash, hash.Size())
// })
// }
Loading
Loading