Skip to content

Commit

Permalink
implement loadPkcs12 function
Browse files Browse the repository at this point in the history
  • Loading branch information
nitram509 committed Oct 2, 2024
1 parent 740a19c commit faf2f7a
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 141 deletions.
4 changes: 2 additions & 2 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import (
)

const (
magic uint32 = 0xfeedfeed

version01 uint32 = 1
version02 uint32 = 2

privateKeyTag uint32 = 1
trustedCertificateTag uint32 = 2
)

var jksMagicBytes = []byte{0xfe, 0xed, 0xfe, 0xed}

var byteOrder = binary.BigEndian

var whitenerMessage = []byte("Mighty Aphrodite")
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ go 1.22.7

require github.com/stretchr/testify v1.9.0

require golang.org/x/crypto v0.11.0 // indirect

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.5.0
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
64 changes: 1 addition & 63 deletions keystore.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package keystore

import (
"bytes"
"crypto/rand"
"crypto/sha1"
"errors"
Expand Down Expand Up @@ -109,7 +108,7 @@ func (ks KeyStore) Store(w io.Writer, password []byte) error {
return fmt.Errorf("update digest with whitener message: %w", err)
}

if err := e.writeUint32(magic); err != nil {
if err := e.writeBytes(jksMagicBytes); err != nil {
return fmt.Errorf("write magic: %w", err)
}
// always write latest version
Expand Down Expand Up @@ -143,67 +142,6 @@ func (ks KeyStore) Store(w io.Writer, password []byte) error {
return nil
}

// Load reads keystore representation from r and checks its signature.
// It is strongly recommended to fill password slice with zero after usage.
func (ks KeyStore) Load(r io.Reader, password []byte) error {
d := decoder{
r: r,
h: sha1.New(),
}

passwordBytes := passwordBytes(password)
defer zeroing(passwordBytes)

if _, err := d.h.Write(passwordBytes); err != nil {
return fmt.Errorf("update digest with password: %w", err)
}

if _, err := d.h.Write(whitenerMessage); err != nil {
return fmt.Errorf("update digest with whitener message: %w", err)
}

readMagic, err := d.readUint32()
if err != nil {
return fmt.Errorf("read magic: %w", err)
}

if readMagic != magic {
return errors.New("got invalid magic")
}

version, err := d.readUint32()
if err != nil {
return fmt.Errorf("read version: %w", err)
}

entryNum, err := d.readUint32()
if err != nil {
return fmt.Errorf("read number of entries: %w", err)
}

for i := range entryNum {
alias, entry, err := d.readEntry(version)
if err != nil {
return fmt.Errorf("read %d entry: %w", i, err)
}

ks.m[alias] = entry
}

computedDigest := d.h.Sum(nil)

actualDigest, err := d.readBytes(uint32(d.h.Size())) //nolint:gosec
if err != nil {
return fmt.Errorf("read digest: %w", err)
}

if !bytes.Equal(actualDigest, computedDigest) {
return errors.New("got invalid digest")
}

return nil
}

// SetPrivateKeyEntry adds PrivateKeyEntry into keystore by alias encrypted with password.
// It is strongly recommended to fill password slice with zero after usage.
func (ks KeyStore) SetPrivateKeyEntry(alias string, entry PrivateKeyEntry, password []byte) error {
Expand Down
131 changes: 131 additions & 0 deletions keystore_load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package keystore

import (
"bytes"
"crypto/sha1"
"errors"
"fmt"
"io"
"software.sslmate.com/src/go-pkcs12"
)

// Load reads keystore representation from r and checks its signature.
// It is strongly recommended to fill password slice with zero after usage.
func (ks KeyStore) Load(r io.Reader, password []byte) error {
d := decoder{
r: r,
h: sha1.New(),
}

fourBytes, err := d.readBytes(4) //nolint:gomnd,mnd
if err != nil {
return fmt.Errorf("read magic: %w", err)
}

magicBytesReader := bytes.NewReader(fourBytes)
fullReader := io.MultiReader(magicBytesReader, r)

if bytes.Equal(fourBytes, jksMagicBytes) {
return ks.loadJks(fullReader, password)
}

return ks.loadPkcs12(fullReader, password)
}

// loads the old JKS format.
func (ks KeyStore) loadJks(r io.Reader, password []byte) error {
d := decoder{
r: r,
h: sha1.New(),
}

passwordBytes := passwordBytes(password)
defer zeroing(passwordBytes)

if _, err := d.h.Write(passwordBytes); err != nil {
return fmt.Errorf("update digest with password: %w", err)
}

if _, err := d.h.Write(whitenerMessage); err != nil {
return fmt.Errorf("update digest with whitener message: %w", err)
}

fourBytes, err := d.readBytes(4) //nolint:gomnd,mnd
if err != nil {
return fmt.Errorf("read magic: %w", err)
}

if !bytes.Equal(fourBytes, jksMagicBytes) {
return errors.New("got invalid magic bytes from the file, this is no JKS format")
}

version, err := d.readUint32()
if err != nil {
return fmt.Errorf("read version: %w", err)
}

entryNum, err := d.readUint32()
if err != nil {
return fmt.Errorf("read number of entries: %w", err)
}

for i := range entryNum {
alias, entry, err := d.readEntry(version)
if err != nil {
return fmt.Errorf("read %d entry: %w", i, err)
}

ks.m[alias] = entry
}

computedDigest := d.h.Sum(nil)

actualDigest, err := d.readBytes(uint32(d.h.Size())) //nolint:gosec
if err != nil {
return fmt.Errorf("read digest: %w", err)
}

if !bytes.Equal(actualDigest, computedDigest) {
return errors.New("got invalid digest")
}

return nil
}

// loads the newer PKCS12 format.
func (ks KeyStore) loadPkcs12(r io.Reader, password []byte) error {
allData, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("can't read pkcs12 data: %w", err)
}

certs, err := pkcs12.DecodeTrustStore(allData, string(password))
if err != nil {
return fmt.Errorf("can't decode pkcs12 trust strore: %w", err)
}

for _, cert := range certs {
certificate := Certificate{
Type: "X509",
Content: nil,
}

tce := TrustedCertificateEntry{}
tce.CreationTime = cert.NotBefore // there is no better timestamp provided by pkcs12 file
tce.Certificate = certificate
alias := fmt.Sprintf("c_%s,o_%s,ou_%s,cn_%s,s_%s",
cert.Subject.Country,
cert.Subject.Organization,
cert.Subject.OrganizationalUnit,
cert.Subject.CommonName,
cert.Subject.SerialNumber,
)
//Country, Organization, OrganizationalUnit []string
//Locality, Province []string
//StreetAddress, PostalCode []string
//SerialNumber, CommonName string
ks.m[alias] = tce
}

return nil
}
106 changes: 106 additions & 0 deletions keystore_load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package keystore

import (
"encoding/pem"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoad(t *testing.T) {
password := []byte{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}
defer zeroing(password)

f, err := os.Open("./testdata/keystore.jks")
require.NoError(t, err)

defer func() {
err := f.Close()
require.NoError(t, err)
}()

keyStore := New()

err = keyStore.Load(f, password)
require.NoError(t, err)

actualPKE, err := keyStore.GetPrivateKeyEntry("alias", password)
require.NoError(t, err)

expectedCT, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", "2017-09-19 17:41:00.016 +0300 EEST")
require.NoError(t, err)

assert.Truef(t, actualPKE.CreationTime.Equal(expectedCT),
"unexpected private key entry creation time: '%v' '%v'", actualPKE.CreationTime, expectedCT)

assert.Empty(t, actualPKE.CertificateChain, "unexpected private key entry certificate chain length")

pkPEM, err := os.ReadFile("./testdata/key.pem")
require.NoError(t, err)

decodedPK, _ := pem.Decode(pkPEM)

assert.Equal(t, decodedPK.Bytes, actualPKE.PrivateKey, "unexpected private key")
}

func TestLoadKeyPassword(t *testing.T) {
password := []byte{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}
defer zeroing(password)

keyPassword := []byte{'k', 'e', 'y', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}
defer zeroing(keyPassword)

f, err := os.Open("./testdata/keystore_keypass.jks")
require.NoError(t, err)

defer func() {
err := f.Close()
require.NoError(t, err)
}()

keyStore := New()

err = keyStore.Load(f, password)
require.NoError(t, err)

actualPKE, err := keyStore.GetPrivateKeyEntry("alias", keyPassword)
require.NoError(t, err)

expectedCT, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", "2020-10-26 12:01:38.387 +0200 EET")
require.NoError(t, err)

assert.Truef(t, actualPKE.CreationTime.Equal(expectedCT),
"unexpected private key entry creation time: '%v' '%v'", actualPKE.CreationTime, expectedCT)

assert.Lenf(t, actualPKE.CertificateChain, 1,
"unexpected private key entry certificate chain length: '%d' '%d'", len(actualPKE.CertificateChain), 0)

pkPEM, err := os.ReadFile("./testdata/key_keypass.pem")
require.NoError(t, err)

decodedPK, _ := pem.Decode(pkPEM)

assert.Equal(t, decodedPK.Bytes, actualPKE.PrivateKey, "unexpected private key")
}

func TestLoadPkcs12(t *testing.T) {
password := []byte("")

f, err := os.Open("./testdata/keystore_temurin_openjdk_21.0.4_lts.p12")
require.NoError(t, err)

defer func() {
err := f.Close()
require.NoError(t, err)
}()

keyStore := New()

err = keyStore.Load(f, password)
require.NoError(t, err)

assert.Len(t, keyStore.Aliases(), 148)
}
Loading

0 comments on commit faf2f7a

Please sign in to comment.