How can I generate PKCS7 file?

Well… OTP’s :public_key does include the ASN.1 definitions for PKCS#7, but it does not offer any higher-level API for working with those data structures.

Here’s a little something I put together that will generate output similar to your OpenSSL example:

defmodule PKCS7 do
  require Record

  Record.defrecord(:content_info, :ContentInfo, Record.extract(:ContentInfo, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:signed_data, :SignedData, Record.extract(:SignedData, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:signer_info, :SignerInfo, Record.extract(:SignerInfo, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:issuer_and_serial_number, :IssuerAndSerialNumber, Record.extract(:IssuerAndSerialNumber, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:attribute_pkcs7, :"AttributePKCS-7", Record.extract(:"AttributePKCS-7", from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:digest_algorithm_identifier, :DigestAlgorithmIdentifier, Record.extract(:DigestAlgorithmIdentifier, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))
  Record.defrecord(:digest_encryption_algorithm_identifier, :DigestEncryptionAlgorithmIdentifier, Record.extract(:DigestEncryptionAlgorithmIdentifier, from_lib: "public_key/include/OTP-PUB-KEY.hrl"))

  @idData {1, 2, 840, 113549, 1, 7, 1}
  @idSignedData {1, 2, 840, 113549, 1, 7, 2}

  @idContentType {1, 2, 840, 113549, 1, 9, 3}
  @idMessageDigest {1, 2, 840, 113549, 1, 9, 4}
  @idSigningTime {1, 2, 840, 113549, 1, 9, 5}

  @idSha1 {1, 3, 14, 3, 2, 26}
  @idRsaEncryption {1, 2, 840, 113549, 1, 1, 1}

  def sign(input, cert, key) do
    attributes = {:aaSet, [
      attribute_pkcs7(type: @idContentType, values: [@idData]),
      attribute_pkcs7(type: @idSigningTime, values: [X509.DateTime.new()]),
      attribute_pkcs7(type: @idMessageDigest, values: [:crypto.hash(:sha, input)])
    ]}

    signature = :public_key.sign(aa_tbs(attributes), :sha, key)

    content_info(
      contentType: @idSignedData,
      content: signed_data(
        version: :sdVer1,
        digestAlgorithms: {:daSet, [alg_sha1()]},
        contentInfo: {:ContentInfo, @idData, input},
        certificates: {:certSet, [certificate: cert]},
        # crls: asn1_NOVALUE,
        signerInfos: {:siSet,
          [
            signer_info(
              version: :siVer1,
              issuerAndSerialNumber: issuer_and_serial_number(
                issuer: X509.Certificate.issuer(cert),
                serialNumber: X509.Certificate.serial(cert)
              ),
              digestAlgorithm: alg_sha1(),
              authenticatedAttributes: attributes,
              digestEncryptionAlgorithm: alg_rsa(),
              encryptedDigest: signature
              # unauthenticatedAttributes: :asn1_NOVALUE
            )
          ]
        }
      )
    )
  end

  # rfc2630#section-5.4:
  # A separate encoding of the
  # signedAttributes field is performed for message digest calculation.
  # The IMPLICIT [0] tag in the signedAttributes field is not used for
  # the DER encoding, rather an EXPLICIT SET OF tag is used.  That is,
  # the DER encoding of the SET OF tag, rather than of the IMPLICIT [0]
  # tag, is to be included in the message digest calculation along with
  # the length and content octets of the SignedAttributes value.
  defp aa_tbs(aa) do
    <<0xA0, value :: binary>> = :public_key.der_encode(:SignerInfoAuthenticatedAttributes, aa)
    <<0x31, value :: binary>>
  end

  defp alg_sha1 do
    digest_algorithm_identifier(algorithm: @idSha1, parameters: {:asn1_OPENTYPE, <<5, 0>>})
  end

  defp alg_rsa do
    digest_encryption_algorithm_identifier(algorithm: @idRsaEncryption, parameters: {:asn1_OPENTYPE, <<5, 0>>})
  end
end

At this point it only supports RSA and SHA-1. The certificate needs to be passed in as a Certificate record, rather than an OTPCertificate record. So if you use X509.Certificate.from_pem to read it, make sure to pass :Certificate as the second argument.

You can convert the output to DER format using :public_key.der_encode(:ContentInfo, ci), or to PEM format with :public_key.pem_entry_encode(:ContentInfo, ci) |> List.wrap() |> :public_key.pem_encode().

7 Likes