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

Support multi PEM parsing and speed up PEM parsing in general #39

Merged
merged 5 commits into from
Sep 18, 2023

Conversation

dnadoba
Copy link
Member

@dnadoba dnadoba commented Sep 12, 2023

Motivation

Trust roots on Ubuntu and other linux distributions are stored in a single PEM file that contains multiple certificates. We want to support loading these in the future and therefore need to be able to parse multiple certificates from a single PEM file.

Modificaitons

  • add PEMDocument.parseMultiple(pemString:) that returns an array of PEMDocuments
  • use new multi PEM parser for PEMDocument(pemString:) as well to speed up parsing and reduce allocations significantly

Result

TL;DR: Parsing & decoding a PEM document is now ~5x faster and mallocs ~12x less. This allows us to parse the WebPKI trust roots from its PEM string representation to the Swift type Certificate from swift-certificates in under 5ms.

I have run a couple benchmarks (Swift 5.8.1 on arm64 (M1 Max) in docker) that parses 130 certificates (100 times in a loop) found at /etc/ssl/certs on Ubuntu.
The first test just parse the PEM String to a PEMDocument:

----------------------------------------------------------------------------------------------------------------------------
Parse WebPKI Roots from PEM to PEMDocument  metrics
----------------------------------------------------------------------------------------------------------------------------

╒══════════════════════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│          Time (wall clock) (ms)          │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞══════════════════════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│            asn1-1.0.0-beta.1             │    1008 │    1008 │    1008 │    1008 │    1008 │    1008 │    1008 │       1 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│               Current_run                │     204 │     205 │     206 │     208 │     212 │     212 │     212 │       5 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│                    Δ                     │    -804 │    -803 │    -802 │    -800 │    -796 │    -796 │    -796 │       4 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│              Improvement %               │      80 │      80 │      80 │      79 │      79 │      79 │      79 │       4 │
╘══════════════════════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

╒══════════════════════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│            Malloc (total) (K)            │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞══════════════════════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│            asn1-1.0.0-beta.1             │    1212 │    1212 │    1212 │    1212 │    1212 │    1212 │    1212 │       1 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│               Current_run                │      96 │      96 │      96 │      96 │      96 │      96 │      96 │       5 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│                    Δ                     │   -1116 │   -1116 │   -1116 │   -1116 │   -1116 │   -1116 │   -1116 │       4 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│              Improvement %               │      92 │      92 │      92 │      92 │      92 │      92 │      92 │       4 │
╘══════════════════════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

The second test parse the PEM String to a PEMDocument and subsequently as a Certificate:

----------------------------------------------------------------------------------------------------------------------------
Parse WebPKI Roots from PEM to Certificate metrics
----------------------------------------------------------------------------------------------------------------------------

╒══════════════════════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│          Time (wall clock) (ms)          │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞══════════════════════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│            asn1-1.0.0-beta.1             │    1311 │    1311 │    1311 │    1311 │    1311 │    1311 │    1311 │       1 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│               Current_run                │     477 │     477 │     479 │     481 │     481 │     481 │     481 │       3 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│                    Δ                     │    -834 │    -834 │    -832 │    -830 │    -830 │    -830 │    -830 │       2 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│              Improvement %               │      64 │      64 │      63 │      63 │      63 │      63 │      63 │       2 │
╘══════════════════════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

╒══════════════════════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│            Malloc (total) (K)            │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞══════════════════════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│            asn1-1.0.0-beta.1             │    1763 │    1763 │    1763 │    1763 │    1763 │    1763 │    1763 │       1 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│               Current_run                │     646 │     646 │     646 │     646 │     646 │     646 │     646 │       3 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│                    Δ                     │   -1117 │   -1117 │   -1117 │   -1117 │   -1117 │   -1117 │   -1117 │       2 │
├──────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│              Improvement %               │      63 │      63 │      63 │      63 │      63 │      63 │      63 │       2 │
╘══════════════════════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

I also run a benchmark that uses the new PEMDocument.parseMultiple(pemString:) method:

Parse WebPKI Roots from multi PEM to PEMDocument 
╒════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                 │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) (K)     │      96 │      96 │      96 │      96 │      96 │      96 │      96 │       5 │
├────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (wall clock) (ms) │     209 │     211 │     213 │     214 │     219 │     219 │     219 │       5 │
╘════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

which is roughly the same as parsing each PEM individually:

Parse WebPKI Roots from PEM to PEMDocument 
╒════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                 │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) (K)     │      96 │      96 │      96 │      96 │      96 │      96 │      96 │       5 │
├────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (wall clock) (ms) │     205 │     205 │     205 │     206 │     207 │     207 │     207 │       5 │
╘════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

Note that macOS is even faster, likely because of the different base64 decode implementation:

Parse WebPKI Roots from PEM to PEMDocument 
╒════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                 │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) (K)     │      82 │      82 │      82 │      82 │      82 │      82 │      82 │       8 │
├────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (wall clock) (ms) │     135 │     135 │     135 │     136 │     137 │     137 │     137 │       8 │
╘════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛
Parse WebPKI Roots from multi PEM to PEMDocument 
╒════════════════════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╤═════════╕
│ Metric                 │      p0 │     p25 │     p50 │     p75 │     p90 │     p99 │    p100 │ Samples │
╞════════════════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════╡
│ Malloc (total) (K)     │      83 │      83 │      83 │      83 │      83 │      83 │      83 │       8 │
├────────────────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Time (wall clock) (ms) │     138 │     138 │     139 │     139 │     143 │     143 │     143 │       8 │
╘════════════════════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╧═════════╛

var pemString = pemString.utf8[...]
var pemDocuments = [PEMDocument]()
while true {
guard let lazyPEMDocument = try pemString.readNextPEMDocument() else {
Copy link
Contributor

Choose a reason for hiding this comment

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

This tends to read better as while let.

guard let data = Data(base64Encoded: base64EncodedDERString, options: .ignoreUnknownCharacters) else {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument not correctly base64 encoded")
}
guard data.isEmpty == false else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer if to guard == false.

guard data.isEmpty == false else {
throw ASN1Error.invalidPEMDocument(reason: "PEMDocument has an empty body")
}
guard let type = String(discriminator) else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this failable initializer coming from?

Copy link
Member Author

Choose a reason for hiding this comment

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

/// First find the BEGIN marker: `-----BEGIN <SOME DISCRIMINATOR>-----
guard
let beginDiscriminatorPrefix = self.firstRange(of: "-----BEGIN ".utf8[...]),
let beginDiscriminatorSuffix = self[beginDiscriminatorPrefix.upperBound...].firstRange(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we encapsulate this pattern in a helper function? We do it twice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also this function could provide a few ranges, instead of the two we have here, so we can do more semantic named range lookups later in the function.

Copy link
Member Author

Choose a reason for hiding this comment

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

have refactored it into this method:

extension Substring.UTF8View {
    func firstRangesOf(
        `prefix`: Substring,
        suffix: Substring
    ) -> (
        prefix: Range<Index>,
        infix: Range<Index>,
        suffix: Range<Index>
    )? 
}

/// -----END <SOME DISCRIMINATOR>-----
/// ```
/// This function attempts find the BEGIN and END marker.
/// It then tries to extract the discriminator and the base64 encoded.
Copy link
Contributor

Choose a reason for hiding this comment

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

The base64 encoded what?

private func checkLineLengthsOfBase64EncodedString() throws {
var message = self
let lastIndex = message.index(before: message.endIndex)
while message.isEmpty == false {
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid explicit comparison to false, prefer !message.isEmpty.

@dnadoba
Copy link
Member Author

dnadoba commented Sep 13, 2023

Address all comments in this commit .

@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Sep 18, 2023
@dnadoba dnadoba enabled auto-merge (squash) September 18, 2023 10:20
@dnadoba dnadoba merged commit 12c24ff into main Sep 18, 2023
@dnadoba dnadoba deleted the dn-multi-pem branch September 18, 2023 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants