Skip to content

Commit

Permalink
feat(jks-check): added config to strictly check for key in jks
Browse files Browse the repository at this point in the history
  • Loading branch information
utsavmaniyar committed Jan 22, 2025
1 parent c0d2c3c commit 7ce4e75
Show file tree
Hide file tree
Showing 13 changed files with 890 additions and 3 deletions.
2 changes: 1 addition & 1 deletion config/rules/filename.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ rules:
SolutionID: 11
Severity: 2
Confidence: 2
Postprocess: ''
Postprocess: jks
CWE:
- CWE-312
- CWE-321
Expand Down
2 changes: 2 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Usage of go-earlybird:
Skip scanning comments in files -- applies only to the 'content' module
-stream
Use stream IO as input instead of file(s)
-strict-jks
Checks for private keys in the JKS file and only return finding if found. If not passed, it will flag jks file. Default is false.
-suppress
Suppress reporting of the secret found (important if output is going to Slack or other logs)
-update
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/americanexpress/earlybird/v4

go 1.23
go 1.23.5

require (
code.sajari.com/docconv v1.3.8
Expand Down
1 change: 1 addition & 0 deletions pkg/config/structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ type EarlybirdConfig struct {
WorkerCount int
WorkLength int
HideMeta bool
StrictJKS bool
ModuleConfigs ModuleConfigs
AdjustedSeverityCategories []AdjustedSeverityCategory
}
1 change: 1 addition & 0 deletions pkg/core/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var (
ptrGitStreamInput = flag.Bool("git-commit-stream", false, "Use stream IO of Git commit log as input instead of file(s) -- e.g., 'cat secrets.text > go-earlybird'")
ptrVerbose = flag.Bool("verbose", false, "Reports details about file reads")
ptrSuppressSecret = flag.Bool("suppress", false, "Suppress reporting of the secret found (important if output is going to Slack or other logs)")
ptrStrictJKS = flag.Bool("strict-jks", false, "Checks for private keys in the JKS file and return hits only if found")
ptrWorkerCount = flag.Int("workers", 100, "Set number of workers.")
ptrWorkLength = flag.Int("worksize", 2500, "Set Line Wrap Length.")
ptrMaxFileSize = flag.Int64("max-file-size", 10240000, "Maximum file size to scan (in bytes)")
Expand Down
1 change: 1 addition & 0 deletions pkg/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ func (eb *EarlybirdCfg) ConfigInit() {
eb.Config.MaxFileSize = *ptrMaxFileSize
eb.Config.VerboseEnabled = *ptrVerbose
eb.Config.Suppress = *ptrSuppressSecret
eb.Config.StrictJKS = *ptrStrictJKS
eb.Config.OutputFormat = *ptrOutputFormat
eb.Config.WithConsole = *ptrWithConsole
eb.Config.OutputFile = *ptrOutputFile
Expand Down
160 changes: 160 additions & 0 deletions pkg/jks/jks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
Package jks provides routines for manipulating Java Keystore files.
*/
package jks

import (
"crypto/sha1"
"crypto/x509"
"time"
"unicode/utf16"
)

const (
// MagicNumber is written at the start of each .jks file.
MagicNumber uint32 = 0xFEEDFEED

// DigestSeparator is used to build the file's verification digest. The
// digest is over the keystore password encoded as UTF-16, then this
// string (yes, really — check the OpenJDK source) encoded as UTF-8, and
// then the actual file data.
DigestSeparator = "Mighty Aphrodite"

// CertType is the certificate type string that is encoded into each
// certificate's header in the keystore.
CertType = "X.509"
)

// Keystore represents a single JKS file. It holds a list of certificates and a
// list of keypairs (private keys with associated certificate chains).
type Keystore struct {
// Certs is a list of CA certificates to trust. It may contain either
// root or intermediate CA certificates. It should not contain end-user
// certificates.
Certs []*Cert

// Keypairs is a list of private keys. Each key may have a certificate
// chain associated with it.
Keypairs []*Keypair
}

// Options for manipulating a keystore. These allow the caller to specify the
// password(s) used, or to skip the digest verification if the password is
// unknown.
type Options struct {
// Password is used as part of a SHA-1 digest over the .jks file.
Password string

// SkipVerifyDigest can be set to skip digest verification when loading
// a keystore file. This will inhibit errors from Parse if you don't
// know the password.
SkipVerifyDigest bool

// KeyPasswords are used to generate the "encryption" keys for stored
// private keys. The map's key is the alias of the private key, and the
// value is the password. If there is no entry in the map for a given
// alias, then the top-level Password is inherited. Empty strings are
// interpreted as an empty password, so use delete() if you truly want
// to delete values.
KeyPasswords map[string]string
}

// Cert holds a certificate to trust.
type Cert struct {
// Alias is a name used to refer to this certificate.
Alias string

// Timestamp records when this record was created.
Timestamp time.Time

// Raw is the raw X.509 certificate marshalled in DER form.
Raw []byte

// CertErr is set if there is an error parsing the certificate.
CertErr error

// Cert is the parsed X.509 certificate.
Cert *x509.Certificate
}

// Keypair holds a private key and an associated certificate chain.
type Keypair struct {
// Alias is a name used to refer to this keypair.
Alias string

// Timestamp records when this record was created.
Timestamp time.Time

// PrivKeyErr is set if an error is encountered during decryption or
// unmarshalling of the decrypted key.
PrivKeyErr error

// EncryptedKey is the raw PKCS#8 marshalled EncryptedPrivateKeyInfo.
EncryptedKey []byte

// RawKey is the raw PKCS#8 marshalled PrivateKeyInfo, after it has
// been decrypted. It will not have been set if decryption failed.
RawKey []byte

// PrivateKey is the unmarshalled private key. It will not have been
// set if decryption failed or if unmarshalling failed.
PrivateKey interface{}

// CertChain is a chain of certificates associated with the private key.
// The first entry in the chain (index 0) should correspond to
// PrivateKey; there should then follow any intermediate CAs. In
// general the root CA should not be part of the chain.
CertChain []*KeypairCert
}

// KeypairCert is an entry in the certificate chain associated with a Keypair.
type KeypairCert struct {
// Raw X.509 certificate data (in DER form).
Raw []byte

// Cert is the parsed X.509 certificate. It is nil if the certificate
// could not be parsed.
Cert *x509.Certificate

// CertErr records any error encountered while parsing a certificate.
CertErr error
}

var defaultOptions = Options{
SkipVerifyDigest: true,
}

// ComputeDigest performs the custom hash function over the given file data.
// DO NOT RE-USE THIS CODE: this is an atrocious way to perform message
// authentication. Use the HMAC example from
// https://github.com/lwithers/go-crypto-examples instead. Note this construct
// is vulnerable to a length extension attack, which is actually exploitable if
// the JKS reader code does not properly check the "number of entries" value.
func ComputeDigest(raw []byte, passwd string) []byte {
// compute SHA-1 digest over the construct:
// UTF-16(password) + UTF-8(DigestSeparator) + raw
md := sha1.New()
p := PasswordUTF16(passwd)
md.Write(p)
md.Write([]byte(DigestSeparator))
md.Write(raw)
return md.Sum(nil)
}

// PasswordUTF16 returns a password encoded in UTF-16, big-endian byte order.
func PasswordUTF16(passwd string) []byte {
var u []byte
for _, r := range passwd {
if r < 0x10000 {
u = append(u, byte((r>>8)&0xFF))
u = append(u, byte(r&0xFF))
} else {
r1, r2 := utf16.EncodeRune(r)
u = append(u, byte((r1>>8)&0xFF))
u = append(u, byte(r1&0xFF))
u = append(u, byte((r2>>8)&0xFF))
u = append(u, byte(r2&0xFF))
}
}
return u
}
70 changes: 70 additions & 0 deletions pkg/jks/jks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package jks

import (
"bytes"
"encoding/binary"
"encoding/hex"
"testing"
"unicode/utf16"
)

// TestComputeDigest is a regression test for the digest function.
func TestComputeDigest(t *testing.T) {
t.Run("empty", testComputeDigest("", "",
"569D05A766C473698C0B58EBAEAE0A25EB10BACC"))
t.Run("regr", testComputeDigest("input data", "password",
"74DDD13B68919674D4409A19AB284019A1DA57C8"))
}

func testComputeDigest(in, passwd, expHex string) func(*testing.T) {
return func(t *testing.T) {
exp, err := hex.DecodeString(expHex)
if err != nil {
t.Fatalf("error decoding expHex: %v", err)
}
out := ComputeDigest([]byte(in), passwd)
if !bytes.Equal(out, exp) {
t.Errorf("output sequence (len %d) ≠ expected",
len(out))
t.Errorf("out %X", out)
}
}
}

// TestPasswordUTF16 checks that our UTF-16 encoding routine works as expected.
// The test cases incorporate empty strings and Unicode strings with characters
// outside the BMP (basic multilingual plane), i.e. ones that need encoding as
// UTF-16 surrogate pairs.
func TestPasswordUTF16(t *testing.T) {
t.Run("empty", testPasswordBytes("", nil))
t.Run("ascii-1", testPasswordBytes("ascii",
[]byte{0, 'a', 0, 's', 0, 'c', 0, 'i', 0, 'i'}))
t.Run("ascii-2", testPasswordUTF16("ascii"))
t.Run("utf8", testPasswordUTF16("a≤b"))
t.Run("surrogate", testPasswordUTF16("z1\U00016000\u2340•—@.µ"))
}

func testPasswordBytes(in string, exp []byte) func(*testing.T) {
return func(t *testing.T) {
out := PasswordUTF16(in)
if !bytes.Equal(out, exp) {
t.Errorf("output sequence ‘%X’ ≠ expected ‘%X’",
out, exp)
}
}
}

func testPasswordUTF16(in string) func(*testing.T) {
return func(t *testing.T) {
out := PasswordUTF16(in)
expStr := utf16.Encode([]rune(in))
exp := make([]byte, len(expStr)*2)
for i, v := range expStr {
binary.BigEndian.PutUint16(exp[i*2:], v)
}
if !bytes.Equal(out, exp) {
t.Errorf("output sequence ‘%X’ ≠ expected ‘%X’",
out, exp)
}
}
}
Loading

0 comments on commit 7ce4e75

Please sign in to comment.