[crypto] crypto: PKCS#12 encode support

411 views
Skip to first unread message

Christopher Boumenot (Gerrit)

unread,
Feb 29, 2016, 8:20:47 PM2/29/16
to Ian Lance Taylor, golang-co...@googlegroups.com
Christopher Boumenot uploaded a change:
https://go-review.googlesource.com/20075

crypto: PKCS#12 encode support

Fixes golang/go#14125

Change-Id: Ic56b45a0fc4d5a5fa38d2b3ea563d50e59fb1fbb
---
M pkcs12/bmp-string_test.go
M pkcs12/crypto.go
M pkcs12/crypto_test.go
M pkcs12/errors.go
M pkcs12/mac.go
M pkcs12/pkcs12.go
M pkcs12/pkcs12_test.go
A pkcs12/pkcs8.go
A pkcs12/pkcs8_test.go
M pkcs12/safebags.go
A pkcs12/safebags_test.go
11 files changed, 742 insertions(+), 19 deletions(-)



diff --git a/pkcs12/bmp-string_test.go b/pkcs12/bmp-string_test.go
index 7fca55f..527449f 100644
--- a/pkcs12/bmp-string_test.go
+++ b/pkcs12/bmp-string_test.go
@@ -22,6 +22,14 @@
{"\u2115 - Double-struck
N", "21150020002d00200044006f00750062006c0065002d00730074007200750063006b0020004e0000",
false},
// any character outside the BMP should trigger an error.
{"\U0001f000 East wind (Mahjong)", "", true},
+ // odd length string
+}
+
+func TestBMPStringDecode(t *testing.T) {
+ _, err := decodeBMPString([]byte("a"))
+ if err == nil {
+ t.Fatalf("expected decode to fail, but it succeeded")
+ }
}

func TestBMPString(t *testing.T) {
diff --git a/pkcs12/crypto.go b/pkcs12/crypto.go
index 4bd4470..3d68b44 100644
--- a/pkcs12/crypto.go
+++ b/pkcs12/crypto.go
@@ -8,11 +8,18 @@
"bytes"
"crypto/cipher"
"crypto/des"
+ "crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
+ "io"

"golang.org/x/crypto/pkcs12/internal/rc2"
+)
+
+const (
+ pbeIterationCount = 2048
+ pbeSaltSizeBytes = 8
)

var (
@@ -124,6 +131,36 @@
return
}

+func pad(src []byte, blockSize int) []byte {
+ paddingLength := blockSize - len(src)%blockSize
+ paddingText := bytes.Repeat([]byte{byte(paddingLength)}, paddingLength)
+ return append(src, paddingText...)
+}
+
+func pbEncrypt(plainText, salt, password []byte, iterations int)
(cipherText []byte, err error) {
+ _, err = io.ReadFull(rand.Reader, salt)
+ if err != nil {
+ return nil, errors.New("pkcs12: failed to create a random salt value: "
+ err.Error())
+ }
+
+ cipherType := shaWithTripleDESCBC{}
+ key := cipherType.deriveKey(salt, password, iterations)
+ iv := cipherType.deriveIV(salt, password, iterations)
+
+ block, err := cipherType.create(key)
+ if err != nil {
+ return nil, errors.New("pkcs12: failed to create a block cipher: " +
err.Error())
+ }
+
+ paddedPlainText := pad(plainText, block.BlockSize())
+
+ encrypter := cipher.NewCBCEncrypter(block, iv)
+ cipherText = make([]byte, len(paddedPlainText))
+ encrypter.CryptBlocks(cipherText, paddedPlainText)
+
+ return cipherText, nil
+}
+
// decryptable abstracts a object that contains ciphertext.
type decryptable interface {
Algorithm() pkix.AlgorithmIdentifier
diff --git a/pkcs12/crypto_test.go b/pkcs12/crypto_test.go
index eb4dae8..db834fd 100644
--- a/pkcs12/crypto_test.go
+++ b/pkcs12/crypto_test.go
@@ -6,8 +6,10 @@

import (
"bytes"
+ "crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
+ "io"
"testing"
)

@@ -79,17 +81,9 @@
}

func TestPbDecrypt(t *testing.T) {
+ salt := []byte("\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8")
for i, test := range pbDecryptTests {
- decryptable := testDecryptable{
- data: test.in,
- algorithm: pkix.AlgorithmIdentifier{
- Algorithm: sha1WithTripleDES,
- Parameters: pbeParams{
- Salt: []byte("\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8"),
- Iterations: 4096,
- }.RawASN1(),
- },
- }
+ decryptable := makeTestDecryptable(test.in, salt)
password, _ := bmpString("sesame")

plaintext, err := pbDecrypt(decryptable, password)
@@ -104,6 +98,54 @@
}
}

+func TestRoundTripPkc12EncryptDecrypt(t *testing.T) {
+ salt := []byte{0xfe, 0xee, 0xfa, 0xce}
+ password := salt
+
+ // Sweep the possible padding lengths
+ for i := 25; i < 9; i++ {
+ bs := make([]byte, i)
+ _, err := io.ReadFull(rand.Reader, bs)
+ if err != nil {
+ t.Fatalf("failed to read: %s", err)
+ }
+
+ cipherText, err := pbEncrypt(bs, salt, password, 4096)
+ if err != nil {
+ t.Fatalf("failed to encrypt: %s\n", err)
+ }
+
+ if len(cipherText)%8 != 0 {
+ t.Fatalf("plain text was not padded as expected")
+ }
+
+ decryptable := makeTestDecryptable(cipherText, salt)
+ plainText, err := pbDecrypt(decryptable, password)
+ if err != nil {
+ t.Fatalf("failed to decrypt: %s\n", err)
+ }
+
+ if !bytes.Equal(bs, plainText) {
+ t.Fatalf("got %x, but wanted %x", bs, plainText)
+ }
+ }
+}
+
+func makeTestDecryptable(bytes, salt []byte) testDecryptable {
+ decryptable := testDecryptable{
+ data: bytes,
+ algorithm: pkix.AlgorithmIdentifier{
+ Algorithm: sha1WithTripleDES,
+ Parameters: pbeParams{
+ Salt: salt,
+ Iterations: 4096,
+ }.RawASN1(),
+ },
+ }
+
+ return decryptable
+}
+
type testDecryptable struct {
data []byte
algorithm pkix.AlgorithmIdentifier
diff --git a/pkcs12/errors.go b/pkcs12/errors.go
index 7377ce6..36ad6e9 100644
--- a/pkcs12/errors.go
+++ b/pkcs12/errors.go
@@ -17,7 +17,12 @@

// NotImplementedError indicates that the input is not currently supported.
type NotImplementedError string
+type EncodeError string

func (e NotImplementedError) Error() string {
return "pkcs12: " + string(e)
}
+
+func (e EncodeError) Error() string {
+ return "pkcs12: encode error: " + string(e)
+}
diff --git a/pkcs12/mac.go b/pkcs12/mac.go
index 5f38aa7..76ad0cd 100644
--- a/pkcs12/mac.go
+++ b/pkcs12/mac.go
@@ -32,14 +32,19 @@
return NotImplementedError("unknown digest algorithm: " +
macData.Mac.Algorithm.Algorithm.String())
}

- key := pbkdf(sha1Sum, 20, 64, macData.MacSalt, password,
macData.Iterations, 3, 20)
-
- mac := hmac.New(sha1.New, key)
- mac.Write(message)
- expectedMAC := mac.Sum(nil)
+ expectedMAC := computeMac(message, macData.Iterations, macData.MacSalt,
password)

if !hmac.Equal(macData.Mac.Digest, expectedMAC) {
return ErrIncorrectPassword
}
return nil
}
+
+func computeMac(message []byte, iterations int, salt, password []byte)
[]byte {
+ key := pbkdf(sha1Sum, 20, 64, salt, password, iterations, 3, 20)
+
+ mac := hmac.New(sha1.New, key)
+ mac.Write(message)
+
+ return mac.Sum(nil)
+}
diff --git a/pkcs12/pkcs12.go b/pkcs12/pkcs12.go
index ad6341e..8f733cf 100644
--- a/pkcs12/pkcs12.go
+++ b/pkcs12/pkcs12.go
@@ -11,6 +11,7 @@

import (
"crypto/ecdsa"
+ "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
@@ -18,6 +19,7 @@
"encoding/hex"
"encoding/pem"
"errors"
+ "io"
)

var (
@@ -27,12 +29,18 @@
oidFriendlyName = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1,
9, 20})
oidLocalKeyID = asn1.ObjectIdentifier([]int{1, 2, 840, 113549, 1,
9, 21})
oidMicrosoftCSPName = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 311,
17, 1})
+
+ localKeyId = []byte{0x01, 0x00, 0x00, 0x00}
)

type pfxPdu struct {
Version int
AuthSafe contentInfo
MacData macData `asn1:"optional"`
+}
+
+type dataContent struct {
+ data []byte `asn1:"tag:4"`
}

type contentInfo struct {
@@ -142,7 +150,7 @@
return nil, err
}
block.Bytes = certsData
- case bag.Id.Equal(oidPKCS8ShroundedKeyBag):
+ case bag.Id.Equal(oidPKCS8ShroudedKeyBag):
block.Type = privateKeyType

key, err := decodePkcs8ShroudedKeyBag(bag.Value.Bytes, password)
@@ -242,11 +250,10 @@
}
certificate = certs[0]

- case bag.Id.Equal(oidPKCS8ShroundedKeyBag):
+ case bag.Id.Equal(oidPKCS8ShroudedKeyBag):
if privateKey != nil {
err = errors.New("pkcs12: expected exactly one key bag")
}
-
if privateKey, err = decodePkcs8ShroudedKeyBag(bag.Value.Bytes,
encodedPassword); err != nil {
return nil, nil, err
}
@@ -263,8 +270,192 @@
return
}

+func getLocalKeyId(id []byte) (attribute pkcs12Attribute, err error) {
+ octetString := asn1.RawValue{Tag: 4, Class: 0, IsCompound: false, Bytes:
id}
+ bytes, err := asn1.Marshal(octetString)
+ if err != nil {
+ return
+ }
+
+ attribute = pkcs12Attribute{
+ Id: oidLocalKeyID,
+ Value: asn1.RawValue{Tag: 17, Class: 0, IsCompound: true, Bytes: bytes},
+ }
+
+ return attribute, nil
+}
+
+func convertToRawVal(val interface{}) (raw asn1.RawValue, err error) {
+ bytes, err := asn1.Marshal(val)
+ if err != nil {
+ return
+ }
+
+ _, err = asn1.Unmarshal(bytes, &raw)
+ return raw, nil
+}
+
+func makeSafeBags(oid asn1.ObjectIdentifier, value []byte) ([]safeBag,
error) {
+ attribute, err := getLocalKeyId(localKeyId)
+
+ if err != nil {
+ return nil, EncodeError("local key id: " + err.Error())
+ }
+
+ bag := make([]safeBag, 1)
+ bag[0] = safeBag{
+ Id: oid,
+ Value: asn1.RawValue{Tag: 0, Class: 2, IsCompound: true, Bytes:
value},
+ Attributes: []pkcs12Attribute{attribute},
+ }
+
+ return bag, nil
+}
+
+func makeCertBagContentInfo(derBytes []byte) (*contentInfo, error) {
+ certBag1 := certBag{
+ Id: oidCertTypeX509Certificate,
+ Data: derBytes,
+ }
+
+ bytes, err := asn1.Marshal(certBag1)
+ if err != nil {
+ return nil, EncodeError("encoding cert bag: " + err.Error())
+ }
+
+ certSafeBags, err := makeSafeBags(oidCertBag, bytes)
+ if err != nil {
+ return nil, EncodeError("safe bags: " + err.Error())
+ }
+
+ return makeContentInfo(certSafeBags)
+}
+
+func makeShroudedKeyBagContentInfo(privateKey interface{}, password
[]byte) (*contentInfo, error) {
+ shroudedKeyBagBytes, err := encodePkcs8ShroudedKeyBag(privateKey,
password)
+ if err != nil {
+ return nil, EncodeError("encode PKCS#8 shrouded key bag: " + err.Error())
+ }
+
+ safeBags, err := makeSafeBags(oidPKCS8ShroudedKeyBag, shroudedKeyBagBytes)
+ if err != nil {
+ return nil, EncodeError("safe bags: " + err.Error())
+ }
+
+ return makeContentInfo(safeBags)
+}
+
+func makeContentInfo(val interface{}) (*contentInfo, error) {
+ fullBytes, err := asn1.Marshal(val)
+ if err != nil {
+ return nil, EncodeError("contentInfo raw value marshal: " + err.Error())
+ }
+
+ octetStringVal := asn1.RawValue{Tag: 4, Class: 0, IsCompound: false,
Bytes: fullBytes}
+ octetStringFullBytes, err := asn1.Marshal(octetStringVal)
+ if err != nil {
+ return nil, EncodeError("raw contentInfo to octet string: " +
err.Error())
+ }
+
+ contentInfo := contentInfo{ContentType: oidDataContentType}
+ contentInfo.Content = asn1.RawValue{Tag: 0, Class: 2, IsCompound: true,
Bytes: octetStringFullBytes}
+
+ return &contentInfo, nil
+}
+
+func makeContentInfos(derBytes []byte, privateKey interface{}, password
[]byte) ([]contentInfo, error) {
+ shroudedKeyContentInfo, err := makeShroudedKeyBagContentInfo(privateKey,
password)
+ if err != nil {
+ return nil, EncodeError("shrouded key content info: " + err.Error())
+ }
+
+ certBagContentInfo, err := makeCertBagContentInfo(derBytes)
+ if err != nil {
+ return nil, EncodeError("cert bag content info: " + err.Error())
+ }
+
+ contentInfos := make([]contentInfo, 2)
+ contentInfos[0] = *shroudedKeyContentInfo
+ contentInfos[1] = *certBagContentInfo
+
+ return contentInfos, nil
+}
+
+func makeSalt(saltByteCount int) ([]byte, error) {
+ salt := make([]byte, saltByteCount)
+ _, err := io.ReadFull(rand.Reader, salt)
+ return salt, err
+}
+
+// Encode converts a certificate and a private key to the PKCS#12 byte
stream format.
+//
+// derBytes is a DER encoded certificate.
+// privateKey is an RSA
+func Encode(derBytes []byte, privateKey interface{}, password string)
(pfxBytes []byte, err error) {
+ secret, err := bmpString(password)
+ if err != nil {
+ return nil, ErrIncorrectPassword
+ }
+
+ contentInfos, err := makeContentInfos(derBytes, privateKey, secret)
+ if err != nil {
+ return nil, err
+ }
+
+ // Marhsal []contentInfo so we can re-constitute the byte stream that will
+ // be suitable for computing the MAC
+ bytes, err := asn1.Marshal(contentInfos)
+ if err != nil {
+ return nil, err
+ }
+
+ // Unmarshal as an asn1.RawValue so, we can compute the MAC against
the .Bytes
+ var contentInfosRaw asn1.RawValue
+ err = unmarshal(bytes, &contentInfosRaw)
+ if err != nil {
+ return nil, err
+ }
+
+ authSafeContentInfo, err := makeContentInfo(contentInfosRaw)
+ if err != nil {
+ return nil, EncodeError("authSafe content info: " + err.Error())
+ }
+
+ salt, err := makeSalt(pbeSaltSizeBytes)
+ if err != nil {
+ return nil, EncodeError("salt value: " + err.Error())
+ }
+
+ // Compute the MAC for marshaled bytes of contentInfos, which includes the
+ // cert bag, and the shrouded key bag.
+ digest := computeMac(contentInfosRaw.FullBytes, pbeIterationCount, salt,
secret)
+
+ pfx := pfxPdu{
+ Version: 3,
+ AuthSafe: *authSafeContentInfo,
+ MacData: macData{
+ Iterations: pbeIterationCount,
+ MacSalt: salt,
+ Mac: digestInfo{
+ Algorithm: pkix.AlgorithmIdentifier{
+ Algorithm: oidSHA1,
+ },
+ Digest: digest,
+ },
+ },
+ }
+
+ bytes, err = asn1.Marshal(pfx)
+ if err != nil {
+ return nil, EncodeError("marshal PFX PDU: " + err.Error())
+ }
+
+ return bytes, err
+}
+
func getSafeContents(p12Data, password []byte) (bags []safeBag,
updatedPassword []byte, err error) {
pfx := new(pfxPdu)
+
if err := unmarshal(p12Data, pfx); err != nil {
return nil, nil, errors.New("pkcs12: error reading P12 data: " +
err.Error())
}
@@ -335,6 +526,7 @@
if err := unmarshal(data, &safeContents); err != nil {
return nil, nil, err
}
+
bags = append(bags, safeContents...)
}

diff --git a/pkcs12/pkcs12_test.go b/pkcs12/pkcs12_test.go
index 14dd2a6..2ac382a 100644
--- a/pkcs12/pkcs12_test.go
+++ b/pkcs12/pkcs12_test.go
@@ -5,11 +5,19 @@
package pkcs12

import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
"crypto/rsa"
"crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
+ "fmt"
+ "math/big"
"testing"
+ "time"
)

func TestPfx(t *testing.T) {
@@ -29,6 +37,61 @@
t.Errorf("expected common name to be %q, but found %q", commonName,
cert.Subject.CommonName)
}
}
+}
+
+func TestPfxRoundTriRsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key := testPfxRoundTrip(t, privateKey)
+
+ actualPrivateKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ t.Fatalf("failed to decode private key")
+ }
+
+ if privateKey.D.Cmp(actualPrivateKey.D) != 0 {
+ t.Errorf("priv.D")
+ }
+}
+
+func TestPfxRoundTriEcdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key := testPfxRoundTrip(t, privateKey)
+
+ actualPrivateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ t.Fatalf("failed to decode private key")
+ }
+
+ if privateKey.D.Cmp(actualPrivateKey.D) != 0 {
+ t.Errorf("priv.D")
+ }
+}
+
+func testPfxRoundTrip(t *testing.T, privateKey interface{}) interface{} {
+ certificateBytes, err := newCertificate("hostname", privateKey)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ bytes, err := Encode(certificateBytes, privateKey, "sesame")
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+
+ key, _, err := Decode(bytes, "sesame")
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+
+ return key
}

func TestPEM(t *testing.T) {
@@ -86,6 +149,52 @@
_ = config
}

+func newCertificate(hostname string, privateKey interface{}) ([]byte,
error) {
+ t, _ := time.Parse("2006-01-02", "2016-01-01")
+ notBefore := t
+ notAfter := notBefore.Add(365 * 24 * time.Hour)
+
+ serialNumber, err := rand.Int(rand.Reader,
new(big.Int).Lsh(big.NewInt(1), 128))
+ if err != nil {
+ err := fmt.Errorf("Failed to Generate Serial Number: %v", err)
+ return nil, err
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Issuer: pkix.Name{
+ CommonName: hostname,
+ },
+ Subject: pkix.Name{
+ CommonName: hostname,
+ },
+
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ }
+
+ var publicKey interface{}
+ switch key := privateKey.(type) {
+ case *rsa.PrivateKey:
+ publicKey = key.Public()
+ case *ecdsa.PrivateKey:
+ publicKey = key.Public()
+ default:
+ panic(fmt.Sprintf("unsupported private key type: %T", privateKey))
+ }
+
+ derBytes, err := x509.CreateCertificate(rand.Reader, &template,
&template, publicKey, privateKey)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to Generate derBytes: " + err.Error())
+ }
+
+ return derBytes, nil
+}
+
var testdata = map[string]string{
// 'null' password test case
"Windows Azure Tools":
`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`,
diff --git a/pkcs12/pkcs8.go b/pkcs12/pkcs8.go
new file mode 100644
index 0000000..1eb8850
--- /dev/null
+++ b/pkcs12/pkcs8.go
@@ -0,0 +1,64 @@
+package pkcs12
+
+import (
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "errors"
+)
+
+// pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. See
+// ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn
+// and RFC5208.
+type pkcs8 struct {
+ Version int
+ Algo pkix.AlgorithmIdentifier
+ PrivateKey []byte
+ // optional attributes omitted.
+}
+
+var (
+ oidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
+ oidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
+
+ nullAsn = asn1.RawValue{Tag: 5}
+)
+
+// marshalPKCS8PrivateKey converts a private key to PKCS#8 encoded form.
+// See http://www.rsa.com/rsalabs/node.asp?id=2130 and RFC5208.
+func marshalPKCS8PrivateKey(key interface{}) (der []byte, err error) {
+ pkcs := pkcs8{
+ Version: 0,
+ }
+
+ switch key := key.(type) {
+ case *rsa.PrivateKey:
+ pkcs.Algo = pkix.AlgorithmIdentifier{
+ Algorithm: oidPublicKeyRSA,
+ Parameters: nullAsn,
+ }
+ pkcs.PrivateKey = x509.MarshalPKCS1PrivateKey(key)
+ case *ecdsa.PrivateKey:
+ bytes, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return nil, errors.New("x509: failed to marshal to PKCS#8: " +
err.Error())
+ }
+
+ pkcs.Algo = pkix.AlgorithmIdentifier{
+ Algorithm: oidPublicKeyECDSA,
+ Parameters: nullAsn,
+ }
+ pkcs.PrivateKey = bytes
+ default:
+ return nil, errors.New("x509: PKCS#8 only RSA and ECDSA private keys
supported")
+ }
+
+ bytes, err := asn1.Marshal(pkcs)
+ if err != nil {
+ return nil, errors.New("x509: failed to marshal to PKCS#8: " +
err.Error())
+ }
+
+ return bytes, nil
+}
diff --git a/pkcs12/pkcs8_test.go b/pkcs12/pkcs8_test.go
new file mode 100644
index 0000000..3c6a1d2
--- /dev/null
+++ b/pkcs12/pkcs8_test.go
@@ -0,0 +1,138 @@
+package pkcs12
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/asn1"
+ "testing"
+)
+
+func TestRoundTripPkcs8Rsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ key, err := x509.ParsePKCS8PrivateKey(bytes)
+ if err != nil {
+ t.Fatalf("failed to parse private key: %s", err)
+ }
+
+ actualPrivateKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ t.Fatalf("expected key to be of type *rsa.PrivateKey, but actual
was %T", key)
+ }
+
+ if actualPrivateKey.Validate() != nil {
+ t.Fatalf("private key did not validate")
+ }
+
+ if actualPrivateKey.N.Cmp(privateKey.N) != 0 {
+ t.Errorf("private key's N did not round trip")
+ }
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Errorf("private key's D did not round trip")
+ }
+ if actualPrivateKey.E != privateKey.E {
+ t.Errorf("private key's E did not round trip")
+ }
+ if actualPrivateKey.Primes[0].Cmp(privateKey.Primes[0]) != 0 {
+ t.Errorf("private key's P did not round trip")
+ }
+ if actualPrivateKey.Primes[1].Cmp(privateKey.Primes[1]) != 0 {
+ t.Errorf("private key's Q did not round trip")
+ }
+}
+
+func TestRoundTripPkcs8Ecdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ key, err := x509.ParsePKCS8PrivateKey(bytes)
+ if err != nil {
+ t.Fatalf("failed to parse private key: %s", err)
+ }
+
+ actualPrivateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ t.Fatalf("expected key to be of type *ecdsa.PrivateKey, but actual
was %T", key)
+ }
+
+ // sanity check, not exhaustive
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Errorf("private key's D did not round trip")
+ }
+ if actualPrivateKey.X.Cmp(privateKey.X) != 0 {
+ t.Errorf("private key's X did not round trip")
+ }
+ if actualPrivateKey.Y.Cmp(privateKey.Y) != 0 {
+ t.Errorf("private key's Y did not round trip")
+ }
+ if actualPrivateKey.Curve.Params().B.Cmp(privateKey.Curve.Params().B) !=
0 {
+ t.Errorf("private key's Curve.B did not round trip")
+ }
+}
+
+func TestNullParametersPkcs8Rsa(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ checkNullParameter(t, privateKey)
+}
+
+func TestNullParametersPkcs8Ecdsa(t *testing.T) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ checkNullParameter(t, privateKey)
+}
+
+func checkNullParameter(t *testing.T, privateKey interface{}) {
+ bytes, err := marshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatalf("failed to marshal private key: %s", err)
+ }
+
+ var pkcs pkcs8
+ rest, err := asn1.Unmarshal(bytes, &pkcs)
+ if err != nil {
+ t.Fatalf("failed to unmarshal PKCS#8: %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ // Only version == 0 is known and valid
+ if pkcs.Version != 0 {
+ t.Errorf("expected version=0, but actual=%d", pkcs.Version)
+ }
+
+ // ensure a NULL parameter is inserted
+ if pkcs.Algo.Parameters.Tag != 5 {
+ t.Errorf("expected parameters to be NULL, but actual tag=%d, class=%d,
isCompound=%d, bytes=%x",
+ pkcs.Algo.Parameters.Tag,
+ pkcs.Algo.Parameters.Class,
+ pkcs.Algo.Parameters.IsCompound,
+ pkcs.Algo.Parameters.Bytes)
+ }
+}
diff --git a/pkcs12/safebags.go b/pkcs12/safebags.go
index def1f7b..bbd1526 100644
--- a/pkcs12/safebags.go
+++ b/pkcs12/safebags.go
@@ -6,6 +6,7 @@

import (
"crypto/x509"
+ "crypto/x509/pkix"
"encoding/asn1"
"errors"
)
@@ -13,7 +14,7 @@
var (
// see https://tools.ietf.org/html/rfc7292#appendix-D
oidCertTypeX509Certificate = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 9, 22, 1})
- oidPKCS8ShroundedKeyBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 2})
+ oidPKCS8ShroudedKeyBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 2})
oidCertBag = asn1.ObjectIdentifier([]int{1, 2, 840,
113549, 1, 12, 10, 1, 3})
)

@@ -22,6 +23,53 @@
Data []byte `asn1:"tag:0,explicit"`
}

+func getAlgorithmParams(salt []byte, iterations int) (asn1.RawValue,
error) {
+ params := pbeParams{
+ Salt: salt,
+ Iterations: iterations,
+ }
+
+ return convertToRawVal(params)
+}
+
+func encodePkcs8ShroudedKeyBag(privateKey interface{}, password []byte)
(bytes []byte, err error) {
+ privateKeyBytes, err := marshalPKCS8PrivateKey(privateKey)
+
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 private key: " +
err.Error())
+ }
+
+ salt, err := makeSalt(pbeSaltSizeBytes)
+ if err != nil {
+ return nil, errors.New("pkcs12: error creating PKCS#8 salt: " +
err.Error())
+ }
+
+ pkData, err := pbEncrypt(privateKeyBytes, salt, password,
pbeIterationCount)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag
when encrypting cert bag: " + err.Error())
+ }
+
+ params, err := getAlgorithmParams(salt, pbeIterationCount)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key bag
algorithm's parameters: " + err.Error())
+ }
+
+ pkinfo := encryptedPrivateKeyInfo{
+ AlgorithmIdentifier: pkix.AlgorithmIdentifier{
+ Algorithm: oidPBEWithSHAAnd3KeyTripleDESCBC,
+ Parameters: params,
+ },
+ EncryptedData: pkData,
+ }
+
+ bytes, err = asn1.Marshal(pkinfo)
+ if err != nil {
+ return nil, errors.New("pkcs12: error encoding PKCS#8 shrouded key
bag: " + err.Error())
+ }
+
+ return bytes, err
+}
+
func decodePkcs8ShroudedKeyBag(asn1Data, password []byte) (privateKey
interface{}, err error) {
pkinfo := new(encryptedPrivateKeyInfo)
if err = unmarshal(asn1Data, pkinfo); err != nil {
diff --git a/pkcs12/safebags_test.go b/pkcs12/safebags_test.go
new file mode 100644
index 0000000..07b5fef
--- /dev/null
+++ b/pkcs12/safebags_test.go
@@ -0,0 +1,75 @@
+package pkcs12
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/asn1"
+ "testing"
+)
+
+// Assert the default algorithm parameters are in the correct order,
+// and default to the correct value. Defaults are based on OpenSSL.
+// 1. IterationCount, defaults to 2,048 long.
+// 2. Salt, is 8 bytes long.
+func TestDefaultAlgorithmParametersPkcs8ShroudedKeyBag(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ password := []byte("sesame")
+ bytes, err := encodePkcs8ShroudedKeyBag(privateKey, password)
+ if err != nil {
+ t.Fatalf("failed to encode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ var pkinfo encryptedPrivateKeyInfo
+ rest, err := asn1.Unmarshal(bytes, &pkinfo)
+ if err != nil {
+ t.Fatalf("failed to unmarshal encryptedPrivateKeyInfo %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ var params pbeParams
+ rest, err = asn1.Unmarshal(pkinfo.Algorithm().Parameters.FullBytes,
&params)
+ if err != nil {
+ t.Fatalf("failed to unmarshal encryptedPrivateKeyInfo %s", err)
+ }
+
+ if len(rest) != 0 {
+ t.Fatalf("unexpected trailing bytes of len=%d, bytes=%x", len(rest),
rest)
+ }
+
+ if params.Iterations != pbeIterationCount {
+ t.Errorf("expected iteration count to be %d, but actual=%d",
pbeIterationCount, params.Iterations)
+ }
+ if len(params.Salt) != pbeSaltSizeBytes {
+ t.Errorf("expected the number of salt bytes to be %d, but actual=%d",
pbeSaltSizeBytes, len(params.Salt))
+ }
+}
+
+func TestRoundTripPkcs8ShroudedKeyBag(t *testing.T) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 512)
+ if err != nil {
+ t.Fatalf("failed to generate a private key: %s", err)
+ }
+
+ password := []byte("sesame")
+ bytes, err := encodePkcs8ShroudedKeyBag(privateKey, password)
+ if err != nil {
+ t.Fatalf("failed to encode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ key, err := decodePkcs8ShroudedKeyBag(bytes, password)
+ if err != nil {
+ t.Fatalf("failed to decode PKCS#8 shrouded key bag: %s", err)
+ }
+
+ actualPrivateKey := key.(*rsa.PrivateKey)
+ if actualPrivateKey.D.Cmp(privateKey.D) != 0 {
+ t.Fatalf("failed to round-trip rsa.PrivateKey.D")
+ }
+}

--
https://go-review.googlesource.com/20075

Brad Fitzpatrick (Gerrit)

unread,
Mar 1, 2016, 1:05:18 AM3/1/16
to Christopher Boumenot, Brad Fitzpatrick, Adam Langley, golang-co...@googlegroups.com
Brad Fitzpatrick has posted comments on this change.

crypto: PKCS#12 encode support

Patch Set 1:

(1 comment)

R=agl

Unless he wants to assign a different reviewer.

https://go-review.googlesource.com/#/c/20075/1//COMMIT_MSG
Commit Message:

Line 7: crypto: PKCS#12 encode support
the part before the colon is the directory name in the repo, so:

pkcs12: add PKCS#12 encoding support


--
https://go-review.googlesource.com/20075
Gerrit-Reviewer: Adam Langley <a...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-HasComments: Yes

Christopher Boumenot (Gerrit)

unread,
Mar 2, 2016, 12:13:03 PM3/2/16
to Adam Langley, Brad Fitzpatrick, golang-co...@googlegroups.com
Christopher Boumenot uploaded a new patch set:
https://go-review.googlesource.com/20075

pkcs12: add PKCS#12 encoding support

Fixes golang/go#14125

Change-Id: Ic56b45a0fc4d5a5fa38d2b3ea563d50e59fb1fbb
---
M pkcs12/bmp-string_test.go
M pkcs12/crypto.go
M pkcs12/crypto_test.go
M pkcs12/errors.go
M pkcs12/mac.go
M pkcs12/pkcs12.go
M pkcs12/pkcs12_test.go
A pkcs12/pkcs8.go
A pkcs12/pkcs8_test.go
M pkcs12/safebags.go
A pkcs12/safebags_test.go
11 files changed, 746 insertions(+), 19 deletions(-)

Adam Langley (Gerrit)

unread,
Mar 2, 2016, 1:37:44 PM3/2/16
to Christopher Boumenot, Brad Fitzpatrick, golang-co...@googlegroups.com
Adam Langley uploaded a new patch set:
https://go-review.googlesource.com/20075

x/crypto/pkcs12: add PKCS#12 encoding support

Fixes golang/go#14125

Change-Id: Ic56b45a0fc4d5a5fa38d2b3ea563d50e59fb1fbb
---
M pkcs12/bmp-string_test.go
M pkcs12/crypto.go
M pkcs12/crypto_test.go
M pkcs12/errors.go
M pkcs12/mac.go
M pkcs12/pkcs12.go
M pkcs12/pkcs12_test.go
A pkcs12/pkcs8.go
A pkcs12/pkcs8_test.go
M pkcs12/safebags.go
A pkcs12/safebags_test.go
11 files changed, 745 insertions(+), 19 deletions(-)

Adam Langley (Gerrit)

unread,
Mar 2, 2016, 1:46:13 PM3/2/16
to Christopher Boumenot, Brad Fitzpatrick, golang-co...@googlegroups.com
Adam Langley has posted comments on this change.

x/crypto/pkcs12: add PKCS#12 encoding support

Patch Set 2:

I fixed a very, very minor things.

However, the encoding in pkcs12.go isn't really viable. The shallow nature
of safeBag and other types may have made sense for parsing but for
serialisation new types might be needed.

Also, while parsing PKCS#12 files is a plausible use since they do exist,
they should be strongly discouraged. Supporting their serialisation seems
counter to this.

--
https://go-review.googlesource.com/20075
Gerrit-Reviewer: Adam Langley <a...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-HasComments: No

Christopher Boumenot (Gerrit)

unread,
Mar 2, 2016, 2:20:18 PM3/2/16
to Adam Langley, Brad Fitzpatrick, golang-co...@googlegroups.com
Christopher Boumenot has posted comments on this change.

x/crypto/pkcs12: add PKCS#12 encoding support

Patch Set 3:

(1 comment)

> I fixed a very, very minor things.
>
> However, the encoding in pkcs12.go isn't really viable. The shallow
> nature of safeBag and other types may have made sense for parsing
> but for serialisation new types might be needed.
>
> Also, while parsing PKCS#12 files is a plausible use since they do
> exist, they should be strongly discouraged. Supporting their
> serialisation seems counter to this.

Thank you for the review, and fixes - I clearly missed the padding sweep.

Encoding was written with the intent to interchange .pfx files, which is
narrow. The parsing code is heavily biased towards this fact, and likewise
so is this serialization code. Decode and encode are now a matched, but
limited pair.

Please let me know if you think this is acceptable or not. I would prefer
to share, but I can retract the PR, and go with an repo internal approach.

https://go-review.googlesource.com/#/c/20075/1//COMMIT_MSG
Commit Message:

Line 7: x/crypto/pkcs12: add PKCS#12 encoding support
> the part before the colon is the directory name in the repo, so:
Done


--
https://go-review.googlesource.com/20075
Gerrit-Reviewer: Adam Langley <a...@golang.org>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@golang.org>
Gerrit-Reviewer: Christopher Boumenot <chr...@microsoft.com>
Gerrit-HasComments: Yes
Reply all
Reply to author
Forward
0 new messages