Skip to content

Commit

Permalink
Merge pull request #28 from fxnn/#27
Browse files Browse the repository at this point in the history
fix #27 workerId == publicKeyFingerprint
  • Loading branch information
fxnn authored Jan 2, 2018
2 parents aa3c62c + 7556547 commit d39fbdd
Show file tree
Hide file tree
Showing 16 changed files with 408 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Created by .ignore support plugin (hsz.mobi)
### Project-specific
*.boltdb*
*.pem

### JetBrains template
# For this project, we don't want to commit any IDE-specific files
Expand Down
55 changes: 52 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,19 @@ However, it is subject to change, and some points might be of quite low priority
might be an option.
* When we want the Worker to authentificate against the drop, it could
use client certificates while connecting with TLS.
* The Worker provides a public key to the Drop. The key pair could be
generated on first startup.
* The Worker provides a public key to the Drop.
The key pair could be generated on first startup.
* When the User wants to send a request to a Worker, he identifies the
Worker using a unique identifier. This must be configured in advance.
Worker using a unique identifier.
* The identifier must be pre-known to the user, the Drop should not
provide a list of known workers, which makes it harder to find
vulnerable endpoints.
* The identifier _equals the public key fingerprint_.
This makes identiy spoofing harder and allows the user to trust
that the Worker he connects to is the one he actually expects.
* User retrieves the Worker's public key from the Drop and encrypts
requests to the Worker therewith.
The key is authenticated against the Worker's id (see above).
* User includes its own public key in its requests to the Worker, which
in turn encrypts responses therewith.

Expand Down Expand Up @@ -248,3 +255,45 @@ As for a response, at least the following information need to be contained.
* If we want to apply routing over multiple hops, we might also need a unique
identifier of the originating drop, and possibly an ordered list of all drops
that are still to address.

### Public Key Fingerprinting

The task of creating a public key fingerprint, and therefore a Worker
Id, is defined by the following algorithm.
The algorithm can be configured using the parameters

* `HashFunction`. The cryptographic hash function to be used, i.e.
SHA-256.
* `Encoding`. The function for mapping the hash sum to its string
representation, i.e. Base32.
Base32 is chosen as default, because it performs good when it has to
be read by human users.
* `ChallengeLevel`. At a value of `0`, it is very cheap to calculate
the fingerprint, but it's also quite cheap to calculate a hash
collision.
* `FingerprintLength`. When equal to the number of bytes the
`HashFunction` produces, the fingerprint consists of the full hashsum,
providing maximum protection against hash collision search, but also
making the fingerprint difficult to use.

The algorithm consists of the following steps.

* Bring the public key into a binary representation,
* Add other information which must be protected against tampering:
* the `ChallengeLevel` itself,
* Add a `challengeSolution`, which is initially `0`,
* Calculate the hashsum of these data using `HashFunction`,
* Verify that the leftmost `ChallengeLevel` bits of the hashsum are
zero,
* If not so, increase the `challengeSolution` by one and recalculate
the hash,
* Encode the hashsum without the first `(ChallengeLevel+7)/8` bytes
using the `Encoding`,
* Group the encoded hashsum in pairs of two characters,
shorten it to the first `FingerprintLength` groups and
separate the pairs by colons `:`.

The params `ChallengeLevel` and `FingerprintLength` should be chosen in
a way that low `FingerprintLength` is compensated with a high
`CalculationCost` and vice versa.

46 changes: 37 additions & 9 deletions cmd/deadbox.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"crypto/rsa"
"fmt"
"io/ioutil"
"log"
Expand All @@ -12,6 +13,7 @@ import (

"github.com/boltdb/bolt"
"github.com/fxnn/deadbox/config"
"github.com/fxnn/deadbox/crypto"
"github.com/fxnn/deadbox/daemon"
"github.com/fxnn/deadbox/drop"
"github.com/fxnn/deadbox/worker"
Expand All @@ -20,6 +22,7 @@ import (
const (
filePermOnlyUserCanReadOrWrite = 0600
dbFileExtension = "boltdb"
privateKeyFileExtension = "pem"
)

func main() {
Expand All @@ -46,11 +49,11 @@ func startDaemons(cfg *config.Application) []daemon.Daemon {
var daemons = make([]daemon.Daemon, 0, len(cfg.Drops)+len(cfg.Workers))

for _, dp := range cfg.Drops {
daemons = append(daemons, serveDrop(dp, cfg))
daemons = append(daemons, serveDrop(&dp, cfg))
}

for _, wk := range cfg.Workers {
daemons = append(daemons, runWorker(wk, cfg))
daemons = append(daemons, runWorker(&wk, cfg))
}

return daemons
Expand All @@ -61,24 +64,36 @@ func waitForShutdownRequest() {
log.Println(<-ch)
}

func runWorker(wcfg config.Worker, acfg *config.Application) daemon.Daemon {
func runWorker(wcfg *config.Worker, acfg *config.Application) daemon.Daemon {
var k = readOrCreatePrivateKey(acfg, wcfg)
var id = generateWorkerId(k, wcfg.PublicKeyFingerprintLength, wcfg.PublicKeyFingerprintChallengeLevel)

var b = openDb(acfg, wcfg.Name)
var k = readOrCreatePrivateKeyFile(wcfg.PrivateKeyFile)
var d daemon.Daemon = worker.New(wcfg, b, k)
var d daemon.Daemon = worker.New(wcfg, id, b, k)
d.OnStop(b.Close)
d.Start()

return d
}

func readOrCreatePrivateKeyFile(fileName string) []byte {
func generateWorkerId(privateKey *rsa.PrivateKey, fingerprintLength uint, challengeLevel uint) string {
if fingerprint, err := crypto.FingerprintPublicKey(&privateKey.PublicKey, challengeLevel, fingerprintLength); err != nil {
panic(err)
} else {
return fingerprint
}
}

func readOrCreatePrivateKey(acfg *config.Application, wcfg *config.Worker) *rsa.PrivateKey {
fileName := privateKeyFileName(acfg.PrivateKeyPath, wcfg.Name)
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
if !os.IsNotExist(err) {
panic(fmt.Errorf("couldn't read file %s: %s", fileName, err))
}

bytes, err = worker.GeneratePrivateKeyBytes()
log.Printf("worker '%s' has no private key, generating one", wcfg.Name)
bytes, err = worker.GeneratePrivateKeyBytes(wcfg.PrivateKeySize)
if err != nil {
panic(fmt.Errorf("couldn't generate private key: %s", err))
}
Expand All @@ -89,10 +104,19 @@ func readOrCreatePrivateKeyFile(fileName string) []byte {
}
}

return bytes
if privateKey, err := crypto.UnmarshalPrivateKeyFromPEMBytes(bytes); err != nil {
panic(fmt.Errorf("couldn't read private key from file %s: %s", fileName, err))
} else {
if privateKey.N.BitLen() != wcfg.PrivateKeySize {
log.Printf("worker '%s' has configured key size '%d', but existing key has size '%d'",
wcfg.Name, wcfg.PrivateKeySize, privateKey.N.BitLen())
}

return privateKey
}
}

func serveDrop(dcfg config.Drop, acfg *config.Application) daemon.Daemon {
func serveDrop(dcfg *config.Drop, acfg *config.Application) daemon.Daemon {
var b = openDb(acfg, dcfg.Name)
var d daemon.Daemon = drop.New(dcfg, b)
d.OnStop(b.Close)
Expand Down Expand Up @@ -120,3 +144,7 @@ func openDb(cfg *config.Application, name string) *bolt.DB {
func dbFileName(cfg *config.Application, name string) string {
return filepath.Join(cfg.DbPath, name+"."+dbFileExtension)
}

func privateKeyFileName(path string, workerName string) string {
return filepath.Join(path, workerName+"."+privateKeyFileExtension)
}
6 changes: 6 additions & 0 deletions config/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ const DefaultPort = "6545"
// Application configuration, created once in application lifecycle.
type Application struct {
// DbPath points to a directory were the database files are stored in.
// The files are named after the Drop / Worker name and created if not existing.
DbPath string

// PrivateKeyPath refers to a path containing private key files for each Drop and Worker.
// The files will contain ASN.1 PKCS#1 DER encoded private RSA keys.
// They are named after the Drop / Worker name and created if not existing.
PrivateKeyPath string

// Workers configured in this application
Workers []Worker

Expand Down
10 changes: 7 additions & 3 deletions config/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ func Dummy() *Application {
DropUrl: dropUrl,
UpdateRegistrationIntervalInSeconds: DefaultUpdateRegistrationIntervalInSeconds,
RegistrationTimeoutInSeconds: DefaultRegistrationTimeoutInSeconds,
PublicKeyFingerprintChallengeLevel: DefaultPublicKeyFingerprintChallengeLevel,
PublicKeyFingerprintLength: DefaultPublicKeyFingerprintLength,
PrivateKeySize: DefaultPrivateKeySize,
}
d := Drop{
Name: "Default Drop",
ListenAddress: ":" + DefaultPort,
MaxWorkerTimeoutInSeconds: DefaultMaxWorkerTimeoutInSeconds,
}
app := &Application{
DbPath: "./",
Workers: []Worker{w},
Drops: []Drop{d},
DbPath: "./",
PrivateKeyPath: "./",
Workers: []Worker{w},
Drops: []Drop{d},
}
return app
}
28 changes: 20 additions & 8 deletions config/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import (
"net/url"
)

const DefaultUpdateRegistrationIntervalInSeconds = 10
const DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationIntervalInSeconds
const (
DefaultUpdateRegistrationIntervalInSeconds = 10
DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationIntervalInSeconds
DefaultPublicKeyFingerprintLength = 8
DefaultPublicKeyFingerprintChallengeLevel = 21
DefaultPrivateKeySize = 4096
)

// Worker configuration, created once per configured worker.
type Worker struct {
// Name identifies this worker uniquely
// Name identifies this worker for human users
Name string

// DropUrl identifies the drop instances this worker should
// connect to
// DropUrl identifies the drop instances this worker should connect to
DropUrl *url.URL

// UpdateRegistrationIntervalInSeconds specifies how often the worker sends a registration update to the drop.
Expand All @@ -23,7 +27,15 @@ type Worker struct {
// sending an update.
RegistrationTimeoutInSeconds int

// PrivateKeyFile refers to a file containing the ASN.1 PKCS#1 DER encoded private RSA key.
// If not existant, it will be created.
PrivateKeyFile string
// PublicKeyFingerprintLength influences the length of the public keys fingerprint. The greater the length, the
// more reliable the fingerprint is, but the harder it is to remember for human users.
PublicKeyFingerprintLength uint

// PublicKeyFingerprintChallengeLevel influences the time it takes to generate the fingerprint. The greater the
// level, the more secure the fingerprint is against pre-image attacks, but the longer it takes to generate and
// validate the fingerprint.
PublicKeyFingerprintChallengeLevel uint

// PrivateKeySize is the size of the private RSA key in bits, mostly 2048 oder 4096.
PrivateKeySize int
}
Loading

1 comment on commit d39fbdd

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on d39fbdd Jan 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puzzle 27-00e8c46c discovered in crypto/fingerprint.go and submitted as #29. Please, remember that the puzzle was not necessarily added in this particular commit. Maybe it was added earlier, but
we discovered it only now.

Please sign in to comment.