Checking in some work. Keygen is done (with confirmation of data formatting output pending), and dh params is way easier than I thought it would be. We shouldn't need to regenerate dhparams. i *think*.

This commit is contained in:
brent s. 2020-09-24 04:38:29 -04:00
parent b80b823c02
commit c22786204a
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
10 changed files with 360 additions and 76 deletions

1
TODO
View File

@ -19,6 +19,7 @@
-- Check if haveged is running. If not and installed, start it.
-- Generate moduli
-- Render to /etc/ssh/moduli format
--- custom moduli marshaler/unmarshaler? (e.g. https://stackoverflow.com/a/50211222)
-- Write to dest

- Config

2
go.mod
View File

@ -1,4 +1,4 @@
module git.square-r00t.net/sshsecure
module r00t2.io/sshsecure

go 1.15


View File

@ -17,3 +17,55 @@
*/

package moduli

import (
"fmt"

"r00t2.io/sshsecure/sharedconsts"
)

// Misc
const (
// Where to find an up-to-date copy of the upstream moduli and its SHA3-512 (NIST) checksum.
pregenURL string = "https://anongit.mindrot.org/openssh.git/tree/moduli"
// This is the best way I could think of to verify integrity, since the file itself doesn't have a signature or anything like that.
pregenCksum string = "106EDB19A936608D065D2E8E81F7BDE7" +
"434AF80EF81102E9440B99ACB98FBEF8" +
"CC2F4B6BFD76828337BDB1F2CF34D859" +
"045285DCE6B0DE7D7D93A9EE61F8CC96"
// The tag name to use for struct tags (marshal/unmarshaling)
parseTag string = "sshmoduli"
// The recommended minimum moduli to have available.
recMinMod int = 400
// The
)

// The header line on the /etc/ssh/moduli file.
var header = string(
fmt.Sprintf(
"# %v\n"+
"# Time Type Tests Tries Size Generator Modulus\n", sharedconsts.IDCmnt,
),
)

// For parsing/rendering /etc/ssh/moduli
const (
// Golang has no strftime formatting codes. It operates on *display of a specific time*.
// What a dumb language.
timeFormat string = "20060102150405" // %Y%m%d%H%M%S
)

// For validation
var (
validTypes = []uint8{
0, // Unknown, not tested
2, // "Safe" prime; (p-1)/2 is also prime.
4, // Sophie Germain; 2p+1 is also prime.
}
validTests = []byte{
0x00, // Not tested.
0x01, // Composite number - not prime.
0x02, // Sieve of Eratosthenes.
0x04, // Probabilistic Miller-Rabin primality tests.
}
)

View File

@ -17,3 +17,44 @@
*/

package moduli

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"net/http"

"golang.org/x/crypto/sha3"
)

func getPregen() ([]byte, error) {
// get the pregenerated moduli
resp, err := http.Get(pregenURL)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(fmt.Sprintf("returned status code %v: %v", resp.StatusCode, resp.Status))
}
defer resp.Body.Close()
b := make([]byte, resp.ContentLength)
if _, err = resp.Body.Read(b); err != nil {
return nil, err
}
// and compare the SHA3-512 (NIST) checksum.
s := sha3.New512()
if _, err = s.Write(b); err != nil {
// TODO: return nil instead of b?
return b, err
}
goodCksum, err := hex.DecodeString(pregenCksum)
if err != nil {
return nil, err
}
// We just compare the bytestrings.
if bytes.Compare(s.Sum(nil), goodCksum) != 0 {
return nil, errors.New("checksums do not match")
}
return b, nil
}

119
moduli/parser.go Normal file
View File

@ -0,0 +1,119 @@
/*
SSHSecure - a program to harden OpenSSH from defaults
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package moduli

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"
"time"
)

var reSkipLine, _ = regexp.Compile(`^\s*(#.*)?$`)

// Marshal returns the /etc/ssh/moduli format of m.
// Format of: Time Type Tests Tries Size Generator Modulus
// TODO: remember to write newline at end
func (m *Moduli) Marshal() ([]byte, error) {
var b bytes.Buffer
b.Write([]byte(header))
for _, i := range m.Params {
line, err := i.marshalEntry()
if err != nil {
return b.Bytes(), err
} else {
b.Write(line)
}
}
return b.Bytes(), nil
}

// marshalEntry is used to parse a specific DH entry into the moduli.
func (m *ModuliEntry) marshalEntry() ([]byte, error) {
mod := hex.EncodeToString(m.Modulus.Bytes())
s := fmt.Sprintf(
"%v %v %v %v %v %v %v\n",
m.Time.Format(timeFormat),
string(m.Type),
string(m.Tests),
string(m.Trials),
string(m.Size),
string(m.Generator),
mod,
)
return []byte(s), nil
}

// Unmarshal writes the Moduli format into m from the /etc/ssh/moduli format in data.
func Unmarshal(data []byte, m Moduli) error {
var lines []string
var entries []ModuliEntry
lines = strings.Split(string(data), "\n")
for _, line := range lines {
e := ModuliEntry{}
if reSkipLine.MatchString(line) {
continue
}
l := strings.Fields(line)
if err := unmarshalEntry(l, e); err != nil {
return err
}
entries = append(entries, e)
}
m.Params = entries
return nil
}

func unmarshalEntry(line []string, m ModuliEntry) error {
if len(line) != 7 {
return errors.New("field count mismatch")
}
if m.Time, err = time.Parse(timeFormat, line[0]); err != nil {
return err
}
// Numeric types. Cast to uint8. There's probably a better way to do this but golang's pretty ugly with this stuff no matter what.
// Type, Tests, Trials, Size, Generator
conv := [5]uint8{}
for idx := 1; idx <= 5; idx++ {
v := line[idx]
newv, err := strconv.Atoi(v)
if err != nil {
return err
}
conv[idx-1] = uint8(newv)
}
m.Type = conv[0]
m.Tests = conv[1]
m.Trials = conv[2]
m.Size = conv[3]
m.Generator = conv[4]
// And the modulus convert to big.Int.
modb, err := hex.DecodeString(line[6])
if err != nil {
return err
}
m.Modulus = big.Int{}
m.Modulus.SetBytes(modb)
return nil
}

View File

@ -5,3 +5,4 @@ https://security.stackexchange.com/a/113058
https://github.com/Luzifer/go-dhparam
https://github.com/mimoo/test_DHparams
https://github.com/hyperreality/cryptopals-2/blob/517c1907b2041e6f7ef18930eca2aa3a24fb73d8/dh.go
https://sosedoff.com/2016/07/16/golang-struct-tags.html

View File

@ -17,3 +17,62 @@
*/

package moduli

import (
"math/big"
"time"
)

// Moduli contains all data needed for generated /etc/ssh/moduli. of ModuliEntry entries.
type Moduli struct {
Header string
Params []ModuliEntry
}

// Moduli is a struct reflecting the format of a single /etc/ssh/moduli entry. See moduli(5) for details.
type ModuliEntry struct {
Time time.Time // YYYYMMDDHHSS
/*
// man 5 moduli:
Decimal number specifying the internal structure of the prime modulus. Supported types are:
0 Unknown, not tested.
2 "Safe" prime; (p-1)/2 is also prime.
4 Sophie Germain; 2p+1 is also prime.
Moduli candidates initially produced by ssh-keygen(1) are Sophie Germain primes (type 4).
Further primality testing with ssh-keygen(1) produces safe prime moduli (type 2) that are ready for use in sshd(8).
Other types are not used by OpenSSH.
*/
Type uint8
/*
// man 5 moduli:
Decimal number indicating the type of primality tests that the number has been
subjected to represented as a bitmask of the following values:
0x00 Not tested.
0x01 Composite number not prime.
0x02 Sieve of Eratosthenes.
0x04 Probabilistic Miller-Rabin primality tests.
The ssh-keygen(1) moduli candidate generation uses the Sieve of Eratosthenes (flag 0x02).
Subsequent ssh-keygen(1) primality tests are Miller-Rabin tests (flag 0x04).
*/
Tests uint8
/*
// man 5 moduli:
Decimal number indicating the number of primality trials that have been performed on the modulus.
*/
Trials uint8
/*
// man 5 moduli:
Decimal number indicating the size of the prime in bits.
*/
Size uint8
/*
// man 5 moduli:
The recommended generator for use with this modulus (hexadecimal).
*/
Generator uint8
/*
// man 5 moduli:
The modulus itself in hexadecimal.
*/
Modulus big.Int
}

13
sharedconsts/const.go Normal file
View File

@ -0,0 +1,13 @@
package sharedconsts

import (
"fmt"
)

// "Meta". Used for comment strings, etc.
const (
projName = "SSHSecure"
projUrl = "https://git.square-r00t.net/SSHSecure"
)

var IDCmnt = string(fmt.Sprintf("Autogenerated by %v (%v)", projName, projUrl))

View File

@ -23,10 +23,6 @@ const (
KeyV1Magic string = "openssh-key-v1"
)

// "Meta". Used for comment strings, etc.

const projUrl = "https://git.square-r00t.net/SSHSecure"

// Defaults.
const (
defCipher string = CipherAes256Ctr

View File

@ -19,10 +19,12 @@
package sshkeys

import (
`bytes`
"bytes"
"crypto/rand"
"errors"
"fmt"

"r00t2.io/sshsecure/sharedconsts"
)

func (k *EncryptedSSHKeyV1) validate() error {
@ -86,7 +88,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error {
// Currently, OpenSSH has an option for multiple private keys. However, it is hardcoded to 1.
// If multiple key support is added in the future, will need to re-tool how I do this, perhaps, in the future. TODO.
pk := SSHPrivKey{
Comment: fmt.Sprintf("Autogenerated via SSHSecure (%v)", projUrl),
Comment: sharedconsts.IDCmnt,
}
pk.Checksum = make([]byte, 4)
if _, err := rand.Read(pk.Checksum); err != nil {
@ -358,7 +360,7 @@ func (pk *SSHPrivKey) keyBlob(c *SSHCrypt, encrypt bool) (*[]byte, error) {
// Add padding
pad := 0
n := 0
for len(privkeyBytes.Bytes()) % pk.BlockSize != 0 { // 4.0.1.5 (ED25519), 4.0.1.8 (RSA)
for len(privkeyBytes.Bytes())%pk.BlockSize != 0 { // 4.0.1.5 (ED25519), 4.0.1.8 (RSA)
n++
pad = n & pk.BlockSize
privkeyBytes.Write(getSingleByteInt(pad))