You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中如何实现PGP/MIME邮件签名?签名无效问题排查

PGP/MIME邮件签名无效问题排查与解决方案

问题描述

用Java实现RFC3156定义的PGP/MIME邮件签名,使用PGPainless SOP和Angus Mail库。纯文本签名功能正常,未签名邮件发送正常,但签名后的邮件在Thunderbird、Evolution中均提示“存在签名但无效”。尝试过移除首尾换行、替换CRLF、切换哈希算法等操作,问题依然存在。

最小复现代码

@Test
public void testSendSignedMessage() throws MessagingException, IOException {
    String SECRET_KEY = "...";
    String PASSWORD = "...";

    Properties properties = new Properties();
    properties.put("mail.smtp.auth", true);
    properties.put("mail.smtp.ssl.enable", "true");
    properties.put("mail.smtp.host", "smtp.cia.gov");
    properties.put("mail.smtp.port", "465");

    properties.put("mail.store.protocol", "imaps");
    properties.put("mail.imap.host", "imap.cia.gov");
    properties.put("mail.imap.port", "993");
    properties.put("mail.imap.ssl.enable", "true");

    Session session = Session.getInstance(properties, new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication("jason.bourne@cia.gov", "treadstone");
        }
    });

    MimeMessage email = new MimeMessage(session);

    email.setFrom("jason.bourne@cia.gov");
    email.setRecipient(Message.RecipientType.TO, new InternetAddress("pamela.landy@cia.gov"));
    email.setSubject("Get some rest");

    MimeBodyPart textPart = new MimeBodyPart();
    textPart.setText("You look tired.");

    MimeMultipart multipart = new MimeMultipart();
    multipart.addBodyPart(textPart);

    email.setContent(multipart);
    this.signMessage(email, SECRET_KEY.getBytes(), PASSWORD.getBytes(), session);
    Transport.send(email);
}

private void signMessage(MimeMessage email, byte[] secretKey, byte[] password, Session session)
        throws MessagingException, IOException 
{
    Multipart signedMultipart = new MimeMultipart();
    MimeBodyPart contentBody = new MimeBodyPart();
    MimeBodyPart signatureBody = new MimeBodyPart();

    Multipart contentMultipart = (Multipart) email.getContent();
    contentBody.setContent(contentMultipart);
    signedMultipart.addBodyPart(contentBody);

    Part temporaryEmail = new MimeMessage(session);
    temporaryEmail.setContent(contentMultipart);

    OutputStream outputStream = new ByteArrayOutputStream();
    temporaryEmail.writeTo(outputStream);
    String boundary = this.parseBoundary(String.valueOf(outputStream)).orElse("");

    signatureBody.attachFile(this.createSignatureAttachment(contentMultipart, secretKey, password, boundary));
    signatureBody.setHeader("Content-Type", "application/pgp-signature; name=\"signature.asc\"");
    signatureBody.setHeader("Content-Description", "OpenPGP signature");
    signatureBody.setHeader("Content-Disposition", "attachment; filename=\"signature.asc\"");

    signedMultipart.addBodyPart(signatureBody);
    email.setContent(signedMultipart);

    outputStream = new ByteArrayOutputStream();
    email.writeTo(outputStream);
    boundary = parseBoundary(String.valueOf(outputStream)).orElse("");

    email.setHeader(
        "Content-Type",
        "multipart/signed; micalg=\"pgp-sha256\"; protocol=\"application/pgp-signature\"%s".formatted(
            boundary.isEmpty() ? "" : "; boundary=\"%s\"".formatted(boundary)
        )
    );
}

private File createSignatureAttachment(
    Multipart message,
    byte[] secretKey,
    byte[] password,
    String boundary
) throws IOException, MessagingException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    message.writeTo(outputStream);

    String messageContent =
        outputStream
            .toString(StandardCharsets.UTF_8)
            .replaceFirst("^-*%s-*".formatted(boundary), "") // Remove the leading boundary
            .replaceFirst("-*%s-*$".formatted(boundary), "") // Remove the trailing boundary
            .replaceFirst("^[\n\r]+", "") // Remove leading newlines
            .replaceFirst("[\n\r\\s]+$", "") // Remove trailing newlines and whitespace
            .replaceAll("(?<!\r)\n", "\r\n"); // Convert line endings to CRLF

    SOP sop = new SOPImpl();
    DetachedSign signature = sop.sign().key(secretKey).withKeyPassword(password);

    InputStream inputStream =
        signature.data(
            new ByteArrayInputStream(messageContent.getBytes())
        ).toByteArrayAndResult().getInputStream();

    Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"));

    File signatureDirectory =
        tempDirectory
            .resolve("signature-" + UUID.randomUUID())
            .toFile();

    signatureDirectory.mkdirs();
    File encryptedFile = new File(signatureDirectory, "signature.asc");

    try (FileOutputStream fileOutputStream = new FileOutputStream(encryptedFile)) {
        fileOutputStream.write(inputStream.readAllBytes());
    }

    return encryptedFile;
}

private Optional<String> parseBoundary(final String contentTypeHeader) {
    Matcher matcher =
        Pattern
            .compile("boundary=\"(?<boundary>.+)\"", Pattern.CASE_INSENSITIVE)
            .matcher(contentTypeHeader);

    if (matcher.find())
        return Optional.of(matcher.group("boundary"));

    return Optional.empty();
}

依赖配置

<dependency>
    <groupId>jakarta.mail</groupId>
    <artifactId>jakarta.mail-api</artifactId>
    <version>${jakarta-mail.version}</version>
</dependency>
<dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
    <version>${angus-mail.version}</version>
</dependency>
<dependency>
    <groupId>org.pgpainless</groupId>
    <artifactId>pgpainless-sop</artifactId>
    <version>${pgpainless.version}</version>
</dependency>

生成的邮件正文

------=_Part_1_166454155.1747153500629
Content-Type: multipart/mixed; 
    boundary="----=_Part_0_1604052588.1747153500624"

------=_Part_0_1604052588.1747153500624
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit

You look tired.
------=_Part_0_1604052588.1747153500624--

------=_Part_1_166454155.1747153500629
Content-Type: application/pgp-signature; name="signature.asc"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="signature.asc"
Content-Description: OpenPGP signature

-----BEGIN PGP SIGNATURE-----

iHUEABYKACcFg...
-----END PGP SIGNATURE-----

------=_Part_1_166454155.1747153500629--

错误分析与修正

核心错误点

  1. 签名内容被手动篡改:RFC3156要求签名的是multipart/signed第一个part的完整原始MIME内容(包括所有头字段、换行符、边界),手动裁剪边界、移除换行的操作破坏了字节流一致性,导致客户端验证时内容和签名时的内容不匹配。
  2. 临时邮件错误引入额外头:用temporaryEmail.writeTo()输出的内容包含邮件头(如From/To),但实际需要签名的是邮件的正文multipart部分,不是整个邮件。
  3. 签名part的Content-Disposition错误:PGP/MIME规范中,签名part应该是inline而非attachment,客户端会忽略作为附件的签名,无法关联到正文内容。
  4. 手动设置boundary易出错:应该让MimeMultipart自动管理boundary,手动解析再设置容易出现格式差异(比如空格、换行位置)。

修正后的关键代码

private void signMessage(MimeMessage email, byte[] secretKey, byte[] password, Session session)
        throws MessagingException, IOException {
    // 提取原始内容part
    Multipart contentMultipart = (Multipart) email.getContent();
    MimeBodyPart contentBody = new MimeBodyPart();
    contentBody.setContent(contentMultipart);

    // 直接对contentBody的原始字节流签名,不做任何修改
    ByteArrayOutputStream contentOut = new ByteArrayOutputStream();
    contentBody.writeTo(contentOut);
    byte[] contentBytes = contentOut.toByteArray();

    // 生成分离式签名
    SOP sop = new SOPImpl();
    DetachedSign signature = sop.sign().key(secretKey).withKeyPassword(password);
    byte[] signatureBytes = signature.data(new ByteArrayInputStream(contentBytes))
            .toByteArrayAndResult().getBytes();

    // 创建签名part
    MimeBodyPart signatureBody = new MimeBodyPart();
    signatureBody.setContent(signatureBytes, "application/pgp-signature");
    signatureBody.setHeader("Content-Description", "OpenPGP signature");
    // 必须设置为inline,不能是attachment
    signatureBody.setHeader("Content-Disposition", "inline; filename=\"signature.asc\"");

    // 创建multipart/signed容器
    MimeMultipart signedMultipart = new MimeMultipart("signed");
    signedMultipart.addBodyPart(contentBody);
    signedMultipart.addBodyPart(signatureBody);
    // 手动设置协议和哈希算法,符合RFC3156
    signedMultipart.setSubType("signed; protocol=\"application/pgp-signature\"; micalg=\"pgp-sha256\"");

    // 替换邮件内容
    email.setContent(signedMultipart);
    // 让JavaMail自动处理Content-Type头,无需手动设置boundary
}

替代库推荐

  • PGPainless Mail模块:直接提供PGPMime类,一键生成PGP/MIME签名/加密邮件,完全符合RFC3156,无需手动拼接MIME结构,代码量大幅减少。依赖只需添加pgpainless-mail。
  • BouncyCastle Mail:功能全面,但需要手动处理更多MIME细节,学习成本较高。

内容的提问来源于stack exchange,提问作者Pixelcode

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:17:02