Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
stv0g committed May 15, 2024
1 parent 23270f7 commit fd362c2
Show file tree
Hide file tree
Showing 50 changed files with 2,191 additions and 49 deletions.
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
58 changes: 46 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
<!--
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` as used by the Nitrokey 3 tokens for TOTP, HOTP, HMAC and reverse HOTP calculation.

## Install
## Features

## Usage
- Calculation of
- Time-based One-time Passwords (TOTP)
- Hash-based One-time Passwords (HOTP)
- Hash-based Message Authentication Codes (HMAC)
- Reverse HMAC
- Static password safe entries
- PIN based authentication
- Reset of applet
- Credential management
- Add
- Get
- List
- Remove
- Update

### 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):

- Yubico HMAC slots (as used by KeePassXC)
- Challenge Response Authentication
- `CalculateAll` instruction
- Is disabled by default on Nitrokey 3's

## Tested devices

- Nitrokey 3 (firmware 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)
- [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
}
}
62 changes: 62 additions & 0 deletions calculate.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 (
"encoding/binary"
"errors"
"fmt"
"time"

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

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

// ErrUnknownName = errors.New("no such name configured")
// ErrMultipleMatches = errors.New("multiple matches found")
// ErrTouchRequired = errors.New("touch required")
// ErrTouchCallbackRequired = errors.New("touch callback required")
// ErrChallengeRequired = errors.New("challenge required")

// 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

0 comments on commit fd362c2

Please sign in to comment.