Skip to content

Certificate with a subkey on an unknown ECC curve fails to parse as a whole (InvalidPacketContent) #869

Open

Description

@CyberKnight-cmd

Hi, and thanks for rPGP!

Summary: a certificate that contains a subkey on an unknown ECC curve (ECDSA, EdDSALegacy or ECDH with an OID rPGP doesn't know, whose key material isn't an MPI) fails to parse as a whole with InvalidPacketContent. Because of this, signatures made by the certificate's known primary key can't be verified either. Subkeys with an unknown algorithm id are already handled: they parse as PublicParams::Unknown, and the certificate stays usable.

The interoperability test suite expects a certificate like this to stay usable for its other keys, with the unknown subkey ignored. That's the upgrade path for new algorithms (PQC, for example) deployed alongside classical keys. Sequoia (sqop 0.37.3) does this.

Found with the OpenPGP interoperability test suite, commit 601692566e9d, group "Mock PQ subkey" (verify-only). These six cases fail with rPGP and pass with sqop:

  • ECDSA, unknown curve, opaque encoding, small / … big
  • EdDSA, unknown curve, opaque encoding, small / … big
  • ECDH, unknown curve, opaque encoding, small / … big

The … MPI encoding variants and all Unknown algo … variants pass. I re-checked the three small cases locally from the suite's saved artifacts, and they fail with the error below. The big variants failed in the same suite run, but the results file leaves out their artifacts because of their size, so I haven't re-checked them on their own.

Minimal reproducer: any valid binary v4 certificate (gpg --export <fingerprint> > cert.gpg) with one public-subkey packet appended. A binding signature isn't needed to trigger the error.

// cargo add pgp@0.21.0
use pgp::composed::{Deserializable as _, SignedPublicKey};

fn main() {
    let cert = std::fs::read(std::env::args().nth(1).expect("cert.gpg")).unwrap();
    for (alg, name) in [(19u8, "ECDSA"), (22, "EdDSALegacy"), (18, "ECDH"), (99, "unknown algorithm 99 (control)")] {
        // v4 public-subkey packet (tag 14): version 4, creation time,
        // algorithm, OID 1.3.6.1.4.1.37210.99, then 4 bytes that aren't an MPI.
        let oid = [0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xa2, 0x5a, 0x63];
        let mut body = vec![4, 0x5e, 0x0b, 0xe1, 0x00, alg, oid.len() as u8];
        body.extend_from_slice(&oid);
        body.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
        let mut c = cert.clone();
        c.extend([0xC0 | 14, body.len() as u8]);
        c.extend(body);
        println!("{name}: {:?}", SignedPublicKey::from_bytes(&c[..]).map(|_| "ok"));
    }
}

Output, identical on 0.21.0 and on main at 136e5c883096:

ECDSA: Err(Message { message: "error while parsing composed key: InvalidPacketContent { source: InvalidInput { backtrace: None } }", backtrace: None })
EdDSALegacy: Err(… same …)
ECDH: Err(… same …)
unknown algorithm 99 (control): Ok("ok")

Expected: the unknown-curve subkey parses as an unusable subkey (as PublicParams::Unknown does for unknown algorithms, or as a variant of EcdsaPublicParams::Unsupported that keeps the raw bytes when they aren't an MPI). The certificate stays usable for its other keys.

Context: we use rPGP to verify AppImage signatures in ULAI (GPL-3.0). We don't want to work around this by filtering packets ourselves. For now ULAI reports such certificates as "unsupported". Happy to test a fix.

Activity

  1. hko-s commented on Oct 22, 2026

    @hko-s
    Contributor

    Thanks for reporting this. The certificate parser should ideally not fail for this case, indeed.

    We recently changed the (one level lower) key packet parser to be more strict - and reject ECDSA/EdDSALegacy key packets whose key data is not MPI-shaped (1e3a886).
    I think this change (in itself) is correct, since RFC 9580 is unequivocal about requiring key packets with these algorithms to use MPI encoded key material.

    However, agreed that the certificate parser should be able to skip such subkeys.

    Out of curiosity: What type of subkeys are you using that contain non-MPI key material?

  2. CyberKnight-cmd commented on Oct 23, 2026

    @CyberKnight-cmd
    Author

    Thanks for the quick reply!

    We don't use subkeys like this ourselves, and I haven't seen one in a real certificate. ULAI only accepts RSA and Ed25519 keys. I found the problem through the "Mock PQ subkey" cases in the interop test suite.

    The stricter packet parsing in 1e3a886 makes sense to me, given what RFC 9580 says about MPI encoding. My concern is just the certificate level: if a publisher's certificate one day has a subkey we can't use, we'd still like to verify signatures from their other keys rather than lose the whole certificate.

    Happy to send a PR for this. Looking at key_parser.rs, the bad subkey packet comes back as a hard InvalidPacketContent, so the subkey loop stops and the whole key fails. I was thinking the subkey loop could just skip a subkey it can't parse (plus its signatures) and keep going. Would that work for you, or would you rather keep it around as some kind of unknown subkey? I'll add tests from the suite's unknown-curve cases either way.

  3. hko-s commented on Oct 23, 2026

    @hko-s
    Contributor

    I'm aware of that test case and have thought about it. Frankly, I'm unsure if I agree of disagree with the test case:

    There is no draft that allows anything like that kind of subkey. It's just syntactically wrong, as of today.

    Of course it's still possible - and probably not terribly wrong to skip such a subkey (including its signatures). If you make such a PR, I'd be happy to consider it. Having a PR would also give us some insights if rPGP's CI will raise issues about the approach. That would be useful in and of itself.

    I don't have a lot of bandwidth to think this through more deeply, right now, but as a medium term project, this sounds useful to look into.
    I'd also want to read up what kinds of constraints the key_parser should enforce. E.g. looking at RFC 9580 section 10.1.3, there is a hard requirement for key packet version alignment within each "transferable key": subkeys for v4 primaries must also be v4, and analogously above for v6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions