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()
.