Add tool to search for certificates containing debian weak keys (#3077)

Fixes #3074.
This commit is contained in:
Roland Bracewell Shoemaker 2017-09-13 10:59:58 -07:00 committed by Jacob Hoffman-Andrews
parent 08d2018c10
commit 2d3fc8c4b4
5 changed files with 212 additions and 13 deletions

121
cmd/weak-key-search/main.go Normal file
View File

@ -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)
}

View File

@ -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"})
}

View File

@ -40,7 +40,7 @@ type KeyPolicy struct {
AllowRSA bool // Whether RSA keys should be allowed.
AllowECDSANISTP256 bool // Whether ECDSA NISTP256 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.
@ -54,7 +54,7 @@ func NewKeyPolicy(weakKeyFile string) (KeyPolicy, error) {
AllowECDSANISTP384: true,
}
if weakKeyFile != "" {
keyList, err := loadSuffixes(weakKeyFile)
keyList, err := LoadWeakRSASuffixes(weakKeyFile)
if err != nil {
return KeyPolicy{}, err
}

View File

@ -16,11 +16,11 @@ import (
type truncatedHash [10]byte
type weakKeys struct {
type WeakRSAKeys struct {
suffixes map[truncatedHash]struct{}
}
func loadSuffixes(path string) (*weakKeys, error) {
func LoadWeakRSASuffixes(path string) (*WeakRSAKeys, error) {
f, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
@ -32,7 +32,7 @@ func loadSuffixes(path string) (*weakKeys, error) {
return nil, err
}
wk := &weakKeys{suffixes: make(map[truncatedHash]struct{})}
wk := &WeakRSAKeys{suffixes: make(map[truncatedHash]struct{})}
for _, suffix := range suffixList {
err := wk.addSuffix(suffix)
if err != nil {
@ -42,7 +42,7 @@ func loadSuffixes(path string) (*weakKeys, error) {
return wk, nil
}
func (wk *weakKeys) addSuffix(str string) error {
func (wk *WeakRSAKeys) addSuffix(str string) error {
var suffix truncatedHash
decoded, err := hex.DecodeString(str)
if err != nil {
@ -56,7 +56,7 @@ func (wk *weakKeys) addSuffix(str string) error {
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 := sha1.Sum([]byte(fmt.Sprintf("Modulus=%X\n", key.N.Bytes())))
var suffix truncatedHash

View File

@ -20,12 +20,12 @@ func TestKnown(t *testing.T) {
testKey := rsa.PublicKey{N: mod}
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")
// a3853d0c563765e504c18df20e6961a16398b85a
test.AssertNotError(t, err, "weakKeys.addSuffix failed")
test.Assert(t, wk.Known(&testKey), "weakKeys.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.AssertNotError(t, err, "WeakRSAKeys.addSuffix failed")
test.Assert(t, wk.Known(&testKey), "WeakRSAKeys.Known failed to find suffix that has been added")
test.Assert(t, !wk.Known(&otherKey), "WeakRSAKeys.Known found a suffix that has not been added")
}
func TestLoadKeys(t *testing.T) {
@ -40,7 +40,7 @@ func TestLoadKeys(t *testing.T) {
err = ioutil.WriteFile(tempPath, []byte("[\"8df20e6961a16398b85a\"]"), os.ModePerm)
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.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")
}