Skip to content

Commit

Permalink
feat: Support multiple keypairs per host (#154)
Browse files Browse the repository at this point in the history
* chore: update gitignore

* chore: compute coverage

* support for multiple pubkey-pairs per host
  • Loading branch information
soerenschneider authored Aug 18, 2022
1 parent 4b7f669 commit edca9a5
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
/dyndns-server
/dyndns-client
/.idea
/keypair.json
dist/
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SIGNATURE_KEYFILE = ~/.signify/github.sec
DOCKER_PREFIX = ghcr.io/soerenschneider

tests:
go test ./... -tags client,server
go test ./... -tags client,server -cover

clean:
rm -rf ./$(BUILD_DIR)
Expand Down
33 changes: 19 additions & 14 deletions conf/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
)

type ServerConf struct {
KnownHosts map[string]string `json:"known_hosts"`
HostedZoneId string `json:"hosted_zone_id"`
MetricsListener string `json:"metrics_listen",omitempty`
KnownHosts map[string][]string `json:"known_hosts"`
HostedZoneId string `json:"hosted_zone_id"`
MetricsListener string `json:"metrics_listen",omitempty`
MqttConfig
VaultConfig
}
Expand Down Expand Up @@ -57,22 +57,27 @@ func (conf *ServerConf) Validate() error {
return conf.MqttConfig.Validate()
}

func (conf *ServerConf) DecodePublicKeys() map[string]verification.VerificationKey {
var ret = map[string]verification.VerificationKey{}
func (conf *ServerConf) DecodePublicKeys() map[string][]verification.VerificationKey {
var ret = map[string][]verification.VerificationKey{}

for key, val := range conf.KnownHosts {
if len(val) == 0 {
for host, configuredPubkeys := range conf.KnownHosts {
if len(configuredPubkeys) == 0 {
metrics.PublicKeyErrors.Inc()
log.Info().Msgf("Empty publickey for host %s", key)
log.Info().Msgf("No publickey defined for host %s", host)
continue
}

publicKey, err := verification.PubkeyFromString(val)
if err == nil {
ret[key] = publicKey
} else {
metrics.PublicKeyErrors.Inc()
log.Info().Msgf("Could not initialize publicKey for host %s: %v", key, err)
for i, key := range configuredPubkeys {
publicKey, err := verification.PubkeyFromString(key)
if err != nil {
metrics.PublicKeyErrors.Inc()
log.Info().Msgf("Could not initialize %d. publicKey for host %s: %v", i, host, err)
} else {
if ret[host] == nil {
ret[host] = make([]verification.VerificationKey, len(configuredPubkeys))
}
ret[host] = append(ret[host], publicKey)
}
}
}

Expand Down
53 changes: 53 additions & 0 deletions conf/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package conf

import (
"reflect"
"testing"
)

func TestReadServerConfig(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want *ServerConf
wantErr bool
}{
{
name: "happy path",
args: args{"../contrib/server.json"},
want: &ServerConf{
KnownHosts: map[string][]string{
"host": []string{"key1", "key2"},
},
HostedZoneId: "hosted-zone-id-x",
MetricsListener: ":666",
MqttConfig: MqttConfig{
Brokers: []string{"broker-1", "broker-2"},
ClientId: "my-client-id",
},
VaultConfig: VaultConfig{
RoleName: "my-role-name",
VaultAddr: "https://vault:8200",
AppRoleId: "my-approle-id",
AppRoleSecret: "my-approle-secret",
VaultToken: "the-holy-token",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ReadServerConfig(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("ReadServerConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReadServerConfig() got = %v, want %v", got, tt.want)
}
})
}
}
20 changes: 20 additions & 0 deletions contrib/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"known_hosts": {
"host": [
"key1",
"key2"
]
},
"hosted_zone_id": "hosted-zone-id-x",
"metrics_listen": ":666",
"brokers": [
"broker-1",
"broker-2"
],
"client_id": "my-client-id",
"vault_role_name": "my-role-name",
"vault_addr": "https://vault:8200",
"vault_app_role_id": "my-approle-id",
"vault_app_role_secret": "my-approle-secret",
"vault_token": "the-holy-token"
}
33 changes: 22 additions & 11 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
const timestampGracePeriod = -24 * time.Hour

type Server struct {
knownHosts map[string]verification.VerificationKey
knownHosts map[string][]verification.VerificationKey
listener events.EventListener
requests chan common.Envelope
propagator dns.Propagator
Expand Down Expand Up @@ -58,23 +58,34 @@ func (server *Server) isCached(env common.Envelope) bool {
return entry.Equals(&env.PublicIp)
}

func (server *Server) verifyMessage(env common.Envelope) error {
hostPublicKeys, ok := server.knownHosts[env.PublicIp.Host]
if !ok {
metrics.PublicKeyMissing.WithLabelValues(env.PublicIp.Host).Inc()
return fmt.Errorf("message for unknown domain '%s' received", env.PublicIp.Host)
}

for _, hostPublicKey := range hostPublicKeys {
verified := hostPublicKey.Verify(env.Signature, env.PublicIp)
if verified {
return nil
}
}

metrics.SignatureVerificationsFailed.WithLabelValues(env.PublicIp.Host).Inc()
return fmt.Errorf("verifying signature FAILED for host '%s'", env.PublicIp.Host)
}

func (server *Server) handlePropagateRequest(env common.Envelope) error {
err := env.Validate()
if err != nil {
metrics.MessageValidationsFailed.WithLabelValues(env.PublicIp.Host, "invalid_fields").Inc()
return fmt.Errorf("invalid envelope received: %v", err)
}

hostPublicKey, ok := server.knownHosts[env.PublicIp.Host]
if !ok {
metrics.PublicKeyMissing.WithLabelValues(env.PublicIp.Host).Inc()
return fmt.Errorf("message for unknown domain '%s' received", env.PublicIp.Host)
}

verified := hostPublicKey.Verify(env.Signature, env.PublicIp)
if !verified {
metrics.SignatureVerificationsFailed.WithLabelValues(env.PublicIp.Host).Inc()
return fmt.Errorf("verifying signature FAILED for host '%s'", env.PublicIp.Host)
err = server.verifyMessage(env)
if err != nil {
return err
}

if env.PublicIp.Timestamp.Before(time.Now().Add(timestampGracePeriod)) {
Expand Down
140 changes: 140 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package server

import (
"github.com/soerenschneider/dyndns/internal/common"
"github.com/soerenschneider/dyndns/internal/events"
"github.com/soerenschneider/dyndns/internal/verification"
"github.com/soerenschneider/dyndns/server/dns"
"testing"
"time"
)

type SimpleVerifier struct {
verificationResult bool
}

func (s SimpleVerifier) Verify(signature string, ip common.ResolvedIp) bool {
return s.verificationResult
}

func TestServer_verifyMessage(t *testing.T) {
type fields struct {
knownHosts map[string][]verification.VerificationKey
listener events.EventListener
requests chan common.Envelope
propagator dns.Propagator
cache map[string]common.ResolvedIp
}
type args struct {
env common.Envelope
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "happy path",
fields: fields{
knownHosts: map[string][]verification.VerificationKey{
"my-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
&SimpleVerifier{true},
},
"other-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
&SimpleVerifier{true},
},
},
listener: nil,
requests: nil,
propagator: nil,
cache: map[string]common.ResolvedIp{},
},
args: args{
env: common.Envelope{
PublicIp: common.ResolvedIp{
IpV4: "8.8.4.4",
Host: "my-host.tld",
Timestamp: time.Now(),
},
Signature: "dummy-value",
},
},
wantErr: false,
},

{
name: "validation not successful",
fields: fields{
knownHosts: map[string][]verification.VerificationKey{
"my-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
},
"other-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
},
},
listener: nil,
requests: nil,
propagator: nil,
cache: map[string]common.ResolvedIp{},
},
args: args{
env: common.Envelope{
PublicIp: common.ResolvedIp{
IpV4: "8.8.4.4",
Host: "my-host.tld",
Timestamp: time.Now(),
},
Signature: "dummy-value",
},
},
wantErr: true,
},

{
name: "ho host",
fields: fields{
knownHosts: map[string][]verification.VerificationKey{
"my-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
},
"other-host.tld": []verification.VerificationKey{
&SimpleVerifier{false},
},
},
listener: nil,
requests: nil,
propagator: nil,
cache: map[string]common.ResolvedIp{},
},
args: args{
env: common.Envelope{
PublicIp: common.ResolvedIp{
IpV4: "8.8.4.4",
Host: "not-found.tld",
Timestamp: time.Now(),
},
Signature: "dummy-value",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := &Server{
knownHosts: tt.fields.knownHosts,
listener: tt.fields.listener,
requests: tt.fields.requests,
propagator: tt.fields.propagator,
cache: tt.fields.cache,
}
if err := server.verifyMessage(tt.args.env); (err != nil) != tt.wantErr {
t.Errorf("verifyMessage() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

0 comments on commit edca9a5

Please sign in to comment.