Add tool to search for certificates containing debian weak keys (#3077)
Fixes #3074.
This commit is contained in:
parent
08d2018c10
commit
2d3fc8c4b4
|
|
@ -0,0 +1,121 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
"github.com/letsencrypt/boulder/goodkey"
|
||||||
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type certInfo struct {
|
||||||
|
serial string
|
||||||
|
der []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var limit = 1000
|
||||||
|
|
||||||
|
func getCerts(work chan certInfo, moreThan, lessThan time.Time, db *sql.DB) {
|
||||||
|
query := "SELECT serial, der FROM certificates WHERE issued >= ? and issued < ? ORDER BY issued LIMIT ? OFFSET ?"
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
rows, err := db.Query(query, moreThan, lessThan, limit, i)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
cmd.FailOnError(err, "db.Query failed")
|
||||||
|
} else if err == sql.ErrNoRows {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
prev := i
|
||||||
|
for rows.Next() {
|
||||||
|
i++
|
||||||
|
var c certInfo
|
||||||
|
if err := rows.Scan(&c.serial, &c.der); err != nil {
|
||||||
|
cmd.FailOnError(err, "rows.Scan failed")
|
||||||
|
}
|
||||||
|
work <- c
|
||||||
|
}
|
||||||
|
if i == prev {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(work)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doWork(work chan certInfo, parallelism int, wkl *goodkey.WeakRSAKeys, log blog.Logger) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < parallelism; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for ci := range work {
|
||||||
|
cert, err := x509.ParseCertificate(ci.der)
|
||||||
|
cmd.FailOnError(err, "x509.ParseCertificate failed")
|
||||||
|
if rk := cert.PublicKey.(*rsa.PublicKey); wkl.Known(rk) {
|
||||||
|
log.Info(fmt.Sprintf("cert contains weak key: %s", ci.serial))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
cmd.SyslogConfig
|
||||||
|
cmd.DBConfig
|
||||||
|
WeakKeyFile string
|
||||||
|
Parallelism uint
|
||||||
|
IssuedMoreThan string
|
||||||
|
IssuedLessThan string
|
||||||
|
BufferSize uint
|
||||||
|
BatchSize uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configFile := flag.String("config", "", "Path to configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var c config
|
||||||
|
err := cmd.ReadConfigFile(*configFile, &c)
|
||||||
|
cmd.FailOnError(err, fmt.Sprintf("Failed to read and parse configuration file %q", *configFile))
|
||||||
|
|
||||||
|
if c.Parallelism == 0 {
|
||||||
|
log.Fatal("parallelism must be > 0")
|
||||||
|
}
|
||||||
|
bufferSize := 5000
|
||||||
|
if c.BufferSize > 0 {
|
||||||
|
bufferSize = int(c.BatchSize)
|
||||||
|
}
|
||||||
|
if c.BatchSize > 0 {
|
||||||
|
limit = int(c.BatchSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, log := cmd.StatsAndLogging(c.SyslogConfig)
|
||||||
|
|
||||||
|
moreThan, err := time.Parse(time.RFC3339, c.IssuedMoreThan)
|
||||||
|
cmd.FailOnError(err, fmt.Sprintf("failed to parse issuedMoreThan %q", c.IssuedMoreThan))
|
||||||
|
lessThan, err := time.Parse(time.RFC3339, c.IssuedLessThan)
|
||||||
|
cmd.FailOnError(err, fmt.Sprintf("failed to parse issuedLessThan %q", c.IssuedLessThan))
|
||||||
|
|
||||||
|
wkl, err := goodkey.LoadWeakRSASuffixes(c.WeakKeyFile)
|
||||||
|
cmd.FailOnError(err, fmt.Sprintf("failed to load weak key list %q", c.WeakKeyFile))
|
||||||
|
|
||||||
|
uri, err := c.DBConfig.URL()
|
||||||
|
cmd.FailOnError(err, "Failed to load DB URI")
|
||||||
|
db, err := sql.Open("mysql", uri)
|
||||||
|
cmd.FailOnError(err, "failed to connect to DB")
|
||||||
|
|
||||||
|
work := make(chan certInfo, bufferSize)
|
||||||
|
|
||||||
|
go getCerts(work, moreThan, lessThan, db)
|
||||||
|
doWork(work, int(c.Parallelism), wkl, log)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmhodges/clock"
|
||||||
|
"github.com/letsencrypt/boulder/core"
|
||||||
|
"github.com/letsencrypt/boulder/goodkey"
|
||||||
|
blog "github.com/letsencrypt/boulder/log"
|
||||||
|
"github.com/letsencrypt/boulder/metrics"
|
||||||
|
"github.com/letsencrypt/boulder/sa"
|
||||||
|
"github.com/letsencrypt/boulder/sa/satest"
|
||||||
|
"github.com/letsencrypt/boulder/test"
|
||||||
|
"github.com/letsencrypt/boulder/test/vars"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSearch(t *testing.T) {
|
||||||
|
dbMap, err := sa.NewDbMap(vars.DBConnSA, 0)
|
||||||
|
test.AssertNotError(t, err, "sa.NewDbMap failed")
|
||||||
|
fc := clock.NewFake()
|
||||||
|
log := blog.UseMock()
|
||||||
|
ssa, err := sa.NewSQLStorageAuthority(dbMap, fc, log, metrics.NewNoopScope())
|
||||||
|
test.AssertNotError(t, err, "sa.NewSQLStorageAuthority failed")
|
||||||
|
defer test.ResetSATestDatabase(t)
|
||||||
|
|
||||||
|
reg := satest.CreateWorkingRegistration(t, ssa)
|
||||||
|
|
||||||
|
k, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
test.AssertNotError(t, err, "rsa.GenerateKey failed")
|
||||||
|
temp := x509.Certificate{SerialNumber: big.NewInt(1337), Subject: pkix.Name{CommonName: "yup"}}
|
||||||
|
|
||||||
|
derA, err := x509.CreateCertificate(rand.Reader, &temp, &temp, &k.PublicKey, k)
|
||||||
|
test.AssertNotError(t, err, "x509.CreateCertificate failed")
|
||||||
|
// DER of a RSA private key generated with the flawed Debian PRNG
|
||||||
|
wkBytes, err := hex.DecodeString("308204a30201000282010100d673252af6723c3f72529403eab7c30def3c52f97e799825f4a70191c616adcf1ece1113f1625971074c492c592025fdeadbdb146a081826bdf0d77c3c913dcf1b6f0b3b78f5108d2e493ad0eee8ca5c021711adc13d358e61133870fcd19c8e5c22403959782aa82e72aee53a3d491e3912ce27b27e1a85ea69c19a527d28f7934c9823b7e56fdd657dac83fdc65bb22a98d843df73238919781b714c81a5e2afec71f5c54aa2a27c590ad94c03c1062d50efcffac743e3c8a3ae056846a1d756eb862bf4224169d467c35215ade0afcc11e85fe629afb802c4786ff2e9c929bccf502b3d3b8876c6a11785cc398b389f1d86bdd9cb0bd4ec13956ec3fa270d0203010001028201007db89180a76c7f3f9ef1248f4b4aa212883f70518e4110dea7984506460043b35a56ea922b8041f94e92fd8eff4d2698bed8578e973ed991d4e6de1d9a907790f47f5c31688f1b3df975bb02841d7b8d0738a90799731df3b39b860a4f5d3f9002199e5740c97f108bf275f032fd7ce1380a7b4bb08bd756ccff651de8e03164284d9aaa31c849deaa8c092c516fe963abbff8dad1c4b54dc4f573388580ed1d9a48d0787562443bad1b9ea08f94e3df7364a845e2d17366d2650ab745a885c514eec68927f6d629b888e2e609cc42e686e5ab9c46c32f6708df75781096a54cd544e7ae42da373a551abe117877297d4fcb0ff95e97d2e07d3869b22ff4b8e102818100f3508098dc7be50795f95a58e7564f3b47adfd936aee8c71415af2e1e7756367d529ecf4c44d7706c99bf16144733e045b49d1f697cea6ea01fb41928d0f626a397f973a459150d3ec71d915dd0d3d5f03599babec1b9b9cef16bb3b4ec8a712fe9df0df2352b02ee447f63adafd95397867ac728e8c5dbbe9be045791e1793902818100e1a1637136fc530a12950a54f7a3c14146586c7371c18433462c18825492ec02a4eee3e74e38ccdfac0dbe6ade624eac67c5983b610b14e51bd1a3edf76bf82af767426cf0156ee2d895c05866b19219876a10d0ee89c87a9f6eaa25595f4b11e5273eaad6d468a87eec0ab94228b99eb051bc5593ee337153aba07dd41bc075028180409d544943e433023cb5a7648caac307bf15598dd88bd9080a8f18891d6a732793d83a7115e06c8784eac0c34fe63ac5f5683935ff4285d90705ae7838b5a931046bf9c123d05f62a81be3c68699897ebde9020a39fd6ae9d624773c5cc3b47abadb3ea8433d26448da2fea4ca9b2511ca03de2bdde730cd42598fd5a18bfb21028180102208083a544463bdfc6626b9263e553a806c10bd1b87265b681fc081e7977480f28bdd281cab997aa5e8ed9f450c370b9c774c179e413a3888feddaf094b4f572d4cf4991e0f35ad22d803fa23cc3c8310346f9bfec214f27d69310e78dfd741b952a3c8849b8f20b423f82720de54d86a9fbac6bf0b7298f6f69cc8a3cb59028181009d91baf2e82e83e9085b7fdd82c89ff3df2ec2a52aaf2bc31906470529f0e1b80254fd3b34b4372b50715c83ff4587159aee22a90ac4e2ad736651f33c9395efaf48fe57d0355d295f4cbca85249d3473632ff35cc29fff142fd7ab1c6e5b163909591a17054c3ff731f11549251dd2436fdf1bc969b91360eeb4e6f01c7e0b1")
|
||||||
|
test.AssertNotError(t, err, "hex.DecodeString failed")
|
||||||
|
wk, err := x509.ParsePKCS1PrivateKey(wkBytes)
|
||||||
|
test.AssertNotError(t, err, "x509.ParsePKCS1PrivateKey failed")
|
||||||
|
derB, err := x509.CreateCertificate(rand.Reader, &temp, &temp, &wk.PublicKey, wk)
|
||||||
|
test.AssertNotError(t, err, "x509.CreateCertificate failed")
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
err = dbMap.Insert(
|
||||||
|
&core.Certificate{RegistrationID: reg.ID, Serial: "cert-a", DER: derA, Issued: now.Add(time.Hour * -2)},
|
||||||
|
&core.Certificate{RegistrationID: reg.ID, Serial: "cert-b", DER: derA, Issued: now},
|
||||||
|
&core.Certificate{RegistrationID: reg.ID, Serial: "cert-c", DER: derB, Issued: now},
|
||||||
|
&core.Certificate{RegistrationID: reg.ID, Serial: "cert-d", DER: derA, Issued: now.Add(time.Hour * 2)},
|
||||||
|
)
|
||||||
|
test.AssertNotError(t, err, "dbMap.Insert failed")
|
||||||
|
|
||||||
|
limit = 1
|
||||||
|
w := make(chan certInfo, 10)
|
||||||
|
getCerts(w, now.Add(-time.Hour), now.Add(time.Hour), dbMap.Db)
|
||||||
|
test.AssertEquals(t, len(w), 2)
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "weak-keys")
|
||||||
|
test.AssertNotError(t, err, "Failed to create temporary directory")
|
||||||
|
tempPath := filepath.Join(tempDir, "a.json")
|
||||||
|
// Truncated SHA1 hash of the modulus of the above RSA key
|
||||||
|
err = ioutil.WriteFile(tempPath, []byte("[\"8df20e6961a16398b85a\"]"), os.ModePerm)
|
||||||
|
test.AssertNotError(t, err, "Failed to create temporary file")
|
||||||
|
|
||||||
|
wkl, err := goodkey.LoadWeakRSASuffixes(tempPath)
|
||||||
|
test.AssertNotError(t, err, "Failed to load suffixes from directory")
|
||||||
|
doWork(w, 1, wkl, log)
|
||||||
|
test.AssertEquals(t, len(w), 0)
|
||||||
|
test.AssertDeepEquals(t, log.GetAllMatching("INFO: cert contains weak key: .*"), []string{"INFO: cert contains weak key: cert-c"})
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ type KeyPolicy struct {
|
||||||
AllowRSA bool // Whether RSA keys should be allowed.
|
AllowRSA bool // Whether RSA keys should be allowed.
|
||||||
AllowECDSANISTP256 bool // Whether ECDSA NISTP256 keys should be allowed.
|
AllowECDSANISTP256 bool // Whether ECDSA NISTP256 keys should be allowed.
|
||||||
AllowECDSANISTP384 bool // Whether ECDSA NISTP384 keys should be allowed.
|
AllowECDSANISTP384 bool // Whether ECDSA NISTP384 keys should be allowed.
|
||||||
weakRSAList *weakKeys
|
weakRSAList *WeakRSAKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyPolicy returns a KeyPolicy that allows RSA, ECDSA256 and ECDSA384.
|
// NewKeyPolicy returns a KeyPolicy that allows RSA, ECDSA256 and ECDSA384.
|
||||||
|
|
@ -54,7 +54,7 @@ func NewKeyPolicy(weakKeyFile string) (KeyPolicy, error) {
|
||||||
AllowECDSANISTP384: true,
|
AllowECDSANISTP384: true,
|
||||||
}
|
}
|
||||||
if weakKeyFile != "" {
|
if weakKeyFile != "" {
|
||||||
keyList, err := loadSuffixes(weakKeyFile)
|
keyList, err := LoadWeakRSASuffixes(weakKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return KeyPolicy{}, err
|
return KeyPolicy{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ import (
|
||||||
|
|
||||||
type truncatedHash [10]byte
|
type truncatedHash [10]byte
|
||||||
|
|
||||||
type weakKeys struct {
|
type WeakRSAKeys struct {
|
||||||
suffixes map[truncatedHash]struct{}
|
suffixes map[truncatedHash]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSuffixes(path string) (*weakKeys, error) {
|
func LoadWeakRSASuffixes(path string) (*WeakRSAKeys, error) {
|
||||||
f, err := ioutil.ReadFile(path)
|
f, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -32,7 +32,7 @@ func loadSuffixes(path string) (*weakKeys, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
wk := &weakKeys{suffixes: make(map[truncatedHash]struct{})}
|
wk := &WeakRSAKeys{suffixes: make(map[truncatedHash]struct{})}
|
||||||
for _, suffix := range suffixList {
|
for _, suffix := range suffixList {
|
||||||
err := wk.addSuffix(suffix)
|
err := wk.addSuffix(suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -42,7 +42,7 @@ func loadSuffixes(path string) (*weakKeys, error) {
|
||||||
return wk, nil
|
return wk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wk *weakKeys) addSuffix(str string) error {
|
func (wk *WeakRSAKeys) addSuffix(str string) error {
|
||||||
var suffix truncatedHash
|
var suffix truncatedHash
|
||||||
decoded, err := hex.DecodeString(str)
|
decoded, err := hex.DecodeString(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -56,7 +56,7 @@ func (wk *weakKeys) addSuffix(str string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wk *weakKeys) Known(key *rsa.PublicKey) bool {
|
func (wk *WeakRSAKeys) Known(key *rsa.PublicKey) bool {
|
||||||
// Hash input is in the format "Modulus={upper-case hex of modulus}\n"
|
// Hash input is in the format "Modulus={upper-case hex of modulus}\n"
|
||||||
hash := sha1.Sum([]byte(fmt.Sprintf("Modulus=%X\n", key.N.Bytes())))
|
hash := sha1.Sum([]byte(fmt.Sprintf("Modulus=%X\n", key.N.Bytes())))
|
||||||
var suffix truncatedHash
|
var suffix truncatedHash
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ func TestKnown(t *testing.T) {
|
||||||
testKey := rsa.PublicKey{N: mod}
|
testKey := rsa.PublicKey{N: mod}
|
||||||
otherKey := rsa.PublicKey{N: big.NewInt(2020)}
|
otherKey := rsa.PublicKey{N: big.NewInt(2020)}
|
||||||
|
|
||||||
wk := &weakKeys{suffixes: make(map[truncatedHash]struct{})}
|
wk := &WeakRSAKeys{suffixes: make(map[truncatedHash]struct{})}
|
||||||
err = wk.addSuffix("8df20e6961a16398b85a")
|
err = wk.addSuffix("8df20e6961a16398b85a")
|
||||||
// a3853d0c563765e504c18df20e6961a16398b85a
|
// a3853d0c563765e504c18df20e6961a16398b85a
|
||||||
test.AssertNotError(t, err, "weakKeys.addSuffix failed")
|
test.AssertNotError(t, err, "WeakRSAKeys.addSuffix failed")
|
||||||
test.Assert(t, wk.Known(&testKey), "weakKeys.Known failed to find suffix that has been added")
|
test.Assert(t, wk.Known(&testKey), "WeakRSAKeys.Known failed to find suffix that has been added")
|
||||||
test.Assert(t, !wk.Known(&otherKey), "weakKeys.Known found a suffix that has not been added")
|
test.Assert(t, !wk.Known(&otherKey), "WeakRSAKeys.Known found a suffix that has not been added")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadKeys(t *testing.T) {
|
func TestLoadKeys(t *testing.T) {
|
||||||
|
|
@ -40,7 +40,7 @@ func TestLoadKeys(t *testing.T) {
|
||||||
err = ioutil.WriteFile(tempPath, []byte("[\"8df20e6961a16398b85a\"]"), os.ModePerm)
|
err = ioutil.WriteFile(tempPath, []byte("[\"8df20e6961a16398b85a\"]"), os.ModePerm)
|
||||||
test.AssertNotError(t, err, "Failed to create temporary file")
|
test.AssertNotError(t, err, "Failed to create temporary file")
|
||||||
|
|
||||||
wk, err := loadSuffixes(tempPath)
|
wk, err := LoadWeakRSASuffixes(tempPath)
|
||||||
test.AssertNotError(t, err, "Failed to load suffixes from directory")
|
test.AssertNotError(t, err, "Failed to load suffixes from directory")
|
||||||
test.Assert(t, wk.Known(&testKey), "weakKeys.Known failed to find suffix that has been added")
|
test.Assert(t, wk.Known(&testKey), "WeakRSAKeys.Known failed to find suffix that has been added")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue