|
15 | 15 | package scti
|
16 | 16 |
|
17 | 17 | import (
|
| 18 | + "bufio" |
18 | 19 | "bytes"
|
19 | 20 | "context"
|
20 | 21 | "encoding/base64"
|
| 22 | + "encoding/hex" |
21 | 23 | "encoding/json"
|
22 | 24 | "encoding/pem"
|
23 | 25 | "io"
|
@@ -346,3 +348,261 @@ func TestGetRoots(t *testing.T) {
|
346 | 348 | }
|
347 | 349 | })
|
348 | 350 | }
|
| 351 | + |
| 352 | +// TODO(phboneff): this could just be a parseBodyJSONChain test |
| 353 | +func TestAddChainWhitespace(t *testing.T) { |
| 354 | + // Throughout we use variants of a hard-coded POST body derived from a chain of: |
| 355 | + // testdata.LeafSignedByFakeIntermediateCertPEM, testdata.FakeIntermediateCertPEM |
| 356 | + cert, rest := pem.Decode([]byte(testdata.CertFromIntermediate)) |
| 357 | + if len(rest) > 0 { |
| 358 | + t.Fatalf("got %d bytes remaining after decoding cert, want 0", len(rest)) |
| 359 | + } |
| 360 | + certB64 := base64.StdEncoding.EncodeToString(cert.Bytes) |
| 361 | + intermediate, rest := pem.Decode([]byte(testdata.IntermediateFromRoot)) |
| 362 | + if len(rest) > 0 { |
| 363 | + t.Fatalf("got %d bytes remaining after decoding intermediate, want 0", len(rest)) |
| 364 | + } |
| 365 | + intermediateB64 := base64.StdEncoding.EncodeToString(intermediate.Bytes) |
| 366 | + |
| 367 | + // Break the JSON into chunks: |
| 368 | + intro := "{\"chain\"" |
| 369 | + // followed by colon then the first line of the PEM file |
| 370 | + chunk1a := "[\"" + certB64[:64] |
| 371 | + // straight into rest of first entry |
| 372 | + chunk1b := certB64[64:] + "\"" |
| 373 | + // followed by comma then |
| 374 | + chunk2 := "\"" + intermediateB64 + "\"" |
| 375 | + epilog := "]}\n" |
| 376 | + |
| 377 | + var tests = []struct { |
| 378 | + descr string |
| 379 | + body string |
| 380 | + want int |
| 381 | + }{ |
| 382 | + { |
| 383 | + descr: "valid", |
| 384 | + body: intro + ":" + chunk1a + chunk1b + "," + chunk2 + epilog, |
| 385 | + want: http.StatusOK, |
| 386 | + }, |
| 387 | + { |
| 388 | + descr: "valid-space-between", |
| 389 | + body: intro + " : " + chunk1a + chunk1b + " , " + chunk2 + epilog, |
| 390 | + want: http.StatusOK, |
| 391 | + }, |
| 392 | + { |
| 393 | + descr: "valid-newline-between", |
| 394 | + body: intro + " : " + chunk1a + chunk1b + ",\n" + chunk2 + epilog, |
| 395 | + want: http.StatusOK, |
| 396 | + }, |
| 397 | + { |
| 398 | + descr: "invalid-raw-newline-in-string", |
| 399 | + body: intro + ":" + chunk1a + "\n" + chunk1b + "," + chunk2 + epilog, |
| 400 | + want: http.StatusBadRequest, |
| 401 | + }, |
| 402 | + { |
| 403 | + descr: "valid-escaped-newline-in-string", |
| 404 | + body: intro + ":" + chunk1a + "\\n" + chunk1b + "," + chunk2 + epilog, |
| 405 | + want: http.StatusOK, |
| 406 | + }, |
| 407 | + } |
| 408 | + |
| 409 | + log := setupTestLog(t) |
| 410 | + server := setupTestServer(t, log, path.Join(prefix, "ct/v1/add-chain")) |
| 411 | + defer server.Close() |
| 412 | + |
| 413 | + for _, test := range tests { |
| 414 | + t.Run(test.descr, func(t *testing.T) { |
| 415 | + resp, err := http.Post(server.URL+"/ct/v1/add-chain", "application/json", strings.NewReader(test.body)) |
| 416 | + if err != nil { |
| 417 | + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddChainPath, err) |
| 418 | + } |
| 419 | + if got, want := resp.StatusCode, test.want; got != want { |
| 420 | + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddChainPath, got, want) |
| 421 | + } |
| 422 | + }) |
| 423 | + } |
| 424 | +} |
| 425 | + |
| 426 | +func TestAddChain(t *testing.T) { |
| 427 | + var tests = []struct { |
| 428 | + descr string |
| 429 | + chain []string |
| 430 | + want int |
| 431 | + err error |
| 432 | + }{ |
| 433 | + { |
| 434 | + descr: "leaf-only", |
| 435 | + chain: []string{testdata.CertFromIntermediate}, |
| 436 | + want: http.StatusBadRequest, |
| 437 | + }, |
| 438 | + { |
| 439 | + descr: "wrong-entry-type", |
| 440 | + chain: []string{testdata.PreCertFromIntermediate}, |
| 441 | + want: http.StatusBadRequest, |
| 442 | + }, |
| 443 | + { |
| 444 | + descr: "success-without-root", |
| 445 | + chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot}, |
| 446 | + want: http.StatusOK, |
| 447 | + }, |
| 448 | + { |
| 449 | + descr: "success", |
| 450 | + chain: []string{testdata.CertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM}, |
| 451 | + want: http.StatusOK, |
| 452 | + }, |
| 453 | + } |
| 454 | + |
| 455 | + log := setupTestLog(t) |
| 456 | + server := setupTestServer(t, log, path.Join(prefix, "ct/v1/add-chain")) |
| 457 | + defer server.Close() |
| 458 | + |
| 459 | + for _, test := range tests { |
| 460 | + t.Run(test.descr, func(t *testing.T) { |
| 461 | + pool := loadCertsIntoPoolOrDie(t, test.chain) |
| 462 | + chain := createJSONChain(t, *pool) |
| 463 | + |
| 464 | + resp, err := http.Post(server.URL+"/ct/v1/add-chain", "application/json", chain) |
| 465 | + if err != nil { |
| 466 | + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddChainPath, err) |
| 467 | + } |
| 468 | + if got, want := resp.StatusCode, test.want; got != want { |
| 469 | + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddChainPath, got, want) |
| 470 | + } |
| 471 | + if test.want == http.StatusOK { |
| 472 | + var gotRsp types.AddChainResponse |
| 473 | + if err := json.NewDecoder(resp.Body).Decode(&gotRsp); err != nil { |
| 474 | + t.Fatalf("json.Decode()=%v; want nil", err) |
| 475 | + } |
| 476 | + if got, want := types.Version(gotRsp.SCTVersion), types.V1; got != want { |
| 477 | + t.Errorf("resp.SCTVersion=%v; want %v", got, want) |
| 478 | + } |
| 479 | + if got, want := gotRsp.ID, demoLogID[:]; !bytes.Equal(got, want) { |
| 480 | + t.Errorf("resp.ID=%v; want %v", got, want) |
| 481 | + } |
| 482 | + if got, want := gotRsp.Timestamp, fakeTimeMillis; got != want { |
| 483 | + t.Errorf("resp.Timestamp=%d; want %d", got, want) |
| 484 | + } |
| 485 | + if got, want := hex.EncodeToString(gotRsp.Signature), "040300067369676e6564"; got != want { |
| 486 | + t.Errorf("resp.Signature=%s; want %s", got, want) |
| 487 | + } |
| 488 | + // TODO(phboneff): read from the log and compare values |
| 489 | + // TODO(phboneff): add a test with a backend write failure |
| 490 | + // TODO(phboneff): check that the index is in the SCT |
| 491 | + // TODO(phboneff): add a test with a not after range |
| 492 | + // TODO(phboneff): add a test with a start date only |
| 493 | + // TODO(phboneff): add duplicate tests |
| 494 | + } |
| 495 | + }) |
| 496 | + } |
| 497 | +} |
| 498 | + |
| 499 | +func TestAddPreChain(t *testing.T) { |
| 500 | + var tests = []struct { |
| 501 | + descr string |
| 502 | + chain []string |
| 503 | + want int |
| 504 | + err error |
| 505 | + }{ |
| 506 | + { |
| 507 | + descr: "leaf-signed-by-different", |
| 508 | + chain: []string{testdata.PrecertPEMValid, testdata.FakeIntermediateCertPEM}, |
| 509 | + want: http.StatusBadRequest, |
| 510 | + }, |
| 511 | + { |
| 512 | + descr: "wrong-entry-type", |
| 513 | + chain: []string{testdata.TestCertPEM}, |
| 514 | + want: http.StatusBadRequest, |
| 515 | + }, |
| 516 | + { |
| 517 | + descr: "success", |
| 518 | + chain: []string{testdata.PrecertPEMValid, testdata.CACertPEM}, |
| 519 | + want: http.StatusOK, |
| 520 | + }, |
| 521 | + { |
| 522 | + descr: "success-with-intermediate", |
| 523 | + chain: []string{testdata.PreCertFromIntermediate, testdata.IntermediateFromRoot, testdata.CACertPEM}, |
| 524 | + want: http.StatusOK, |
| 525 | + }, |
| 526 | + { |
| 527 | + descr: "success-without-root", |
| 528 | + chain: []string{testdata.PrecertPEMValid}, |
| 529 | + want: http.StatusOK, |
| 530 | + }, |
| 531 | + } |
| 532 | + |
| 533 | + log := setupTestLog(t) |
| 534 | + server := setupTestServer(t, log, path.Join(prefix, "ct/v1/add-pre-chain")) |
| 535 | + defer server.Close() |
| 536 | + |
| 537 | + for _, test := range tests { |
| 538 | + t.Run(test.descr, func(t *testing.T) { |
| 539 | + pool := loadCertsIntoPoolOrDie(t, test.chain) |
| 540 | + chain := createJSONChain(t, *pool) |
| 541 | + |
| 542 | + resp, err := http.Post(server.URL+"/ct/v1/add-pre-chain", "application/json", chain) |
| 543 | + if err != nil { |
| 544 | + t.Fatalf("http.Post(%s)=(_,%q); want (_,nil)", types.AddPreChainPath, err) |
| 545 | + } |
| 546 | + if got, want := resp.StatusCode, test.want; got != want { |
| 547 | + t.Errorf("http.Post(%s)=(%d,nil); want (%d,nil)", types.AddPreChainPath, got, want) |
| 548 | + } |
| 549 | + if test.want == http.StatusOK { |
| 550 | + var gotRsp types.AddChainResponse |
| 551 | + if err := json.NewDecoder(resp.Body).Decode(&gotRsp); err != nil { |
| 552 | + t.Fatalf("json.Decode()=%v; want nil", err) |
| 553 | + } |
| 554 | + if got, want := types.Version(gotRsp.SCTVersion), types.V1; got != want { |
| 555 | + t.Errorf("resp.SCTVersion=%v; want %v", got, want) |
| 556 | + } |
| 557 | + if got, want := gotRsp.ID, demoLogID[:]; !bytes.Equal(got, want) { |
| 558 | + t.Errorf("resp.ID=%v; want %v", got, want) |
| 559 | + } |
| 560 | + if got, want := gotRsp.Timestamp, fakeTimeMillis; got != want { |
| 561 | + t.Errorf("resp.Timestamp=%d; want %d", got, want) |
| 562 | + } |
| 563 | + if got, want := hex.EncodeToString(gotRsp.Signature), "040300067369676e6564"; got != want { |
| 564 | + t.Errorf("resp.Signature=%s; want %s", got, want) |
| 565 | + } |
| 566 | + // TODO(phboneff): read from the log and compare values |
| 567 | + // TODO(phboneff): add a test with a backend write failure |
| 568 | + // TODO(phboneff): check that the index is in the SCT |
| 569 | + // TODO(phboneff): add a test with a not after range |
| 570 | + // TODO(phboneff): add a test with a start date only |
| 571 | + // TODO(phboneff): add duplicate tests |
| 572 | + } |
| 573 | + }) |
| 574 | + } |
| 575 | +} |
| 576 | + |
| 577 | +func createJSONChain(t *testing.T, p x509util.PEMCertPool) io.Reader { |
| 578 | + t.Helper() |
| 579 | + var req types.AddChainRequest |
| 580 | + for _, rawCert := range p.RawCertificates() { |
| 581 | + req.Chain = append(req.Chain, rawCert.Raw) |
| 582 | + } |
| 583 | + |
| 584 | + var buffer bytes.Buffer |
| 585 | + // It's tempting to avoid creating and flushing the intermediate writer but it doesn't work |
| 586 | + writer := bufio.NewWriter(&buffer) |
| 587 | + err := json.NewEncoder(writer).Encode(&req) |
| 588 | + if err := writer.Flush(); err != nil { |
| 589 | + t.Error(err) |
| 590 | + } |
| 591 | + |
| 592 | + if err != nil { |
| 593 | + t.Fatalf("Failed to create test json: %v", err) |
| 594 | + } |
| 595 | + |
| 596 | + return bufio.NewReader(&buffer) |
| 597 | +} |
| 598 | + |
| 599 | +func loadCertsIntoPoolOrDie(t *testing.T, certs []string) *x509util.PEMCertPool { |
| 600 | + t.Helper() |
| 601 | + pool := x509util.NewPEMCertPool() |
| 602 | + for _, cert := range certs { |
| 603 | + if !pool.AppendCertsFromPEM([]byte(cert)) { |
| 604 | + t.Fatalf("couldn't parse test certs: %v", certs) |
| 605 | + } |
| 606 | + } |
| 607 | + return pool |
| 608 | +} |
0 commit comments