Commit 5cacbcb9 authored by Yawning Angel 's avatar Yawning Angel

nonvoting: Add a basic non-voting authority.

This is a rudimentary authority that functions "well enough" for simple
testing on a single system.  It uses a "RESTful" (ick) interface over
plain old HTTP to accept pre-configured nodes POSTing descriptors, and
nodes and clients GETing documents.

It still needs:

 * To do something with the network Parameter block. (Requires changing
   the PKI interface).
 * Persisting received descriptors and generated documents to disk.
 * A better network topology generator.
 * Cleanups.

... but it's the definition of "Good enough for debugging".
parent 287f731c
*.swp
*~
// client.go - Katzenpost non-voting authority client.
// Copyright (C) 2017 Yawning Angel.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package client implements the Katzenpost non-voting authority client.
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/katzenpost/authority/nonvoting/internal/constants"
"github.com/katzenpost/authority/nonvoting/internal/s11n"
"github.com/katzenpost/core/crypto/eddsa"
"github.com/katzenpost/core/log"
"github.com/katzenpost/core/pki"
"github.com/katzenpost/core/utils"
"github.com/op/go-logging"
"golang.org/x/net/context/ctxhttp"
)
const clientTimeout = 30 * time.Second
var httpClient = &http.Client{Timeout: clientTimeout}
// Config is a nonvoting authority pki.Client instance.
type Config struct {
// LogBackend is the `core/log` Backend instance to use for logging.
LogBackend *log.Backend
// Address is the authority's address to connect to for posting and
// fetching documents.
Address string
// PublicKey is the authority's public key to use when validating documents.
PublicKey *eddsa.PublicKey
}
func (cfg *Config) validate() error {
if cfg.LogBackend == nil {
return fmt.Errorf("nonvoting/client: LogBackend is mandatory")
}
if err := utils.EnsureAddrIPPort(cfg.Address); err != nil {
return fmt.Errorf("nonvoting/client: Invalid Address: %v", err)
}
if cfg.PublicKey == nil {
return fmt.Errorf("nonvoting/client: PublicKey is mandatory")
}
return nil
}
type client struct {
cfg *Config
log *logging.Logger
}
func (c *client) Post(ctx context.Context, epoch uint64, signingKey *eddsa.PrivateKey, d *pki.MixDescriptor) error {
c.log.Debugf("Post(ctx, %d, %v, %+v)", epoch, signingKey.PublicKey(), d)
// Ensure that the descriptor we are about to post is well formed.
if err := s11n.IsDescriptorWellFormed(d, epoch); err != nil {
return err
}
// Make a serialized + signed + serialized descriptor.
signed, err := s11n.SignDescriptor(signingKey, d)
if err != nil {
return err
}
c.log.Debugf("Signed descriptor: '%v'", signed)
// Post it to the right place.
u := postURLForEpoch(c.cfg.Address, epoch)
c.log.Debugf("Posting descriptor to: %v", u)
r := bytes.NewReader([]byte(signed))
resp, err := ctxhttp.Post(ctx, httpClient, u, constants.JoseMIMEType, r)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusAccepted:
return nil
default:
// TODO: The authority rejected the POST for some reason, the
// right thing to do is to probably return an error indicating
// that the server should give up trying to upload a descriptor
// for this epoch.
//
// See: https://github.com/Katzenpost/server/issues/11
return fmt.Errorf("nonvoting/client: Post() rejected by authority: %v", resp.StatusCode)
}
// NOTREACHED
}
func (c *client) Get(ctx context.Context, epoch uint64) (*pki.Document, error) {
c.log.Debugf("Get(ctx, %d)", epoch)
// Download the document.
u := getURLForEpoch(c.cfg.Address, epoch)
c.log.Debugf("Getting document from: %v", u)
resp, err := ctxhttp.Get(ctx, httpClient, u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// TODO: Likewise with Post() this should probably return an
// error indicating that the client should not retry fetching
// this document, based on the status code.
//
// Anything other than a 500 (Internal Server Error) probably should
// indicate failure...
return nil, fmt.Errorf("nonvoting/Client: Get() rejected by authority: %v", resp.StatusCode)
}
// Read in the body.
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// Validate the document.
doc, err := s11n.VerifyAndParseDocument(b, c.cfg.PublicKey, epoch)
if err != nil {
return nil, err
}
c.log.Debugf("Document: %v", doc)
return doc, nil
}
// New constructs a new pki.Client instance.
func New(cfg *Config) (pki.Client, error) {
if cfg == nil {
return nil, fmt.Errorf("nonvoting/client: cfg is mandatory")
}
if err := cfg.validate(); err != nil {
return nil, err
}
c := new(client)
c.cfg = cfg
c.log = cfg.LogBackend.GetLogger("pki/nonvoting/client")
return c, nil
}
func postURLForEpoch(addr string, epoch uint64) string {
u := &url.URL{
Scheme: "http",
Host: addr,
Path: fmt.Sprintf("%v%v", constants.V0PostBase, epoch),
}
return u.String()
}
func getURLForEpoch(addr string, epoch uint64) string {
u := &url.URL{
Scheme: "http",
Host: addr,
Path: fmt.Sprintf("%v%v", constants.V0GetBase, epoch),
}
return u.String()
}
// constants.go - Katzenpost non-voting authority constants.
// Copyright (C) 2017 Yawning Angel.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package constants provides the Katzenpost non-voting authority constants.
package constants
const (
// V0PostBase is the base URL path for descriptor uploads.
V0PostBase = "/v0/post/"
// V0GetBase is the base URL path for document fetches.
V0GetBase = "/v0/get/"
// JoseMIMEType is the MIME type for JOSE documents.
JoseMIMEType = "application/jose"
)
// descriptor.go - Katzenpost Non-voting authority descriptor s11n.
// Copyright (C) 2017 Yawning Angel.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package s11n implements serialization routines for the various PKI
// data structures.
package s11n
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/katzenpost/core/crypto/eddsa"
"github.com/katzenpost/core/pki"
"github.com/katzenpost/core/sphinx/constants"
"github.com/katzenpost/core/utils"
"gopkg.in/square/go-jose.v2"
)
const nodeDescriptorVersion = "nonvoting-v0"
type nodeDescriptor struct {
// Version uniquely identifies the descriptor format as being for the
// non-voting authority so that it can be rejected when unexpectedly
// posted to, or received from an authority, or if the version changes.
Version string
pki.MixDescriptor
}
// SignDescriptor signs and serializes the descriptor with the provided signing
// key.
func SignDescriptor(signingKey *eddsa.PrivateKey, base *pki.MixDescriptor) (string, error) {
d := new(nodeDescriptor)
d.MixDescriptor = *base
d.Version = nodeDescriptorVersion
// Serialize the descriptor.
payload, err := json.Marshal(d)
if err != nil {
return "", err
}
// Sign the descriptor.
k := jose.SigningKey{
Algorithm: jose.EdDSA,
Key: *signingKey.InternalPtr(),
}
signer, err := jose.NewSigner(k, nil)
if err != nil {
return "", err
}
signed, err := signer.Sign(payload)
if err != nil {
return "", err
}
// Serialize the key, descriptor and signature.
return signed.CompactSerialize()
}
// VerifyAndParseDescriptor verifies the signature and deserializes the
// descriptor. MixDescriptors returned from this routine are guaranteed
// to have been correctly self signed by the IdentityKey listed in the
// MixDescriptor.
func VerifyAndParseDescriptor(b []byte, epoch uint64) (*pki.MixDescriptor, error) {
signed, err := jose.ParseSigned(string(b))
if err != nil {
return nil, err
}
// So the descriptor is going to be signed by the node's key, which may
// be new to the authority (which is doing the decoding). In an ideal
// world this is where embedding the public key in the header solves this
// problem, but the library doesn't support doing so for EdDSA signatures.
//
// Since the descriptors themselves include (perhaps redundantly) a copy
// of the IdentityKey used to sign the descriptor, we can reach into
// the unverified payload to pull it out instead.
//
// This is wasteful on the CPU side since it's de-serializing the payload
// twice, but this isn't a critical path operation, nor is the non-voting
// authority something that will do this a lot.
if len(signed.Signatures) != 1 {
return nil, fmt.Errorf("nonvoting: Expected 1 signature, got: %v", len(signed.Signatures))
}
alg := signed.Signatures[0].Header.Algorithm
if alg != "EdDSA" {
return nil, fmt.Errorf("nonvoting: Unsupported signature algorithm: '%v'", alg)
}
candidatePk, err := extractSignedDescriptorPublicKey(b)
if err != nil {
return nil, err
}
// Verify that the descriptor is signed by the key in the header.
payload, err := signed.Verify(*candidatePk.InternalPtr())
if err != nil {
return nil, err
}
// Parse the payload.
d := new(nodeDescriptor)
if err = json.Unmarshal(payload, d); err != nil {
return nil, err
}
// Ensure the descriptor is well formed.
if d.Version != nodeDescriptorVersion {
return nil, fmt.Errorf("nonvoting: Invalid Descriptor Version: '%v'", d.Version)
}
if err = IsDescriptorWellFormed(&d.MixDescriptor, epoch); err != nil {
return nil, err
}
// And as the final check, ensure that the key embedded in the descriptor
// matches the key we teased out of the payload, that we used to validate
// the signature.
if !candidatePk.Equal(d.IdentityKey) {
return nil, fmt.Errorf("nonvoting: Descriptor signing key mismatch")
}
return &d.MixDescriptor, nil
}
func extractSignedDescriptorPublicKey(b []byte) (*eddsa.PublicKey, error) {
// Per RFC 7515:
//
// In the JWS Compact Serialization, a JWS is represented as the
// concatenation:
//
// BASE64URL(UTF8(JWS Protected Header)) || '.' ||
// BASE64URL(JWS Payload) || '.' ||
// BASE64URL(JWS Signature)
//
// The JOSE library used doesn't support embedding EdDSA JWK Public Keys
// so this reaches into the (unverified) payload, to pull out the
// descriptor's PublicKey.
spl := bytes.Split(b, []byte{'.'})
if len(spl) != 3 {
return nil, fmt.Errorf("nonvoting: Splitting at '.' returned unexpected number of sections: %v", len(spl))
}
payload, err := base64.RawURLEncoding.DecodeString(string(spl[1]))
if err != nil {
return nil, fmt.Errorf("nonvoting: (Early) Failed to decode: %v", err)
}
d := new(nodeDescriptor)
if err = json.Unmarshal(payload, d); err != nil {
return nil, fmt.Errorf("nonvoting: (Early) Failed to deserialize: %v", err)
}
candidatePk := d.IdentityKey
if candidatePk == nil {
return nil, fmt.Errorf("nonvoting: (Early) Descriptor missing IdentityKey")
}
return candidatePk, nil
}
// IsDescriptorWellFormed validates the descriptor and returns a descriptive
// error iff there are any problems that would make it unusable as part of
// a PKI Document.
func IsDescriptorWellFormed(d *pki.MixDescriptor, epoch uint64) error {
if d.Name == "" {
return fmt.Errorf("nonvoting: Descriptor missing Name")
}
if len(d.Name) > constants.NodeIDLength {
return fmt.Errorf("nonvoting: Descriptor Name '%v' exceeds max length", d.Name)
}
if d.LinkKey == nil {
return fmt.Errorf("nonvoting: Descriptor missing LinkKey")
}
if d.IdentityKey == nil {
return fmt.Errorf("nonvoting: Descriptor missing IdentityKey")
}
if d.MixKeys[epoch] == nil {
return fmt.Errorf("nonvoting: Descriptor missing MixKey[%v]", epoch)
}
for e := range d.MixKeys {
// TODO: Should this check that the epochs in MixKey are sequential?
if e < epoch || e >= epoch+3 {
return fmt.Errorf("nonvoting: Descriptor contains MixKey for invalid epoch: %v", d)
}
}
if len(d.Addresses) == 0 {
return fmt.Errorf("nonvoting: Descriptor missing Addresses")
}
for _, v := range d.Addresses {
if err := utils.EnsureAddrIPPort(v); err != nil {
return fmt.Errorf("nonvoting: Descriptor containx invalid Address '%v': %v", v, err)
}
}
if d.Layer != pki.LayerProvider && d.Layer != 0 {
return fmt.Errorf("nonvoting: Descriptor self-assigned Layer: '%v'", d.Layer)
}
return nil
}
// descriptor_test.go - Descriptor s11n tests.
// Copyright (C) 2017 Yawning Angel
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package s11n
import (
"crypto/rand"
"testing"
"github.com/katzenpost/core/crypto/ecdh"
"github.com/katzenpost/core/crypto/eddsa"
"github.com/katzenpost/core/pki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const debugTestEpoch = 0x23
func TestDescriptor(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
d := new(pki.MixDescriptor)
err := IsDescriptorWellFormed(d, debugTestEpoch)
assert.Error(err, "IsDescriptorWellFormed(bad)")
// Build a well formed descriptor.
d.Name = "hydra-dominatus.example.net"
d.Addresses = []string{"192.0.2.1:4242"}
d.Layer = 0
d.LoadWeight = 23
identityPriv, err := eddsa.NewKeypair(rand.Reader)
require.NoError(err, "eddsa.NewKeypair()")
d.IdentityKey = identityPriv.PublicKey()
linkPriv, err := ecdh.NewKeypair(rand.Reader)
require.NoError(err, "ecdh.NewKeypair()")
d.LinkKey = linkPriv.PublicKey()
d.MixKeys = make(map[uint64]*ecdh.PublicKey)
for e := debugTestEpoch; e < debugTestEpoch+3; e++ {
mPriv, err := ecdh.NewKeypair(rand.Reader)
require.NoError(err, "[%d]: ecdh.NewKeypair()", e)
d.MixKeys[uint64(e)] = mPriv.PublicKey()
}
err = IsDescriptorWellFormed(d, debugTestEpoch)
require.NoError(err, "IsDescriptorWellFormed(good)")
t.Logf("Descriptor: '%v'", d)
// Sign the descriptor.
signed, err := SignDescriptor(identityPriv, d)
require.NoError(err, "SignDescriptor()")
t.Logf("signed descriptor: '%v'", signed)
// Verify and deserialize the signed descriptor.
dd, err := VerifyAndParseDescriptor([]byte(signed), debugTestEpoch)
require.NoError(err, "VerifyAndParseDescriptor()")
t.Logf("Deserialized descriptor: '%v'", dd)
// Ensure the base and de-serialized descriptors match.
assert.Equal(d.Name, dd.Name, "Name")
assert.Equal(d.Addresses, dd.Addresses, "Addresses")
assert.Equal(d.Layer, dd.Layer, "Layer")
assert.Equal(d.LoadWeight, dd.LoadWeight, "LoadWeight")
assert.Equal(d.IdentityKey.Bytes(), dd.IdentityKey.Bytes(), "IdentityKey")
assert.Equal(d.LinkKey.Bytes(), dd.LinkKey.Bytes(), "LinkKey")
require.Equal(len(d.MixKeys), len(dd.MixKeys), "len(MixKeys)")
for k, v := range d.MixKeys {
vv := dd.MixKeys[k]
require.NotNil(vv)
require.Equal(v.Bytes(), vv.Bytes(), "MixKeys[%v]", k)
}
}
// document.go - Katzenpost Non-voting authority document s11n.
// Copyright (C) 2017 Yawning Angel.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package s11n
import (
"encoding/json"
"fmt"
"github.com/katzenpost/core/crypto/eddsa"
"github.com/katzenpost/core/pki"
"gopkg.in/square/go-jose.v2"
)
const documentVersion = "nonvoting-document-v0"
type document struct {
// Version uniquely identifies the document format as being for the
// non-voting authority so that it can be rejected when unexpectedly
// received or if the version changes.
Version string
pki.Document
}
// SignDocument signs and serializes the document with the provided signing key.
func SignDocument(signingKey *eddsa.PrivateKey, base *pki.Document) (string, error) {
d := new(document)
d.Document = *base
d.Version = documentVersion
// Serialize the document.
payload, err := json.Marshal(d)
if err != nil {
return "", err
}
// Sign the descriptor.
k := jose.SigningKey{
Algorithm: jose.EdDSA,
Key: *signingKey.InternalPtr(),
}
signer, err := jose.NewSigner(k, nil)
if err != nil {
return "", err
}
signed, err := signer.Sign(payload)
if err != nil {
return "", err
}
// Serialize the key, descriptor and signature.
return signed.CompactSerialize()
}
// VerifyAndParseDocument verifies the signautre and deserializes the document.
func VerifyAndParseDocument(b []byte, publicKey *eddsa.PublicKey, epoch uint64) (*pki.Document, error) {
signed, err := jose.ParseSigned(string(b))
if err != nil {
return nil, err
}
// Sanity check the signing algorithm and number of signatures, and
// validate the signature with the provided public key.
if len(signed.Signatures) != 1 {
return nil, fmt.Errorf("nonvoting: Expected 1 signature, got: %v", len(signed.Signatures))
}
alg := signed.Signatures[0].Header.Algorithm
if alg != "EdDSA" {
return nil, fmt.Errorf("nonvoting: Unsupported signature algorithm: '%v'", alg)
}
payload, err := signed.Verify(*publicKey.InternalPtr())
if err != nil {
return nil, err
}
// Parse the payload.
d := new(document)
if err = json.Unmarshal(payload, d); err != nil {
return nil, err
}
// Ensure the document is well formed.
if d.Version != documentVersion {
return nil, fmt.Errorf("nonvoting: Invalid Document Version: '%v'", d.Version)
}
if err = IsDocumentWellFormed(&d.Document, epoch); err != nil {
return nil, err
}
// Fixup the Layer field in all the Topology MixDescriptors.
for layer, nodes := range d.Topology {
for _, desc := range nodes {
desc.Layer = uint8(layer)
}
}
return &d.Document, nil
}
// IsDocumentWellFormed validates the document and returns a descriptive error
// iff there are any problems that invalidates the document.
func IsDocumentWellFormed(d *pki.Document, epoch uint64) error {
if d.Epoch != epoch {
return fmt.Errorf("nonvoting: Invalid Document Epoch: '%v'", d.Epoch)
}
pks := make(map[[eddsa.PublicKeySize]byte]bool)
if len(d.Topology) == 0 {
return fmt.Errorf("nonvoting: Document contains no Topology")
}
for layer, nodes := range d.Topology {
if len(nodes) == 0 {
return fmt.Errorf("nonvoting: Document Topology layer %d contains no nodes", layer)
}
for _, desc := range nodes {
if err := IsDescriptorWellFormed(desc, epoch); err != nil {
return err
}
pk := desc.IdentityKey.ByteArray()
if _, ok := pks[pk]; ok {
return fmt.Errorf("nonvoting: Document contains multiple entries for %v", desc.IdentityKey)
}
pks[pk] = true
}
}
if len(d.Providers) == 0 {
return fmt.Errorf("nonvoting: Document contains no Providers")
}
for _, desc := range d.Providers {
if err := IsDescriptorWellFormed(desc, epoch); err != nil {
return err
}
if desc.Layer != pki.LayerProvider {
return fmt.Errorf("nonvoting: Document lists %v as a Provider with layer %v", desc.IdentityKey, desc.Layer)
}
pk := desc.IdentityKey.ByteArray()
if _, ok := pks[pk]; ok {
return fmt.Errorf("nonvoting: Document contains multiple entries for %v", desc.IdentityKey)
}
pks[pk] = true
}
return nil
}
// document_test.go - Document s11n tests.
// Copyright (C) 2017 Yawning Angel
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package s11n
import (
"crypto/rand"
"fmt"
"testing"
"github.com/katzenpost/core/crypto/ecdh"
"github.com/katzenpost/core/crypto/eddsa"
"github.com/katzenpost/core/pki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func genDescriptor(require *require.Assertions, idx int, layer int) *pki.MixDescriptor {
d := new(pki.MixDescriptor)
d.Name = fmt.Sprintf("gen%d.example.net", idx)
d.Addresses = []string{fmt.Sprintf("192.0.2.%d:4242", idx)}
d.Layer = uint8(layer)
d.LoadWeight = 23
identityPriv, err := eddsa.NewKeypair(rand.Reader)
require.NoError(err, "eddsa.NewKeypair()")
d.IdentityKey = identityPriv.PublicKey()
linkPriv, err := ecdh.NewKeypair(rand.Reader)
require.NoError(err, "ecdh.NewKeypair()")
d.LinkKey = linkPriv.PublicKey()
d.MixKeys = make(map[uint64]*ecdh.PublicKey)
for e := debugTestEpoch; e < debugTestEpoch+3; e++ {
mPriv, err := ecdh