lib/x509/pkcs7.ex

defmodule X509.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(
    :enveloped_data,
    :EnvelopedData,
    Record.extract(:EnvelopedData, 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(
    :recipient_info,
    :RecipientInfo,
    Record.extract(:RecipientInfo, from_lib: "public_key/include/OTP-PUB-KEY.hrl")
  )

  Record.defrecord(
    :encrypted_content_info,
    :EncryptedContentInfo,
    Record.extract(:EncryptedContentInfo, 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"
    )
  )

  Record.defrecord(
    :content_encryption_algorithm_identifier,
    :ContentEncryptionAlgorithmIdentifier,
    Record.extract(:ContentEncryptionAlgorithmIdentifier,
      from_lib: "public_key/include/OTP-PUB-KEY.hrl"
    )
  )

  @idData {1, 2, 840, 113_549, 1, 7, 1}
  @idSignedData {1, 2, 840, 113_549, 1, 7, 2}
  @idEnvelopedData {1, 2, 840, 113_549, 1, 7, 3}

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

  @idSha1 {1, 3, 14, 3, 2, 26}
  @idRsaEncryption {1, 2, 840, 113_549, 1, 1, 1}
  @idAes256Cbc {2, 16, 840, 1, 101, 3, 4, 1, 42}

  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

  def encrypt(input, cert) do
    key = :crypto.strong_rand_bytes(32)
    iv = :crypto.strong_rand_bytes(16)

    padded_input = with_padding(input)
    ciphertext = :crypto.crypto_one_time(:aes_256_cbc, key, iv, padded_input, true)

    IO.inspect(Base.encode16(ciphertext))

    recipient_public_key = X509.Certificate.public_key(cert)
    encrypted_key = :public_key.encrypt_public(key, recipient_public_key)

    content_info(
      contentType: @idEnvelopedData,
      content:
        enveloped_data(
          version: :edVer0,
          recipientInfos:
            {:riSet,
             [
               recipient_info(
                 version: :riVer0,
                 issuerAndSerialNumber:
                   issuer_and_serial_number(
                     issuer: X509.Certificate.issuer(cert),
                     serialNumber: X509.Certificate.serial(cert)
                   ),
                 keyEncryptionAlgorithm: alg_rsa(),
                 encryptedKey: encrypted_key
               )
             ]},
          encryptedContentInfo:
            encrypted_content_info(
              contentType: @idData,
              contentEncryptionAlgorithm: alg_aes256cbc(iv),
              encryptedContent: ciphertext
            )
        )
    )
  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 with_padding(input) do
    case rem(byte_size(input), 16) do
      0 ->
        input

      n ->
        pad_len = 16 - n
        input <> String.duplicate(<<pad_len>>, pad_len)
    end
  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

  defp alg_aes256cbc(iv) do
    content_encryption_algorithm_identifier(
      algorithm: @idAes256Cbc,
      parameters: {:asn1_OPENTYPE, <<4, 16, iv::binary>>}
    )
  end
end