Skip to content

PEM read-side refactor#1700

Closed
kaduk wants to merge 6 commits intoopenssl:masterfrom
kaduk:pem
Closed

PEM read-side refactor#1700
kaduk wants to merge 6 commits intoopenssl:masterfrom
kaduk:pem

Conversation

@kaduk
Copy link
Contributor

@kaduk kaduk commented Oct 12, 2016

WIP partially since we still have to make up our mind how strict we want the parser to be (and with flags, we even can offer multiple options).

See also RT4698

@tlhackque
Copy link

My comments on RT4698 still apply - I don't quite understand the state change to "rejected but issue tracked on (this) PR".

Whitespace (anywhere) and odd line lengths are common UI issues. Non-base64 chars have pluses and minuses. For the former, consider an e-mail that gets quoted with '>' when forwarded. The latter are described in the RFC.

As I mentioned in the RT, the OpenXPKI project has encountered this issue; see openxpki/openxpki/issues/437.

I maintain the Perl CPAN module Crypt::PKCS10, where I parse and output CSRs and public keys.

When parsing, I remove all whitespace, optionally remove non-base64 characters, and treat the result as one long string. That's a lot simpler than insisting on 64 character lines (except the last). And it accepts all valid input. Of course, that's Perl. But in C code it's easy to do the same thing by processing one character at a time.

Errors are best caught when parsing the asn.1 (and validating signatures), not by enforcing a strict punch-card era text format. I recommend this.

For output, of course, I produce strictly compliant PEM.

In other applications, it's a bit more involved to detect the encryption header - but once past that, I use the same approach.

Bottom line, I encourage the Unix "accept liberal, produce conservative". I'd settle for an option, but I don't think it's necessary - and OpenSSL has enough of those as is.

@richsalz
Copy link
Contributor

First off, I never said there weren’t interop concerns. I am sorry if my quibbles/jokes over MUST sent the conversation down the wrong chute.

The ticket is closed because we’re moving to GitHub. Please raise your concerns here, or perhaps Ben has addressed them.

@tlhackque
Copy link

I understood the spirit of your comments, no problem. I literally didn't understand why 'rejected' was the chosen state for the RT - but that's why I commented here. 'rejected' is usually a strong dismissive statement from a maintainer.

The original purpose of this PR was addressing refactoring and choice of memory pools, not the issue I raised. I'm certainly OK with merging them for convenience; I'm grateful that Ben volunteered.

At first glance, Ben has re-worked the existing code, but not liberalized the parsing. His comment at the top of this thread says "WIP", "we still need to decide..."

The code seems to have a lot of legacy and complexity; I haven't done a detailed review but applaud the courage in touching it. If you take my advice, it could well get simpler.

I think my concerns are adequately listed here (incorporating by reference the RT thread).

I'll watch this thread and am willing to participate if that's useful.

@richsalz
Copy link
Contributor

I suppose I will use "resolved" in closing the others. Thanks!

@kaduk
Copy link
Contributor Author

kaduk commented Oct 13, 2016

It is WIP largely because we need to decide what we want the behavior to be.
Having flags gives us options; there could be a flag for bug-for-bug compatibility, for ignoring all whitespace, all non-b64 characters, for enforcing line lengths, etc.
I would like some feedback from the team about which features are desired and what the defaults should be.
Unfortunately, I injured my hand yesterday and cannot type out the full list of open questions as I see it.

@tlhackque
Copy link

Yes, flags provide options - but also complexity for users, maintenance and testing.

I settled on one option - whether to ignore non-b64. While I don't think using them as a covert channel is a serious risk in OpenSSL data, it's a simple accommodation and sometimes catches corruption. e.g. '=' in the middle of a string can happen when someone copies quoted printable...

Whitespace is harmless. So are random line lengths.

I don't have a case for bug-for-bug compatibility here. Currently, valid data aborts processing.

I think I saw that OpenSSL will sometimes accept PEM data without the END label. If someone relies on that, I'd have little sympathy. That's the only case that comes to mind of OpenSSL's PEM processing being too liberal.

One could easily implement a separate tool for 'is this strict PEM?'. Anyone who might want to validate that they're producing strict PEM would be better served by such a tool than by OpenSSL's idiosyncrasies... And I think that reducing the options in OpenSSL code would be a good thing.

Of course once you accept liberal input, openssl x -in foo -out bar will produce strictly compliant PEM.

Hope your injury heals soon.

@richsalz
Copy link
Contributor

There is code in openssl that depends on some of the current behavior. I forget details. See the issue/PR from AGL that code a beer-mug in it.

@FdaSilvaYY
Copy link
Contributor

FdaSilvaYY commented Oct 13, 2016

It is #1209 "Rewrite base64 code."

@tlhackque
Copy link

Cute, but I know of no RFC that considers '-' as a comment character. That's the 'covert channel' argument from the RFC. Note that Google reports 'no ill effects' from its removal.

Most of the image could be retained with "ignore Base64" - only 7 distinct characters in the image are valid Base64. oOCQCG0 There are plenty of other graphics with which to replace them.

Why would asn1parse care about the PEM? It should get binary the same way that all PEM consumers do, and parse the binary. If it doesn't, it should - if for no other reason than that as a debug parser, it should see the same bits as everything else.

@richsalz
Copy link
Contributor

Should doesn't always equal the way things work, and considerations of the deployed based.

@tlhackque
Copy link

By 'should', I meant that if necessary, simplifying asn1parse to use the new input routines becomes part of this work - or maybe it's a separate bug against asn1parse.

A diagnostic tool can be evil if it sees different bits from the real consumer.

Is the '-' comment line behavior documented? Is that really what allows the image - it looks as though it may be relying on the 'if the line is not 64 characters long, the PEM is over' logic?

Anyhow, my 3 cents is that if someone relies on this, it's not a hardship to deal with it externally
e.g. sed -e'/^-/d' file.pem | openssl foo

Compatibility is important - but it's always a trade-off.

I've said enough. I'll yield to your team's judgement - my primary concern is that OpenSSL accept RFC-compliant input.

@richsalz
Copy link
Contributor

what's going on with this now?

@kaduk
Copy link
Contributor Author

kaduk commented Nov 15, 2016

I timed out on waiting for feedback about what flags the dev team was interested in seeing, and was going to default to implementing what @tlhackque wanted and the secure memory flag and remove the WIP label, but then started blocking on time to actually do that work.
It seems somewhat unlikely that time to do that work will appear this week while I'm at the IETF.

@richsalz
Copy link
Contributor

Might be worth trying to start discussion on the tls-dev list and perhaps close this.

Copy link
Member

Choose a reason for hiding this comment

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

What about tabs? Maybe use an appropriate ctype function... isspace() or isblank()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This part is for "bug-for-bug" compatibility with the historical code, and is in fact part of what inspired me to do the wholesale refactor.

Choose a reason for hiding this comment

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

Note also that <= ' ' in ASCII includes all control characters: NUL (000) thru US (0x1F). So it does include tab [HT]. (and FF, and VT and CR and LF ...) In EBCDIC, it also happens to do more or less the same thing, though it's a toss-up as to whether that was in the mind of the original author...

ctype would be the right thing for new (sane) code that wants to detect whitespace.

@kaduk kaduk changed the title WIP PEM read-side refactor PEM read-side refactor Dec 7, 2016
@kaduk
Copy link
Contributor Author

kaduk commented Dec 7, 2016

Rebased to lateset master, with some updates: renamed flag from "weak eol" to "eay compatible", since that's what it's intended to be, and added a b64-only (i.e., "strict") option, with the default behavior being to treat control characters as whitespace (i.e., ignored) ... which means that ASCII non-base64 characters get passed through to EVP_Decode, for now.
This is all up for discussion, of course, but I removed the WIP label since I am actually proposing this change.

Copy link
Contributor Author

@kaduk kaduk left a comment

Choose a reason for hiding this comment

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

A few questions for reviewers noted inline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This TODO should be resolved before this gets merged ... thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

You might want to have a look in crypto/evp/encode.c...

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 wasn't super keen on exposing that stuff to the whole library and leaving it in encode.c, nor was I super enthusiastic about splitting it out into a more generic place. But, if that's how it has to be, I can do it...

Copy link

@tlhackque tlhackque Dec 7, 2016

Choose a reason for hiding this comment

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

Not sure why you would want a table; in all codesets those ranges are contiguous. The issues come when you assume the relative placements of upper vs lower case, or digits vs letters, or where other graphic (e.g. + and /) appear relative to other characters.

Not that this is particularly performance sensitive, but a table (256 bytes? bigger?) would require memory and data cache; the range tests are compact (minimum instruction and no data cache impact) and cover the cases. I don't think a table adds clarity to the code. The ranges capture it. This isn't crypto, so no reason to care about constant time. So, I'd stick with what you have.

You might return -1 for invalid, and the encoded value for OK (e.g. 0 for 'A' & 26 for 'a'); that would let the caller do something like if( (nibble = isb64(in)) < 0 ) return error (or ignore); val |= nibble << state; It's easy to do: if( c >= 'a' && c <= 'z' ) return (c - 'a') + 26; etc.

You might want a state machine somewhere.; that might work as a (small) table. (I haven't reviewed the whole commit, and don't plan to. But noticed the questions.)

The ranges look right to me, assuming you handle pad ('=') elsewhere. Ref: rfc4648, which has a table.

My inclination for default is to be lax; interpret B64 chars, ignore everything else. Pad only at the end, and only the permissible cases (0,1,2). There's a case for exception if non-B64 other than whitespace (which is sp,ht,lf,cr,ff, & often vt; sometimes NUL) or proper padding. The EAY line length-sensitive weirdness is at best a desperate fallback for compatibility; should not be a default. (Even though it is old behavior, it causes reasonable input to be rejected.)

I'm assuming that the apps / consumers will be updated as needed to resolve the external manifestations. Hopefully a consistent option: -pemfmt [obsolete|strict] with default = lax ?

Hope this helps. Thanks for working on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this a reasonable error reason?

Copy link
Member

Choose a reason for hiding this comment

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

I think so

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Input requested on which BIO_get routine is better to use here.

Copy link
Member

Choose a reason for hiding this comment

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

Both do more or less the same thing, the crucial thing is how you want the data.

@levitte
Copy link
Member

levitte commented Feb 27, 2017

I've just had another look at this work (prompted by @kaduk's comment in #2746)... and I wonder, are there thoughts of having the resulting EVP_PKEY et al from PEM_read_bio_PrivateKey and friends in secure memory as well?

@kaduk
Copy link
Contributor Author

kaduk commented Feb 27, 2017

are there thoughts of having the resulting EVP_PKEY et al from PEM_read_bio_PrivateKey and friends in secure memory as well?

Probably makes sense as a later addition ... there are several codepaths that could be dispatched to by that function and it's not clear that it would need to be done at the same time. Thanks for pointing it out!

@levitte
Copy link
Member

levitte commented Feb 27, 2017

Later addition is fine, I was only asking about the intention going forward.

@kaduk
Copy link
Contributor Author

kaduk commented Apr 3, 2017

Rebased and fixed a couple needless behavior changes exposed by 04-test_pem.t.

Copy link
Member

Choose a reason for hiding this comment

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

Internal functions should have lower case prefix

Copy link
Member

Choose a reason for hiding this comment

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

This is repeated in all condition branches, so I's suggest replacing these two lines with len++;...

Copy link
Member

Choose a reason for hiding this comment

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

These four lines with len = i;...

Copy link
Member

Choose a reason for hiding this comment

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

These four lines with len = i;...

Copy link
Member

Choose a reason for hiding this comment

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

And finally insert these two lines here:

    linebuf[len++] = '\n';
    linebuf[len] = '\0';

Copy link
Member

Choose a reason for hiding this comment

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

Our C block comments should look like this:

/*
 * text
 * text
 */

Copy link
Member

Choose a reason for hiding this comment

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

We'd like our block comments to look like this:

/*
 * text
 * text
 */

Copy link
Member

Choose a reason for hiding this comment

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

Move this one down with PEM_bytes_read_bio

@levitte
Copy link
Member

levitte commented Apr 12, 2017

Apart from my remarks of the changes, you need to fix the documentation further, as noted by find-doc-nits:

(cd .; /usr/bin/perl util/find-doc-nits -n ) >doc-nits
if [ -s doc-nits ] ; then cat doc-nits; rm doc-nits ; exit 1; fi
doc/man3/PEM_bytes_read_bio.pod:1: missing copyright
doc/man3/PEM_read_bio_flags.pod:1: PEM_FLAG_SECURE missing from NAME section
doc/man3/PEM_read_bio_flags.pod:1: PEM_FLAG_EAY_COMPATIBLE missing from NAME section
doc/man3/PEM_read_bio_flags.pod:1: PEM_FLAG_ONLY_B64 missing from NAME section
doc/man3/PEM_read_bio_flags.pod:1: missing copyright
doc/man3/PEM_read_bio_flags.pod:1: period in NAME section
make: *** [doc-nits] Error 1

Also, you need to rebase on a fresher master

@kaduk
Copy link
Contributor Author

kaduk commented Apr 12, 2017

Looking at the commits again caused me to feel uneasy about the lack of test coverage for PEM_FLAG_ONLY_B64, which turns out to have been justified.
As it stands, that flag is not usable with encrypted files, though it would take some thinking to determine if ther's a way to fix that that does not involve a second pass over things.

@levitte
Copy link
Member

levitte commented Apr 12, 2017

Looking at the commits again caused me to feel uneasy about the lack of test coverage for PEM_FLAG_ONLY_B64, which turns out to have been justified.
As it stands, that flag is not usable with encrypted files, though it would take some thinking to determine if ther's a way to fix that that does not involve a second pass over things.

Depends what you mean with "encrypted". If "encrypted by PEM headers", then yes, you're right, but if it's encrypted in a PKCS#8 container, then PEM_FLAG_ONLY_B64 should be possible to use, no?

@richsalz
Copy link
Contributor

As for the elephant, remember when I tried to remove unused function codes and we decided we can't?

@kaduk
Copy link
Contributor Author

kaduk commented Apr 28, 2017

Removing the function codes entirely would break people's build.
Just not returning them anymore is somewhat different.

Copy link
Contributor

@richsalz richsalz left a comment

Choose a reason for hiding this comment

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

more feedback coming.

Copy link
Contributor

Choose a reason for hiding this comment

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

RFC reference?

Copy link
Contributor

Choose a reason for hiding this comment

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

we tend to use _ex suffix for this kind of new API.

@kaduk
Copy link
Contributor Author

kaduk commented Apr 28, 2017

(The many red 'x'es were due to a change in my local copy that didn't make it into a commit.)
Rebased, squashed, and attempted to take all feedback so far.

Copy link
Contributor

Choose a reason for hiding this comment

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

remove the curly and the return and just use if/else.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

use the ?: operator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

these are new functions? get rid of the _flag part from the 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.

Done, both pem_malloc() and pem_free() now.

Copy link
Contributor

Choose a reason for hiding this comment

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

use the ctype functions and then this probably becomes a define:

/* NOTE |c| is evaluated multiple times! */
#define b64(c) (islanum(c) || c == '+' || c == '/' || c == '=')

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.
Put parens around the bare 'c's out of habit; hopefully that's not problematic.

Copy link
Contributor

Choose a reason for hiding this comment

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

are you sure that shouldn't be "const char" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no semantic difference between "const char" and "char const", only stylistic.

Copy link
Contributor

Choose a reason for hiding this comment

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

good. then use 'const char' which we use everywhere but once:

; g grep 'char const' | grep -v constant_
crypto/evp/bio_md.c:static int md_write(BIO *h, char const *buf, int num);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Which, of course, gives new readers the wrong instincts when confronted with char * const *c, but that's not a discussion to have here.
(Done.)

Copy link
Contributor

Choose a reason for hiding this comment

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

move the || to the next line with the evp-decode call. and indent that call an extra shift-width (4)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

use the ?: operator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

we almost never use 'cleanup' as a goto target. it's either end or err more than 90% of the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay. Went with 'end', as the normal/success control flow also goes through the code after the label, which makes it not exactly the error-handling as would be implied by 'err'.

Copy link
Contributor

Choose a reason for hiding this comment

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

memleak of ctx. use a goto. or move this test before the ctx assignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (goto).

Copy link
Contributor

Choose a reason for hiding this comment

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

how can this be true? taillen is negative?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If both the starting len and taillen are zero, i.e., there was no content in the PEM file, just the BEGIN/END lines.
Mostly, IIRC, it was intended to avoid malloc(0), but I originally wrote this quite some time ago and could be misremembering.

Copy link
Contributor

Choose a reason for hiding this comment

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

add a comment for that. assuming you do so, and since someone else will review, +1 from me.

kaduk added 5 commits May 4, 2017 11:17
The extended function includes a 'flags' argument to allow callers
to specify different requested behaviors.  In particular, callers can
request that temporary storage buffers are allocated from the secure heap,
which could be relevant when loading private key material.

Refactor PEM_read_bio to use BIO_mems instead of BUFs directly,
use some helper routines to reduce the overall function length, and make
some of the checks more reasonable.
Split the PEM_bytes_read_bio() implementation out into a
pem_bytes_read_bio_flags() helper, to allow it to pass PEM_FLAG_SECURE
as needed.  Adjust the cleanup to properly use OPENSSL_secure_free()
when needed, and reimplement PEM_bytes_read() as a wrapper around
the _flags helper.

Add documentation for PEM_bytes_read_bio() and the new secmem variant.
We now have a version of PEM_read_bytes that can use temporary
buffers allocated from the secure heap; use them to handle this
sensitive information.

Note that for PEM_read_PrivateKey, the i/o still goes through
stdio since the input is a FILE pointer.  Standard I/O performs
additional buffering, which cannot be changed to use the OpenSSL
secure heap for temporary storage.  As such, it is recommended
to use BIO_new_file() and PEM_read_bio_PrivateKey() instead.
Get some trivial test coverage that this flag does what it claims to.

[extended tests]
@kaduk
Copy link
Contributor Author

kaduk commented May 4, 2017

And rebased again so the CI will actually take a look.

@levitte
Copy link
Member

levitte commented May 4, 2017

This should be taken out of WIP at this point, no?

@kaduk kaduk changed the title WIP PEM read-side refactor PEM read-side refactor May 4, 2017
@kaduk
Copy link
Contributor Author

kaduk commented May 4, 2017

This should be taken out of WIP at this point, no?

I could have sworn that I already had done so; maybe it reverted back somehow (on rebase?).
Regardless, removed WIP from the title.

@kaduk
Copy link
Contributor Author

kaduk commented May 4, 2017

Ah, the history feed has

 richsalz changed the title from PEM read-side refactor to WIP PEM read-side refactor 6 days ago

Shrug.

@kaduk
Copy link
Contributor Author

kaduk commented May 8, 2017

@levitte it seems that Rich is happy now, and the WIP is gone; can you take another look?

Copy link
Member

@levitte levitte left a comment

Choose a reason for hiding this comment

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

Looks good to me

@levitte
Copy link
Member

levitte commented May 8, 2017

@richsalz, wanna merge or shall I?

@richsalz
Copy link
Contributor

richsalz commented May 8, 2017

go ahead.

@levitte
Copy link
Member

levitte commented May 8, 2017

Merged. 204afd8 to fa3ed5b

@levitte levitte closed this May 8, 2017
levitte pushed a commit that referenced this pull request May 8, 2017
The extended function includes a 'flags' argument to allow callers
to specify different requested behaviors.  In particular, callers can
request that temporary storage buffers are allocated from the secure heap,
which could be relevant when loading private key material.

Refactor PEM_read_bio to use BIO_mems instead of BUFs directly,
use some helper routines to reduce the overall function length, and make
some of the checks more reasonable.

Reviewed-by: Rich Salz <[email protected]>
Reviewed-by: Richard Levitte <[email protected]>
(Merged from #1700)
levitte pushed a commit that referenced this pull request May 8, 2017
Split the PEM_bytes_read_bio() implementation out into a
pem_bytes_read_bio_flags() helper, to allow it to pass PEM_FLAG_SECURE
as needed.  Adjust the cleanup to properly use OPENSSL_secure_free()
when needed, and reimplement PEM_bytes_read() as a wrapper around
the _flags helper.

Add documentation for PEM_bytes_read_bio() and the new secmem variant.

Reviewed-by: Rich Salz <[email protected]>
Reviewed-by: Richard Levitte <[email protected]>
(Merged from #1700)
levitte pushed a commit that referenced this pull request May 8, 2017
We now have a version of PEM_read_bytes that can use temporary
buffers allocated from the secure heap; use them to handle this
sensitive information.

Note that for PEM_read_PrivateKey, the i/o still goes through
stdio since the input is a FILE pointer.  Standard I/O performs
additional buffering, which cannot be changed to use the OpenSSL
secure heap for temporary storage.  As such, it is recommended
to use BIO_new_file() and PEM_read_bio_PrivateKey() instead.

Reviewed-by: Rich Salz <[email protected]>
Reviewed-by: Richard Levitte <[email protected]>
(Merged from #1700)
levitte pushed a commit that referenced this pull request May 8, 2017
Get some trivial test coverage that this flag does what it claims to.

[extended tests]

Reviewed-by: Rich Salz <[email protected]>
Reviewed-by: Richard Levitte <[email protected]>
(Merged from #1700)
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