generated from cunicu/skeleton
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
50 changed files
with
2,194 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
// }) | ||
// } |
Oops, something went wrong.