mirror of https://github.com/tailscale/tailscale/
wgkey: new package
This is a replacement for the key-related parts of the wireguard-go wgcfg package. This is almost a straight copy/paste from the wgcfg package. I have slightly changed some of the exported functions and types to avoid stutter, added and tweaked some comments, and removed some now-unused code. To avoid having wireguard-go depend on this new package, wgcfg will keep its key types. We translate into and out of those types at the last minute. These few remaining uses will be eliminated alongside the rest of the wgcfg package. Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>bradfitz/grafana_auth_proxy
parent
13b554fed9
commit
56a7652dc9
@ -0,0 +1,241 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package wgkey contains types and helpers for WireGuard keys.
|
||||
// It is very similar to package tailscale.com/types/key,
|
||||
// which is also used for curve25519 keys.
|
||||
// These keys are used for WireGuard clients;
|
||||
// those keys are used in other curve25519 clients.
|
||||
package wgkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// Size is the number of bytes in a curve25519 key.
|
||||
const Size = 32
|
||||
|
||||
// A Key is a curve25519 key.
|
||||
// It is used by WireGuard to represent public keys.
|
||||
type Key [Size]byte
|
||||
|
||||
// NewPreshared generates a new random Key.
|
||||
func NewPreshared() (*Key, error) {
|
||||
var k [Size]byte
|
||||
_, err := rand.Read(k[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*Key)(&k), nil
|
||||
}
|
||||
|
||||
func Parse(b64 string) (*Key, error) { return parseBase64(base64.StdEncoding, b64) }
|
||||
|
||||
func ParseHex(s string) (Key, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Key{}, fmt.Errorf("invalid hex key (%q): %w", s, err)
|
||||
}
|
||||
if len(b) != Size {
|
||||
return Key{}, fmt.Errorf("invalid hex key (%q): length=%d, want %d", s, len(b), Size)
|
||||
}
|
||||
|
||||
var key Key
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func ParsePrivateHex(v string) (Private, error) {
|
||||
k, err := ParseHex(v)
|
||||
if err != nil {
|
||||
return Private{}, err
|
||||
}
|
||||
pk := Private(k)
|
||||
if pk.IsZero() {
|
||||
// Do not clamp a zero key, pass the zero through
|
||||
// (much like NaN propagation) so that IsZero reports
|
||||
// a useful result.
|
||||
return pk, nil
|
||||
}
|
||||
pk.clamp()
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
func (k Key) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Key) String() string { return k.ShortString() }
|
||||
func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
if k == nil {
|
||||
return true
|
||||
}
|
||||
var zeros Key
|
||||
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||
}
|
||||
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
if k == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `"%x"`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
if k == nil {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON on nil pointer")
|
||||
}
|
||||
if len(b) < 3 || b[0] != '"' || b[len(b)-1] != '"' {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
key, err := ParseHex(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Key) LessThan(b *Key) bool {
|
||||
for i := range a {
|
||||
if a[i] < b[i] {
|
||||
return true
|
||||
} else if a[i] > b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Private is a curve25519 key.
|
||||
// It is used by WireGuard to represent private keys.
|
||||
type Private [Size]byte
|
||||
|
||||
// NewPrivate generates a new curve25519 secret key.
|
||||
// It conforms to the format described on https://cr.yp.to/ecdh.html.
|
||||
func NewPrivate() (Private, error) {
|
||||
k, err := NewPreshared()
|
||||
if err != nil {
|
||||
return Private{}, err
|
||||
}
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
return (Private)(*k), nil
|
||||
}
|
||||
|
||||
func ParsePrivate(b64 string) (*Private, error) {
|
||||
k, err := parseBase64(base64.StdEncoding, b64)
|
||||
return (*Private)(k), err
|
||||
}
|
||||
|
||||
func (k *Private) String() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k *Private) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k *Private) Equal(k2 Private) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Private) IsZero() bool {
|
||||
pk := Key(*k)
|
||||
return pk.IsZero()
|
||||
}
|
||||
|
||||
func (k *Private) clamp() {
|
||||
k[0] &= 248
|
||||
k[31] = (k[31] & 127) | 64
|
||||
}
|
||||
|
||||
// Public computes the public key matching this curve25519 secret key.
|
||||
func (k *Private) Public() Key {
|
||||
pk := Key(*k)
|
||||
if pk.IsZero() {
|
||||
panic("Tried to generate emptyPrivate.Public()")
|
||||
}
|
||||
var p [Size]byte
|
||||
curve25519.ScalarBaseMult(&p, (*[Size]byte)(k))
|
||||
return (Key)(p)
|
||||
}
|
||||
|
||||
func (k Private) MarshalText() ([]byte, error) {
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `privkey:%x`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Private) UnmarshalText(b []byte) error {
|
||||
s := string(b)
|
||||
if !strings.HasPrefix(s, `privkey:`) {
|
||||
return errors.New("wgkey.Private: UnmarshalText not given a private-key string")
|
||||
}
|
||||
s = strings.TrimPrefix(s, `privkey:`)
|
||||
key, err := ParseHex(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Private: UnmarshalText: %v", err)
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBase64(enc *base64.Encoding, s string) (*Key, error) {
|
||||
k, err := enc.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid key (%q): %w", s, err)
|
||||
}
|
||||
if len(k) != Size {
|
||||
return nil, fmt.Errorf("invalid key (%q): length=%d, want %d", s, len(k), Size)
|
||||
}
|
||||
var key Key
|
||||
copy(key[:], k)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func ParseSymmetric(b64 string) (Symmetric, error) {
|
||||
k, err := parseBase64(base64.StdEncoding, b64)
|
||||
if err != nil {
|
||||
return Symmetric{}, err
|
||||
}
|
||||
return Symmetric(*k), nil
|
||||
}
|
||||
|
||||
func ParseSymmetricHex(s string) (Symmetric, error) {
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return Symmetric{}, fmt.Errorf("invalid symmetric hex key (%q): %w", s, err)
|
||||
}
|
||||
if len(b) != chacha20poly1305.KeySize {
|
||||
return Symmetric{}, fmt.Errorf("invalid symmetric hex key length (%q): length=%d, want %d", s, len(b), chacha20poly1305.KeySize)
|
||||
}
|
||||
var key Symmetric
|
||||
copy(key[:], b)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Symmetric is a chacha20poly1305 key.
|
||||
// It is used by WireGuard to represent pre-shared symmetric keys.
|
||||
type Symmetric [chacha20poly1305.KeySize]byte
|
||||
|
||||
func (k Symmetric) Base64() string { return base64.StdEncoding.EncodeToString(k[:]) }
|
||||
func (k Symmetric) String() string { return "sym:" + k.Base64()[:8] }
|
||||
func (k Symmetric) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Symmetric) IsZero() bool { return k.Equal(Symmetric{}) }
|
||||
func (k Symmetric) Equal(k2 Symmetric) bool {
|
||||
return subtle.ConstantTimeCompare(k[:], k2[:]) == 1
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgkey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyBasics(t *testing.T) {
|
||||
k1, err := NewPreshared()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := k1.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(k1[:], k2[:]) {
|
||||
t.Fatalf("k1 %v != k2 %v", k1[:], k2[:])
|
||||
}
|
||||
if b1, b2 := k1.String(), k2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with PrivateKey", func(t *testing.T) {
|
||||
k2 := new(Private)
|
||||
if err := k2.UnmarshalText(b); err == nil {
|
||||
t.Fatalf("successfully decoded key as private key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to NewPreshared should make a new key.
|
||||
k3, err := NewPreshared()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(k1[:], k3[:]) {
|
||||
t.Fatalf("k1 %v == k3 %v", k1[:], k3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := k1.String(), k3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
})
|
||||
}
|
||||
func TestPrivateKeyBasics(t *testing.T) {
|
||||
pri, err := NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := pri.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("JSON round-trip", func(t *testing.T) {
|
||||
// should preserve the keys
|
||||
pri2 := new(Private)
|
||||
if err := pri2.UnmarshalText(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(pri[:], pri2[:]) {
|
||||
t.Fatalf("pri %v != pri2 %v", pri[:], pri2[:])
|
||||
}
|
||||
if b1, b2 := pri.String(), pri2.String(); b1 != b2 {
|
||||
t.Fatalf("base64-encoded keys do not match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri2.Public().String(); pub1 != pub2 {
|
||||
t.Fatalf("base64-encoded public keys do not match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON incompatible with Key", func(t *testing.T) {
|
||||
k2 := new(Key)
|
||||
if err := k2.UnmarshalJSON(b); err == nil {
|
||||
t.Fatalf("successfully decoded private key as key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("second key", func(t *testing.T) {
|
||||
// A second call to New should make a new key.
|
||||
pri3, err := NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(pri[:], pri3[:]) {
|
||||
t.Fatalf("pri %v == pri3 %v", pri[:], pri3[:])
|
||||
}
|
||||
// Check for obvious comparables to make sure we are not generating bad strings somewhere.
|
||||
if b1, b2 := pri.String(), pri3.String(); b1 == b2 {
|
||||
t.Fatalf("base64-encoded keys match: %s, %s", b1, b2)
|
||||
}
|
||||
if pub1, pub2 := pri.Public().String(), pri3.Public().String(); pub1 == pub2 {
|
||||
t.Fatalf("base64-encoded public keys match: %s, %s", pub1, pub2)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue