diff --git a/pkcs12.go b/pkcs12.go index 6c8afac..45ee012 100644 --- a/pkcs12.go +++ b/pkcs12.go @@ -625,6 +625,31 @@ func Encode(rand io.Reader, privateKey interface{}, certificate *x509.Certificat // the end-entity certificate bag have the LocalKeyId attribute set to the SHA-1 // fingerprint of the end-entity certificate. func (enc *Encoder) Encode(privateKey interface{}, certificate *x509.Certificate, caCerts []*x509.Certificate, password string) (pfxData []byte, err error) { + return enc.EncodeWithFriendlyName(privateKey, certificate, caCerts, password, "", "") +} + +// EncodeWithFriendlyName produces pfxData containing one private key (privateKey), an +// end-entity certificate (certificate), and any number of CA certificates +// (caCerts). +// +// The pfxData is encrypted and authenticated with keys derived from +// the provided password. +// +// If keyFriendlyName is non-empty, it will be set as the friendly name (alias) for +// the private key. If keyFriendlyName is empty, no friendly name attribute will be added +// to the private key. +// +// If certFriendlyName is non-empty, it will be set as the friendly name (alias) for +// the end-entity certificate. If certFriendlyName is empty, no friendly name attribute +// will be added to the certificate. +// +// EncodeWithFriendlyName emulates the behavior of OpenSSL's PKCS12_create: it creates two +// SafeContents: one that's encrypted with the certificate encryption algorithm +// and contains the certificates, and another that is unencrypted and contains the +// private key shrouded with the key encryption algorithm. The private key bag and +// the end-entity certificate bag have the LocalKeyId attribute set to the SHA-1 +// fingerprint of the end-entity certificate. +func (enc *Encoder) EncodeWithFriendlyName(privateKey interface{}, certificate *x509.Certificate, caCerts []*x509.Certificate, password string, keyFriendlyName string, certFriendlyName string) (pfxData []byte, err error) { if enc.macAlgorithm == nil && enc.certAlgorithm == nil && enc.keyAlgorithm == nil && password != "" { return nil, errors.New("pkcs12: password must be empty") } @@ -647,8 +672,40 @@ func (enc *Encoder) Encode(privateKey interface{}, certificate *x509.Certificate return nil, err } + // Build certificate attributes + certAttributes := []pkcs12Attribute{localKeyIdAttr} + + // Add friendly name attribute to certificate if provided + if certFriendlyName != "" { + bmpFriendlyName, err := bmpString(certFriendlyName) + if err != nil { + return nil, err + } + + encodedFriendlyName, err := asn1.Marshal(asn1.RawValue{ + Class: 0, + Tag: 30, + IsCompound: false, + Bytes: bmpFriendlyName, + }) + if err != nil { + return nil, err + } + + certFriendlyNameAttr := pkcs12Attribute{ + Id: oidFriendlyName, + Value: asn1.RawValue{ + Class: 0, + Tag: 17, + IsCompound: true, + Bytes: encodedFriendlyName, + }, + } + certAttributes = append(certAttributes, certFriendlyNameAttr) + } + var certBags []safeBag - if certBag, err := makeCertBag(certificate.Raw, []pkcs12Attribute{localKeyIdAttr}); err != nil { + if certBag, err := makeCertBag(certificate.Raw, certAttributes); err != nil { return nil, err } else { certBags = append(certBags, *certBag) @@ -682,6 +739,35 @@ func (enc *Encoder) Encode(privateKey interface{}, certificate *x509.Certificate } keyBag.Attributes = append(keyBag.Attributes, localKeyIdAttr) + // Add friendly name attribute to key if provided + if keyFriendlyName != "" { + bmpFriendlyName, err := bmpString(keyFriendlyName) + if err != nil { + return nil, err + } + + encodedFriendlyName, err := asn1.Marshal(asn1.RawValue{ + Class: 0, + Tag: 30, + IsCompound: false, + Bytes: bmpFriendlyName, + }) + if err != nil { + return nil, err + } + + friendlyNameAttr := pkcs12Attribute{ + Id: oidFriendlyName, + Value: asn1.RawValue{ + Class: 0, + Tag: 17, + IsCompound: true, + Bytes: encodedFriendlyName, + }, + } + keyBag.Attributes = append(keyBag.Attributes, friendlyNameAttr) + } + // Construct an authenticated safe with two SafeContents. // The first SafeContents is encrypted and contains the cert bags. // The second SafeContents is unencrypted and contains the shrouded key bag. diff --git a/pkcs12_test.go b/pkcs12_test.go index f563577..3568fec 100644 --- a/pkcs12_test.go +++ b/pkcs12_test.go @@ -9,6 +9,7 @@ import ( "crypto/rsa" "crypto/tls" "crypto/x509" + "encoding/asn1" "encoding/base64" "encoding/pem" "testing" @@ -169,6 +170,285 @@ func TestPBES2_AES192CBC(t *testing.T) { } } +func TestEncodeWithBothFriendlyNames(t *testing.T) { + privateKey, cert := getTestCertAndKey(t) + keyFriendlyName := "my-test-key" + certFriendlyName := "my-test-cert" + password := "test-password" + + // Encode with both friendly names + pfxData, err := Modern.EncodeWithFriendlyName(privateKey, cert, nil, password, keyFriendlyName, certFriendlyName) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } + + // Verify we can decode it back + decodedKey, decodedCert, err := Decode(pfxData, password) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } + + if decodedKey == nil { + t.Fatal("Decoded key is nil") + } + + if decodedCert == nil { + t.Fatal("Decoded cert is nil") + } + + // Verify the key matches + rsaKey, ok := decodedKey.(*rsa.PrivateKey) + if !ok { + t.Fatal("Decoded key is not RSA private key") + } + + if rsaKey.D.Cmp(privateKey.D) != 0 { + t.Fatal("Decoded private key does not match original") + } + + // Verify the friendly names are in the PKCS#12 file + encodedPassword, err := bmpStringZeroTerminated(password) + if err != nil { + t.Fatal(err) + } + bags, _, err := getSafeContents(pfxData, encodedPassword, 2, 2) + if err != nil { + t.Fatalf("Failed to get safe contents: %v", err) + } + + // Verify key friendly name + actualKeyName, err := getKeyFriendlyName(bags) + if err != nil { + t.Fatalf("Failed to get key friendly name: %v", err) + } + if actualKeyName != keyFriendlyName { + t.Errorf("Expected key friendly name %q, got %q", keyFriendlyName, actualKeyName) + } + + // Verify cert friendly name + actualCertName, err := getCertFriendlyName(bags) + if err != nil { + t.Fatalf("Failed to get cert friendly name: %v", err) + } + if actualCertName != certFriendlyName { + t.Errorf("Expected cert friendly name %q, got %q", certFriendlyName, actualCertName) + } +} + +func TestEncodeWithoutFriendlyNames(t *testing.T) { + privateKey, cert := getTestCertAndKey(t) + password := "test-password" + + // Encode without friendly names + pfxData, err := Modern.EncodeWithFriendlyName(privateKey, cert, nil, password, "", "") + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } + + // Verify we can decode it back + _, _, err = Decode(pfxData, password) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } + + // Verify no friendly name attributes are present + encodedPassword, err := bmpStringZeroTerminated(password) + if err != nil { + t.Fatal(err) + } + bags, _, err := getSafeContents(pfxData, encodedPassword, 2, 2) + if err != nil { + t.Fatalf("Failed to get safe contents: %v", err) + } + + if hasKeyFriendlyName(bags) { + t.Error("Found key friendly name when none should be present") + } + + if hasCertFriendlyName(bags) { + t.Error("Found cert friendly name when none should be present") + } +} + +func TestEncodeWithOnlyKeyFriendlyName(t *testing.T) { + privateKey, cert := getTestCertAndKey(t) + keyFriendlyName := "my-test-key" + password := "test-password" + + // Encode with only key friendly name + pfxData, err := Modern.EncodeWithFriendlyName(privateKey, cert, nil, password, keyFriendlyName, "") + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } + + // Verify the key has a friendly name but the cert doesn't + encodedPassword, err := bmpStringZeroTerminated(password) + if err != nil { + t.Fatal(err) + } + bags, _, err := getSafeContents(pfxData, encodedPassword, 2, 2) + if err != nil { + t.Fatalf("Failed to get safe contents: %v", err) + } + + if !hasKeyFriendlyName(bags) { + t.Error("Key friendly name not found when it should be present") + } + + if hasCertFriendlyName(bags) { + t.Error("Certificate friendly name found when it should not be present") + } +} + +func TestEncodeWithOnlyCertFriendlyName(t *testing.T) { + privateKey, cert := getTestCertAndKey(t) + certFriendlyName := "my-test-cert" + password := "test-password" + + // Encode with only certificate friendly name + pfxData, err := Modern.EncodeWithFriendlyName(privateKey, cert, nil, password, "", certFriendlyName) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } + + // Verify the cert has a friendly name but the key doesn't + encodedPassword, err := bmpStringZeroTerminated(password) + if err != nil { + t.Fatal(err) + } + bags, _, err := getSafeContents(pfxData, encodedPassword, 2, 2) + if err != nil { + t.Fatalf("Failed to get safe contents: %v", err) + } + + if hasKeyFriendlyName(bags) { + t.Error("Key friendly name found when it should not be present") + } + + if !hasCertFriendlyName(bags) { + t.Error("Certificate friendly name not found when it should be present") + } +} + +func TestEncodeBackwardCompatibility(t *testing.T) { + privateKey, cert := getTestCertAndKey(t) + password := "test-password" + + // Test that the old Encode method still works + pfxData, err := Modern.Encode(privateKey, cert, nil, password) + if err != nil { + t.Fatalf("Failed to encode with old method: %v", err) + } + + // Verify we can decode it back + _, _, err = Decode(pfxData, password) + if err != nil { + t.Fatalf("Failed to decode: %v", err) + } +} + +// Helper function to get test certificate and key +func getTestCertAndKey(t *testing.T) (*rsa.PrivateKey, *x509.Certificate) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + // Get a certificate from testdata + var certDER []byte + for _, base64P12 := range testdata { + p12, _ := base64.StdEncoding.DecodeString(base64P12) + _, cert, err := Decode(p12, "") + if err == nil { + certDER = cert.Raw + break + } + } + + if len(certDER) == 0 { + t.Fatal("Failed to get test certificate") + } + + cert, err := x509.ParseCertificate(certDER) + if err != nil { + t.Fatal(err) + } + + return privateKey, cert +} + +// Helper function to find and decode a friendly name from a specific bag type +func findFriendlyName(bags []safeBag, bagOID asn1.ObjectIdentifier) (string, bool, error) { + for _, bag := range bags { + if bag.Id.Equal(bagOID) { + for _, attr := range bag.Attributes { + if attr.Id.Equal(oidFriendlyName) { + var rawValue asn1.RawValue + if err := unmarshal(attr.Value.Bytes, &rawValue); err != nil { + return "", false, err + } + decodedName, err := decodeBMPString(rawValue.Bytes) + if err != nil { + return "", false, err + } + return decodedName, true, nil + } + } + } + } + return "", false, nil +} + +// Helper function to check if a key bag has a friendly name +func hasKeyFriendlyName(bags []safeBag) bool { + for _, bag := range bags { + if bag.Id.Equal(oidPKCS8ShroundedKeyBag) || bag.Id.Equal(oidKeyBag) { + for _, attr := range bag.Attributes { + if attr.Id.Equal(oidFriendlyName) { + return true + } + } + } + } + return false +} + +// Helper function to check if a cert bag has a friendly name +func hasCertFriendlyName(bags []safeBag) bool { + for _, bag := range bags { + if bag.Id.Equal(oidCertBag) { + for _, attr := range bag.Attributes { + if attr.Id.Equal(oidFriendlyName) { + return true + } + } + } + } + return false +} + +// Helper function to get and verify a key's friendly name +func getKeyFriendlyName(bags []safeBag) (string, error) { + for _, bag := range bags { + if bag.Id.Equal(oidPKCS8ShroundedKeyBag) || bag.Id.Equal(oidKeyBag) { + name, found, err := findFriendlyName([]safeBag{bag}, bag.Id) + if err != nil { + return "", err + } + if found { + return name, nil + } + } + } + return "", nil +} + +// Helper function to get and verify a cert's friendly name +func getCertFriendlyName(bags []safeBag) (string, error) { + name, _, err := findFriendlyName(bags, oidCertBag) + return name, err +} + var testdata = map[string]string{ // 'null' password test case "Windows Azure Tools": `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`,