Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor fixes and new support for multi-value attributes #20

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,51 @@ We aim to make the usage as stupid simple as possible, so there are only 2 expor
}
```

If you want support for multi-value attributes in the subject and issuer, pass
the `multivalue: true` option to the parse functions. Doing so will yield this
output where lists are used as values for the attributes.

```elixir
%{
extensions: %{
authorityInfoAccess: "CA Issuers - URI:http://certificates.godaddy.com/repository/gd_intermediate.crt\nOCSP - URI:http://ocsp.godaddy.com/\n",
authorityKeyIdentifier: "keyid:FD:AC:61:32:93:6C:45:D6:E2:EE:85:5F:9A:BA:E7:76:99:68:CC:E7\n",
basicConstraints: "CA:FALSE",
certificatePolicies: "Policy: 2.16.840.1.114413.1.7.23.1\n CPS: http://certificates.godaddy.com/repository/",
crlDistributionPoints: "Full Name:\n URI:http://crl.godaddy.com/gds1-90.crl",
extendedKeyUsage: "TLS Web server authentication, TLS Web client authentication",
keyUsage: "Digital Signature, Key Encipherment",
subjectAltName: "DNS:acaline.com, DNS:www.acaline.com",
subjectKeyIdentifier: "E6:61:14:4E:5A:4B:51:0C:4E:6C:5E:3C:79:61:65:D4:BD:64:94:BE"
},
fingerprint: "FA:BE:B5:9B:ED:C2:2B:42:7E:B1:45:C8:9A:8A:73:16:4A:A0:10:09",
issuer: %{
C: ["US"],
CN: ["Go Daddy Secure Certification Authority"],
L: ["Scottsdale"],
O: ["GoDaddy.com, Inc."],
OU: ["http://certificates.godaddy.com/repository"],
ST: ["Arizona"],
aggregated: "/C=US/CN=Go Daddy Secure Certification Authority/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certificates.godaddy.com/repository/ST=Arizona",
emailAddress: []
},
not_after: 1398523877,
not_before: 1366987877,
serial_number: "27ACAE30B9F323",
signature_algorithm: "sha, rsa",
subject: %{
C: [],
CN: ["www.acaline.com"],
L: [],
O: [],
OU: ["Domain Control Validated"],
ST: [],
aggregated: "/CN=www.acaline.com/OU=Domain Control Validated",
emailAddress: []
}
}
```

### parse_der

Parses a DER-encoded X509 certificate
Expand Down
3 changes: 1 addition & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

import Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
Expand Down
154 changes: 129 additions & 25 deletions lib/easy_ssl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,65 @@ defmodule EasySSL do
OU: "Domain Control Validated",
ST: nil,
aggregated: "/CN=www.acaline.com/OU=Domain Control Validated",
emailAddress: nil
emailAddress: []
}
}

# Pass in a binary (from Base.decode64, or some other source) and parse
# with multivalues as part of subject and issuer attributes.
iex(1)> EasySSL.parse_der(<<...>>, multivalue: true)
%{
extensions: %{
authorityInfoAccess: "CA Issuers - URI:http://certificates.godaddy.com/repository/gd_intermediate.crt\\nOCSP - URI:http://ocsp.godaddy.com/\\n",
authorityKeyIdentifier: "keyid:FD:AC:61:32:93:6C:45:D6:E2:EE:85:5F:9A:BA:E7:76:99:68:CC:E7\\n",
basicConstraints: "CA:FALSE",
certificatePolicies: "Policy: 2.16.840.1.114413.1.7.23.1\\n CPS: http://certificates.godaddy.com/repository/",
crlDistributionPoints: "Full Name:\\n URI:http://crl.godaddy.com/gds1-90.crl",
extendedKeyUsage: "TLS Web server authentication, TLS Web client authentication",
keyUsage: "Digital Signature, Key Encipherment",
subjectAltName: "DNS:acaline.com, DNS:www.acaline.com",
subjectKeyIdentifier: "E6:61:14:4E:5A:4B:51:0C:4E:6C:5E:3C:79:61:65:D4:BD:64:94:BE"
},
fingerprint: "FA:BE:B5:9B:ED:C2:2B:42:7E:B1:45:C8:9A:8A:73:16:4A:A0:10:09",
issuer: %{
C: ["US"],
CN: ["Go Daddy Secure Certification Authority"],
L: ["Scottsdale"],
O: ["GoDaddy.com, Inc."],
OU: ["http://certificates.godaddy.com/repository"],
ST: ["Arizona"],
aggregated: "/C=US/CN=Go Daddy Secure Certification Authority/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certificates.godaddy.com/repository/ST=Arizona",
emailAddress: []
},
not_after: 1398523877,
not_before: 1366987877,
serial_number: "27ACAE30B9F323",
signature_algorithm: "sha, rsa",
subject: %{
C: [],
CN: ["www.acaline.com"],
L: [],
O: [],
OU: ["Domain Control Validated"],
ST: [],
aggregated: "/CN=www.acaline.com/OU=Domain Control Validated",
emailAddress: []
}
}
"""
def parse_der(certificate_der, opts \\ [all_domains: false, serialize: false]) when is_binary(certificate_der) do
def parse_der(certificate_der, opts \\ [all_domains: false, serialize: false, multivalue: false]) when is_binary(certificate_der) do
cert = :public_key.pkix_decode_cert(certificate_der, :otp) |> get_field(:tbsCertificate)

serialized_certificate = %{}
|> Map.put(:fingerprint, certificate_der |> fingerprint_cert)
|> Map.put(:serial_number, cert |> get_field(:serialNumber) |> Integer.to_string(16))
|> Map.put(:signature_algorithm, cert |> parse_signature_algo)
|> Map.put(:subject, cert |> parse_rdnsequence(:subject))
|> Map.put(:issuer, cert |> parse_rdnsequence(:issuer))
|> Map.put(:subject, cert |> parse_rdnsequence(:subject, multivalue: opts[:multivalue]))
|> Map.put(:issuer, cert |> parse_rdnsequence(:issuer, multivalue: opts[:multivalue]))
|> Map.put(:extensions, cert |> parse_extensions)
|> Map.merge(parse_expiry(cert))


Enum.reduce(opts, serialized_certificate, fn {option, flag}, serialized_certificate ->
case option do
:all_domains when flag == true ->
Expand All @@ -93,6 +136,7 @@ defmodule EasySSL do
:serialize when flag == true ->
serialized_certificate
|> Map.put(:as_der, Base.encode64(certificate_der))

_ -> serialized_certificate
end
end)
Expand Down Expand Up @@ -168,12 +212,54 @@ defmodule EasySSL do
OU: "Domain Control Validated",
ST: nil,
aggregated: "/CN=www.acaline.com/OU=Domain Control Validated",
emailAddress: nil
emailAddress: []
}
}

# Pass in a binary (from Base.decode64, or some other source) and parse
# with multivalues as part of subject and issuer attributes.
iex(1)> EasySSL.parse_pem("-----BEGIN CERTIFICATE-----\\nMII...", multivalue: true)
%{
extensions: %{
authorityInfoAccess: "CA Issuers - URI:http://certificates.godaddy.com/repository/gd_intermediate.crt\\nOCSP - URI:http://ocsp.godaddy.com/\\n",
authorityKeyIdentifier: "keyid:FD:AC:61:32:93:6C:45:D6:E2:EE:85:5F:9A:BA:E7:76:99:68:CC:E7\\n",
basicConstraints: "CA:FALSE",
certificatePolicies: "Policy: 2.16.840.1.114413.1.7.23.1\\n CPS: http://certificates.godaddy.com/repository/",
crlDistributionPoints: "Full Name:\\n URI:http://crl.godaddy.com/gds1-90.crl",
extendedKeyUsage: "TLS Web server authentication, TLS Web client authentication",
keyUsage: "Digital Signature, Key Encipherment",
subjectAltName: "DNS:acaline.com, DNS:www.acaline.com",
subjectKeyIdentifier: "E6:61:14:4E:5A:4B:51:0C:4E:6C:5E:3C:79:61:65:D4:BD:64:94:BE"
},
fingerprint: "FA:BE:B5:9B:ED:C2:2B:42:7E:B1:45:C8:9A:8A:73:16:4A:A0:10:09",
issuer: %{
C: ["US"],
CN: ["Go Daddy Secure Certification Authority"],
L: ["Scottsdale"],
O: ["GoDaddy.com, Inc."],
OU: ["http://certificates.godaddy.com/repository"],
ST: ["Arizona"],
aggregated: "/C=US/CN=Go Daddy Secure Certification Authority/L=Scottsdale/O=GoDaddy.com, Inc./OU=http://certificates.godaddy.com/repository/ST=Arizona",
emailAddress: []
},
not_after: 1398523877,
not_before: 1366987877,
serial_number: "27ACAE30B9F323",
signature_algorithm: "sha, rsa",
subject: %{
C: [],
CN: ["www.acaline.com"],
L: [],
O: [],
OU: ["Domain Control Validated"],
ST: [],
aggregated: "/CN=www.acaline.com/OU=Domain Control Validated",
emailAddress: []
}
}
"""
def parse_pem(cert_charlist) when is_list(cert_charlist) do parse_pem(cert_charlist |> to_string) end
def parse_pem(cert_pem, opts \\ [all_domains: false, return_base64: false]) do
def parse_pem(cert_pem, opts \\ [all_domains: false, serialize: false, multivalue: false]) do
cert_regex = ~r/^\-{5}BEGIN\sCERTIFICATE\-{5}\n(?<certificate>[^\-]+)\-{5}END\sCERTIFICATE\-{5}/
match = Regex.named_captures(cert_regex, cert_pem)

Expand Down Expand Up @@ -276,15 +362,15 @@ defmodule EasySSL do
|> Enum.join(", ")
end

defp parse_rdnsequence(cert, field) do
defp parse_rdnsequence(cert, field, opts) do
rdnsequence = %{
:CN => nil,
:C => nil,
:L => nil,
:ST => nil,
:O => nil,
:OU => nil,
:emailAddress => nil
:CN => [],
:C => [],
:L => [],
:ST => [],
:O => [],
:OU => [],
:emailAddress => []
}

{:rdnSequence, rdnsequence_attribute} = cert |> get_field(field)
Expand All @@ -304,13 +390,34 @@ defmodule EasySSL do
end

case attr_atom do
nil -> rdnsequence
_ -> %{rdnsequence | attr_atom => attribute_value |> coerce_to_string |> to_string}
nil ->
rdnsequence
_ ->
attribute_value = attribute_value |> coerce_to_string |> to_string
new_attribute_values = [ attribute_value | rdnsequence[attr_atom] ]
%{rdnsequence | attr_atom => new_attribute_values}
end
end)

Map.put(rdnsequence, :aggregated, rdnsequence |> aggregate_rdnsequence)
case opts[:multivalue] do
true ->
# Reverse order of multi attribute values to honor order in cert
rdnsequence = Map.new(rdnsequence, fn {k, v} -> {k, Enum.reverse(v)} end)
Map.put(rdnsequence, :aggregated, rdnsequence |> aggregate_rdnsequence)

_ ->
# Save only the last value, then aggregate into a string
aggregated = Map.new(rdnsequence, fn
{k, [h|_]} -> {k, [h]}
{k, []} -> {k, []}
end) |> aggregate_rdnsequence

# Change list values to single elements, then add aggregated string
Map.new(rdnsequence, fn
{k, [h|_]} -> {k, h}
{k, []} -> {k, nil}
end) |> Map.put(:aggregated, aggregated)
end
end

defp parse_crl_distribution_points(crl_distribution_points)
Expand All @@ -334,14 +441,11 @@ defmodule EasySSL do
end

defp aggregate_rdnsequence(rdnsequence) do
rdnsequence
# Filter out empty values
|> Enum.filter(fn {_, v} -> v != nil end)
# Turn everything in to a string so C=blah.com
|> Enum.map(fn {k, v} -> (k |> to_string) <> "=" <> (v |> to_string) end)
# Add a buffer to the front to
|> Enum.join("/")
|> String.replace_prefix("", "/")
[ :C, :CN, :L, :O, :OU, :ST, :emailAddress ]
|> Enum.filter(& !Enum.empty?(rdnsequence[&1]))
|> Enum.map(fn k -> (k |> to_string) <> "=" <> (rdnsequence[k] |> Enum.join(", ")) end)
|> Enum.join("/")
|> String.replace_prefix("", "/")
end

defp parse_extensions(cert) do
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ defmodule EasySSL.MixProject do
{:excoveralls, "~> 0.8", only: :test},
{:ex_doc, "~> 0.16", only: :dev, runtime: false},
{:poison, "~> 2.0", only: :test},
{:ssl_verify_fun, "~> 1.1", manager: :rebar3, override: true}
]
end

Expand Down
Binary file added test/data/der/www.espn.com.der
Binary file not shown.
56 changes: 56 additions & 0 deletions test/easy_ssl_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,60 @@ defmodule EasySSLTest do
assert get_in(cert, [:subject, :emailAddress]) == "[email protected]"
assert get_in(cert, [:issuer, :emailAddress]) == "[email protected]"
end

test "parses attributes in subject in backwards compat manner" do
cert = File.read!(@der_cert_dir <> "www.espn.com.der") |> EasySSL.parse_der()
assert Map.has_key?(cert, :subject)

# Attributes are parsed as before as single values.
assert cert.subject[:C] == "US"

# The espn cert subject has zero OU attributes. Using parse_der without
# the multivalue option should result in same behavior pre-support for
# multivalue. I.e. the OU attribute should be nil.
assert cert.subject[:OU] == nil
end

test "parses attributes in subject correctly with multivalue option" do
cert = File.read!(@der_cert_dir <> "www.espn.com.der") |> EasySSL.parse_der(multivalue: true)
assert Map.has_key?(cert, :subject)

# Attributes are parsed as before as single values.
assert cert.subject[:C] == ["US"]

# The espn cert subject has zero OU attributes. Using parse_der without
# the multivalue option should result in same behavior pre-support for
# multivalue. I.e. the OU attribute should be nil.
assert cert.subject[:OU] == []
end

test "parses issuer with multiple OU values in backwards compat manner" do
cert = File.read!(@der_cert_dir <> "www.espn.com.der") |> EasySSL.parse_der()
assert Map.has_key?(cert, :issuer)

# The espn cert issuer has two OU attributes. Using parse_der without the
# multivalue option should result in same behavior pre-support for
# multivalue. I.e. the first OU attribute is discarded.
assert cert.issuer[:OU] == "(c) 2012 Entrust, Inc. - for authorized use only"

# Likewise for aggregated, it should only contain the 2nd attribute.
assert cert.issuer.aggregated == "/C=US/CN=Entrust Certification Authority - L1K/O=Entrust, Inc./OU=(c) 2012 Entrust, Inc. - for authorized use only"
end

test "parses issuer with multiple OU values correctly with multivalue option" do
cert = File.read!(@der_cert_dir <> "www.espn.com.der") |> EasySSL.parse_der(multivalue: true)
assert Map.has_key?(cert, :issuer)

# The espn cert issuer has two OU attributes. Using parse_der with the
# multivalue option should result in a list containing both in the order
# they appear in the cert.
assert cert.issuer[:OU] == [
"See www.entrust.net/legal-terms",
"(c) 2012 Entrust, Inc. - for authorized use only"]

# Likewise for aggregated, it should only contain the both attribute
# values separated by a comma.
assert cert.issuer.aggregated == "/C=US/CN=Entrust Certification Authority - L1K/O=Entrust, Inc./OU=See www.entrust.net/legal-terms, (c) 2012 Entrust, Inc. - for authorized use only"
end

end