Closed Bug 398693 Opened 17 years ago Closed 17 years ago

DER_AsciiToTime produces incorrect output for dates 1950-1970

Categories

(NSS :: Libraries, defect, P2)

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: nelson, Assigned: nelson)

References

Details

Attachments

(2 files, 2 obsolete files)

Attached is a certificate with an expiration date in 1955.  
The validity dates in the certificate are:
       UTCTime '041215145453Z'   <- 2004-12-15 14:54:53 GMT
       UTCTime '550315145453Z'   <- 1955-03-15 14:54:53 GMT

But the NSS pp program, in all presently released versions of NSS, 
displays those dates as:
            Not Before: Wed Dec 15 14:54:53 2004
            Not After : Wed Mar 16 14:54:53 1955   (wrong date)

The date of the month is off by 1.  
I believe this happens in dates in the range of 1950-1969.

I have written a patch to DER_AsciiToTime that fixes this, and is more
provably correct than the present code (IMO).  It produces this output:
            Not Before: Wed Dec 15 14:54:53 2004
            Not After : Tue Mar 15 14:54:53 1955
Attached patch patch v1 (obsolete) — Splinter Review
I think this patch is a little more "obviously" correct,
and the results speak for themselves. 

This patch applies to the trunk.
Attachment #283692 - Flags: review?
Comment on attachment 283692 [details] [diff] [review]
patch v1

Wan-Teh, please review this small patch.
Attachment #283692 - Flags: review? → review?(wtc)
Of course, I see one comment I didn't change, it should be changed as:
-     /* Compute the number of seconds in the years elapsed since 1970 */
+     /* Compute the number of seconds in the years elapsed since 1950 */
Status: NEW → ASSIGNED
Comment on attachment 283692 [details] [diff] [review]
patch v1

requesting second review, for branch.
Attachment #283692 - Flags: superreview?(rrelyea)
Attachment #283692 - Flags: superreview?(rrelyea) → superreview+
Comment on attachment 283692 [details] [diff] [review]
patch v1

If year is the number of elapsed years since January 1, 1950, the
calculation below is wrong:

>     if (((year % 4) == 0) && (month < 3)) {
> 	days--;
>     }

Please try your patch on the date 1952-02-01 (a leap year, and the
month is before March).

I believe you can fix this bug with just this change:

-    days += (year - 68)/4;
+    days += year/4 - 68/4;
It's clearer to write that line as:

-    days += (year - 68)/4;
+    days += year/4 - 70/4;
We should rewrite this function to parse the input string into
a PRExplodedTime structure, and then call PR_ImplodeTime to
get the output.
Comment on attachment 283692 [details] [diff] [review]
patch v1

(In reply to comment #5)

> >     /* compute number of elapsed leap days since January 1, 1950 */
> >     days += (year + 2)/4;  
> If year is the number of elapsed years since January 1, 1950, the
> calculation below is wrong:
> >     if (((year % 4) == 0) && (month < 3)) {
> > 	    days--;
> >     }

You're right.  I could fix that by adding 2 to the year, as I do on
the previous line, but I could also rewrite this entire function by 
prepending either "19" or "20" to the entire input string, and then
calling DER_GeneralizedTimeToTime, which calls PR_ImplodeTime.
I think I'll do that.
Attachment #283692 - Attachment is obsolete: true
Attachment #283692 - Flags: review?(wtc)
Attached patch patch v2 - rewrite (obsolete) — Splinter Review
This patch rewrites DER_AsciiToTime.  
The function now converts the UTCTime into a GeneralizedTime by 
prepending either "19" or "20", and then calls DER_GeneralizedTimeToTime 
to do the actual work.   It also moves some code from DER_UTCTimeToTime 
to DER_AsciiToTime, to avoid duplicating that code. 

Bob & Wan-Teh, please review.
Attachment #283834 - Flags: superreview?(wtc)
Attachment #283834 - Flags: review?(rrelyea)
Comment on attachment 283834 [details] [diff] [review]
patch v2 - rewrite

You should move the comment:
     /*
     ** Maximum valid UTCTime is yymmddhhmmss+0000 which is 17 bytes.
     ** 20 should be large enough for all valid encoded times. 
     */
to DER_AsciiToTime to explain the size of localBuf.

You didn't copy the memset call, which ensures localBuf is null terminated
for the PORT_Strlen(localBuf) call later to succeed.  Also,

>+    PORT_Strncpy(localBuf+2, string, 17);

may read beyond the end of the 'string' buffer because 'string' doesn't
need to be null terminated.  Consider an input of 11-byte yymmddhhmmZ,
not null terminated, with nonzero bytes after the 'Z'.

I like this patch but it still needs work.  Sorry.
Attachment #283834 - Flags: superreview?(wtc) → superreview-
Attachment #283834 - Attachment is obsolete: true
Attachment #283834 - Flags: review?(rrelyea)
Comment on attachment 283834 [details] [diff] [review]
patch v2 - rewrite

Wan-Teh, localBuf IS guaranteed to be NUL terminated.

+    PORT_Strncpy(localBuf+2, string, 17);
+    localBuf[19] = '\0';

However, I agree that there is a problem with the patch to
DER_UTCTimeToTime, so I will submit another patch.
Comment on attachment 283834 [details] [diff] [review]
patch v2 - rewrite

Please ignore my comment about not copying the memset call.
The only problem is the PORT_Strncpy call.

Your change to DER_UTCTimeToTime may introduce subtle changes
because it loses the zero padding for time->len < sizeof localBuf.
This could also allow DER_AsciiToTime to read beyond the end
of the time->data buffer, for example, if time->data is
"0710060800+1" and time->len is 12, the statement
    CAPTURE(hourOff,string+11,loser);
in DER_AsciiToTime (original version) will read one byte beyond
the end of buffer.  (In your version the PORT_Strncpy call will
do that.)
Comment on attachment 284255 [details] [diff] [review]
even bigger rewrite

This rewrite combines the code for parsing both UTCTime and 
GeneralizedTime into a single function, which calls PR_ImplodeTime.
I believe it preserves all the semantics of the 3 rewritten functions:
DER_AsciiToTime, DER_UTCTimeToTime, and DER_GeneralizedTimeToTime
including the bizarre rule rule that the input string for 
DER_AsciiToTime need not be NUL terminated as long as it contains a
valid UTCTime.
Attachment #284255 - Flags: superreview?(julien.pierre.boogz)
Attachment #284255 - Flags: review?(wtc)
Comment on attachment 284255 [details] [diff] [review]
even bigger rewrite

r=wtc.

>+    p += 2; \

It'd be better to put the macro parameter p in parentheses.

>+    while (len < sizeof localBuf) {
>+        localBuf[len++] = '\0';

We need only one null byte after the copy of time->data in localBuf.
So you can replace while by if.

>+    if (string == NULL || dst == NULL) {
>+	PORT_SetError(SEC_ERROR_INVALID_ARGS);
>+	return SECFailure;
>+    }

For maximum backward compatibility, if string is NULL, we
should go to loser to set the SEC_ERROR_INVALID_TIME error code.

Would be nice to document that der_TimeStringToTime reads at most
17+generalized bytes, or until the first null byte,  from string.
Attachment #284255 - Flags: review?(wtc) → review+
Comment on attachment 284255 [details] [diff] [review]
even bigger rewrite

This looks OK except for a few nits, in order of those I care the most about to least :

1) In DER_UTCTimeToTime, there is code that attempts to make a copy and NULL-terminate the input. But it will fail if time->len is 20 (the size of localBuf).

This is not fatal because the subsequent code will not read more than 20 bytes even if the string is not NULL terminated. But it would be nice to fix still.

The problem is :

len = PR_MIN(time->len, sizeof localBuf);

In that case, the 20 byte string will be copied, but not terminated.

The second argument to PR_MIN should probably simply be sizeof localBuf -1 .

The same problem exists in DER_GeneralizedTimeToTime.

2) Since the 2 functions appear to be identical except for the values 11/13 and UTC_STRING / GEN_STRING, I think there is an opportunity to move the remaining common code to der_TimeStringToTime.

3) The CAPTURE macro does not check boundaries of the string it extracts characters from. It appears the logic is OK because der_TimeStringToTime will terminate if CAPTURE encounters non-digits, and the string is always NULL-terminated, except for the case I mentioned earlier, in which the code doesn't try to read all the way to the end of the string.
Attachment #284255 - Flags: superreview?(julien.pierre.boogz) → superreview+
Julien,

The string in localBuf doesn't need to be null-terminated if it
is 17+generalized bytes or longer, where generalized is 0 or 2.
The original code doesn't truncate long input.  Nelson's code
truncates long input to 20 bytes.  Since we're truncating, we
might as well truncate to 19 bytes so we can always null terminate
localBuf.

You're right that the CAPTURE macro and the der_TimeStringToTime
function work together to prevent us from reading beyond the
boundary of the string.  This is not obvious, which is why I
suggested at the end of comnment 14 to document this with a comment.
Yeah, the old DER_AsciiToTime function and the new der_TimeStringToTime 
function don't need or care about trailing NUL characters, as such.
They don't do strcpy or strlen or other string operations that depend on 
trailing NULs.  

They parse until they get a complete time string, or until they come to a 
character that is not one of the 13 valid characters allowed in a time 
string (which are "01234567489+-Z".  

The new function will never attempt to parse more than 19 characters of
input, which is why there's no need to add a trailing pad character to 
the string if it's maximum length.  I agree with Wan-Teh that the buffer
could have been 19 characters instead of 20.  20 just seemed like a nice
round number.  :)

Padding is only needed in the case where the input SECItem buffer is 
shorter than the maximum length time string.  When we copy it into the 
buffer, a valid time string character past the end of the original string 
might make the resultant string look valid even if the original was not.  
So, if the original string was shorter than maximum length, we want to 
ensure that the first character after the original string is a character 
not from the set of valid time string characters.  A trailing blank or 
comma would do just as well as a trailing NUL.  

There certainly could be more comments in the code.  This entire comment, 
for example, could go in there.  Maybe it's best to put in a URL that 
refers to this bug instead. ?
Wan-Teh,

I am aware that the string doesn't really need to be NULL-terminated except if it's short. But I had to figure this out for myself by reading the code. It was not obvious that it was OK not to NULL-terminate when the string is 20 bytes or longer.

Nelson,

Some extra comments would be useful. I think they should be in the source, not a bugzilla URL, to improve readability.
I extended the comment for the DER_AsciiToTime function and committed the fix
on the trunk only.

Checking in util/dertime.c;
new revision: 1.10; previous revision: 1.9
Status: ASSIGNED → RESOLVED
Closed: 17 years ago
Priority: -- → P2
Resolution: --- → FIXED
Target Milestone: 3.11.8 → 3.12
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: