Skip to content

Commit 76329cc

Browse files
jshacpu
authored andcommitted
Add check for correct time in SCTs. (letsencrypt#3570)
In publisher and in the integration test, check that SCTs are in a reasonable range. Also, update CreateTestingSignedSCT (used by ct-test-srv) to produce SCTs correctly with a timetamp in Unix epoch milliseconds.
1 parent 268d9b1 commit 76329cc

File tree

4 files changed

+110
-12
lines changed

4 files changed

+110
-12
lines changed

publisher/publisher.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ func (pub *Impl) singleLogSubmit(
316316
if err != nil {
317317
return nil, err
318318
}
319+
timestamp := time.Unix(int64(sct.Timestamp)/1000, 0)
320+
if timestamp.Sub(time.Now()) > time.Minute {
321+
return nil, fmt.Errorf("SCT Timestamp was too far in the future (%s)", timestamp)
322+
}
323+
// For regular certificates, we could get an old SCT, but that shouldn't
324+
// happen for precertificates.
325+
if isPrecert && timestamp.Sub(time.Now()) < -10*time.Minute {
326+
return nil, fmt.Errorf("SCT Timestamp was too far in the past (%s)", timestamp)
327+
}
319328

320329
// Only store the SCT if it was for a certificate, we have no need for
321330
// the precert once it is embedded in a certificate
@@ -341,7 +350,7 @@ func sctToInternal(sct *ct.SignedCertificateTimestamp, serial string) core.Signe
341350

342351
// CreateTestingSignedSCT is used by both the publisher tests and ct-test-serv, which is
343352
// why it is exported. It creates a signed SCT based on the provided chain.
344-
func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool) []byte {
353+
func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool, timestamp time.Time) []byte {
345354
chain := make([]ct.ASN1Cert, len(req))
346355
for i, str := range req {
347356
b, err := base64.StdEncoding.DecodeString(str)
@@ -364,11 +373,11 @@ func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool) []b
364373
// Sign the SCT
365374
rawKey, _ := x509.MarshalPKIXPublicKey(&k.PublicKey)
366375
logID := sha256.Sum256(rawKey)
367-
timestamp := uint64(time.Now().Unix())
376+
timestampMillis := uint64(timestamp.UnixNano()) / 1e6
368377
serialized, _ := ct.SerializeSCTSignatureInput(ct.SignedCertificateTimestamp{
369378
SCTVersion: ct.V1,
370379
LogID: ct.LogID{KeyID: logID},
371-
Timestamp: timestamp,
380+
Timestamp: timestampMillis,
372381
}, ct.LogEntry{Leaf: *leaf})
373382
hashed := sha256.Sum256(serialized)
374383
var ecdsaSig struct {
@@ -389,7 +398,7 @@ func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool) []b
389398
}
390399
jsonSCTObj.SCTVersion = ct.V1
391400
jsonSCTObj.ID = base64.StdEncoding.EncodeToString(logID[:])
392-
jsonSCTObj.Timestamp = timestamp
401+
jsonSCTObj.Timestamp = timestampMillis
393402
ds := ct.DigitallySigned{
394403
Algorithm: cttls.SignatureAndHashAlgorithm{
395404
Hash: cttls.SHA256,

publisher/publisher_test.go

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,32 @@ func logSrv(k *ecdsa.PrivateKey) *testLogSrv {
153153
if r.URL.Path == "/ct/v1/add-pre-chain" {
154154
precert = true
155155
}
156-
sct := CreateTestingSignedSCT(jsonReq.Chain, k, precert)
156+
sct := CreateTestingSignedSCT(jsonReq.Chain, k, precert, time.Now())
157+
fmt.Fprint(w, string(sct))
158+
atomic.AddInt64(&testLog.submissions, 1)
159+
})
160+
161+
testLog.Server = httptest.NewUnstartedServer(m)
162+
testLog.Server.Start()
163+
return testLog
164+
}
165+
166+
// lyingLogSrv always signs SCTs with the timestamp it was given.
167+
func lyingLogSrv(k *ecdsa.PrivateKey, timestamp time.Time) *testLogSrv {
168+
testLog := &testLogSrv{}
169+
m := http.NewServeMux()
170+
m.HandleFunc("/ct/", func(w http.ResponseWriter, r *http.Request) {
171+
decoder := json.NewDecoder(r.Body)
172+
var jsonReq ctSubmissionRequest
173+
err := decoder.Decode(&jsonReq)
174+
if err != nil {
175+
return
176+
}
177+
precert := false
178+
if r.URL.Path == "/ct/v1/add-pre-chain" {
179+
precert = true
180+
}
181+
sct := CreateTestingSignedSCT(jsonReq.Chain, k, precert, timestamp)
157182
fmt.Fprint(w, string(sct))
158183
atomic.AddInt64(&testLog.submissions, 1)
159184
})
@@ -185,7 +210,7 @@ func retryableLogSrv(k *ecdsa.PrivateKey, retries int, after *int) *httptest.Ser
185210
if err != nil {
186211
return
187212
}
188-
sct := CreateTestingSignedSCT(jsonReq.Chain, k, false)
213+
sct := CreateTestingSignedSCT(jsonReq.Chain, k, false, time.Now())
189214
w.WriteHeader(http.StatusOK)
190215
fmt.Fprint(w, string(sct))
191216
} else {
@@ -281,28 +306,88 @@ func TestBasicSuccessful(t *testing.T) {
281306

282307
// Precert
283308
trueBool := true
309+
issuerBundle, precert, err := makePrecert(k)
310+
test.AssertNotError(t, err, "Failed to create test leaf")
311+
pub.issuerBundle = issuerBundle
312+
_, err = pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{LogURL: &pub.ctLogs[0].uri, LogPublicKey: &pub.ctLogs[0].logID, Der: precert, Precert: &trueBool})
313+
test.AssertNotError(t, err, "Certificate submission failed")
314+
test.AssertEquals(t, len(log.GetAllMatching("Failed to.*")), 0)
315+
}
316+
317+
func makePrecert(k *ecdsa.PrivateKey) ([]ct.ASN1Cert, []byte, error) {
284318
rootTmpl := x509.Certificate{
285319
SerialNumber: big.NewInt(0),
286320
Subject: pkix.Name{CommonName: "root"},
287321
BasicConstraintsValid: true,
288322
IsCA: true,
289323
}
290324
rootBytes, err := x509.CreateCertificate(rand.Reader, &rootTmpl, &rootTmpl, k.Public(), k)
291-
test.AssertNotError(t, err, "Failed to create test root")
292-
pub.issuerBundle = []ct.ASN1Cert{ct.ASN1Cert{Data: rootBytes}}
325+
if err != nil {
326+
return nil, nil, err
327+
}
293328
root, err := x509.ParseCertificate(rootBytes)
294-
test.AssertNotError(t, err, "Failed to parse test root")
329+
if err != nil {
330+
return nil, nil, err
331+
}
295332
precertTmpl := x509.Certificate{
296333
SerialNumber: big.NewInt(0),
297334
ExtraExtensions: []pkix.Extension{
298335
{Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}, Critical: true, Value: []byte{0x05, 0x00}},
299336
},
300337
}
301338
precert, err := x509.CreateCertificate(rand.Reader, &precertTmpl, root, k.Public(), k)
339+
if err != nil {
340+
return nil, nil, err
341+
}
342+
return []ct.ASN1Cert{ct.ASN1Cert{Data: rootBytes}}, precert, err
343+
}
344+
345+
func TestTimestampVerificationFuture(t *testing.T) {
346+
pub, _, k := setup(t)
347+
348+
server := lyingLogSrv(k, time.Now().Add(time.Hour))
349+
defer server.Close()
350+
port, err := getPort(server.URL)
351+
test.AssertNotError(t, err, "Failed to get test server port")
352+
addLog(t, pub, port, &k.PublicKey)
353+
354+
// Precert
355+
trueBool := true
356+
issuerBundle, precert, err := makePrecert(k)
302357
test.AssertNotError(t, err, "Failed to create test leaf")
358+
pub.issuerBundle = issuerBundle
359+
303360
_, err = pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{LogURL: &pub.ctLogs[0].uri, LogPublicKey: &pub.ctLogs[0].logID, Der: precert, Precert: &trueBool})
304-
test.AssertNotError(t, err, "Certificate submission failed")
305-
test.AssertEquals(t, len(log.GetAllMatching("Failed to.*")), 0)
361+
if err == nil {
362+
t.Fatal("Expected error for lying log server, got none")
363+
}
364+
if !strings.HasPrefix(err.Error(), "SCT Timestamp was too far in the future") {
365+
t.Fatalf("Got wrong error: %s", err)
366+
}
367+
}
368+
369+
func TestTimestampVerificationPast(t *testing.T) {
370+
pub, _, k := setup(t)
371+
372+
server := lyingLogSrv(k, time.Now().Add(-time.Hour))
373+
defer server.Close()
374+
port, err := getPort(server.URL)
375+
test.AssertNotError(t, err, "Failed to get test server port")
376+
addLog(t, pub, port, &k.PublicKey)
377+
378+
// Precert
379+
trueBool := true
380+
issuerBundle, precert, err := makePrecert(k)
381+
test.AssertNotError(t, err, "Failed to create test leaf")
382+
pub.issuerBundle = issuerBundle
383+
384+
_, err = pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{LogURL: &pub.ctLogs[0].uri, LogPublicKey: &pub.ctLogs[0].logID, Der: precert, Precert: &trueBool})
385+
if err == nil {
386+
t.Fatal("Expected error for lying log server, got none")
387+
}
388+
if !strings.HasPrefix(err.Error(), "SCT Timestamp was too far in the past") {
389+
t.Fatalf("Got wrong error: %s", err)
390+
}
306391
}
307392

308393
func TestGoodRetry(t *testing.T) {

test/ct-test-srv/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
7272
}
7373

7474
w.WriteHeader(http.StatusOK)
75-
w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert))
75+
w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert, time.Now()))
7676
case "/submissions":
7777
if r.Method != "GET" {
7878
http.NotFound(w, r)

test/integration-test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,10 @@ def test_sct_embedding():
491491
raise Exception("SCT contains wrong version")
492492
if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE:
493493
raise Exception("SCT contains wrong entry type")
494+
delta = sct.timestamp - datetime.datetime.now()
495+
if abs(delta) > datetime.timedelta(hours=1):
496+
raise Exception("Delta between SCT timestamp and now was too great "
497+
"%s vs %s (%s)" % (sct.timestamp, datetime.datetime.now(), delta))
494498

495499
exit_status = 1
496500
tempdir = tempfile.mkdtemp()

0 commit comments

Comments
 (0)