From 5da232a01baf6473e422b8184fee1052b648e49a Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Mon, 1 Jan 2018 17:03:47 +0100 Subject: [PATCH 01/14] #27: describe in README.md --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa1b02e..0e7b8d8 100644 --- a/README.md +++ b/README.md @@ -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. @@ -248,3 +255,35 @@ 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 + +Creating a public key fingerprint, and therefore a Worker Id, +incorporates the following steps. + +* Bring the public key into a binary representation, +* Add other information which must be protected against tampering: + * Encryption type, i.e. RSA+AES +* Add a modifier, which is an arbitrarily chosen number, +* Calculate the hashsum of these data using `HashFunction`, +* Verify that the first `CalculationCost` bits of the hashsum are zero, + * If not so, increase the modifier and recalculate the hash, +* Shorten the hashsum to `FingerprintLength` bytes, +* Represent the hashsum as uppercase hexadecimal, group them in pairs of + two and separate the pairs by colons `:`. + +This algorithm can be configured using the two parameters + +* `HashFunction`. The cryptographic hash function to be used, i.e. + SHA-256. +* `CalculationCost`. 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 params `CalculationCost` and `FingerprintLength` should be chosen in +a way that low `FingerprintLength` is compensated with a high +`CalculationCost` and vice versa. From 5cd5f0008d0ccff78bc623f0ea59532ce01f3472 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 14:10:45 +0100 Subject: [PATCH 02/14] #27: implement fingerprint algorithm (WIP) --- README.md | 13 +++-- crypto/fingerprint.go | 112 +++++++++++++++++++++++++++++++++++++ crypto/fingerprint_test.go | 73 ++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 crypto/fingerprint.go create mode 100644 crypto/fingerprint_test.go diff --git a/README.md b/README.md index 0e7b8d8..082924b 100644 --- a/README.md +++ b/README.md @@ -266,17 +266,18 @@ incorporates the following steps. * Encryption type, i.e. RSA+AES * Add a modifier, which is an arbitrarily chosen number, * Calculate the hashsum of these data using `HashFunction`, -* Verify that the first `CalculationCost` bits of the hashsum are zero, +* Verify that the last `ChallengeLevel` bytes of the hashsum are zero, * If not so, increase the modifier and recalculate the hash, -* Shorten the hashsum to `FingerprintLength` bytes, -* Represent the hashsum as uppercase hexadecimal, group them in pairs of - two and separate the pairs by colons `:`. +* Encode the hashsum in Base32, +* Group the encoded hashsum in pairs of two characters, + shorten it to the first `FingerprintLength` groups and + separate the pairs by colons `:`. This algorithm can be configured using the two parameters * `HashFunction`. The cryptographic hash function to be used, i.e. SHA-256. -* `CalculationCost`. At a value of `0`, it is very cheap to calculate +* `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 @@ -284,6 +285,6 @@ This algorithm can be configured using the two parameters providing maximum protection against hash collision search, but also making the fingerprint difficult to use. -The params `CalculationCost` and `FingerprintLength` should be chosen in +The params `ChallengeLevel` and `FingerprintLength` should be chosen in a way that low `FingerprintLength` is compensated with a high `CalculationCost` and vice versa. diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go new file mode 100644 index 0000000..3bcda22 --- /dev/null +++ b/crypto/fingerprint.go @@ -0,0 +1,112 @@ +package crypto + +import ( + "bytes" + "crypto" + "crypto/rsa" + "encoding/base32" + "encoding/binary" + "fmt" + "reflect" +) + +const ( + hashSeparator = "\000\000\000" + hashFunction = crypto.SHA256 + fingerprintGroupSeparator = ":" +) + +func FingerprintPublicKey( + key *rsa.PublicKey, + encryptionType string, + challengeLevel int, + fingerprintLengthInGroups int, +) (string, error) { + encoding := base32.StdEncoding.WithPadding(base32.NoPadding) + + keyBytes, err := marshalPublicKey(key) + if err != nil { + return "", fmt.Errorf("marshalling public key failed: %s", err) + } + + var hashSum []byte + for modifier := 0; !isPassChallenge(hashSum, challengeLevel); modifier++ { + hashSum, err = generateHashSum(modifier, keyBytes, encryptionType, hashFunction) + if err != nil { + return "", fmt.Errorf("calculating hash sum failed: %s", err) + } + } + + hashSumString := encoding.EncodeToString(hashSum[challengeLevel:]) + fingerprint := generateGroupedFingerprint(hashSumString, + fingerprintLengthInGroups, + fingerprintGroupSeparator) + + return fingerprint, nil +} + +func isPassChallenge(hashInput []byte, challengeLevel int) bool { + if hashInput == nil { + return false + } + + for _, b := range hashInput[:challengeLevel] { + if b != 0 { + return false + } + } + + return true +} + +func generateHashSum(modifier int, + keyBytes []byte, + encryptionType string, + hashFunction crypto.Hash, +) ([]byte, error) { + hashInput, err := generateHashInput(modifier, keyBytes, encryptionType) + if err != nil { + return nil, fmt.Errorf("generating hash input failed: %s", err) + } + + hash := hashFunction.New() + _, err = hash.Write(hashInput) + if err != nil { + hashType := reflect.TypeOf(hash) + return nil, fmt.Errorf("%s: %s", hashType, err) + } + + return hash.Sum([]byte{}), nil +} + +func generateHashInput(modifier int, keyBytes []byte, encryptionType string) ([]byte, error) { + var hashInputBuffer bytes.Buffer + + hashInputBuffer.Write(keyBytes) + hashInputBuffer.WriteString(hashSeparator) + + hashInputBuffer.WriteString(encryptionType) + hashInputBuffer.WriteString(hashSeparator) + + err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(modifier)) + if err != nil { + return nil, fmt.Errorf("writing modifier failed: %s", err) + } + + // @todo #27 add validity time + + return hashInputBuffer.Bytes(), nil +} + +func generateGroupedFingerprint(hashSumString string, numberOfGroups int, groupSeparator string) string { + var groupedFingerprintBuffer bytes.Buffer + for groupIdx := 0; groupIdx < numberOfGroups; groupIdx++ { + groupedFingerprintBuffer.WriteString(hashSumString[2*groupIdx : 2*groupIdx+2]) + + if groupIdx < numberOfGroups-1 { + groupedFingerprintBuffer.WriteString(groupSeparator) + } + } + fingerprint := groupedFingerprintBuffer.String() + return fingerprint +} diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go new file mode 100644 index 0000000..26bbf3f --- /dev/null +++ b/crypto/fingerprint_test.go @@ -0,0 +1,73 @@ +package crypto + +import ( + "crypto/rsa" + "math/big" + "testing" +) + +func TestSomething(t *testing.T) { + + key := &rsa.PublicKey{ + N: big.NewInt(42), + E: 13, + } + encryptionType := encryptionTypeAESPlusRSA + + assertFingerprint(t, "6W:HJ:QI:MS:AF:VL:HD:LB", 0, 8, key, encryptionType) + assertFingerprint(t, "UO:4H:KV:XF:SL:GP:OH:AN", 1, 8, key, encryptionType) + assertFingerprint(t, "VY:NC:DT:LR:I5:OJ:5B:SC", 2, 8, key, encryptionType) + assertFingerprint(t, "6S:GL:5D:TN:A4:CJ:GZ:P6", 3, 8, key, encryptionType) + +} + +func assertFingerprint( + t *testing.T, + expected string, + challengeLevel int, + fingerprintLengthInGroups int, + key *rsa.PublicKey, + encryptionType string, +) { + t.Helper() + + fingerprint, err := FingerprintPublicKey(key, encryptionType, challengeLevel, fingerprintLengthInGroups) + if err != nil { + t.Fatalf("generating fingerprint failed: %s", err) + } + + if fingerprint != expected { + t.Fatalf("unexpected fingerprint: %s", fingerprint) + } +} + +func TestIsPassChallenge(t *testing.T) { + + assertTrue(isPassChallenge([]byte{0}, 1), t) + assertTrue(isPassChallenge([]byte{0}, 0), t) + + assertFalse(isPassChallenge([]byte{1}, 1), t) + assertTrue(isPassChallenge([]byte{1}, 0), t) + + assertFalse(isPassChallenge([]byte{1, 0}, 2), t) + assertFalse(isPassChallenge([]byte{1, 0}, 1), t) + assertFalse(isPassChallenge([]byte{1, 1}, 1), t) + assertTrue(isPassChallenge([]byte{1, 0}, 0), t) + assertTrue(isPassChallenge([]byte{0, 1}, 1), t) + assertTrue(isPassChallenge([]byte{0, 0}, 2), t) + +} + +func assertTrue(actual bool, t *testing.T) { + t.Helper() + if actual != true { + t.Fatalf("expected true, but was false") + } +} + +func assertFalse(actual bool, t *testing.T) { + t.Helper() + if actual != false { + t.Fatalf("expected false, but was true") + } +} From 5f3342f53162482a755e67a12323c03982033647 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 19:29:24 +0100 Subject: [PATCH 03/14] #27: code improvements, README.md fix --- README.md | 4 ++-- crypto/fingerprint.go | 36 +++++++++++++++++++++++++----------- crypto/fingerprint_test.go | 36 ++++++++++++++++++------------------ 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 082924b..c177bc0 100644 --- a/README.md +++ b/README.md @@ -266,9 +266,9 @@ incorporates the following steps. * Encryption type, i.e. RSA+AES * Add a modifier, which is an arbitrarily chosen number, * Calculate the hashsum of these data using `HashFunction`, -* Verify that the last `ChallengeLevel` bytes of the hashsum are zero, +* Verify that the first `ChallengeLevel` bytes of the hashsum are zero, * If not so, increase the modifier and recalculate the hash, -* Encode the hashsum in Base32, +* Encode the hashsum without the first `ChallengeLevel` bytes in Base32, * Group the encoded hashsum in pairs of two characters, shorten it to the first `FingerprintLength` groups and separate the pairs by colons `:`. diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 3bcda22..8b44f26 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -16,28 +16,25 @@ const ( fingerprintGroupSeparator = ":" ) +var fingerprintEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) + func FingerprintPublicKey( key *rsa.PublicKey, encryptionType string, challengeLevel int, fingerprintLengthInGroups int, ) (string, error) { - encoding := base32.StdEncoding.WithPadding(base32.NoPadding) - keyBytes, err := marshalPublicKey(key) if err != nil { return "", fmt.Errorf("marshalling public key failed: %s", err) } - var hashSum []byte - for modifier := 0; !isPassChallenge(hashSum, challengeLevel); modifier++ { - hashSum, err = generateHashSum(modifier, keyBytes, encryptionType, hashFunction) - if err != nil { - return "", fmt.Errorf("calculating hash sum failed: %s", err) - } + hashSum, _, err := findChallengeSolution(keyBytes, encryptionType, hashFunction, challengeLevel) + if err != nil { + return "", fmt.Errorf("generating hashsum failed: %s", err) } - hashSumString := encoding.EncodeToString(hashSum[challengeLevel:]) + hashSumString := fingerprintEncoding.EncodeToString(hashSum[challengeLevel:]) fingerprint := generateGroupedFingerprint(hashSumString, fingerprintLengthInGroups, fingerprintGroupSeparator) @@ -45,6 +42,22 @@ func FingerprintPublicKey( return fingerprint, nil } +func findChallengeSolution( + keyBytes []byte, + encryptionType string, + hashFunction crypto.Hash, + challengeLevel int, +) (hashSum []byte, challengeSolution int, err error) { + for challengeSolution = 0; !isPassChallenge(hashSum, challengeLevel); challengeSolution++ { + hashSum, err = generateHashSum(challengeSolution, keyBytes, encryptionType, hashFunction) + if err != nil { + return + } + } + + return +} + func isPassChallenge(hashInput []byte, challengeLevel int) bool { if hashInput == nil { return false @@ -59,12 +72,13 @@ func isPassChallenge(hashInput []byte, challengeLevel int) bool { return true } -func generateHashSum(modifier int, +func generateHashSum( + challengeSolution int, keyBytes []byte, encryptionType string, hashFunction crypto.Hash, ) ([]byte, error) { - hashInput, err := generateHashInput(modifier, keyBytes, encryptionType) + hashInput, err := generateHashInput(challengeSolution, keyBytes, encryptionType) if err != nil { return nil, fmt.Errorf("generating hash input failed: %s", err) } diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 26bbf3f..2e07820 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestSomething(t *testing.T) { +func TestFingerprintPublicKey(t *testing.T) { key := &rsa.PublicKey{ N: big.NewInt(42), @@ -21,6 +21,23 @@ func TestSomething(t *testing.T) { } +func TestIsPassChallenge(t *testing.T) { + + assertTrue(isPassChallenge([]byte{0}, 1), t) + assertTrue(isPassChallenge([]byte{0}, 0), t) + + assertFalse(isPassChallenge([]byte{1}, 1), t) + assertTrue(isPassChallenge([]byte{1}, 0), t) + + assertFalse(isPassChallenge([]byte{1, 0}, 2), t) + assertFalse(isPassChallenge([]byte{1, 0}, 1), t) + assertFalse(isPassChallenge([]byte{1, 1}, 1), t) + assertTrue(isPassChallenge([]byte{1, 0}, 0), t) + assertTrue(isPassChallenge([]byte{0, 1}, 1), t) + assertTrue(isPassChallenge([]byte{0, 0}, 2), t) + +} + func assertFingerprint( t *testing.T, expected string, @@ -41,23 +58,6 @@ func assertFingerprint( } } -func TestIsPassChallenge(t *testing.T) { - - assertTrue(isPassChallenge([]byte{0}, 1), t) - assertTrue(isPassChallenge([]byte{0}, 0), t) - - assertFalse(isPassChallenge([]byte{1}, 1), t) - assertTrue(isPassChallenge([]byte{1}, 0), t) - - assertFalse(isPassChallenge([]byte{1, 0}, 2), t) - assertFalse(isPassChallenge([]byte{1, 0}, 1), t) - assertFalse(isPassChallenge([]byte{1, 1}, 1), t) - assertTrue(isPassChallenge([]byte{1, 0}, 0), t) - assertTrue(isPassChallenge([]byte{0, 1}, 1), t) - assertTrue(isPassChallenge([]byte{0, 0}, 2), t) - -} - func assertTrue(actual bool, t *testing.T) { t.Helper() if actual != true { From 29930c84ccca5918d67e2d0212e0dd912b722a20 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 21:09:07 +0100 Subject: [PATCH 04/14] #27 fingerprint: challengeLevel on bit level * allows for more fine-grained performance tuning --- README.md | 6 ++++-- crypto/fingerprint.go | 22 +++++++++++++++++----- crypto/fingerprint_test.go | 37 ++++++++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c177bc0..bdaeb69 100644 --- a/README.md +++ b/README.md @@ -266,9 +266,11 @@ incorporates the following steps. * Encryption type, i.e. RSA+AES * Add a modifier, which is an arbitrarily chosen number, * Calculate the hashsum of these data using `HashFunction`, -* Verify that the first `ChallengeLevel` bytes of the hashsum are zero, +* Verify that the leftmost `ChallengeLevel` bits of the hashsum are + zero, * If not so, increase the modifier and recalculate the hash, -* Encode the hashsum without the first `ChallengeLevel` bytes in Base32, +* Encode the hashsum without the first `(ChallengeLevel+7)/8` bytes in + Base32, * Group the encoded hashsum in pairs of two characters, shorten it to the first `FingerprintLength` groups and separate the pairs by colons `:`. diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 8b44f26..9708804 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -21,7 +21,7 @@ var fingerprintEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) func FingerprintPublicKey( key *rsa.PublicKey, encryptionType string, - challengeLevel int, + challengeLevel uint, fingerprintLengthInGroups int, ) (string, error) { keyBytes, err := marshalPublicKey(key) @@ -34,7 +34,8 @@ func FingerprintPublicKey( return "", fmt.Errorf("generating hashsum failed: %s", err) } - hashSumString := fingerprintEncoding.EncodeToString(hashSum[challengeLevel:]) + numberOfOmittedBytes := (challengeLevel + 7) / 8 + hashSumString := fingerprintEncoding.EncodeToString(hashSum[numberOfOmittedBytes:]) fingerprint := generateGroupedFingerprint(hashSumString, fingerprintLengthInGroups, fingerprintGroupSeparator) @@ -46,7 +47,7 @@ func findChallengeSolution( keyBytes []byte, encryptionType string, hashFunction crypto.Hash, - challengeLevel int, + challengeLevel uint, ) (hashSum []byte, challengeSolution int, err error) { for challengeSolution = 0; !isPassChallenge(hashSum, challengeLevel); challengeSolution++ { hashSum, err = generateHashSum(challengeSolution, keyBytes, encryptionType, hashFunction) @@ -58,17 +59,28 @@ func findChallengeSolution( return } -func isPassChallenge(hashInput []byte, challengeLevel int) bool { +func isPassChallenge(hashInput []byte, challengeLevel uint) bool { if hashInput == nil { return false } + if challengeLevel == 0 { + return true + } - for _, b := range hashInput[:challengeLevel] { + idxOfFirstNonZeroByte := challengeLevel / 8 // note, that '/' is always floor'd + for _, b := range hashInput[:idxOfFirstNonZeroByte] { if b != 0 { return false } } + if uint(len(hashInput)) > idxOfFirstNonZeroByte { + lastByteRequiredToContainZeroBit := hashInput[idxOfFirstNonZeroByte] + numberOfRequiredZeroBits := challengeLevel % 8 + shouldBeZero := lastByteRequiredToContainZeroBit >> (8 - numberOfRequiredZeroBits) + return shouldBeZero == 0 + } + return true } diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 2e07820..1c01a12 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -15,9 +15,10 @@ func TestFingerprintPublicKey(t *testing.T) { encryptionType := encryptionTypeAESPlusRSA assertFingerprint(t, "6W:HJ:QI:MS:AF:VL:HD:LB", 0, 8, key, encryptionType) - assertFingerprint(t, "UO:4H:KV:XF:SL:GP:OH:AN", 1, 8, key, encryptionType) - assertFingerprint(t, "VY:NC:DT:LR:I5:OJ:5B:SC", 2, 8, key, encryptionType) - assertFingerprint(t, "6S:GL:5D:TN:A4:CJ:GZ:P6", 3, 8, key, encryptionType) + assertFingerprint(t, "AX:MZ:N4:UJ:JZ:B7:EF:US", 4, 8, key, encryptionType) + assertFingerprint(t, "UO:4H:KV:XF:SL:GP:OH:AN", 8, 8, key, encryptionType) + assertFingerprint(t, "VY:NC:DT:LR:I5:OJ:5B:SC", 16, 8, key, encryptionType) + assertFingerprint(t, "67:R3:EW:3E:JI:FE:2V:FB", 21, 8, key, encryptionType) } @@ -26,22 +27,36 @@ func TestIsPassChallenge(t *testing.T) { assertTrue(isPassChallenge([]byte{0}, 1), t) assertTrue(isPassChallenge([]byte{0}, 0), t) - assertFalse(isPassChallenge([]byte{1}, 1), t) + assertFalse(isPassChallenge([]byte{1}, 8), t) + assertTrue(isPassChallenge([]byte{1}, 7), t) + assertTrue(isPassChallenge([]byte{1}, 1), t) assertTrue(isPassChallenge([]byte{1}, 0), t) - assertFalse(isPassChallenge([]byte{1, 0}, 2), t) - assertFalse(isPassChallenge([]byte{1, 0}, 1), t) - assertFalse(isPassChallenge([]byte{1, 1}, 1), t) - assertTrue(isPassChallenge([]byte{1, 0}, 0), t) - assertTrue(isPassChallenge([]byte{0, 1}, 1), t) - assertTrue(isPassChallenge([]byte{0, 0}, 2), t) + assertFalse(isPassChallenge([]byte{255}, 8), t) + assertFalse(isPassChallenge([]byte{255}, 1), t) + assertTrue(isPassChallenge([]byte{255}, 0), t) + + assertTrue(isPassChallenge([]byte{1, 0}, 7), t) + assertFalse(isPassChallenge([]byte{1, 0}, 8), t) + assertFalse(isPassChallenge([]byte{1, 0}, 9), t) + assertFalse(isPassChallenge([]byte{1, 1}, 8), t) + + assertTrue(isPassChallenge([]byte{0, 1}, 8), t) + assertTrue(isPassChallenge([]byte{0, 1}, 15), t) + assertFalse(isPassChallenge([]byte{0, 1}, 16), t) + + assertTrue(isPassChallenge([]byte{0, 255}, 8), t) + assertFalse(isPassChallenge([]byte{0, 255}, 9), t) + + assertTrue(isPassChallenge([]byte{0, 0, 255}, 16), t) + assertFalse(isPassChallenge([]byte{0, 0, 255}, 17), t) } func assertFingerprint( t *testing.T, expected string, - challengeLevel int, + challengeLevel uint, fingerprintLengthInGroups int, key *rsa.PublicKey, encryptionType string, From cfa3777d9bf4e2ed0ff019a426c602f4272a9fe7 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 21:24:35 +0100 Subject: [PATCH 05/14] #27 fingerprint: include the challengeLevel in the hashsum --- README.md | 44 ++++++++++++++++++++++---------------- crypto/fingerprint.go | 22 +++++++++++++------ crypto/fingerprint_test.go | 10 ++++----- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index bdaeb69..3720968 100644 --- a/README.md +++ b/README.md @@ -258,27 +258,16 @@ As for a response, at least the following information need to be contained. ### Public Key Fingerprinting -Creating a public key fingerprint, and therefore a Worker Id, -incorporates the following steps. - -* Bring the public key into a binary representation, -* Add other information which must be protected against tampering: - * Encryption type, i.e. RSA+AES -* Add a modifier, which is an arbitrarily chosen number, -* Calculate the hashsum of these data using `HashFunction`, -* Verify that the leftmost `ChallengeLevel` bits of the hashsum are - zero, - * If not so, increase the modifier and recalculate the hash, -* Encode the hashsum without the first `(ChallengeLevel+7)/8` bytes in - Base32, -* Group the encoded hashsum in pairs of two characters, - shorten it to the first `FingerprintLength` groups and - separate the pairs by colons `:`. - -This algorithm can be configured using the two parameters +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. @@ -287,6 +276,25 @@ This algorithm can be configured using the two parameters 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: + * Encryption type, i.e. RSA+AES + * 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. + diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 9708804..48b38e1 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -50,7 +50,7 @@ func findChallengeSolution( challengeLevel uint, ) (hashSum []byte, challengeSolution int, err error) { for challengeSolution = 0; !isPassChallenge(hashSum, challengeLevel); challengeSolution++ { - hashSum, err = generateHashSum(challengeSolution, keyBytes, encryptionType, hashFunction) + hashSum, err = generateHashSum(challengeLevel, challengeSolution, keyBytes, encryptionType, hashFunction) if err != nil { return } @@ -85,12 +85,13 @@ func isPassChallenge(hashInput []byte, challengeLevel uint) bool { } func generateHashSum( + challengeLevel uint, challengeSolution int, keyBytes []byte, encryptionType string, hashFunction crypto.Hash, ) ([]byte, error) { - hashInput, err := generateHashInput(challengeSolution, keyBytes, encryptionType) + hashInput, err := generateHashInput(challengeLevel, challengeSolution, keyBytes, encryptionType) if err != nil { return nil, fmt.Errorf("generating hash input failed: %s", err) } @@ -105,7 +106,12 @@ func generateHashSum( return hash.Sum([]byte{}), nil } -func generateHashInput(modifier int, keyBytes []byte, encryptionType string) ([]byte, error) { +func generateHashInput( + challengeLevel uint, + challengeSolution int, + keyBytes []byte, + encryptionType string, +) ([]byte, error) { var hashInputBuffer bytes.Buffer hashInputBuffer.Write(keyBytes) @@ -114,9 +120,13 @@ func generateHashInput(modifier int, keyBytes []byte, encryptionType string) ([] hashInputBuffer.WriteString(encryptionType) hashInputBuffer.WriteString(hashSeparator) - err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(modifier)) - if err != nil { - return nil, fmt.Errorf("writing modifier failed: %s", err) + if err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(challengeLevel)); err != nil { + return nil, fmt.Errorf("writing challengeLevel failed: %s", err) + } + hashInputBuffer.WriteString(hashSeparator) + + if err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(challengeSolution)); err != nil { + return nil, fmt.Errorf("writing challengeSolution failed: %s", err) } // @todo #27 add validity time diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 1c01a12..afe207a 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -14,11 +14,11 @@ func TestFingerprintPublicKey(t *testing.T) { } encryptionType := encryptionTypeAESPlusRSA - assertFingerprint(t, "6W:HJ:QI:MS:AF:VL:HD:LB", 0, 8, key, encryptionType) - assertFingerprint(t, "AX:MZ:N4:UJ:JZ:B7:EF:US", 4, 8, key, encryptionType) - assertFingerprint(t, "UO:4H:KV:XF:SL:GP:OH:AN", 8, 8, key, encryptionType) - assertFingerprint(t, "VY:NC:DT:LR:I5:OJ:5B:SC", 16, 8, key, encryptionType) - assertFingerprint(t, "67:R3:EW:3E:JI:FE:2V:FB", 21, 8, key, encryptionType) + assertFingerprint(t, "QX:HG:W6:YO:R2:AC:N4:R3", 0, 8, key, encryptionType) + assertFingerprint(t, "BC:5X:HY:BO:VU:IB:IW:QC", 4, 8, key, encryptionType) + assertFingerprint(t, "GA:PB:TP:LR:WY:YU:TG:C7", 8, 8, key, encryptionType) + assertFingerprint(t, "4B:NN:B2:63:ZY:IK:UF:XG", 16, 8, key, encryptionType) + assertFingerprint(t, "YL:IK:5C:KX:6B:LO:Z7:TN", 21, 8, key, encryptionType) } From f2981a4e6ccb66f726856edfee10376880ff7fc1 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 22:17:06 +0100 Subject: [PATCH 06/14] #27 fingerprint: performance improvements --- crypto/fingerprint.go | 48 +++++++++++++++++------------------- crypto/fingerprint_test.go | 50 ++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 48b38e1..ca7e85c 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -29,9 +29,14 @@ func FingerprintPublicKey( return "", fmt.Errorf("marshalling public key failed: %s", err) } - hashSum, _, err := findChallengeSolution(keyBytes, encryptionType, hashFunction, challengeLevel) + hashInputPrefix, err := generateHashInput(challengeLevel, keyBytes, encryptionType) if err != nil { - return "", fmt.Errorf("generating hashsum failed: %s", err) + return "", fmt.Errorf("generating hash input failed: %s", err) + } + + hashSum, _, err := findChallengeSolution(hashInputPrefix, hashFunction, challengeLevel) + if err != nil { + return "", fmt.Errorf("generating hash sum failed: %s", err) } numberOfOmittedBytes := (challengeLevel + 7) / 8 @@ -44,13 +49,18 @@ func FingerprintPublicKey( } func findChallengeSolution( - keyBytes []byte, - encryptionType string, + hashInputPrefix []byte, hashFunction crypto.Hash, challengeLevel uint, ) (hashSum []byte, challengeSolution int, err error) { - for challengeSolution = 0; !isPassChallenge(hashSum, challengeLevel); challengeSolution++ { - hashSum, err = generateHashSum(challengeLevel, challengeSolution, keyBytes, encryptionType, hashFunction) + var hashInputSuffix = make([]byte, 8) // HINT: will be filled each round + var zeroHashSum = make([]byte, hashFunction.Size()) // HINT: used for comparison later + + for challengeSolution = 0; !isPassChallenge(zeroHashSum, hashSum, challengeLevel); challengeSolution++ { + binary.BigEndian.PutUint64(hashInputSuffix, uint64(challengeSolution)) + hashInput := append(hashInputPrefix, hashInputSuffix...) + + hashSum, err = generateHashSum(hashInput, hashFunction) if err != nil { return } @@ -59,7 +69,7 @@ func findChallengeSolution( return } -func isPassChallenge(hashInput []byte, challengeLevel uint) bool { +func isPassChallenge(zeroHashSum []byte, hashInput []byte, challengeLevel uint) bool { if hashInput == nil { return false } @@ -68,10 +78,8 @@ func isPassChallenge(hashInput []byte, challengeLevel uint) bool { } idxOfFirstNonZeroByte := challengeLevel / 8 // note, that '/' is always floor'd - for _, b := range hashInput[:idxOfFirstNonZeroByte] { - if b != 0 { - return false - } + if !bytes.Equal(zeroHashSum[:idxOfFirstNonZeroByte], hashInput[:idxOfFirstNonZeroByte]) { + return false } if uint(len(hashInput)) > idxOfFirstNonZeroByte { @@ -85,19 +93,11 @@ func isPassChallenge(hashInput []byte, challengeLevel uint) bool { } func generateHashSum( - challengeLevel uint, - challengeSolution int, - keyBytes []byte, - encryptionType string, + hashInput []byte, hashFunction crypto.Hash, ) ([]byte, error) { - hashInput, err := generateHashInput(challengeLevel, challengeSolution, keyBytes, encryptionType) - if err != nil { - return nil, fmt.Errorf("generating hash input failed: %s", err) - } - hash := hashFunction.New() - _, err = hash.Write(hashInput) + _, err := hash.Write(hashInput) if err != nil { hashType := reflect.TypeOf(hash) return nil, fmt.Errorf("%s: %s", hashType, err) @@ -108,7 +108,6 @@ func generateHashSum( func generateHashInput( challengeLevel uint, - challengeSolution int, keyBytes []byte, encryptionType string, ) ([]byte, error) { @@ -125,12 +124,9 @@ func generateHashInput( } hashInputBuffer.WriteString(hashSeparator) - if err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(challengeSolution)); err != nil { - return nil, fmt.Errorf("writing challengeSolution failed: %s", err) - } - // @todo #27 add validity time + // NOTE: challengeSolution is appended here in each round return hashInputBuffer.Bytes(), nil } diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index afe207a..9549206 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -24,32 +24,32 @@ func TestFingerprintPublicKey(t *testing.T) { func TestIsPassChallenge(t *testing.T) { - assertTrue(isPassChallenge([]byte{0}, 1), t) - assertTrue(isPassChallenge([]byte{0}, 0), t) + assertPassChallenge([]byte{0}, 1, t) + assertPassChallenge([]byte{0}, 0, t) - assertFalse(isPassChallenge([]byte{1}, 8), t) - assertTrue(isPassChallenge([]byte{1}, 7), t) - assertTrue(isPassChallenge([]byte{1}, 1), t) - assertTrue(isPassChallenge([]byte{1}, 0), t) + assertDoesntPassChallenge([]byte{1}, 8, t) + assertPassChallenge([]byte{1}, 7, t) + assertPassChallenge([]byte{1}, 1, t) + assertPassChallenge([]byte{1}, 0, t) - assertFalse(isPassChallenge([]byte{255}, 8), t) - assertFalse(isPassChallenge([]byte{255}, 1), t) - assertTrue(isPassChallenge([]byte{255}, 0), t) + assertDoesntPassChallenge([]byte{255}, 8, t) + assertDoesntPassChallenge([]byte{255}, 1, t) + assertPassChallenge([]byte{255}, 0, t) - assertTrue(isPassChallenge([]byte{1, 0}, 7), t) - assertFalse(isPassChallenge([]byte{1, 0}, 8), t) - assertFalse(isPassChallenge([]byte{1, 0}, 9), t) - assertFalse(isPassChallenge([]byte{1, 1}, 8), t) + assertPassChallenge([]byte{1, 0}, 7, t) + assertDoesntPassChallenge([]byte{1, 0}, 8, t) + assertDoesntPassChallenge([]byte{1, 0}, 9, t) + assertDoesntPassChallenge([]byte{1, 1}, 8, t) - assertTrue(isPassChallenge([]byte{0, 1}, 8), t) - assertTrue(isPassChallenge([]byte{0, 1}, 15), t) - assertFalse(isPassChallenge([]byte{0, 1}, 16), t) + assertPassChallenge([]byte{0, 1}, 8, t) + assertPassChallenge([]byte{0, 1}, 15, t) + assertDoesntPassChallenge([]byte{0, 1}, 16, t) - assertTrue(isPassChallenge([]byte{0, 255}, 8), t) - assertFalse(isPassChallenge([]byte{0, 255}, 9), t) + assertPassChallenge([]byte{0, 255}, 8, t) + assertDoesntPassChallenge([]byte{0, 255}, 9, t) - assertTrue(isPassChallenge([]byte{0, 0, 255}, 16), t) - assertFalse(isPassChallenge([]byte{0, 0, 255}, 17), t) + assertPassChallenge([]byte{0, 0, 255}, 16, t) + assertDoesntPassChallenge([]byte{0, 0, 255}, 17, t) } @@ -73,16 +73,18 @@ func assertFingerprint( } } -func assertTrue(actual bool, t *testing.T) { +func assertPassChallenge(hashInput []byte, challengeLevel uint, t *testing.T) { t.Helper() - if actual != true { + zeroHashSum := make([]byte, len(hashInput)) + if !isPassChallenge(zeroHashSum, hashInput, challengeLevel) { t.Fatalf("expected true, but was false") } } -func assertFalse(actual bool, t *testing.T) { +func assertDoesntPassChallenge(hashInput []byte, challengeLevel uint, t *testing.T) { t.Helper() - if actual != false { + zeroHashSum := make([]byte, len(hashInput)) + if isPassChallenge(zeroHashSum, hashInput, challengeLevel) { t.Fatalf("expected false, but was true") } } From 47c5a4d89e26e787c276979ebc6d8e5282fc3cb3 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 22:22:54 +0100 Subject: [PATCH 07/14] #27 fingerprint: naming, unit testing --- crypto/fingerprint.go | 6 +++--- crypto/fingerprint_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index ca7e85c..949219c 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -29,7 +29,7 @@ func FingerprintPublicKey( return "", fmt.Errorf("marshalling public key failed: %s", err) } - hashInputPrefix, err := generateHashInput(challengeLevel, keyBytes, encryptionType) + hashInputPrefix, err := generateHashInputPrefix(challengeLevel, keyBytes, encryptionType) if err != nil { return "", fmt.Errorf("generating hash input failed: %s", err) } @@ -106,7 +106,7 @@ func generateHashSum( return hash.Sum([]byte{}), nil } -func generateHashInput( +func generateHashInputPrefix( challengeLevel uint, keyBytes []byte, encryptionType string, @@ -132,7 +132,7 @@ func generateHashInput( func generateGroupedFingerprint(hashSumString string, numberOfGroups int, groupSeparator string) string { var groupedFingerprintBuffer bytes.Buffer - for groupIdx := 0; groupIdx < numberOfGroups; groupIdx++ { + for groupIdx := 0; groupIdx < numberOfGroups && 2*groupIdx+2 <= len(hashSumString); groupIdx++ { groupedFingerprintBuffer.WriteString(hashSumString[2*groupIdx : 2*groupIdx+2]) if groupIdx < numberOfGroups-1 { diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 9549206..4125006 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -53,6 +53,22 @@ func TestIsPassChallenge(t *testing.T) { } +func TestGenerateGroupedFingerprint(t *testing.T) { + + assertGroupedFingerprint("", "", 0, t) + assertGroupedFingerprint("", "", 1, t) + + assertGroupedFingerprint("", "ab", 0, t) + assertGroupedFingerprint("ab", "ab", 1, t) + assertGroupedFingerprint("ab:", "ab", 2, t) + + assertGroupedFingerprint("", "abcd", 0, t) + assertGroupedFingerprint("ab", "abcd", 1, t) + assertGroupedFingerprint("ab:cd", "abcd", 2, t) + assertGroupedFingerprint("ab:cd:", "abcd", 3, t) + +} + func assertFingerprint( t *testing.T, expected string, @@ -88,3 +104,11 @@ func assertDoesntPassChallenge(hashInput []byte, challengeLevel uint, t *testing t.Fatalf("expected false, but was true") } } + +func assertGroupedFingerprint(expected string, input string, numberOfGroups int, t *testing.T) { + t.Helper() + actual := generateGroupedFingerprint(input, numberOfGroups, ":") + if actual != expected { + t.Fatalf("expected '%s', but was '%s'", expected, actual) + } +} From 4d9a7986aeba4c843d4e4441e8e0c4a9854fb610 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 22:42:24 +0100 Subject: [PATCH 08/14] #27 use fingerprint as workerId --- README.md | 1 - cmd/deadbox.go | 16 ++++++++++++++-- config/worker.go | 14 +++++++++++--- crypto/fingerprint.go | 7 +------ crypto/fingerprint_test.go | 14 ++++++-------- it/utils_test.go | 6 +++++- worker/facade.go | 7 +++---- worker/registrations.go | 17 ----------------- 8 files changed, 40 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 3720968..5c950f4 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,6 @@ The algorithm consists of the following steps. * Bring the public key into a binary representation, * Add other information which must be protected against tampering: - * Encryption type, i.e. RSA+AES * the `ChallengeLevel` itself, * Add a `challengeSolution`, which is initially `0`, * Calculate the hashsum of these data using `HashFunction`, diff --git a/cmd/deadbox.go b/cmd/deadbox.go index 6bdcb6c..7829f79 100644 --- a/cmd/deadbox.go +++ b/cmd/deadbox.go @@ -12,6 +12,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" @@ -62,14 +63,25 @@ func waitForShutdownRequest() { } func runWorker(wcfg config.Worker, acfg *config.Application) daemon.Daemon { - var b = openDb(acfg, wcfg.Name) var k = readOrCreatePrivateKeyFile(wcfg.PrivateKeyFile) - var d daemon.Daemon = worker.New(wcfg, b, k) + var id = generateWorkerId(k, wcfg.PublicKeyFingerprintLength, wcfg.PublicKeyFingerprintChallengeLevel) + + var b = openDb(acfg, id) + var d daemon.Daemon = worker.New(wcfg, id, b, k) d.OnStop(b.Close) d.Start() return d } +func generateWorkerId(privateKeyBytes []byte, fingerprintLength int, challengeLevel uint) string { + if privateKey, err := crypto.UnmarshalPrivateKeyFromPEMBytes(privateKeyBytes); err != nil { + panic(fmt.Errorf("couldn't read private key file: %s", err)) + } else if fingerprint, err := crypto.FingerprintPublicKey(&privateKey.PublicKey, challengeLevel, fingerprintLength); err != nil { + panic(err) + } else { + return fingerprint + } +} func readOrCreatePrivateKeyFile(fileName string) []byte { bytes, err := ioutil.ReadFile(fileName) diff --git a/config/worker.go b/config/worker.go index bcb66b5..a10eb73 100644 --- a/config/worker.go +++ b/config/worker.go @@ -9,11 +9,10 @@ const DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationInterv // 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. @@ -26,4 +25,13 @@ type Worker struct { // 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 int + + // 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 } diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 949219c..632cc80 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -20,7 +20,6 @@ var fingerprintEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) func FingerprintPublicKey( key *rsa.PublicKey, - encryptionType string, challengeLevel uint, fingerprintLengthInGroups int, ) (string, error) { @@ -29,7 +28,7 @@ func FingerprintPublicKey( return "", fmt.Errorf("marshalling public key failed: %s", err) } - hashInputPrefix, err := generateHashInputPrefix(challengeLevel, keyBytes, encryptionType) + hashInputPrefix, err := generateHashInputPrefix(challengeLevel, keyBytes) if err != nil { return "", fmt.Errorf("generating hash input failed: %s", err) } @@ -109,16 +108,12 @@ func generateHashSum( func generateHashInputPrefix( challengeLevel uint, keyBytes []byte, - encryptionType string, ) ([]byte, error) { var hashInputBuffer bytes.Buffer hashInputBuffer.Write(keyBytes) hashInputBuffer.WriteString(hashSeparator) - hashInputBuffer.WriteString(encryptionType) - hashInputBuffer.WriteString(hashSeparator) - if err := binary.Write(&hashInputBuffer, binary.BigEndian, int64(challengeLevel)); err != nil { return nil, fmt.Errorf("writing challengeLevel failed: %s", err) } diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 4125006..8396225 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -12,13 +12,12 @@ func TestFingerprintPublicKey(t *testing.T) { N: big.NewInt(42), E: 13, } - encryptionType := encryptionTypeAESPlusRSA - assertFingerprint(t, "QX:HG:W6:YO:R2:AC:N4:R3", 0, 8, key, encryptionType) - assertFingerprint(t, "BC:5X:HY:BO:VU:IB:IW:QC", 4, 8, key, encryptionType) - assertFingerprint(t, "GA:PB:TP:LR:WY:YU:TG:C7", 8, 8, key, encryptionType) - assertFingerprint(t, "4B:NN:B2:63:ZY:IK:UF:XG", 16, 8, key, encryptionType) - assertFingerprint(t, "YL:IK:5C:KX:6B:LO:Z7:TN", 21, 8, key, encryptionType) + assertFingerprint(t, "2E:J7:2J:77:GD:VL:P4:RE", 0, 8, key) + assertFingerprint(t, "V4:Y4:QG:23:QU:OE:W2:QJ", 4, 8, key) + assertFingerprint(t, "OR:NI:KT:EI:EL:LR:3U:NE", 8, 8, key) + assertFingerprint(t, "OT:KR:ML:FH:BR:2N:W3:NO", 16, 8, key) + assertFingerprint(t, "G6:UL:U7:7L:ZD:PH:3Z:SY", 21, 8, key) } @@ -75,11 +74,10 @@ func assertFingerprint( challengeLevel uint, fingerprintLengthInGroups int, key *rsa.PublicKey, - encryptionType string, ) { t.Helper() - fingerprint, err := FingerprintPublicKey(key, encryptionType, challengeLevel, fingerprintLengthInGroups) + fingerprint, err := FingerprintPublicKey(key, challengeLevel, fingerprintLengthInGroups) if err != nil { t.Fatalf("generating fingerprint failed: %s", err) } diff --git a/it/utils_test.go b/it/utils_test.go index 7d20ef0..6576e55 100644 --- a/it/utils_test.go +++ b/it/utils_test.go @@ -140,8 +140,12 @@ func runWorkerDaemon(t *testing.T) (worker.Daemonized, []byte) { if err != nil { t.Fatalf("couldn't generate public key bytes: %s", err) } + fingerprint, err := crypto.FingerprintPublicKey(&privateKey.PublicKey, 10, 4) + if err != nil { + t.Fatalf("couldn't fingerprint public key: %s", err) + } - workerDaemon := worker.New(cfg, db, privateKeyBytes) + workerDaemon := worker.New(cfg, fingerprint, db, privateKeyBytes) workerDaemon.OnStop(func() error { if err := db.Close(); err != nil { return err diff --git a/worker/facade.go b/worker/facade.go index cf0a3d1..879105b 100644 --- a/worker/facade.go +++ b/worker/facade.go @@ -38,22 +38,21 @@ type facade struct { privateKeyBytes []byte } -func New(c config.Worker, db *bolt.DB, privateKeyBytes []byte) Daemonized { +func New(c config.Worker, id string, db *bolt.DB, privateKeyBytes []byte) Daemonized { drop := rest.NewClient(c.DropUrl) - id := generateWorkerId() f := &facade{ db: db, dropUrl: c.DropUrl, privateKeyBytes: privateKeyBytes, updateRegistrationInterval: time.Duration(c.UpdateRegistrationIntervalInSeconds) * time.Second, registrations: registrations{ - id: id, + id: model.WorkerId(id), drop: drop, name: c.Name, registrationTimeoutDuration: time.Duration(c.RegistrationTimeoutInSeconds) * time.Second, }, requests: requests{ - id: id, + id: model.WorkerId(id), drop: drop, }, requestProcessors: &requestProcessors{ diff --git a/worker/registrations.go b/worker/registrations.go index b41340c..6913a4c 100644 --- a/worker/registrations.go +++ b/worker/registrations.go @@ -3,16 +3,11 @@ package worker import ( "time" - "crypto/rand" - "encoding/base64" - "fmt" "github.com/fxnn/deadbox/model" ) -const idBytesEntropy = 64 - type registrations struct { id model.WorkerId name string @@ -41,15 +36,3 @@ func (r *registrations) Name() string { func (r *registrations) QuotedNameAndId() string { return fmt.Sprintf("'%s' (%s)", r.name, r.id) } - -func generateWorkerId() model.WorkerId { - - rawBytes := make([]byte, idBytesEntropy) - if _, err := rand.Read(rawBytes); err != nil { - panic(fmt.Sprint("couldn't generate random bytes for worker id:", err)) - } - - encoded := base64.RawURLEncoding.EncodeToString(rawBytes) - return model.WorkerId(encoded) - -} From 643268e0f9507bef6c9db174d10361948c9c7b2d Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 22:46:51 +0100 Subject: [PATCH 09/14] #27 change fingerprintLength to unit --- cmd/deadbox.go | 2 +- config/worker.go | 2 +- crypto/fingerprint.go | 6 +++--- crypto/fingerprint_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/deadbox.go b/cmd/deadbox.go index 7829f79..cac2320 100644 --- a/cmd/deadbox.go +++ b/cmd/deadbox.go @@ -73,7 +73,7 @@ func runWorker(wcfg config.Worker, acfg *config.Application) daemon.Daemon { return d } -func generateWorkerId(privateKeyBytes []byte, fingerprintLength int, challengeLevel uint) string { +func generateWorkerId(privateKeyBytes []byte, fingerprintLength uint, challengeLevel uint) string { if privateKey, err := crypto.UnmarshalPrivateKeyFromPEMBytes(privateKeyBytes); err != nil { panic(fmt.Errorf("couldn't read private key file: %s", err)) } else if fingerprint, err := crypto.FingerprintPublicKey(&privateKey.PublicKey, challengeLevel, fingerprintLength); err != nil { diff --git a/config/worker.go b/config/worker.go index a10eb73..8e596c0 100644 --- a/config/worker.go +++ b/config/worker.go @@ -28,7 +28,7 @@ type Worker struct { // 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 int + 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 diff --git a/crypto/fingerprint.go b/crypto/fingerprint.go index 632cc80..2e0feb1 100644 --- a/crypto/fingerprint.go +++ b/crypto/fingerprint.go @@ -21,7 +21,7 @@ var fingerprintEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) func FingerprintPublicKey( key *rsa.PublicKey, challengeLevel uint, - fingerprintLengthInGroups int, + fingerprintLengthInGroups uint, ) (string, error) { keyBytes, err := marshalPublicKey(key) if err != nil { @@ -125,9 +125,9 @@ func generateHashInputPrefix( return hashInputBuffer.Bytes(), nil } -func generateGroupedFingerprint(hashSumString string, numberOfGroups int, groupSeparator string) string { +func generateGroupedFingerprint(hashSumString string, numberOfGroups uint, groupSeparator string) string { var groupedFingerprintBuffer bytes.Buffer - for groupIdx := 0; groupIdx < numberOfGroups && 2*groupIdx+2 <= len(hashSumString); groupIdx++ { + for groupIdx := uint(0); groupIdx < numberOfGroups && 2*groupIdx+2 <= uint(len(hashSumString)); groupIdx++ { groupedFingerprintBuffer.WriteString(hashSumString[2*groupIdx : 2*groupIdx+2]) if groupIdx < numberOfGroups-1 { diff --git a/crypto/fingerprint_test.go b/crypto/fingerprint_test.go index 8396225..432ae5f 100644 --- a/crypto/fingerprint_test.go +++ b/crypto/fingerprint_test.go @@ -72,7 +72,7 @@ func assertFingerprint( t *testing.T, expected string, challengeLevel uint, - fingerprintLengthInGroups int, + fingerprintLengthInGroups uint, key *rsa.PublicKey, ) { t.Helper() @@ -103,7 +103,7 @@ func assertDoesntPassChallenge(hashInput []byte, challengeLevel uint, t *testing } } -func assertGroupedFingerprint(expected string, input string, numberOfGroups int, t *testing.T) { +func assertGroupedFingerprint(expected string, input string, numberOfGroups uint, t *testing.T) { t.Helper() actual := generateGroupedFingerprint(input, numberOfGroups, ":") if actual != expected { From 923d627aacfe838d77f99bf675d247271534ceef Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 22:56:54 +0100 Subject: [PATCH 10/14] #27 running application works with fingerprints * DB file named after Name, not after Id (as it was previously) * Key file also named after Name, not configured individually * added default configuration values --- .gitignore | 1 + cmd/deadbox.go | 12 +++++++++--- config/application.go | 6 ++++++ config/dummy.go | 9 ++++++--- config/worker.go | 6 ++---- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 1328224..a23ec35 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/cmd/deadbox.go b/cmd/deadbox.go index cac2320..276f5c1 100644 --- a/cmd/deadbox.go +++ b/cmd/deadbox.go @@ -21,6 +21,7 @@ import ( const ( filePermOnlyUserCanReadOrWrite = 0600 dbFileExtension = "boltdb" + privateKeyFileExtension = "pem" ) func main() { @@ -63,10 +64,10 @@ func waitForShutdownRequest() { } func runWorker(wcfg config.Worker, acfg *config.Application) daemon.Daemon { - var k = readOrCreatePrivateKeyFile(wcfg.PrivateKeyFile) + var k = readOrCreatePrivateKeyFile(acfg, wcfg.Name) var id = generateWorkerId(k, wcfg.PublicKeyFingerprintLength, wcfg.PublicKeyFingerprintChallengeLevel) - var b = openDb(acfg, id) + var b = openDb(acfg, wcfg.Name) var d daemon.Daemon = worker.New(wcfg, id, b, k) d.OnStop(b.Close) d.Start() @@ -83,7 +84,8 @@ func generateWorkerId(privateKeyBytes []byte, fingerprintLength uint, challengeL } } -func readOrCreatePrivateKeyFile(fileName string) []byte { +func readOrCreatePrivateKeyFile(cfg *config.Application, name string) []byte { + fileName := privateKeyFileName(cfg, name) bytes, err := ioutil.ReadFile(fileName) if err != nil { if !os.IsNotExist(err) { @@ -132,3 +134,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(cfg *config.Application, name string) string { + return filepath.Join(cfg.PrivateKeyPath, name+"."+privateKeyFileExtension) +} diff --git a/config/application.go b/config/application.go index 7cf1c51..3b7c0c5 100644 --- a/config/application.go +++ b/config/application.go @@ -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 diff --git a/config/dummy.go b/config/dummy.go index 4d979b9..e44d38a 100644 --- a/config/dummy.go +++ b/config/dummy.go @@ -9,6 +9,8 @@ func Dummy() *Application { DropUrl: dropUrl, UpdateRegistrationIntervalInSeconds: DefaultUpdateRegistrationIntervalInSeconds, RegistrationTimeoutInSeconds: DefaultRegistrationTimeoutInSeconds, + PublicKeyFingerprintChallengeLevel: DefaultPublicKeyFingerprintChallengeLevel, + PublicKeyFingerprintLength: DefaultPublicKeyFingerprintLength, } d := Drop{ Name: "Default Drop", @@ -16,9 +18,10 @@ func Dummy() *Application { MaxWorkerTimeoutInSeconds: DefaultMaxWorkerTimeoutInSeconds, } app := &Application{ - DbPath: "./", - Workers: []Worker{w}, - Drops: []Drop{d}, + DbPath: "./", + PrivateKeyPath: "./", + Workers: []Worker{w}, + Drops: []Drop{d}, } return app } diff --git a/config/worker.go b/config/worker.go index 8e596c0..b19ec1e 100644 --- a/config/worker.go +++ b/config/worker.go @@ -6,6 +6,8 @@ import ( const DefaultUpdateRegistrationIntervalInSeconds = 10 const DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationIntervalInSeconds +const DefaultPublicKeyFingerprintLength = 8 +const DefaultPublicKeyFingerprintChallengeLevel = 21 // Worker configuration, created once per configured worker. type Worker struct { @@ -22,10 +24,6 @@ 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 From d2e9ac0ce44242f62c2b51e18a586fcf7667a449 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 23:04:30 +0100 Subject: [PATCH 11/14] fix non-existant bucket error for unknown workers --- drop/requests.go | 7 ++++++- drop/workers.go | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/drop/requests.go b/drop/requests.go index 7348256..996d60e 100644 --- a/drop/requests.go +++ b/drop/requests.go @@ -19,13 +19,18 @@ func (w *requests) WorkerRequests(id model.WorkerId) ([]model.WorkerRequest, err var err error err = w.db.View(func(tx *bolt.Tx) error { + // NOTE: this is a read-only TX, bucket expected to be created on worker registration + if err := assertWorkerExists(tx, id); err != nil { + return err + } + if wb, err := findOrCreateRequestBucket(tx, id); err != nil { return err } else { c := wb.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { var request model.WorkerRequest - if err := json.Unmarshal(v, &request); err != nil { + if err = json.Unmarshal(v, &request); err != nil { return fmt.Errorf("request could not be unmarshalled from DB: %s", err) } result = append(result, request) diff --git a/drop/workers.go b/drop/workers.go index 04ea195..41c9736 100644 --- a/drop/workers.go +++ b/drop/workers.go @@ -75,6 +75,13 @@ func (w *workers) PutWorker(worker *model.Worker) error { if err != nil { return fmt.Errorf("couldn't store worker %v: %v", v, err) } + + // NOTE: now create other buckets that are assumed to exist later + _, err = findOrCreateRequestBucket(tx, worker.Id) + if err != nil { + return fmt.Errorf("couldn't create request bucket for worker %s: %v", string(worker.Id), err) + } + return nil }) } From 162b208bfd4cd89c2391ca71ba1b1b98b29b50a1 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 23:14:48 +0100 Subject: [PATCH 12/14] #27: private key size configurable --- cmd/deadbox.go | 20 ++++++++++---------- config/dummy.go | 1 + config/worker.go | 14 ++++++++++---- crypto/rsa.go | 3 +-- drop/facade.go | 2 +- it/utils_test.go | 6 +++--- worker/facade.go | 6 +++--- worker/processors.go | 2 +- 8 files changed, 30 insertions(+), 24 deletions(-) diff --git a/cmd/deadbox.go b/cmd/deadbox.go index 276f5c1..aea2232 100644 --- a/cmd/deadbox.go +++ b/cmd/deadbox.go @@ -48,11 +48,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 @@ -63,8 +63,8 @@ func waitForShutdownRequest() { log.Println(<-ch) } -func runWorker(wcfg config.Worker, acfg *config.Application) daemon.Daemon { - var k = readOrCreatePrivateKeyFile(acfg, wcfg.Name) +func runWorker(wcfg *config.Worker, acfg *config.Application) daemon.Daemon { + var k = readOrCreatePrivateKeyFile(acfg, wcfg) var id = generateWorkerId(k, wcfg.PublicKeyFingerprintLength, wcfg.PublicKeyFingerprintChallengeLevel) var b = openDb(acfg, wcfg.Name) @@ -84,15 +84,15 @@ func generateWorkerId(privateKeyBytes []byte, fingerprintLength uint, challengeL } } -func readOrCreatePrivateKeyFile(cfg *config.Application, name string) []byte { - fileName := privateKeyFileName(cfg, name) +func readOrCreatePrivateKeyFile(acfg *config.Application, wcfg *config.Worker) []byte { + 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() + bytes, err = worker.GeneratePrivateKeyBytes(wcfg.PrivateKeySize) if err != nil { panic(fmt.Errorf("couldn't generate private key: %s", err)) } @@ -106,7 +106,7 @@ func readOrCreatePrivateKeyFile(cfg *config.Application, name string) []byte { return bytes } -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) @@ -135,6 +135,6 @@ func dbFileName(cfg *config.Application, name string) string { return filepath.Join(cfg.DbPath, name+"."+dbFileExtension) } -func privateKeyFileName(cfg *config.Application, name string) string { - return filepath.Join(cfg.PrivateKeyPath, name+"."+privateKeyFileExtension) +func privateKeyFileName(path string, workerName string) string { + return filepath.Join(path, workerName+"."+privateKeyFileExtension) } diff --git a/config/dummy.go b/config/dummy.go index e44d38a..ad831ed 100644 --- a/config/dummy.go +++ b/config/dummy.go @@ -11,6 +11,7 @@ func Dummy() *Application { RegistrationTimeoutInSeconds: DefaultRegistrationTimeoutInSeconds, PublicKeyFingerprintChallengeLevel: DefaultPublicKeyFingerprintChallengeLevel, PublicKeyFingerprintLength: DefaultPublicKeyFingerprintLength, + PrivateKeySize: DefaultPrivateKeySize, } d := Drop{ Name: "Default Drop", diff --git a/config/worker.go b/config/worker.go index b19ec1e..baefa79 100644 --- a/config/worker.go +++ b/config/worker.go @@ -4,10 +4,13 @@ import ( "net/url" ) -const DefaultUpdateRegistrationIntervalInSeconds = 10 -const DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationIntervalInSeconds -const DefaultPublicKeyFingerprintLength = 8 -const DefaultPublicKeyFingerprintChallengeLevel = 21 +const ( + DefaultUpdateRegistrationIntervalInSeconds = 10 + DefaultRegistrationTimeoutInSeconds = 10 * DefaultUpdateRegistrationIntervalInSeconds + DefaultPublicKeyFingerprintLength = 8 + DefaultPublicKeyFingerprintChallengeLevel = 21 + DefaultPrivateKeySize = 4096 +) // Worker configuration, created once per configured worker. type Worker struct { @@ -32,4 +35,7 @@ type Worker struct { // 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 bytes, mostly 2048 oder 4096. + PrivateKeySize int } diff --git a/crypto/rsa.go b/crypto/rsa.go index 376e40c..e6f8070 100644 --- a/crypto/rsa.go +++ b/crypto/rsa.go @@ -11,7 +11,6 @@ import ( const ( pemBlockTypePrivateKey = "RSA PRIVATE KEY" - rsaKeySize = 2048 ) func GeneratePublicKeyBytes(privateKey *rsa.PrivateKey) ([]byte, error) { @@ -37,7 +36,7 @@ func generatePublicKey(privateKey *rsa.PrivateKey) *rsa.PublicKey { return &privateKey.PublicKey } -func GeneratePrivateKey() (*rsa.PrivateKey, error) { +func GeneratePrivateKey(rsaKeySize int) (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, rsaKeySize) } diff --git a/drop/facade.go b/drop/facade.go index 893ecff..3a4ec9b 100644 --- a/drop/facade.go +++ b/drop/facade.go @@ -29,7 +29,7 @@ type facade struct { *responses } -func New(c config.Drop, db *bolt.DB) Daemonized { +func New(c *config.Drop, db *bolt.DB) Daemonized { f := &facade{ name: c.Name, listenAddress: c.ListenAddress, diff --git a/it/utils_test.go b/it/utils_test.go index 6576e55..96db2c8 100644 --- a/it/utils_test.go +++ b/it/utils_test.go @@ -86,7 +86,7 @@ func assertResponseContent(actualResponse model.WorkerResponse, expectedContent func runDropDaemon(t *testing.T) (daemon.Daemon, model.Drop) { t.Helper() - cfg := config.Drop{ + cfg := &config.Drop{ Name: dropName, ListenAddress: ":" + port, MaxRequestTimeoutInSeconds: config.DefaultMaxRequestTimeoutInSeconds, @@ -117,7 +117,7 @@ func runDropDaemon(t *testing.T) (daemon.Daemon, model.Drop) { func runWorkerDaemon(t *testing.T) (worker.Daemonized, []byte) { t.Helper() - cfg := config.Worker{ + cfg := &config.Worker{ Name: workerName, DropUrl: parseUrlOrPanic("http://localhost:" + port), RegistrationTimeoutInSeconds: config.DefaultRegistrationTimeoutInSeconds, @@ -127,7 +127,7 @@ func runWorkerDaemon(t *testing.T) (worker.Daemonized, []byte) { if err != nil { t.Fatalf("could not open Worker's BoltDB: %s", err) } - privateKeyBytes, err := worker.GeneratePrivateKeyBytes() + privateKeyBytes, err := worker.GeneratePrivateKeyBytes(2048) if err != nil { t.Fatalf("couldn't generate private key: %s", err) } diff --git a/worker/facade.go b/worker/facade.go index 879105b..0082721 100644 --- a/worker/facade.go +++ b/worker/facade.go @@ -38,7 +38,7 @@ type facade struct { privateKeyBytes []byte } -func New(c config.Worker, id string, db *bolt.DB, privateKeyBytes []byte) Daemonized { +func New(c *config.Worker, id string, db *bolt.DB, privateKeyBytes []byte) Daemonized { drop := rest.NewClient(c.DropUrl) f := &facade{ db: db, @@ -105,8 +105,8 @@ func (f *facade) main(stop <-chan struct{}) error { } } -func GeneratePrivateKeyBytes() ([]byte, error) { - if key, err := crypto.GeneratePrivateKey(); err != nil { +func GeneratePrivateKeyBytes(rsaKeySize int) ([]byte, error) { + if key, err := crypto.GeneratePrivateKey(rsaKeySize); err != nil { return nil, fmt.Errorf("could not generate private key: %s", err) } else { return crypto.MarshalPrivateKeyToPEMBytes(key), nil diff --git a/worker/processors.go b/worker/processors.go index 3649d5f..7560ce7 100644 --- a/worker/processors.go +++ b/worker/processors.go @@ -15,7 +15,7 @@ func (r *requestProcessors) requestProcessorForId(requestProcessorId string) (re return p, ok } -func createRequestProcessorsByIdMap(c config.Worker) (result map[string]request.Processor) { +func createRequestProcessorsByIdMap(c *config.Worker) (result map[string]request.Processor) { result = make(map[string]request.Processor) addRequestProcessor(result, echo.New()) return From 723a0c3806d3027ba82820084d362a2c619c42f6 Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 23:24:19 +0100 Subject: [PATCH 13/14] #27: check private key size on startup --- cmd/deadbox.go | 24 +++++++++++++++++------- config/worker.go | 2 +- worker/facade.go | 15 ++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/cmd/deadbox.go b/cmd/deadbox.go index aea2232..c09a5ee 100644 --- a/cmd/deadbox.go +++ b/cmd/deadbox.go @@ -1,6 +1,7 @@ package main import ( + "crypto/rsa" "fmt" "io/ioutil" "log" @@ -64,7 +65,7 @@ func waitForShutdownRequest() { } func runWorker(wcfg *config.Worker, acfg *config.Application) daemon.Daemon { - var k = readOrCreatePrivateKeyFile(acfg, wcfg) + var k = readOrCreatePrivateKey(acfg, wcfg) var id = generateWorkerId(k, wcfg.PublicKeyFingerprintLength, wcfg.PublicKeyFingerprintChallengeLevel) var b = openDb(acfg, wcfg.Name) @@ -74,17 +75,16 @@ func runWorker(wcfg *config.Worker, acfg *config.Application) daemon.Daemon { return d } -func generateWorkerId(privateKeyBytes []byte, fingerprintLength uint, challengeLevel uint) string { - if privateKey, err := crypto.UnmarshalPrivateKeyFromPEMBytes(privateKeyBytes); err != nil { - panic(fmt.Errorf("couldn't read private key file: %s", err)) - } else if fingerprint, err := crypto.FingerprintPublicKey(&privateKey.PublicKey, challengeLevel, fingerprintLength); err != nil { + +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 readOrCreatePrivateKeyFile(acfg *config.Application, wcfg *config.Worker) []byte { +func readOrCreatePrivateKey(acfg *config.Application, wcfg *config.Worker) *rsa.PrivateKey { fileName := privateKeyFileName(acfg.PrivateKeyPath, wcfg.Name) bytes, err := ioutil.ReadFile(fileName) if err != nil { @@ -92,6 +92,7 @@ func readOrCreatePrivateKeyFile(acfg *config.Application, wcfg *config.Worker) [ panic(fmt.Errorf("couldn't read file %s: %s", fileName, err)) } + 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)) @@ -103,7 +104,16 @@ func readOrCreatePrivateKeyFile(acfg *config.Application, wcfg *config.Worker) [ } } - 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 { diff --git a/config/worker.go b/config/worker.go index baefa79..57bcde2 100644 --- a/config/worker.go +++ b/config/worker.go @@ -36,6 +36,6 @@ type Worker struct { // validate the fingerprint. PublicKeyFingerprintChallengeLevel uint - // PrivateKeySize is the size of the private RSA key in bytes, mostly 2048 oder 4096. + // PrivateKeySize is the size of the private RSA key in bits, mostly 2048 oder 4096. PrivateKeySize int } diff --git a/worker/facade.go b/worker/facade.go index 0082721..a09c53f 100644 --- a/worker/facade.go +++ b/worker/facade.go @@ -1,6 +1,7 @@ package worker import ( + "crypto/rsa" "fmt" "net/url" "time" @@ -35,15 +36,15 @@ type facade struct { dropUrl *url.URL updateRegistrationInterval time.Duration registrationTimeoutDuration time.Duration - privateKeyBytes []byte + privateKey *rsa.PrivateKey } -func New(c *config.Worker, id string, db *bolt.DB, privateKeyBytes []byte) Daemonized { +func New(c *config.Worker, id string, db *bolt.DB, privateKey *rsa.PrivateKey) Daemonized { drop := rest.NewClient(c.DropUrl) f := &facade{ db: db, dropUrl: c.DropUrl, - privateKeyBytes: privateKeyBytes, + privateKey: privateKey, updateRegistrationInterval: time.Duration(c.UpdateRegistrationIntervalInSeconds) * time.Second, registrations: registrations{ id: model.WorkerId(id), @@ -66,11 +67,7 @@ func New(c *config.Worker, id string, db *bolt.DB, privateKeyBytes []byte) Daemo func (f *facade) main(stop <-chan struct{}) error { var err error - privateKey, err := crypto.UnmarshalPrivateKeyFromPEMBytes(f.privateKeyBytes) - if err != nil { - return fmt.Errorf("worker %s could not read its private key from file %s: %s", f.QuotedNameAndId(), f.privateKeyBytes, err) - } - publicKeyBytes, err := crypto.GeneratePublicKeyBytes(privateKey) + publicKeyBytes, err := crypto.GeneratePublicKeyBytes(f.privateKey) if err != nil { return fmt.Errorf("worker %s could not export its public key: %s", f.QuotedNameAndId(), err) } @@ -90,7 +87,7 @@ func (f *facade) main(stop <-chan struct{}) error { select { case <-pollRequestTicker.C: // @todo #3 Replace pull with push mechanism (e.g. websocket) - if err := f.pollRequests(f.requestProcessors, privateKey); err != nil { + if err := f.pollRequests(f.requestProcessors, f.privateKey); err != nil { log.Printf("worker %s at drop %s could not poll requests: %s", f.QuotedNameAndId(), f.dropUrl, err) } From 7556547232375f0e048d266fba63bb6faf20de1e Mon Sep 17 00:00:00 2001 From: Felix Neumann Date: Tue, 2 Jan 2018 23:25:06 +0100 Subject: [PATCH 14/14] #27: fix unit test --- it/utils_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/it/utils_test.go b/it/utils_test.go index 96db2c8..acc0df0 100644 --- a/it/utils_test.go +++ b/it/utils_test.go @@ -145,7 +145,7 @@ func runWorkerDaemon(t *testing.T) (worker.Daemonized, []byte) { t.Fatalf("couldn't fingerprint public key: %s", err) } - workerDaemon := worker.New(cfg, fingerprint, db, privateKeyBytes) + workerDaemon := worker.New(cfg, fingerprint, db, privateKey) workerDaemon.OnStop(func() error { if err := db.Close(); err != nil { return err