defmodule GitGud.Packages.Formats.Fdroid.Pkcs7 do
@moduledoc """
Builds the `CERT.RSA` of a signed JAR: a PKCS#7 / CMS `SignedData`
carrying the signing certificate and one detached signature.
## Why this is written out rather than delegated
`docs/fdroid-registry.md` originally planned a Rustler NIF around
RustCrypto's `cms`, on the reasoning that `:public_key` speaks X.509
and PKCS but not CMS. Two things changed that once the OpenBao
backend landed:
**The key is not ours to hold.** The whole point of transit is that
the private key never reaches the app, so a NIF could not simply
"sign" — it would have to hand back bytes to be signed elsewhere and
then be called again to assemble the result. That splits the job
exactly at the boundary where the hard part is already done: the
assembly is DER, and DER is mechanical.
**A JAR signature needs no `signedAttrs`.** PKCS#7 allows the
signature to cover a set of authenticated attributes, which would
mean encoding them, sending them out to be signed, and folding the
result back. `jarsigner` has always also emitted the simpler form,
where the signature is taken directly over the signed content — here,
the bytes of `CERT.SF`. Java's verifier accepts both. The simpler
form is a single call to `GitGud.Packages.Signing.sign/4` over a
buffer we already have.
So what's left is emitting a fixed DER structure, which is this
module, and it adds no build dependency and no system library to
either image stage.
## The structure
ContentInfo ::= SEQUENCE {
contentType OBJECT IDENTIFIER, -- signedData
content [0] EXPLICIT SignedData }
SignedData ::= SEQUENCE {
version INTEGER, -- 1
digestAlgorithms SET OF AlgorithmIdentifier,
encapContentInfo SEQUENCE { eContentType OBJECT IDENTIFIER },
certificates [0] IMPLICIT SET OF Certificate,
signerInfos SET OF SignerInfo }
SignerInfo ::= SEQUENCE {
version INTEGER, -- 1
sid IssuerAndSerialNumber,
digestAlgorithm AlgorithmIdentifier,
signatureAlgorithm AlgorithmIdentifier,
signature OCTET STRING }
`encapContentInfo` carries no `eContent`: the signature is detached,
because the content it covers is `CERT.SF`, which is already its own
member of the JAR.
"""
# DER tags.
@integer 0x02
@bit_string 0x03
@octet_string 0x04
@null 0x05
@oid 0x06
@sequence 0x30
@set 0x31
# OIDs, as encoded bodies.
@oid_signed_data <<0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02>>
@oid_data <<0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x01>>
@oid_sha256 <<0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01>>
@oid_sha1 <<0x2B, 0x0E, 0x03, 0x02, 0x1A>>
@oid_rsa <<0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01>>
@doc """
A `CERT.RSA` for `signature` over content digested with `digest`,
attributing it to `certificate_der`.
The certificate is parsed to lift its issuer and serial number, which
is how a verifier finds the right certificate among the ones carried.
Failing here rather than emitting a structure that points at nothing
is the difference between a clear error and a client saying only
"invalid signature".
"""
@spec sign_data(binary(), binary(), :sha256 | :sha1) ::
{:ok, binary()} | {:error, term()}
def sign_data(certificate_der, signature, digest \\ :sha256)
when is_binary(certificate_der) and is_binary(signature) do
with {:ok, issuer, serial} <- issuer_and_serial(certificate_der) do
signer_info =
seq([
int(1),
seq([issuer, int(serial)]),
algorithm(digest_oid(digest)),
algorithm(@oid_rsa),
tlv(@octet_string, signature)
])
signed_data =
seq([
int(1),
tlv(@set, algorithm(digest_oid(digest))),
seq([tlv(@oid, @oid_data)]),
# [0] IMPLICIT — the certificates are a SET, retagged.
tlv(0xA0, certificate_der),
tlv(@set, signer_info)
])
{:ok, seq([tlv(@oid, @oid_signed_data), tlv(0xA0, signed_data)])}
end
end
@doc """
Issuer (as its raw DER `Name`) and serial number of a certificate.
Both are lifted as encoded bytes rather than re-encoded from a parsed
form: a `Name` that round-trips through a decoder can come back
differing in string types, and a verifier matches it byte for byte.
"""
@spec issuer_and_serial(binary()) :: {:ok, binary(), integer()} | {:error, term()}
def issuer_and_serial(certificate_der) do
with {:ok, tbs} <- first_child(certificate_der),
{:ok, fields} <- children(tbs) do
# TBSCertificate ::= SEQUENCE { version [0] EXPLICIT OPTIONAL,
# serialNumber INTEGER, signature AlgorithmIdentifier,
# issuer Name, ... }
fields =
case fields do
[<<0xA0, _::binary>> | rest] -> rest
all -> all
end
case fields do
[<<@integer, _::binary>> = serial, _signature_alg, issuer | _] ->
{:ok, issuer, decode_integer(serial)}
_ ->
{:error, :unexpected_certificate_shape}
end
end
end
# ── DER writing ──────────────────────────────────────────────────────
defp seq(parts), do: tlv(@sequence, IO.iodata_to_binary(parts))
defp algorithm(oid), do: seq([tlv(@oid, oid), tlv(@null, <<>>)])
defp int(n) when is_integer(n) and n >= 0 do
bytes = :binary.encode_unsigned(n)
# DER integers are signed, so a leading bit of 1 needs a zero byte
# in front or the value reads as negative.
bytes = if :binary.first(bytes) >= 0x80, do: <<0>> <> bytes, else: bytes
tlv(@integer, bytes)
end
@doc false
# A DER BIT STRING with no unused bits — the wrapper an X.509
# signature goes in.
def bit_string(bytes), do: tlv(@bit_string, <<0>> <> bytes)
@doc false
def tlv(tag, value) when is_integer(tag) and is_binary(value),
do: <<tag>> <> length_of(value) <> value
defp length_of(value) when byte_size(value) < 0x80, do: <<byte_size(value)>>
defp length_of(value) do
bytes = :binary.encode_unsigned(byte_size(value))
<<0x80 + byte_size(bytes)>> <> bytes
end
defp digest_oid(:sha1), do: @oid_sha1
defp digest_oid(_sha256), do: @oid_sha256
# ── DER reading, enough to pick a certificate apart ──────────────────
defp first_child(der) do
with {:ok, body} <- value_of(der), {:ok, [child | _]} <- children_of(body) do
{:ok, child}
else
_ -> {:error, :not_a_sequence}
end
end
defp children(der) do
with {:ok, body} <- value_of(der), {:ok, list} <- children_of(body) do
{:ok, list}
else
_ -> {:error, :not_a_sequence}
end
end
defp value_of(<<_tag, rest::binary>> = der) do
with {:ok, len, header} <- read_length(rest),
true <- header + len <= byte_size(der) do
{:ok, binary_part(der, header, len)}
else
_ -> :error
end
end
defp value_of(_), do: :error
defp children_of(body), do: children_of(body, [])
defp children_of(<<>>, acc), do: {:ok, Enum.reverse(acc)}
defp children_of(<<_tag, rest::binary>> = data, acc) do
with {:ok, len, header} <- read_length(rest),
total = header + len,
true <- total <= byte_size(data) do
child = binary_part(data, 0, total)
children_of(binary_part(data, total, byte_size(data) - total), [child | acc])
else
_ -> {:ok, Enum.reverse(acc)}
end
end
defp children_of(_, acc), do: {:ok, Enum.reverse(acc)}
defp read_length(<<len, _::binary>>) when len < 0x80, do: {:ok, len, 2}
defp read_length(<<first, rest::binary>>) when first > 0x80 and first - 0x80 <= 4 do
count = first - 0x80
if byte_size(rest) >= count do
{:ok, :binary.decode_unsigned(binary_part(rest, 0, count)), 2 + count}
else
:error
end
end
defp read_length(_), do: :error
defp decode_integer(<<@integer, _::binary>> = tlv) do
case value_of(tlv) do
{:ok, bytes} -> :binary.decode_unsigned(bytes)
_ -> 0
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
8.1 KiB · text
History
b868907