diff --git a/pkcs12/crypto.go b/pkcs12/crypto.go index 212538c..3f30732 100644 --- a/pkcs12/crypto.go +++ b/pkcs12/crypto.go @@ -80,6 +80,10 @@ func pbDecrypterFor(algorithm pkix.AlgorithmIdentifier, password []byte) (cipher return nil, 0, err } + if params.Iterations < 0 || params.Iterations > maxIterations { + return nil, 0, NotImplementedError("iteration count is invalid or too high") + } + key := cipherType.deriveKey(params.Salt, password, params.Iterations) iv := cipherType.deriveIV(params.Salt, password, params.Iterations) diff --git a/pkcs12/crypto_test.go b/pkcs12/crypto_test.go index eb4dae8..29962ec 100644 --- a/pkcs12/crypto_test.go +++ b/pkcs12/crypto_test.go @@ -13,6 +13,46 @@ import ( var sha1WithTripleDES = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1, 12, 1, 3}) +func TestPbDecrypterForIterationLimit(t *testing.T) { + pass, _ := bmpString("Sesame open") + + tests := []struct { + name string + iterations int + wantErr bool + }{ + {"at limit", maxIterations, false}, + {"over limit", maxIterations + 1, true}, + {"negative", -1, true}, + {"max int", 1<<31 - 1, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params, _ := asn1.Marshal(pbeParams{ + Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + Iterations: tt.iterations, + }) + alg := pkix.AlgorithmIdentifier{ + Algorithm: sha1WithTripleDES, + Parameters: asn1.RawValue{ + FullBytes: params, + }, + } + _, _, err := pbDecrypterFor(alg, pass) + if tt.wantErr { + if _, ok := err.(NotImplementedError); !ok { + t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err) + } + } else { + if err != nil { + t.Errorf("iterations=%d: unexpected error: %v", tt.iterations, err) + } + } + }) + } +} + func TestPbDecrypterFor(t *testing.T) { params, _ := asn1.Marshal(pbeParams{ Salt: []byte{1, 2, 3, 4, 5, 6, 7, 8}, diff --git a/pkcs12/mac.go b/pkcs12/mac.go index 5f38aa7..84039ce 100644 --- a/pkcs12/mac.go +++ b/pkcs12/mac.go @@ -27,11 +27,19 @@ var ( oidSHA1 = asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26}) ) +// maxIterations is a safety limit to prevent CPU exhaustion from +// crafted PKCS#12 files with unreasonable iteration counts. +const maxIterations = 1 << 20 // ~1 million + func verifyMac(macData *macData, message, password []byte) error { if !macData.Mac.Algorithm.Algorithm.Equal(oidSHA1) { return NotImplementedError("unknown digest algorithm: " + macData.Mac.Algorithm.Algorithm.String()) } + if macData.Iterations < 0 || macData.Iterations > maxIterations { + return NotImplementedError("iteration count is invalid or too high") + } + key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password, macData.Iterations, 3, 20) mac := hmac.New(sha1.New, key) diff --git a/pkcs12/mac_test.go b/pkcs12/mac_test.go index 1ed4ff2..5d4a7ec 100644 --- a/pkcs12/mac_test.go +++ b/pkcs12/mac_test.go @@ -5,10 +5,52 @@ package pkcs12 import ( + "crypto/x509/pkix" "encoding/asn1" "testing" ) +func TestVerifyMacIterationLimit(t *testing.T) { + password, _ := bmpString("Sesame open") + message := []byte{11, 12, 13, 14, 15} + + tests := []struct { + name string + iterations int + wantErr bool + }{ + {"at limit", maxIterations, false}, + {"over limit", maxIterations + 1, true}, + {"negative", -1, true}, + {"max int", 1<<31 - 1, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + td := macData{ + Mac: digestInfo{ + Algorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidSHA1, + }, + Digest: nil, // will fail MAC check, but iteration check comes first + }, + MacSalt: []byte{1, 2, 3, 4, 5, 6, 7, 8}, + Iterations: tt.iterations, + } + err := verifyMac(&td, message, password) + if tt.wantErr { + if _, ok := err.(NotImplementedError); !ok { + t.Errorf("iterations=%d: got %v, want NotImplementedError", tt.iterations, err) + } + } else { + if _, ok := err.(NotImplementedError); ok { + t.Errorf("iterations=%d: got unexpected NotImplementedError", tt.iterations) + } + } + }) + } +} + func TestVerifyMac(t *testing.T) { td := macData{ Mac: digestInfo{