231 lines
6.9 KiB
Go
231 lines
6.9 KiB
Go
package rfc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/zmap/zcrypto/x509"
|
|
"github.com/zmap/zlint/v3/lint"
|
|
"github.com/zmap/zlint/v3/util"
|
|
"golang.org/x/crypto/cryptobyte"
|
|
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
|
|
)
|
|
|
|
const (
|
|
utcTimeFormat = "YYMMDDHHMMSSZ"
|
|
generalizedTimeFormat = "YYYYMMDDHHMMSSZ"
|
|
)
|
|
|
|
type crlHasValidTimestamps struct{}
|
|
|
|
/************************************************
|
|
RFC 5280: 5.1.2.4
|
|
CRL issuers conforming to this profile MUST encode thisUpdate as UTCTime for
|
|
dates through the year 2049. CRL issuers conforming to this profile MUST encode
|
|
thisUpdate as GeneralizedTime for dates in the year 2050 or later. Conforming
|
|
applications MUST be able to process dates that are encoded in either UTCTime or
|
|
GeneralizedTime.
|
|
|
|
Where encoded as UTCTime, thisUpdate MUST be specified and interpreted as
|
|
defined in Section 4.1.2.5.1. Where encoded as GeneralizedTime, thisUpdate MUST
|
|
be specified and interpreted as defined in Section 4.1.2.5.2.
|
|
|
|
RFC 5280: 5.1.2.5
|
|
CRL issuers conforming to this profile MUST encode nextUpdate as UTCTime for
|
|
dates through the year 2049. CRL issuers conforming to this profile MUST encode
|
|
nextUpdate as GeneralizedTime for dates in the year 2050 or later. Conforming
|
|
applications MUST be able to process dates that are encoded in either UTCTime or
|
|
GeneralizedTime.
|
|
|
|
Where encoded as UTCTime, nextUpdate MUST be specified and interpreted as
|
|
defined in Section 4.1.2.5.1. Where encoded as GeneralizedTime, nextUpdate MUST
|
|
be specified and interpreted as defined in Section 4.1.2.5.2.
|
|
|
|
RFC 5280: 5.1.2.6
|
|
The time for revocationDate MUST be expressed as described in Section 5.1.2.4.
|
|
|
|
RFC 5280: 4.1.2.5.1
|
|
UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST include
|
|
seconds (i.e., times are YYMMDDHHMMSSZ), even where the number of seconds is
|
|
zero.
|
|
|
|
RFC 5280: 4.1.2.5.2
|
|
GeneralizedTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST
|
|
include seconds (i.e., times are YYYYMMDDHHMMSSZ), even where the number of
|
|
seconds is zero. GeneralizedTime values MUST NOT include fractional seconds.
|
|
************************************************/
|
|
|
|
func init() {
|
|
lint.RegisterRevocationListLint(&lint.RevocationListLint{
|
|
LintMetadata: lint.LintMetadata{
|
|
Name: "e_crl_has_valid_timestamps",
|
|
Description: "CRL thisUpdate, nextUpdate, and revocationDates must be properly encoded",
|
|
Citation: "RFC 5280: 5.1.2.4, 5.1.2.5, and 5.1.2.6",
|
|
Source: lint.RFC5280,
|
|
EffectiveDate: util.RFC5280Date,
|
|
},
|
|
Lint: NewCrlHasValidTimestamps,
|
|
})
|
|
}
|
|
|
|
func NewCrlHasValidTimestamps() lint.RevocationListLintInterface {
|
|
return &crlHasValidTimestamps{}
|
|
}
|
|
|
|
func (l *crlHasValidTimestamps) CheckApplies(c *x509.RevocationList) bool {
|
|
return true
|
|
}
|
|
|
|
func (l *crlHasValidTimestamps) Execute(c *x509.RevocationList) *lint.LintResult {
|
|
input := cryptobyte.String(c.RawTBSRevocationList)
|
|
lintFail := lint.LintResult{
|
|
Status: lint.Error,
|
|
Details: "Failed to re-parse tbsCertList during linting",
|
|
}
|
|
|
|
// Read tbsCertList.
|
|
var tbs cryptobyte.String
|
|
if !input.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Skip (optional) version.
|
|
if !tbs.SkipOptionalASN1(cryptobyte_asn1.INTEGER) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Skip signature.
|
|
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Skip issuer.
|
|
if !tbs.SkipASN1(cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Read thisUpdate.
|
|
var thisUpdate cryptobyte.String
|
|
var thisUpdateTag cryptobyte_asn1.Tag
|
|
if !tbs.ReadAnyASN1Element(&thisUpdate, &thisUpdateTag) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Lint thisUpdate.
|
|
err := lintTimestamp(&thisUpdate, thisUpdateTag)
|
|
if err != nil {
|
|
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
|
}
|
|
|
|
// Peek (optional) nextUpdate.
|
|
if tbs.PeekASN1Tag(cryptobyte_asn1.UTCTime) || tbs.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime) {
|
|
// Read nextUpdate.
|
|
var nextUpdate cryptobyte.String
|
|
var nextUpdateTag cryptobyte_asn1.Tag
|
|
if !tbs.ReadAnyASN1Element(&nextUpdate, &nextUpdateTag) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Lint nextUpdate.
|
|
err = lintTimestamp(&nextUpdate, nextUpdateTag)
|
|
if err != nil {
|
|
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
|
}
|
|
}
|
|
|
|
// Peek (optional) revokedCertificates.
|
|
if tbs.PeekASN1Tag(cryptobyte_asn1.SEQUENCE) {
|
|
// Read sequence of revokedCertificate.
|
|
var revokedSeq cryptobyte.String
|
|
if !tbs.ReadASN1(&revokedSeq, cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Iterate over each revokedCertificate sequence.
|
|
for !revokedSeq.Empty() {
|
|
// Read revokedCertificate.
|
|
var certSeq cryptobyte.String
|
|
if !revokedSeq.ReadASN1Element(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
if !certSeq.ReadASN1(&certSeq, cryptobyte_asn1.SEQUENCE) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Skip userCertificate (serial number).
|
|
if !certSeq.SkipASN1(cryptobyte_asn1.INTEGER) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Read revocationDate.
|
|
var revocationDate cryptobyte.String
|
|
var revocationDateTag cryptobyte_asn1.Tag
|
|
if !certSeq.ReadAnyASN1Element(&revocationDate, &revocationDateTag) {
|
|
return &lintFail
|
|
}
|
|
|
|
// Lint revocationDate.
|
|
err = lintTimestamp(&revocationDate, revocationDateTag)
|
|
if err != nil {
|
|
return &lint.LintResult{Status: lint.Error, Details: err.Error()}
|
|
}
|
|
}
|
|
}
|
|
return &lint.LintResult{Status: lint.Pass}
|
|
}
|
|
|
|
func lintTimestamp(der *cryptobyte.String, tag cryptobyte_asn1.Tag) error {
|
|
// Preserve the original timestamp for length checking.
|
|
derBytes := *der
|
|
var tsBytes cryptobyte.String
|
|
if !derBytes.ReadASN1(&tsBytes, tag) {
|
|
return errors.New("failed to read timestamp")
|
|
}
|
|
tsLen := len(string(tsBytes))
|
|
|
|
var parsedTime time.Time
|
|
switch tag {
|
|
case cryptobyte_asn1.UTCTime:
|
|
// Verify that the timestamp is properly formatted.
|
|
if tsLen != len(utcTimeFormat) {
|
|
return fmt.Errorf("timestamps encoded using UTCTime MUST be specified in the format %q", utcTimeFormat)
|
|
}
|
|
|
|
if !der.ReadASN1UTCTime(&parsedTime) {
|
|
return errors.New("failed to read timestamp encoded using UTCTime")
|
|
}
|
|
|
|
// Verify that the timestamp is prior to the year 2050. This should
|
|
// really never happen.
|
|
if parsedTime.Year() > 2049 {
|
|
return errors.New("ReadASN1UTCTime returned a UTCTime after 2049")
|
|
}
|
|
case cryptobyte_asn1.GeneralizedTime:
|
|
// Verify that the timestamp is properly formatted.
|
|
if tsLen != len(generalizedTimeFormat) {
|
|
return fmt.Errorf(
|
|
"timestamps encoded using GeneralizedTime MUST be specified in the format %q", generalizedTimeFormat,
|
|
)
|
|
}
|
|
|
|
if !der.ReadASN1GeneralizedTime(&parsedTime) {
|
|
return fmt.Errorf("failed to read timestamp encoded using GeneralizedTime")
|
|
}
|
|
|
|
// Verify that the timestamp occurred after the year 2049.
|
|
if parsedTime.Year() < 2050 {
|
|
return errors.New("timestamps prior to 2050 MUST be encoded using UTCTime")
|
|
}
|
|
default:
|
|
return errors.New("unsupported time format")
|
|
}
|
|
|
|
// Verify that the location is UTC.
|
|
if parsedTime.Location() != time.UTC {
|
|
return errors.New("time must be in UTC")
|
|
}
|
|
return nil
|
|
}
|