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 x509 from RustCrypto/formats #348

Closed
wants to merge 2 commits into from

Conversation

carl-wallace
Copy link
Contributor

No description provided.

…o.toml references an x509 crate not in formats repo, this should change as some PRs there are procecessed)
@tony-iqlusion
Copy link
Member

cc @str4d

Comment on lines +21 to +59
fn oid_lookup(oid: &ObjectIdentifier) -> String {
let oidstr = oid.to_string();
if oidstr == "2.5.4.41" {
return "name".to_string();
} else if oidstr == "2.5.4.4" {
return "sn".to_string();
} else if oidstr == "2.5.4.42" {
return "givenName".to_string();
} else if oidstr == "2.5.4.43" {
return "initials".to_string();
} else if oidstr == "2.5.4.44" {
return "generationQualifier".to_string();
} else if oidstr == "2.5.4.3" {
return "cn".to_string();
} else if oidstr == "2.5.4.7" {
return "l".to_string();
} else if oidstr == "2.5.4.8" {
return "st".to_string();
} else if oidstr == "2.5.4.9" {
return "street".to_string();
} else if oidstr == "2.5.4.11" {
return "ou".to_string();
} else if oidstr == "2.5.4.10" {
return "o".to_string();
} else if oidstr == "2.5.4.12" {
return "title".to_string();
} else if oidstr == "2.5.4.46" {
return "dnQualifier".to_string();
} else if oidstr == "2.5.4.6" {
return "c".to_string();
} else if oidstr == "2.5.4.5" {
return "serialNumber".to_string();
} else if oidstr == "2.5.4.65" {
return "pseudonym".to_string();
} else if oidstr == "0.9.2342.19200300.100.1.25" {
return "dc".to_string();
} else if oidstr == "1.2.840.113549.1.9.1" {
return "emailAddress".to_string();
}
Copy link
Member

@tony-iqlusion tony-iqlusion Feb 5, 2022

Choose a reason for hiding this comment

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

You should be able to make OID constants for each of these and compare those, and return a &'static str, something like:

const NAME_OID: ObjectIdentifier = ObjectIdentifier::new("2.5.4.41");

fn oid_lookup(oid: &ObjectIdentifier) -> &'static str {
    match oid {
        NAME_OID => "name",
        ...
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. I will likely move these OIDs and lookup to x509. As noted in the comment, they are duplicated here from certval (but that's probably not the right spot for them either).

Copy link
Contributor Author

@carl-wallace carl-wallace Feb 7, 2022

Choose a reason for hiding this comment

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

As I was implementing this I recalled why String was used. It was to accommodate the case where the OID does not match and the dot notation form is returned. I will add two functions, one that returns a &'static str with empty on a miss and one that calls that and returns a String with dot notation instead of empty.

Copy link
Contributor

@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.

Gave this a brief scan. The API changes from the new x509 crate look nice!


/// Certificates
#[derive(Clone, Debug)]
pub struct Certificate {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should not be removing the owned types and replacing them in the API with Vec<u8>; this loses significant type safety. If the issue is that these names conflict with the ones in the x509 crate, then the latter should be referenced directly as x509::Certificate etc. If the issue is that we want to optimise the parsing case, then we should use a Cow or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue was that the Certificate (or really the Decoder below it) maintains a reference to the buffer. I could add a new type that binds the buffer and an Option together and return that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

What parts of the certificate borrow? It should probably be an owned data structure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, but once the certificate is decoded everything should be owned and no longer reference the input buffer.

Where does the lifetime of borrowed data come into play?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of the buffers in the structure reference points in the original buffer. So this code snip:

        let buf = <read buffer>;
        let cert =  Certificate::from_der(buf.as_slice());

Yields these addresses in a sample run just now:

0x00007fa5cb009200 - buffer (1281 bytes total)
0x00007fa5cb009337 - spki
0x00007fa5cb009459 - first extension
0x00007fa5cb009601 - signature buffer

Copy link
Collaborator

Choose a reason for hiding this comment

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

Aah yeah, an owned form of SPKI seems needed.

There's the PublicKeyDocument document type which we could potentially make work for this application. I think it's kind of like what you're describing where it's a newtype for a Vec<u8> which impls the der::Document trait for decoding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will give a look at preparing a CertificateDocument class and return that instead of the buffer in the spots where I moved from struct to buffer. I'll put that in x509 and move the OIDs and nametostr while at it.

Copy link
Member

Choose a reason for hiding this comment

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

We should not be removing the owned types and replacing them in the API with Vec; this loses significant type safety

There's a "document" type convention for this in the other https://github.com/RustCrypto/formats/ and this PR adds a CertificateDocument type: RustCrypto/formats#393

These types are effectively a newtype wrapper around a Vec<u8> which is guaranteed to parse successfully as a particular type, i.e. a Certificate in this case. They impl the der::Document trait which handles serializing/deserializing DER-encoded documents including PEM (de)serialization as well as reading from and writing to DER or PEM-encoded files.

}
}
let tbs_certificate = TBSCertificate {
version: 2,
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought there was a bug here: version is set to 2, but extensions may be Some, and according to RFC 5280:

issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                     -- If present, version MUST be v2 or v3
subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                     -- If present, version MUST be v2 or v3
extensions      [3]  EXPLICIT Extensions OPTIONAL
                     -- If present, version MUST be v3

It turns out this is not a bug, because the new x509 crate doesn't have a type-safe version field, and this value is the raw encoding of the v3 version, not the numeric version v2.

More generally, I dislike this serialization API, enabled as it is by raw-construcing the same struct used as the output of parsing. It is a brittle approach, prone to exactly the kind of mistake I thought was being made here (and could easily be made by someone else trying to use the crate), because no relative constraints are being placed on any of the fields. In particular, I see no handling in the new x509 crate of the above MUST requirements on version; it looks like the user could construct and serialize a certificate that violates them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct there is nothing here that addresses the version requirement (or relative requirements in general). I will give a look at that (easiest way in this case is likely to disallow non-v3 at decode/encode time).

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, many of the other crates in https://github.com/rustcrypto/formats use a Version enum, e.g:

https://docs.rs/pkcs8/latest/pkcs8/enum.Version.html

It's also computed implicitly from the structure of the document, eliminating the possibility of setting it to the wrong value:

https://docs.rs/pkcs8/latest/pkcs8/struct.PrivateKeyInfo.html#method.version

At decode time, the implicitly computed version is checked against the explicit one in the document to ensure it's set correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will give a look and do something similar (and follow suit in the OCSP and CMS bits as the evolve). Thanks.

Copy link
Member

@tony-iqlusion tony-iqlusion Feb 10, 2022

Choose a reason for hiding this comment

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

More generally, I dislike this serialization API, enabled as it is by raw-construcing the same struct used as the output of parsing. It is a brittle approach, prone to exactly the kind of mistake I thought was being made here (and could easily be made by someone else trying to use the crate), because no relative constraints are being placed on any of the fields.

Regarding this in general, it would be good to add something like a CertificateBuilder type which enforces X.509 best practices. We could even use tools like certlint or zlint to ensure that certificates generated with a CertificateBuilder are following those best practices correctly.

The builder type could own the relevant data for various parts of the certificate, and in doing so, that also solves the lifetime-related concerns around a zero-copy certificate parser/serializer which borrows data.

Choose a reason for hiding this comment

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

@tony-iqlusion @carl-wallace My latest PRs both add Version types and make it trivial to derive Version types.

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.

5 participants