diff --git a/.travis.yml b/.travis.yml index 4ba0506..3b25182 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,3 +14,6 @@ install: before_script: - make check + +script: + - make test diff --git a/Makefile b/Makefile index 02e796e..1dee878 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ depends: build: go build . +test: + go test -v `glide novendor` + check: - go vet -v . + go vet -v `glide novendor` !(gofmt -d $(SOURCE_FILES) | grep .) diff --git a/jceks/README.md b/jceks/README.md new file mode 100644 index 0000000..5744b72 --- /dev/null +++ b/jceks/README.md @@ -0,0 +1,7 @@ +# JCEKS + +Package jceks parses JCEKS (Java Cryptogaphy Extension Key Store) +files and extracts keys and certificates. This module only implements +a fraction of the JCEKS cryptographic protocols. In particular, it +implements the SHA1 signature verification of the key store and the +PBEWithMD5AndDES3CBC cipher for encrypting private keys. diff --git a/jceks/jceks.go b/jceks/jceks.go new file mode 100644 index 0000000..689d8dc --- /dev/null +++ b/jceks/jceks.go @@ -0,0 +1,426 @@ +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package jceks parses JCEKS (Java Cryptogaphy Extension Key Store) +// files and extracts keys and certificates. This module only implements +// a fraction of the JCEKS cryptographic protocols. In particular, it +// implements the SHA1 signature verification of the key store and the +// PBEWithMD5AndDES3CBC cipher for encrypting private keys. +package jceks + +import ( + "bytes" + "crypto/rsa" + "crypto/sha1" + "crypto/subtle" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/binary" + "fmt" + "hash" + "io" + "os" + "time" +) + +const ( + jceksMagic = 0xcececece + jceksVersion = 0x02 + jksMagic = 0xfeedfeed +) + +var ( + oidKeyProtector = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 42, 2, 17, 1, 1} + oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} +) + +type encryptedPrivateKeyInfo struct { + Algo pkix.AlgorithmIdentifier + EncryptedKey []byte +} + +type privateKeyInfo struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte +} + +type privateKeyEntry struct { + date time.Time + encodedKey []byte + certs []*x509.Certificate +} + +func (e *privateKeyEntry) String() string { + return fmt.Sprintf("private-key: %s", e.date) +} + +func (e *privateKeyEntry) Recover(password []byte) (*rsa.PrivateKey, error) { + var eKey encryptedPrivateKeyInfo + _, err := asn1.Unmarshal(e.encodedKey, &eKey) + if err != nil { + return nil, err + } + var decryptedKey []byte + switch { + case eKey.Algo.Algorithm.Equal(oidPBEWithMD5AndDES3CBC): + decryptedKey, err = recoverPBEWithMD5AndDES3CBC(eKey.Algo, eKey.EncryptedKey, password) + case eKey.Algo.Algorithm.Equal(oidKeyProtector): + // JavaSoft proprietary key-protection algorithm (used to protect + // private keys in the keystore implementation that comes with JDK + // 1.2). We shouldn't need this. + fallthrough + default: + return nil, fmt.Errorf("unsupported encrypted-private-key algorithm: %v", eKey.Algo.Algorithm) + } + if err != nil { + return nil, err + } + + var pKey privateKeyInfo + if _, err := asn1.Unmarshal(decryptedKey, &pKey); err != nil { + return nil, err + } + if !pKey.Algo.Algorithm.Equal(oidPublicKeyRSA) { + return nil, fmt.Errorf("unsupported private-key algorithm: %v", pKey.Algo.Algorithm) + } + return x509.ParsePKCS1PrivateKey(pKey.PrivateKey) +} + +type trustedCertEntry struct { + date time.Time + cert *x509.Certificate +} + +func (e *trustedCertEntry) String() string { + return fmt.Sprintf("trusted-cert: %s", e.date) +} + +// KeyStore maintains a map from alias name to the entry for that +// alias. Entries are currently either privateKeyEntry or +// trustedCertEntry. +type KeyStore struct { + entries map[string]interface{} +} + +// readUTF reads a java encoded UTF-8 string. The encoding provides a +// 2-byte prefix indicating the length of the string. +func readUTF(r io.Reader) (string, error) { + var length uint16 + err := binary.Read(r, binary.BigEndian, &length) + if err != nil { + return "", err + } + buf := make([]byte, length) + _, err = io.ReadFull(r, buf) + if err != nil { + return "", err + } + return string(buf), nil +} + +// readBytes reads a byte array from the reader. The encoding provides +// a 4-byte prefix indicating the number of bytes which follow. +func readBytes(r io.Reader) ([]byte, error) { + length, err := readInt32(r) + if err != nil { + return nil, err + } + buf := make([]byte, length) + _, err = io.ReadFull(r, buf) + if err != nil { + return nil, err + } + return buf, nil +} + +func readInt32(r io.Reader) (int32, error) { + var v int32 + err := binary.Read(r, binary.BigEndian, &v) + return v, err +} + +func readUint32(r io.Reader) (uint32, error) { + var v uint32 + err := binary.Read(r, binary.BigEndian, &v) + return v, err +} + +func readDate(r io.Reader) (time.Time, error) { + var v int64 + err := binary.Read(r, binary.BigEndian, &v) + if err != nil { + return time.Time{}, err + } + sec := v / 1000 + nsec := (v - sec*1000) * 1000 * 1000 + return time.Unix(sec, nsec), nil +} + +// Returns a SHA1 hash which has been pre-keyed with the specified +// password according to the JCEKS algorithm. +func getPreKeyedHash(password []byte) hash.Hash { + md := sha1.New() + buf := make([]byte, len(password)*2) + for i := 0; i < len(password); i++ { + buf[i*2+1] = password[i] + } + md.Write(buf) + // Yes, "Mighty Aprhodite" is a constant used by this method. + md.Write([]byte("Mighty Aphrodite")) + return md +} + +func parseHeader(r io.Reader) (uint32, error) { + magic, err := readUint32(r) + if err != nil { + return 0, err + } + if magic != jceksMagic && magic != jksMagic { + return 0, fmt.Errorf("unexpected magic: %08x != (%08x || %08x)", magic, uint32(jceksMagic), uint32(jksMagic)) + } + version, err := readUint32(r) + if err != nil { + return 0, err + } + return version, nil +} + +func (ks *KeyStore) parsePrivateKey(r io.Reader) error { + alias, err := readUTF(r) + if err != nil { + return err + } + entry := &privateKeyEntry{ + certs: []*x509.Certificate{}, + } + entry.date, err = readDate(r) + if err != nil { + return err + } + entry.encodedKey, err = readBytes(r) + if err != nil { + return err + } + nCerts, err := readInt32(r) + if err != nil { + return err + } + for j := 0; j < int(nCerts); j++ { + certType, err := readUTF(r) + if err != nil { + return err + } + if certType != "X.509" { + return fmt.Errorf("unable to handle certificate type: %s", certType) + } + certBytes, err := readBytes(r) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil + } + entry.certs = append(entry.certs, cert) + } + ks.entries[alias] = entry + return nil +} + +func (ks *KeyStore) parseTrustedCert(r io.Reader) error { + alias, err := readUTF(r) + if err != nil { + return err + } + entry := &trustedCertEntry{} + entry.date, err = readDate(r) + if err != nil { + return err + } + + certType, err := readUTF(r) + if err != nil { + return err + } + if certType != "X.509" { + return fmt.Errorf("unable to handle certificate type: %s", certType) + } + certBytes, err := readBytes(r) + if err != nil { + return err + } + entry.cert, err = x509.ParseCertificate(certBytes) + if err != nil { + return nil + } + + ks.entries[alias] = entry + return nil +} + +// Parse parses the key store from the specified reader. +func (ks *KeyStore) Parse(r io.Reader, password []byte) error { + var md hash.Hash + if password != nil { + md = getPreKeyedHash(password) + r = io.TeeReader(r, md) + } + + version, err := parseHeader(r) + if err != nil { + return err + } + if version != jceksVersion { + return fmt.Errorf("unexpected version: %d != %d", version, jceksVersion) + } + + count, err := readInt32(r) + if err != nil { + return err + } + for i := 0; i < int(count); i++ { + tag, err := readInt32(r) + if err != nil { + return err + } + switch tag { + case 1: + // Private-key entry + err := ks.parsePrivateKey(r) + if err != nil { + return err + } + case 2: + // Trusted-cert entry + err := ks.parseTrustedCert(r) + if err != nil { + return err + } + case 3: + // Secret-key entry + fallthrough + default: + panic(fmt.Errorf("unimplemented tag: %d", tag)) + } + } + + if md != nil { + computed := md.Sum([]byte{}) + actual := make([]byte, len(computed)) + _, err := io.ReadFull(r, actual) + if err != nil { + return err + } + if subtle.ConstantTimeCompare(computed, actual) != 1 { + return fmt.Errorf("keystore was tampered with or password was incorrect") + } + } + + return nil +} + +// GetPrivateKeyAndCerts retrieves the specified private key. Returns +// nil if the private key does not exist or alias points to a non +// private key entry. +func (ks *KeyStore) GetPrivateKeyAndCerts(alias string, password []byte) ( + key *rsa.PrivateKey, certs []*x509.Certificate, err error) { + + entry := ks.entries[alias] + if entry == nil { + return + } + switch t := entry.(type) { + case *privateKeyEntry: + if len(t.certs) < 1 { + return nil, nil, fmt.Errorf("key has no certificates") + } + if (t.certs[0].KeyUsage & x509.KeyUsageDigitalSignature) == 0 { + return nil, nil, fmt.Errorf("key cannot be used for digital signatures: %x", t.certs[0].KeyUsage) + } + key, err = t.Recover(password) + if err == nil { + certs = t.certs + return + } + } + return +} + +// GetCert retrieves the specified certificate. Returns nil if the +// certificate does not exist or alias points to a non certificate +// entry. +func (ks *KeyStore) GetCert(alias string) (*x509.Certificate, error) { + entry := ks.entries[alias] + if entry == nil { + return nil, nil + } + switch t := entry.(type) { + case *trustedCertEntry: + return t.cert, nil + } + return nil, nil +} + +// ListPrivateKeys lists the names of the private keys stored in the key store. +func (ks *KeyStore) ListPrivateKeys() []string { + var r []string + for k, v := range ks.entries { + if _, ok := v.(*privateKeyEntry); ok { + r = append(r, k) + } + } + return r +} + +// ListCerts lists the names of the certs stored in the key store. +func (ks *KeyStore) ListCerts() []string { + var r []string + for k, v := range ks.entries { + if _, ok := v.(*trustedCertEntry); ok { + r = append(r, k) + } + } + return r +} + +func (ks *KeyStore) String() string { + var buf bytes.Buffer + for k, v := range ks.entries { + fmt.Fprintf(&buf, "%s\n", k) + fmt.Fprintf(&buf, " %s\n", v) + } + return buf.String() +} + +// Load loads the key store from the specified file. +func Load(filename string, password []byte) (*KeyStore, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + ks := &KeyStore{ + entries: make(map[string]interface{}), + } + err = ks.Parse(file, password) + if err != nil { + return nil, err + } + return ks, err +} diff --git a/jceks/jceks_test.go b/jceks/jceks_test.go new file mode 100644 index 0000000..b6a821b --- /dev/null +++ b/jceks/jceks_test.go @@ -0,0 +1,234 @@ +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jceks + +import ( + "bytes" + "crypto/rsa" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "reflect" + "testing" +) + +var ( + generateTestData = flag.Bool("generate", false, "generate test data") +) + +type testData struct { + certFilename string + keyFilename string + p12Filename string + jceksFilename string + storePassword string + keyPassword string + alias string +} + +func newTestData(prefix string) *testData { + return &testData{ + certFilename: "testdata/" + prefix + ".crt", + keyFilename: "testdata/" + prefix + ".key", + p12Filename: "testdata/" + prefix + ".p12", + jceksFilename: "testdata/" + prefix + ".jceks", + storePassword: prefix + "-store-password", + keyPassword: prefix + "-key-password", + alias: prefix + "-some-alias", + } +} + +func (d *testData) cleanup() { + os.Remove(d.certFilename) + os.Remove(d.keyFilename) + os.Remove(d.p12Filename) + os.Remove(d.jceksFilename) +} + +func runCommand(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + err := cmd.Run() + out := buf.Bytes() + if err != nil { + return "", errors.New(string(out)) + } + return string(out), nil +} + +func (d *testData) generatePrivateKeyAndCert(t *testing.T) { + _, err := runCommand("openssl", "req", "-x509", + "-nodes", "-days", "365", "-newkey", "rsa:2048", + "-subj", "/CN=Test User/O=Test Organization/C=US", + "-extensions", "v3_req", + "-keyout", d.keyFilename, + "-out", d.certFilename) + if err != nil { + t.Fatal(err) + } +} + +func (d *testData) convertPrivateKeyAndCertToPkcs12(t *testing.T) { + _, err := runCommand("openssl", "pkcs12", "-export", + "-in", d.certFilename, + "-inkey", d.keyFilename, + "-name", d.alias, + "-out", d.p12Filename, + "-passout", fmt.Sprintf("pass:%s", d.storePassword)) + if err != nil { + t.Fatal(err) + } +} + +func (d *testData) convertPkcs12ToJceks(t *testing.T) { + _, err := runCommand("keytool", "-importkeystore", + "-alias", d.alias, + "-destkeypass", d.keyPassword, + "-destkeystore", d.jceksFilename, + "-deststorepass", d.storePassword, + "-srckeystore", d.p12Filename, + "-srcstoretype", "PKCS12", + "-srcstorepass", d.storePassword, + "-storetype", "JCEKS") + if err != nil { + t.Fatal(err) + } +} + +func (d *testData) importCertToJceks(t *testing.T) { + _, err := runCommand("keytool", "-importcert", "-noprompt", + "-alias", d.alias, + "-file", d.certFilename, + "-keystore", d.jceksFilename, + "-storepass", d.storePassword, + "-storetype", "JCEKS") + if err != nil { + t.Fatal(err) + } +} + +func equalRSAPublicKey(a, b *rsa.PublicKey) bool { + if a.E != b.E { + return false + } + return a.N.Cmp(b.N) == 0 +} + +func equalRSAPrivateKey(a, b *rsa.PrivateKey) bool { + if !equalRSAPublicKey(&a.PublicKey, &b.PublicKey) { + return false + } + if a.D.Cmp(b.D) != 0 { + return false + } + if len(a.Primes) != len(b.Primes) { + return false + } + for i := 0; i < len(a.Primes); i++ { + if a.Primes[i].Cmp(b.Primes[i]) != 0 { + return false + } + } + return true +} + +func TestPrivateKey(t *testing.T) { + d := newTestData("private-key") + if *generateTestData { + d.cleanup() + d.generatePrivateKeyAndCert(t) + d.convertPrivateKeyAndCertToPkcs12(t) + d.convertPkcs12ToJceks(t) + } + + ks, err := Load(d.jceksFilename, []byte(d.storePassword)) + if err != nil { + t.Fatal(err) + } + key, certs, err := ks.GetPrivateKeyAndCerts(d.alias, []byte(d.keyPassword)) + if err != nil { + t.Fatal(err) + } + if key == nil { + t.Fatal("unable to load key") + } + + expected, err := LoadPEMKey(d.keyFilename) + if err != nil { + t.Fatal(err) + } + if !equalRSAPrivateKey(key, expected) { + t.Fatalf("keys are not equal") + } + + if len(certs) != 1 { + t.Fatalf("unexpected number of certs: %d != 1", len(certs)) + } + + expectedCert, err := LoadPEMCert(d.certFilename) + if err != nil { + t.Fatal(err) + } + + if !certs[0].Equal(expectedCert) { + t.Fatalf("certs are not equal") + } + + keyAliases := ks.ListPrivateKeys() + if !reflect.DeepEqual(keyAliases, []string{d.alias}) { + t.Fatalf("unexpected private key aliases: %s", keyAliases) + } +} + +func TestTrustedCert(t *testing.T) { + d := newTestData("trusted-cert") + if *generateTestData { + d.cleanup() + d.generatePrivateKeyAndCert(t) + d.importCertToJceks(t) + } + + ks, err := Load(d.jceksFilename, []byte(d.storePassword)) + if err != nil { + t.Fatal(err) + } + cert, err := ks.GetCert(d.alias) + if err != nil { + t.Fatal(err) + } + if cert == nil { + t.Fatal("unable to load cert") + } + + expectedCert, err := LoadPEMCert(d.certFilename) + if err != nil { + t.Fatal(err) + } + + if !cert.Equal(expectedCert) { + t.Fatalf("certs are not equal") + } + + certAliases := ks.ListCerts() + if !reflect.DeepEqual(certAliases, []string{d.alias}) { + t.Fatalf("unexpected cert aliases: %s", certAliases) + } +} diff --git a/jceks/pbe.go b/jceks/pbe.go new file mode 100644 index 0000000..d79d197 --- /dev/null +++ b/jceks/pbe.go @@ -0,0 +1,114 @@ +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jceks + +import ( + "bytes" + "crypto/cipher" + "crypto/des" + "crypto/md5" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" +) + +var ( + oidPBEWithMD5AndDES3CBC = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 42, 2, 19, 1} +) + +type pbeParameters struct { + Salt []byte + Iterations int +} + +// Here's how this algorithm works: +// +// 1. Split salt in two halves. If the two halves are identical, +// invert one of them. +// 2. Concatenate password with each of the halves. +// 3. Digest each concatenation with c iterations, where c is the +// iterationCount. Concatenate the output from each digest round with the +// password, and use the result as the input to the next digest operation. +// The digest algorithm is MD5. +// 4. After c iterations, use the 2 resulting digests as follows: +// The 16 bytes of the first digest and the 1st 8 bytes of the 2nd digest +// form the triple DES key, and the last 8 bytes of the 2nd digest form the +// IV. +func recoverPBEWithMD5AndDES3CBC( + algo pkix.AlgorithmIdentifier, encryptedKey, password []byte) ([]byte, error) { + var params pbeParameters + if _, err := asn1.Unmarshal(algo.Parameters.FullBytes, ¶ms); err != nil { + return nil, err + } + + // Convert password to byte array, so that it can be digested. + passwdBytes := make([]byte, len(password)) + for i := 0; i < len(password); i++ { + passwdBytes[i] = password[i] & 0x7f + } + + salt := params.Salt + if len(salt) != 8 { + return nil, fmt.Errorf("unexpected salt length: %d", len(salt)) + } + + if bytes.Compare(salt[0:4], salt[4:]) == 0 { + // First and second half of salt are equal, invert first half. + for i := 0; i < 2; i++ { + salt[i], salt[3-i] = salt[3-i], salt[i] + } + } + + const keyLen = 24 + const blockSize = des.BlockSize + derivedKey := make([]byte, keyLen+blockSize) + // Now digest each half (concatenated with password). For each + // half, go through the loop as many times as specified by the + // iteration count parameter (inner for loop). Concatenate the + // output from each digest round with the password, and use the + // result as the input to the next digest operation. + md := md5.New() + for i := 0; i < 2; i++ { + n := len(salt) / 2 + toBeHashed := salt[i*n : (i+1)*n] + for j := 0; j < params.Iterations; j++ { + md.Write(toBeHashed) + md.Write(passwdBytes) + toBeHashed = md.Sum([]byte{}) + md.Reset() + } + copy(derivedKey[i*len(toBeHashed):], toBeHashed) + } + + cipherKey := derivedKey[0:keyLen] + iv := derivedKey[keyLen:] + + des3, err := des.NewTripleDESCipher(cipherKey) + if err != nil { + return nil, err + } + + decrypter := cipher.NewCBCDecrypter(des3, iv) + if (len(encryptedKey) % decrypter.BlockSize()) != 0 { + return nil, fmt.Errorf("encrypted data must be a multiple of block length: %d %d", + len(encryptedKey), decrypter.BlockSize()) + } + + decryptedKey := make([]byte, len(encryptedKey)) + decrypter.CryptBlocks(decryptedKey, encryptedKey) + return decryptedKey, nil +} diff --git a/jceks/testdata/private-key.crt b/jceks/testdata/private-key.crt new file mode 100644 index 0000000..ce336ff --- /dev/null +++ b/jceks/testdata/private-key.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIJALRGD3GBYHSZMA0GCSqGSIb3DQEBBQUAMD0xEjAQBgNV +BAMTCVRlc3QgVXNlcjEaMBgGA1UEChMRVGVzdCBPcmdhbml6YXRpb24xCzAJBgNV +BAYTAlVTMB4XDTE0MDMxNDE0MTA0NFoXDTE1MDMxNDE0MTA0NFowPTESMBAGA1UE +AxMJVGVzdCBVc2VyMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjELMAkGA1UE +BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDjX4e65+mxX5Ha +SbSHwluBy9v7+pJrOuEco+w1dFjw7g5ijjB/5HW72N9J8LfQrEAMX9FuMue7lE1y +GcgL9Kepuz71jPjyjrHpFNa90dKzrhwkQ6f9aLkKd5D+84ua7/O7/l5yiuD+slvm +dppj+WxfWfQ58bgFjoxwkt2voYa/+pYK52CkykaQ0jtfEIdbftRAT2rLVSD6UljT +9t54xUL80CHgog8nlySTFdpPnTQ2jpYBDziRpO6Agjz6zjOo2tiY+yo/2QL3rhzQ +e8zYVjH+jZ3hKAeQp5kOTdfjkxJzQe3LLm0pQCkaWhglcnZhSqdErVScNmv87nR8 +CEXXK9O/AgMBAAGjGjAYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMA0GCSqGSIb3 +DQEBBQUAA4IBAQAzbdBJFsYq2Rrh0xwHuYTcA/NesrV1wC36kCcVbFI3JVqZ2lZz +MlA89mJ8oOfH2Cg0hB9XlQoysdYiKXwl9tJP6lOimSp2nSn0NOKBaSvvpUyMBocn +29L1IPm6BrauGGDhKxByA3sPc/FSZnCx2p1OO9HG3vAprYJppiiOSTwuRHO4+PBj +gowPrvXGucKHfNlekxN1D6dYi+Zg+61pA/6qDJ+ZKXVECE3NN3H/o6WuOIB5tFVE +tCJNSxo/8KnrU1TMG1e9EvcL3kq3rmXEID6pwmxqg3piKg3Dar/GkpxGmdB306UF +HnRjsjYEj+HWOpfOy1vPbbKMXtC3TayPyGZ6 +-----END CERTIFICATE----- diff --git a/jceks/testdata/private-key.jceks b/jceks/testdata/private-key.jceks new file mode 100644 index 0000000..34c25b9 Binary files /dev/null and b/jceks/testdata/private-key.jceks differ diff --git a/jceks/testdata/private-key.key b/jceks/testdata/private-key.key new file mode 100644 index 0000000..de791dd --- /dev/null +++ b/jceks/testdata/private-key.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA41+HuufpsV+R2km0h8Jbgcvb+/qSazrhHKPsNXRY8O4OYo4w +f+R1u9jfSfC30KxADF/RbjLnu5RNchnIC/Snqbs+9Yz48o6x6RTWvdHSs64cJEOn +/Wi5CneQ/vOLmu/zu/5ecorg/rJb5naaY/lsX1n0OfG4BY6McJLdr6GGv/qWCudg +pMpGkNI7XxCHW37UQE9qy1Ug+lJY0/beeMVC/NAh4KIPJ5ckkxXaT500No6WAQ84 +kaTugII8+s4zqNrYmPsqP9kC964c0HvM2FYx/o2d4SgHkKeZDk3X45MSc0Htyy5t +KUApGloYJXJ2YUqnRK1UnDZr/O50fAhF1yvTvwIDAQABAoIBAGnboCW0s2iRRiaL +CjHqmw/jCZhXILQrxYLADskUUhRZwPjBmnLwup+qaMrT98B/cZJRSgA0Uw9SDHyC +5FAsp6KuOrG34G+NX+dUfGYDukVNWmzH0v54My5cXHVWjjikXqW31+EcJ4RtJbw2 +m8rP5VS/XKVdlH+BzoDa37tVSJc4AraA3XHaU6ebuWojSiHJ3T1L1gTZKmOIL2dw +ETIgxKfQTVwceWBmQv61pG5bxqIsmDpeCWjeULchSvuiHG0xRfzJYewhv8LKVpEJ +/bSb5kciAm+nnigme2kjsp05AGmrncv7XYRrn1caJc30kwGsbv5Y7UJ9ZG5amlL6 +ltBxSQECgYEA+iqKyEdf/3qORker/WvHEL54mrbfSYoB3B6mr7uKb/JuIOk1yYX5 +Fb0xDavxypBvwAUTNpRjjMRr1HpKggfv4ARudgjBxjD5Sy1TaBypYlB2gVuPRHZv +TLbM7erVQVsg2jC36nQboO8ZiKiSpFlZiAn0ieCxmE9ixATzOJp9lwcCgYEA6Kzq +uX/8BMFLmcHodpAakvxTfv3qGgz0GpSQAmu6bo4qyWuYH4hiBe8PDzCfSfwkhWFr +Q06s9rKseSyAzqF5YBjq8+8bKjZyBaC8m4QEpgocZpwQqKicrnngh7LWiYVNLfw/ +qPfrLc0RmgZistWbES1/6Pm3x+Z49PLyVcUAt4kCgYB4RYO7jjUlCrLkLwkNKYfn +EOvC1jC7llIWldXlnvCLqa4wvG5TmMmMHg07WXNBw/c2BjqafvTtdHGzEahIo7A7 +r2W78bHXqyvvbLcw0rbMwYp33qEedSJFa41SxRgJ99nvjISff3rZAJryDLmTsjFN +KhwbPZ+kbmY5f3e/uuaueQKBgDkfyvkD/QHF2yPCwanqMzwHCxDQkhsXNw8Xjkup +2zmtWb/d1JlZSIega5gVHeZyKx08D7OUq05eC44saOtSJZR8SaLd/1Nbzp6nGecs +gF+rd9GRW12tF9qWPZPTSmy093/kwFRhmbHC+SFRlAXH/6w1+YNfW8mOQgARbYG1 +PjnRAoGBAIbnGfWSLl6jlo1W9CVk8AylY3TFmVygbijjrK3UYL/3KLJn6nb2BeW0 +IsigU5qe+za2zW2KDkw//9vqm0omda66ubwU24uXjKwhZbrAAhmy8kFZNRzpMZIF +Q3RqsGxPxWt7Rk79OBxtnPLWc8nxpGTliOUzgeZqnLy1aSAZnngn +-----END RSA PRIVATE KEY----- diff --git a/jceks/testdata/private-key.p12 b/jceks/testdata/private-key.p12 new file mode 100644 index 0000000..864d609 Binary files /dev/null and b/jceks/testdata/private-key.p12 differ diff --git a/jceks/testdata/trusted-cert.crt b/jceks/testdata/trusted-cert.crt new file mode 100644 index 0000000..33ecb4b --- /dev/null +++ b/jceks/testdata/trusted-cert.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIJANpdtwWQVU6qMA0GCSqGSIb3DQEBBQUAMD0xEjAQBgNV +BAMTCVRlc3QgVXNlcjEaMBgGA1UEChMRVGVzdCBPcmdhbml6YXRpb24xCzAJBgNV +BAYTAlVTMB4XDTE0MDMxNDE0MTA0NVoXDTE1MDMxNDE0MTA0NVowPTESMBAGA1UE +AxMJVGVzdCBVc2VyMRowGAYDVQQKExFUZXN0IE9yZ2FuaXphdGlvbjELMAkGA1UE +BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTX69McLtRMUhP +ZSKzyTee1kY6XUyiHqEapxhKxTkT5HyQaKspIeP60keOiCn6y/stwXK/4fjm4XMx +jDIw2HE6/gwLBXPr3IH3x44XXjK9U3SGSCrFhP0aI4dNxLq7oKhzfR15Wz3pw4zY +q3mtT4muefqjkcVUNTwJyzxA5D5km1Coq7xKZ9Demt71NUcAYOfJswFh97bI8QpK +MfaYbUSWw0um+Mz2vlL20Yq2xWjX+CgclnVWjsf/zrbXkuGyWPE1VbTCzwvdbUHW +8cokI+UBQC/FGIxVq11hGn+Tk8cMubnDLEqRf3oiCGx0wvKixP55YlthRElSaNKt +DmduKH2hAgMBAAGjGjAYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMA0GCSqGSIb3 +DQEBBQUAA4IBAQAY6BFSl2zjhsD2udgb7mgOFgCwzWzCis5+Oy+2H/uNjyeBuSgM +qrh779y+BRapxrqBSYo8QWltapAlRJ8MwIYJpHz2FqN4bY38r3WMZ380wVT/wDYh +6WFB3REVKEkhsW2HWX4cOF8m2iD/WOR2x2T0F5EC4W1nofl/oIhgujDNjmerZEMF +JPnXHGRNOpYtyZhWuo+CFh500vV9Azomcw3z3NJ4teW5Z2T0mmOQWLB1cc1rAVYB +W9uqE5GyHcQ6VHdQHjq3luxOvzH88slQ2S/SC9a41l1Z6EAe8wlVEa17wTQ3PR2g +UHFTtH5OUR9neGEx+G+GqnR7jfRAPZNv0DsH +-----END CERTIFICATE----- diff --git a/jceks/testdata/trusted-cert.jceks b/jceks/testdata/trusted-cert.jceks new file mode 100644 index 0000000..7e25f2f Binary files /dev/null and b/jceks/testdata/trusted-cert.jceks differ diff --git a/jceks/testdata/trusted-cert.key b/jceks/testdata/trusted-cert.key new file mode 100644 index 0000000..ada0c2d --- /dev/null +++ b/jceks/testdata/trusted-cert.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA01+vTHC7UTFIT2Uis8k3ntZGOl1Moh6hGqcYSsU5E+R8kGir +KSHj+tJHjogp+sv7LcFyv+H45uFzMYwyMNhxOv4MCwVz69yB98eOF14yvVN0hkgq +xYT9GiOHTcS6u6Coc30deVs96cOM2Kt5rU+Jrnn6o5HFVDU8Ccs8QOQ+ZJtQqKu8 +SmfQ3pre9TVHAGDnybMBYfe2yPEKSjH2mG1ElsNLpvjM9r5S9tGKtsVo1/goHJZ1 +Vo7H/86215LhsljxNVW0ws8L3W1B1vHKJCPlAUAvxRiMVatdYRp/k5PHDLm5wyxK +kX96IghsdMLyosT+eWJbYURJUmjSrQ5nbih9oQIDAQABAoIBAQCV28Ds8P/dEJOz +toBj9sT4V/JybrNmPVD7FHykhi9xawzlVVAEWYLI0UzqQJ+CsBvk1MIGSK+vASgq +eLsc5ldg+7yOE8+b6To78b9L0f0nPYPfsEqivyay4X2MJW4+mCjVuF6tK4M5uOqi +svARb9KtYM3SKgc9LIDkcLLHTwrtR+ZRZNQ0rSXuE2dSJMETCXCdzuSaoQfoOThM +Gmv6Sm04PVws5x2BQso9oThQHZnwCquSaveLMPguxnwanRKvCQkNvtnGzAeFQ7o3 +Lx5Zag4/4L5XLh0QT9+mWH8oRSFysPIk2pMhqzAAUxVjpo0RKmNYw7wEgYdwsXJl +pC2X+iABAoGBAPGroa7gPgDYCzcf7tNYOTz/nDbwUXsv4B0bCsUId+71ExnVwqxI +ubqYB0v23qjX+679CC1qsMb38//CBNlwruyLjxaDct3UFo3IfiXY1hAeN+Anvzeb +gfoD3s2XGHPIG5niViHIEwZKubpzsQ6UXl8HSFBCPFD1piXJ7qs/zeHJAoGBAN/o +LGbWww3vrYKZ+BCd3wIFrxurco94RDq7CfhTAinkc26WTmagyw4kXmZS0mtL8ycn +5PhIvCJaugnEUG1kEWDsYH57GMBG/BVoJC8to0yLrVh5fVzYl343i+KkqA3Y99bE +NFoshK+L1JH3klPhqmN7wnra/VNp2LJm4Nz+4mkZAoGAHjc5VeYPmodoj5HciGwl +a+0BmRTe+yn3OWxiIlR2ulfF9Zr2ZhgJsLzFXMgW+sFWZICafyMxyw7BYR7fAFjI +Zibk0wnIWNflogCJVS4RRZ6hmdMeY1N8IshNGSNlGUTRvqG/5yVey5CYPCmu34XJ +btQ4RGCjrfOovFzNDHhDw5ECgYAE5jWigmx+L5JiWzAcXPf2OV2dg2DcVstXZaRQ +NLDFbeRAtTU99aK7ynvuTT2hb2YAo1TVQfIr5kRP1mXUHu5qaoGqAtOF0YfOiBrS +lXMPR7chSnc9wtd9wYVkDipHM0oo/t4OYw78MFkUYJBpGXT6/EhDG+uTGavOK4Yc +D8+wcQKBgQDU6tlGCHK1kfc1xO6uoxHB7yLVM7lh+irLHJb0gt/zGdJUOHom+4r3 +zBW22vWWrYv2kck0ez6hvwgw1eIys6yY3Cz+8GpR0wVPxj4CiVDWfhQZeuu4rP1d +BFGjC0y1npoI4bJE8G3QiNJYRKaHCaf2hdvNr3AhwDT6QCDo+kQCtQ== +-----END RSA PRIVATE KEY----- diff --git a/jceks/utils_test.go b/jceks/utils_test.go new file mode 100644 index 0000000..e9af8b5 --- /dev/null +++ b/jceks/utils_test.go @@ -0,0 +1,69 @@ +/*- + * Copyright 2016 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jceks + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "strings" +) + +// LoadPEMKey extracts a private key from a PEM file. +func LoadPEMKey(filename string) (*rsa.PrivateKey, error) { + keyPEMBlock, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var keyDERBlock *pem.Block + for { + keyDERBlock, keyPEMBlock = pem.Decode(keyPEMBlock) + if keyDERBlock == nil { + return nil, fmt.Errorf("failed to parse key PEM data") + } + if keyDERBlock.Type == "PRIVATE KEY" || + strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") { + break + } + } + + return x509.ParsePKCS1PrivateKey(keyDERBlock.Bytes) +} + +// LoadPEMCert extracts a certificate from a PEM file. +func LoadPEMCert(filename string) (*x509.Certificate, error) { + certPEMBlock, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var certDERBlock *pem.Block + for { + certDERBlock, certPEMBlock = pem.Decode(certPEMBlock) + if certDERBlock == nil { + return nil, fmt.Errorf("failed to parse certificate PEM data") + } + if certDERBlock.Type == "CERTIFICATE" { + break + } + } + + return x509.ParseCertificate(certDERBlock.Bytes) +}