A bunch of changes

Signed-off-by: Dave Henderson <dhenderson@gmail.com>
This commit is contained in:
Dave Henderson 2018-08-20 20:06:33 -04:00
parent 16a4d2826c
commit a1879b1eb0
No known key found for this signature in database
GPG Key ID: 765A97405DCE5AFA
16 changed files with 274 additions and 203 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
!*/onerng
onerng

10
cmd/onerng/doc.go Normal file
View File

@ -0,0 +1,10 @@
/*
onerng is a OneRNG hardware random number generation utility.
Usage
TODO...
*/
package main

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"

View File

@ -1,11 +1,12 @@
// onerng: OneRNG hardware random number generation utility
package main
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/hairyhenderson/go-onerng/cmd"
)
func main() {
@ -26,5 +27,10 @@ func main() {
}
}()
cmd.Execute(ctx)
cmd := rootCmd(ctx)
initConfig(ctx, cmd)
if err := cmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@ -26,7 +26,7 @@ func readCmd(ctx context.Context) *cobra.Command {
Use: "read",
Short: "read some random data from the OneRNG",
RunE: func(cmd *cobra.Command, args []string) error {
o := onerng.OneRNG{Path: opts.Device}
o := &onerng.OneRNG{Path: opts.Device}
err := o.Init(ctx)
if err != nil {
return errors.Wrapf(err, "init failed")

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@ -11,7 +11,7 @@ import (
var (
cfgFile string
opts Config
opts config
)
func rootCmd(ctx context.Context) *cobra.Command {
@ -30,19 +30,9 @@ correctly, and that the firmware has not been tampered with.`,
}
}
// Execute -
func Execute(ctx context.Context) {
cmd := rootCmd(ctx)
initConfig(ctx, cmd)
if err := cmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func initConfig(ctx context.Context, cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.go-onerng.yaml)")
opts = Config{}
opts = config{}
cmd.PersistentFlags().StringVarP(&opts.Device, "device", "d", "/dev/ttyACM0", "the OneRNG device")
cmd.AddCommand(
@ -69,7 +59,6 @@ func initConfig(ctx context.Context, cmd *cobra.Command) {
}
}
// Config -
type Config struct {
type config struct {
Device string
}

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"bytes"

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@ -14,7 +14,7 @@ func versionCmd(ctx context.Context) *cobra.Command {
Use: "version",
Short: "Display the OneRNG's hardware version",
RunE: func(cmd *cobra.Command, args []string) error {
o := onerng.OneRNG{Path: opts.Device}
o := &onerng.OneRNG{Path: opts.Device}
version, err := o.Version(ctx)
if err != nil {
return err

View File

@ -1,47 +0,0 @@
package onerng
const (
// CmdVersion - print firmware version (as "Version n")
CmdVersion = "cmdv\n"
// CmdFlush - flush entropy pool
CmdFlush = "cmdw\n"
// CmdImage - extract the signed firmware image for verification
CmdImage = "cmdX\n"
// CmdID - print hardware ID
CmdID = "cmdI\n"
// CmdRun - start the task
CmdRun = "cmdO\n"
// CmdPause - stop/pause the task
CmdPause = "cmdo\n"
// CmdAvalancheWhitener - Avalanche noise with whitener (default)
CmdAvalancheWhitener = "cmd0\n"
// CmdAvalanche - Raw avalanche noise
CmdAvalanche = "cmd1\n"
// CmdAvalancheRFWhitener - Avalanche noise and RF noise with whitener
CmdAvalancheRFWhitener = "cmd2\n"
// CmdAvalancheRF - Raw avalanche noise and RF noise
CmdAvalancheRF = "cmd3\n"
// CmdSilent - No noise (necessary for image extraction)
CmdSilent = "cmd4\n"
// CmdSilent2 - No noise
CmdSilent2 = "cmd5\n"
// CmdRFWhitener - RF noise with whitener
CmdRFWhitener = "cmd6\n"
// CmdRF - Raw RF noise
CmdRF = "cmd7\n"
)
const (
// DisableWhitener - Disable the on-board CRC16 generator - no effect if both noise generators are disabled
DisableWhitener ReadMode = 1 << iota
// EnableRF - Enable noise generation from RF
EnableRF
// DisableAvalanche - Disable noise generation from the Avalanche Diode
DisableAvalanche
// Default - Avalanche enabled, RF disabled, Whitener enabled.
Default ReadMode = 0
// Silent - a convenience - everything disabled
Silent ReadMode = 4
)

46
copy.go
View File

@ -1,46 +0,0 @@
package onerng
import (
"context"
"io"
"os"
"strings"
"time"
)
type readerFunc func(p []byte) (n int, err error)
func (rf readerFunc) Read(p []byte) (n int, err error) { return rf(p) }
// io.CopyN/io.Copy with context support
func copyWithContext(ctx context.Context, dst io.Writer, src *os.File, n int64) (int64, error) {
// allow 10 500ms timeouts, for a total of 5s. After this, it's probably worth just giving up
allowedTimeouts := 10
rf := func(p []byte) (int, error) {
// I don't want reads to block forever, but I also don't want to time out immediately
err := src.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
if err != nil {
return 0, err
}
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
n, err := src.Read(p)
if allowedTimeouts > 0 {
if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") {
allowedTimeouts--
return n, nil
}
}
return n, err
}
}
if n < 0 {
return io.Copy(dst, readerFunc(rf))
}
return io.CopyN(dst, readerFunc(rf), n)
}

180
onerng.go
View File

@ -1,3 +1,32 @@
/*
Package onerng provides functions to help interface with the OneRNG hardware RNG.
See http://onerng.info for information about the device, and see especially
http://www.moonbaseotago.com/onerng/theory.html for the theory of operation.
To use this package, you must first plug the OneRNG into an available USB port,
and your OS should auto-detect the device as a USB serial modem. On Linux, you
may need to load the cdc_acm module.
Once you know which device file points to the OneRNG, you can instantiate a
*OneRNG struct instance. All communication with the OneRNG is done through
this instance.
o := &OneRNG{Path: "/dev/ttyACM0"}
version, err := o.Version(context.TODO())
if err != nil {
return err
}
fmt.Printf("version is %d\n", version)
Reading data from the OneRNG can be done with the Read function:
o := &OneRNG{Path: "/dev/ttyACM0"}
_, err = o.Read(context.TODO(), os.Stdout, -1, EnableRF | DisableWhitener)
if err != nil {
return err
}
*/
package onerng
import (
@ -6,7 +35,6 @@ import (
"context"
"crypto/aes"
"crypto/cipher"
"fmt"
"io"
mrand "math/rand"
"os"
@ -20,21 +48,20 @@ import (
// OneRNG - a OneRNG device
type OneRNG struct {
Path string
device *os.File
device io.ReadWriteCloser
}
// ReadMode -
type ReadMode uint32
// cmd sends one or more commands to the OneRNG. The device is not closed on
// completion, as it's usually being read from simultaneously.
func (o *OneRNG) cmd(ctx context.Context, c ...string) error {
err := o.open()
if err != nil {
return err
}
for _, v := range c {
_, err = o.device.WriteString(v)
_, err = o.device.Write([]byte(v))
if err != nil {
return errors.Wrapf(err, "Errored on command %s", v)
return errors.Wrapf(err, "Errored on command %q", v)
}
select {
case <-ctx.Done():
@ -45,6 +72,8 @@ func (o *OneRNG) cmd(ctx context.Context, c ...string) error {
return nil
}
// open the OneRNG device for read/write, if it hasn't already been opened.
// Access it as o.device.
func (o *OneRNG) open() (err error) {
if o.device != nil {
return nil
@ -53,6 +82,7 @@ func (o *OneRNG) open() (err error) {
return err
}
// close the OneRNG device if it hasn't already been closed
func (o *OneRNG) close() error {
if o.device == nil {
return nil
@ -62,7 +92,7 @@ func (o *OneRNG) close() error {
return err
}
// Version -
// Version - query the OneRNG for its hardware version
func (o *OneRNG) Version(ctx context.Context) (int, error) {
err := o.open()
if err != nil {
@ -70,7 +100,7 @@ func (o *OneRNG) Version(ctx context.Context) (int, error) {
}
defer o.close()
err = o.cmd(ctx, CmdPause)
err = o.cmd(ctx, cmdPause)
if err != nil {
return 0, err
}
@ -81,7 +111,7 @@ func (o *OneRNG) Version(ctx context.Context) (int, error) {
errc := make(chan error, 1)
go o.scan(ctx, buf, errc)
err = o.cmd(ctx, CmdSilent, CmdVersion, CmdRun)
err = o.cmd(ctx, noiseCommand(Silent), cmdVersion, cmdRun)
if err != nil {
return 0, err
}
@ -104,7 +134,7 @@ loop:
}
}
err = o.cmd(ctx, CmdPause)
err = o.cmd(ctx, cmdPause)
if err != nil {
return 0, err
}
@ -114,7 +144,7 @@ loop:
return version, err
}
// Identify -
// Identify - query the OneRNG for its ID
func (o *OneRNG) Identify(ctx context.Context) (string, error) {
err := o.open()
if err != nil {
@ -128,7 +158,7 @@ func (o *OneRNG) Identify(ctx context.Context) (string, error) {
errc := make(chan error, 1)
go o.scan(ctx, buf, errc)
err = o.cmd(ctx, CmdSilent, CmdID, CmdRun)
err = o.cmd(ctx, noiseCommand(Silent), cmdID, cmdRun)
if err != nil {
return "", err
}
@ -151,7 +181,7 @@ loop:
}
}
err = o.cmd(ctx, CmdPause)
err = o.cmd(ctx, cmdPause)
if err != nil {
return "", err
}
@ -159,7 +189,7 @@ loop:
return idString, err
}
// Flush -
// Flush the OneRNG's entropy pool
func (o *OneRNG) Flush(ctx context.Context) error {
err := o.open()
if err != nil {
@ -169,11 +199,14 @@ func (o *OneRNG) Flush(ctx context.Context) error {
_, cancel := context.WithCancel(ctx)
defer cancel()
err = o.cmd(ctx, CmdFlush)
err = o.cmd(ctx, cmdFlush)
return err
}
// Image -
// Image extracts the firmware image. This image is padded with random data to
// either 128Kb or 256Kb (depending on hardware), and signed.
//
// See also the Verify function.
func (o *OneRNG) Image(ctx context.Context) ([]byte, error) {
err := o.open()
if err != nil {
@ -181,7 +214,7 @@ func (o *OneRNG) Image(ctx context.Context) ([]byte, error) {
}
defer o.close()
err = o.cmd(ctx, CmdPause, CmdSilent)
err = o.cmd(ctx, cmdPause, noiseCommand(Silent))
if err != nil {
return nil, err
}
@ -194,7 +227,7 @@ func (o *OneRNG) Image(ctx context.Context) ([]byte, error) {
errc := make(chan error, 1)
go o.stream(ctx, 4, buf, errc)
err = o.cmd(ctx, CmdSilent, CmdImage, CmdRun)
err = o.cmd(ctx, noiseCommand(Silent), cmdImage, cmdRun)
if err != nil {
return nil, err
}
@ -224,7 +257,7 @@ loop:
}
}
err = o.cmd(ctx, CmdPause)
err = o.cmd(ctx, cmdPause)
if err != nil {
return nil, err
}
@ -244,24 +277,28 @@ func (o *OneRNG) Init(ctx context.Context) error {
break
}
}
fmt.Fprintf(os.Stderr, "Initialized after %d loops\n", i)
// fmt.Fprintf(os.Stderr, "Initialized after %d loops\n", i)
return nil
}
// Read -
func (o *OneRNG) Read(ctx context.Context, out io.WriteCloser, n int64, flags ReadMode) (written int64, err error) {
// Read n bytes of data from the OneRNG into the given Writer. Set flags to
// configure the OneRNG's. Set n to -1 to continuously read until an error is
// encountered, or the context is cancelled.
//
// The OneRNG device will be closed when the operation completes.
func (o *OneRNG) Read(ctx context.Context, out io.Writer, n int64, flags NoiseMode) (written int64, err error) {
err = o.open()
if err != nil {
return 0, err
}
defer o.close()
err = o.cmd(ctx, NoiseCommand(flags), CmdRun)
err = o.cmd(ctx, noiseCommand(flags), cmdRun)
if err != nil {
return 0, err
}
defer o.cmd(ctx, CmdPause)
defer o.cmd(ctx, cmdPause)
written, err = copyWithContext(ctx, out, o.device, n)
return written, err
@ -282,13 +319,13 @@ func (o *OneRNG) readData(ctx context.Context) (int, error) {
errc := make(chan error, 1)
go o.stream(ctx, 1, buf, errc)
err = o.cmd(ctx, CmdAvalanche, CmdRun)
err = o.cmd(ctx, noiseCommand(Default), cmdRun)
if err != nil {
return 0, err
}
// make sure we always end with a pause/silence/flush
defer o.cmd(ctx, CmdPause, CmdSilent, CmdFlush)
defer o.cmd(ctx, cmdPause, noiseCommand(Silent), cmdFlush)
// blocking read from the channel, with a timeout (from context)
select {
@ -350,15 +387,20 @@ func (o *OneRNG) scan(ctx context.Context, buf chan string, errc chan error) {
}
}
// NoiseCommand - returns the appropriate noise-generation command for the given flags
func NoiseCommand(flags ReadMode) string {
num := strconv.Itoa(int(flags))
return "cmd" + num + "\n"
}
type aesWhitener struct {
out io.WriteCloser
}
const (
// cmdVersion - print firmware version (as "Version n")
cmdVersion = "cmdv\n"
// cmdFlush - flush entropy pool
cmdFlush = "cmdw\n"
// cmdImage - extract the signed firmware image for verification
cmdImage = "cmdX\n"
// cmdID - print hardware ID
cmdID = "cmdI\n"
// cmdRun - start the task
cmdRun = "cmdO\n"
// cmdPause - stop/pause the task
cmdPause = "cmdo\n"
)
// AESWhitener creates a "whitener" that wraps the provided writer. The random
// data that the OneRNG generates is sometimes a little "too" random for some
@ -396,14 +438,76 @@ func (o *OneRNG) key(ctx context.Context) ([]byte, error) {
buf := &bytes.Buffer{}
err = o.cmd(ctx, CmdAvalanche, CmdRun)
err = o.cmd(ctx, noiseCommand(Default), cmdRun)
if err != nil {
return []byte{}, err
}
defer o.cmd(ctx, CmdPause)
defer o.cmd(ctx, cmdPause)
// 16 bytes == AES-128
_, err = copyWithContext(ctx, buf, o.device, 16)
k := buf.Bytes()
return k, err
}
// NoiseMode represents the different noise-generation modes available to the OneRNG
type NoiseMode uint32
const (
// DisableWhitener - Disable the on-board CRC16 generator - no effect if both noise generators are disabled
DisableWhitener NoiseMode = 1 << iota
// EnableRF - Enable noise generation from RF
EnableRF
// DisableAvalanche - Disable noise generation from the Avalanche Diode
DisableAvalanche
// Default mode - Avalanche enabled, RF disabled, Whitener enabled.
Default NoiseMode = 0
// Silent - a convenience - everything disabled
Silent NoiseMode = 4
)
// noiseCommand converts the given mode to the appropriate command to send to the OneRNG
func noiseCommand(flags NoiseMode) string {
num := strconv.Itoa(int(flags))
return "cmd" + num + "\n"
}
type readerFunc func(p []byte) (n int, err error)
func (rf readerFunc) Read(p []byte) (n int, err error) { return rf(p) }
// io.CopyN/io.Copy with cancellation support
func copyWithContext(ctx context.Context, dst io.Writer, src io.Reader, n int64) (int64, error) {
// allow 10 500ms timeouts, for a total of 5s. After this, it's probably worth just giving up
allowedTimeouts := 10
rf := func(p []byte) (int, error) {
if f, ok := src.(*os.File); ok {
// I don't want reads to block forever, but I also don't want to time out immediately
err := f.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
if err != nil {
return 0, err
}
}
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
n, err := src.Read(p)
if allowedTimeouts > 0 {
if err != nil && os.IsTimeout(err) {
allowedTimeouts--
return n, nil
}
}
return n, err
}
}
if n < 0 {
return io.Copy(dst, readerFunc(rf))
}
return io.CopyN(dst, readerFunc(rf), n)
}

View File

@ -1,34 +1,103 @@
package onerng
import (
"fmt"
"bytes"
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNoiseCommand(t *testing.T) {
fmt.Println(DisableWhitener)
fmt.Println(EnableRF)
fmt.Println(DisableAvalanche)
// func TestNoiseCommand(t *testing.T) {
// fmt.Println(DisableWhitener)
// fmt.Println(EnableRF)
// fmt.Println(DisableAvalanche)
fmt.Println(Default)
fmt.Println(Silent)
// fmt.Println(Default)
// fmt.Println(Silent)
testdata := []struct {
flags ReadMode
cmd string
}{
{Default, "cmd0\n"},
{DisableWhitener, "cmd1\n"},
{EnableRF, "cmd2\n"},
{EnableRF | DisableWhitener, "cmd3\n"},
{DisableAvalanche, "cmd4\n"},
{DisableAvalanche | DisableWhitener, "cmd5\n"},
{DisableAvalanche | EnableRF, "cmd6\n"},
{DisableAvalanche | EnableRF | DisableWhitener, "cmd7\n"},
}
for _, d := range testdata {
assert.Equal(t, d.cmd, NoiseCommand(d.flags), d.cmd, d.flags)
}
// testdata := []struct {
// flags NoiseMode
// cmd string
// }{
// {Default, "cmd0\n"},
// {DisableWhitener, "cmd1\n"},
// {EnableRF, "cmd2\n"},
// {EnableRF | DisableWhitener, "cmd3\n"},
// {DisableAvalanche, "cmd4\n"},
// {DisableAvalanche | DisableWhitener, "cmd5\n"},
// {DisableAvalanche | EnableRF, "cmd6\n"},
// {DisableAvalanche | EnableRF | DisableWhitener, "cmd7\n"},
// }
// for _, d := range testdata {
// assert.Equal(t, d.cmd, noiseCommand(d.flags), d.cmd, d.flags)
// }
// }
type fakeDev struct {
closed bool
rbuf *bytes.Buffer
wbuf *bytes.Buffer
}
func (d *fakeDev) reset() {
d.closed = false
d.rbuf.Reset()
d.wbuf.Reset()
}
func (d *fakeDev) Close() error {
d.closed = true
return nil
}
func (d *fakeDev) Read(b []byte) (int, error) {
return d.rbuf.Read(b)
}
func (d *fakeDev) Write(b []byte) (int, error) {
return d.wbuf.Write(b)
}
func TestCmd(t *testing.T) {
d := &fakeDev{rbuf: &bytes.Buffer{}, wbuf: &bytes.Buffer{}}
o := &OneRNG{Path: "/dev/null", device: d}
ctx, cancel := context.WithCancel(context.Background())
err := o.cmd(ctx, "foo", "bar")
assert.NoError(t, err)
assert.Equal(t, "foobar", d.wbuf.String())
d.reset()
cancel()
err = o.cmd(ctx, "foo", "bar")
assert.NoError(t, err)
assert.Equal(t, "foo", d.wbuf.String())
}
func TestClose(t *testing.T) {
d := &fakeDev{}
o := &OneRNG{Path: "/dev/null", device: d}
err := o.close()
assert.NoError(t, err)
assert.True(t, d.closed)
o = &OneRNG{Path: "/dev/null", device: nil}
err = o.close()
assert.NoError(t, err)
assert.Nil(t, o.device)
}
func TestVersion(t *testing.T) {
d := &fakeDev{
wbuf: &bytes.Buffer{},
rbuf: bytes.NewBufferString("dfoawiuhf98h9inf2oifoi2jr\n" +
"dfkjawflihjwfoiuh2rliu13he487631487645t98y23rtoqu3rbno9q34htgfv\n" +
"\r\nVersion 3\r\nas;dlfjaw;oihf2ih2o3iuf2ofnlo2jnlfuhf2iou\n\n"),
}
o := &OneRNG{Path: "/dev/null", device: d}
ctx := context.Background()
v, err := o.Version(ctx)
assert.NoError(t, err)
assert.Equal(t, "cmdo\ncmd4\ncmdv\ncmdO\ncmdo\n", d.wbuf.String())
assert.Equal(t, 3, v)
}

View File

@ -11,25 +11,13 @@ import (
"golang.org/x/crypto/openpgp"
)
// looks like:
// fe ed be ef 20 14 - magic number
// 00 00 04 - len (little endian)
// 00 00 - version (little endian)
// image - len bytes of image
type fwImage struct {
magic [6]byte // must be 0xfeedbeef2014
length [3]byte // LE
version [2]byte // LE
_ [1]byte //
fullimg [262144]byte // full image
// image [261536]byte // 256kb minus end offset
// slen [2]byte // signature length
// sig [543]byte // signature
// endOff [608]byte
}
// Verify - this is a more-or-less straight port from the onerng_verify.py script
// distributed with the OneRNG package
// Verify reads a signed firmware image, extracts the signature, and verifies
// it against the given public key.
//
// Details are printed to Stderr on success, otherwise an error is returned.
//
// This is a more-or-less straight port from the official onerng_verify.py
// script distributed alongside the OneRNG package.
func Verify(ctx context.Context, image io.Reader, pubkey string) error {
var x byte
length := 0
@ -108,9 +96,6 @@ func Verify(ctx context.Context, image io.Reader, pubkey string) error {
signature := bytes.NewBuffer(c[length-endOff+2 : length-endOff+2+klen])
signed := bytes.NewBuffer(c[0 : length-endOff])
// leftovers := c[length-endOff+2+klen : length]
// fmt.Fprintf(os.Stderr, "leftovers (%d):\n%#x\n", len(leftovers), leftovers)
// read public key
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(pubkey))
if err != nil {