// OpenPGP cleartext-signature envelope detection and extraction (RFC 9116 2.3,
// RFC 4880 7). v0.1.0-dev performs NO verification: a detected envelope is
// SignedUnverified — never verified. See docs/limitations.md.
///|
/// Result of scanning an input for a signed envelope.
pub struct SignedPayload {
armor_headers : Array[String]
cleartext_lines : Array[String]
cleartext_offsets : Array[Int]
}
///|
/// Armor headers from the envelope (for example `Hash: SHA256`).
pub fn SignedPayload::armor_headers(self : SignedPayload) -> Array[String] {
self.armor_headers
}
///|
/// Dash-unescaped cleartext lines.
pub fn SignedPayload::cleartext_lines(self : SignedPayload) -> Array[String] {
self.cleartext_lines
}
///|
/// Byte offsets of the cleartext lines in the original input.
pub fn SignedPayload::cleartext_offsets(self : SignedPayload) -> Array[Int] {
self.cleartext_offsets
}
///|
/// Strip one leading RFC 4880 dash-escape (`- ` doubling) from a cleartext line.
fn unescape_dash(line : String) -> String {
if line.has_prefix("- ") {
slice(line, 2, line.length())
} else {
line
}
}
///|
fn signature_error(
line : Int,
offset : Int,
message : String,
) -> SecurityTxtError {
security_txt_error(
SignatureEnvelope,
InvalidSignatureEnvelope,
line,
1,
offset,
message,
)
}
///|
/// Split a signed envelope: Ok(None)=unsigned, Ok(Some(payload))=complete, Err=malformed.
pub fn split_signed_envelope(
lines : Array[String],
offsets : Array[Int],
) -> Result[SignedPayload?, SecurityTxtError] {
if lines.length() == 0 {
return Ok(None)
}
if lines[0] != "-----BEGIN PGP SIGNED MESSAGE-----" {
return Ok(None)
}
// Armor headers end at the first blank line, which MUST be present.
let mut i = 1
let headers : Array[String] = []
let mut seen_blank = false
while i < lines.length() && !seen_blank {
if lines[i].length() == 0 {
seen_blank = true
} else {
headers.push(lines[i])
}
i += 1
}
if !seen_blank {
return Err(
signature_error(
i + 1,
offsets[lines.length() - 1],
"missing blank line after armor headers",
),
)
}
// Cleartext until the signature marker; signers dash-escape any raw line equal to the marker.
let cleartext : Array[String] = []
let clear_offsets : Array[Int] = []
let mut found_signature = false
while i < lines.length() && lines[i] != "-----BEGIN PGP SIGNATURE-----" {
cleartext.push(unescape_dash(lines[i]))
clear_offsets.push(offsets[i])
i += 1
}
if i < lines.length() {
found_signature = true
i += 1
}
if !found_signature {
return Err(
signature_error(
i + 1,
offsets[lines.length() - 1],
"missing PGP BEGIN SIGNATURE marker",
),
)
}
// Signature block until the END marker.
let mut found_end = false
while i < lines.length() && lines[i] != "-----END PGP SIGNATURE-----" {
i += 1
}
if i < lines.length() {
found_end = true
i += 1
}
if !found_end {
return Err(
signature_error(
i + 1,
offsets[lines.length() - 1],
"missing PGP END SIGNATURE marker",
),
)
}
// Nothing but blank lines may follow the signature block.
while i < lines.length() && lines[i].length() == 0 {
i += 1
}
if i < lines.length() {
return Err(
signature_error(
i + 1,
offsets[i],
"unexpected content after the signature block",
),
)
}
Ok(
Some({
armor_headers: headers,
cleartext_lines: cleartext,
cleartext_offsets: clear_offsets,
}),
)
}
///|
/// Extract the cleartext body of a signed message (joined with LF). No verification.
pub fn extract_cleartext(input : String) -> Result[String?, SecurityTxtError] {
let (lines, offsets) = split_lines_text(input)
match split_signed_envelope(lines, offsets) {
Ok(None) => Ok(None)
Ok(Some(payload)) => Ok(Some(payload.cleartext_lines.join("\n")))
Err(err) => Err(err)
}
}