Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use pkcs1 crate to parse RSAPrivateKey ASN.1 object #261

Merged
merged 1 commit into from Oct 26, 2022

Conversation

dwhjames
Copy link
Contributor

following from review conversation:
#260 (comment)

references:

age/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Owner

@str4d str4d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK. The changes here look good, but this PR is blocked on a bugfix upstream. cc @tarcieri

age/src/ssh.rs Outdated Show resolved Hide resolved
Comment on lines 310 to 325
if enc_header.is_some() {
Some(UnsupportedKey::EncryptedPem.into())
} else {
read_asn1::rsa_privkey(&privkey).ok().map(|(_, privkey)| {
let mut ssh_key = vec![];
cookie_factory::gen(
write_ssh::rsa_pubkey(&privkey.to_public_key()),
&mut ssh_key,
)
.expect("can write into a Vec");
UnencryptedKey::SshRsa(ssh_key, Box::new(privkey)).into()
})
use rsa::pkcs1::FromRsaPrivateKey;
rsa::RsaPrivateKey::from_pkcs1_der(&privkey)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked whether we could use rsa::RsaPrivateKey::from_pkcs1_pem here to remove more code, but the pkcs1 crate does not support parsing the legacy Encrypted PEM format, which we need to do here in order to tell users that it is unsupported. So this is the correct change.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More precisely, rsa::RsaPrivateKey::from_pkcs1_pem uses the pem-rfc7468 crate, and RFC 7468 specifically excludes the header portion:

   Unlike legacy PEM encoding [RFC1421], OpenPGP ASCII armor, and the
   OpenSSH key file format, textual encoding does *not* define or permit
   headers to be encoded alongside the data.  Empty space can appear
   between the pre-encapsulation boundary and the base64, but generators
   SHOULD NOT emit such any such spacing.  (The provision for this empty
   area is a throwback to PEM, which defined an "encapsulated header
   portion".)

We are specifically accepting the OpenSSH key file format here, so we need to parse the PEM ourselves.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened RustCrypto/formats#112 upstream to clarify documentation.

Copy link
Contributor Author

@dwhjames dwhjames Oct 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I added RustCrypto/formats#13 Error::HeaderDisallowed with exactly this use case in mind. But I take your point here RustCrypto/formats#112 (comment) that you would still like to verify that there is not just some header, but the exact header that indicates it’s a legacy encrypted PKCS#1 RSA private key.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you recall you also added decode_label to parse the label from the PEM encapsulation boundary https://docs.rs/pem-rfc7468/0.2.3/pem_rfc7468/fn.decode_label.html

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be interesting to add a higher-level parser type which could retain the context of what it was successfully able to parse. It already exists internally in the crate as the Encapsulation struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I suppose the question to @str4d is… how assured do you want to be about the validity of the unsupported key file?

If you parse the PEM label with pem-rfc7468 and you get ENCRYPTED RSA PRIVATE KEY is that sufficient to indicate to the user that this is an unsupported key file? Or do you also want to confirm not just the presence of the header but also its specifics?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you parse the PEM label with pem-rfc7468 and you get ENCRYPTED RSA PRIVATE KEY is that sufficient to indicate to the user that this is an unsupported key file?

No, because ENCRYPTED RSA PRIVATE KEY is not what is used as the label in practice; you instead get the same PEM label as for unencrypted, but with the additional header. Here's what I get from openssl 1.1.1f:

$ openssl genrsa -out example.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
...............+++++
....................+++++
e is 65537 (0x010001)
$ cat example.key | head -n 2
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA9WDz0dl/aYSgxHso3rt9c1ljZ1qvmOUOCa1oyX0dAX3YQqlF
$ openssl rsa -des3 -in example.key -out protected.key
writing RSA key
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
$ cat protected.key | head -n 5
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,9BC872773CF8C93B

U8d2d6DqZNX20UyrOr5fqc+eJCaOzj8G8fPm4gdmvESVzAW95BNQpRHodAEW6Nx5

I want to continue to distinguish three cases, as I do in the current parser:

  • Data has no PEM header.
    • Great, we carry on parsing, and maybe return a supported key!
  • Data has a PEM header matching the structure of the encrypted PEM header.
    • We stop parsing and return Identity::Unsupported(UnsupportedKey::EncryptedPem).
  • Data has an unknown PEM header.
    • We return a parser error.

Per my comment in RustCrypto/formats#112 (comment), I think how we could achieve this is to match on the pem_rfc7468::Error::HeaderDisallowed error, and in that case we re-parse the data ourselves with the following small parser:

preceded(
    tuple((
        tag("-----BEGIN "),
        tag(pem_label),
        tag("-----"),
        line_ending,
    )),
    terminated(rsa_pem_encryption_header, line_ending),
)

This would result in the PEM header being parsed three separate times (once to get Error::HeaderDisallowed, once to get pem_label, once to detect the encryption header), but this would only happen in the error case where a header exists, so it shouldn't add any parsing cost to the hot path.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially add support for the encrypted PEM format format in the pkcs1 crate. It would likely require a custom PEM parser since the format isn't RFC7468-friendly, but that isn't the end of the world.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to leave this code as-is for now.

@str4d
Copy link
Owner

str4d commented Oct 26, 2022

Rebased on current main and squashed the commits together.

@str4d str4d added this to the rage 0.9.0 milestone Oct 26, 2022
@codecov
Copy link

codecov bot commented Oct 26, 2022

Codecov Report

Merging #261 (32ad684) into main (e6f0ed6) will decrease coverage by 0.14%.
The diff coverage is n/a.

@@            Coverage Diff             @@
##             main     #261      +/-   ##
==========================================
- Coverage   38.77%   38.63%   -0.15%     
==========================================
  Files          34       34              
  Lines        3092     3067      -25     
==========================================
- Hits         1199     1185      -14     
+ Misses       1893     1882      -11     
Impacted Files Coverage Δ
age/src/ssh.rs 21.91% <ø> (-2.82%) ⬇️
age/src/ssh/identity.rs 51.88% <ø> (ø)
age/src/util.rs 52.00% <0.00%> (-4.00%) ⬇️
age/src/format.rs 49.43% <0.00%> (-2.22%) ⬇️
age-core/src/plugin.rs 29.87% <0.00%> (-1.22%) ⬇️
age/src/primitives/stream.rs 62.98% <0.00%> (-0.12%) ⬇️
age/src/error.rs 6.32% <0.00%> (+0.15%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Copy link
Owner

@str4d str4d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK. Thanks!

@str4d str4d merged commit 9d33fbb into str4d:main Oct 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants