diff --git a/signer/contentsignaturepki/contentsignature.go b/signer/contentsignaturepki/contentsignature.go index e8bfbd546..093e9c318 100644 --- a/signer/contentsignaturepki/contentsignature.go +++ b/signer/contentsignaturepki/contentsignature.go @@ -10,6 +10,7 @@ import ( "hash" "io" "math/big" + "net/http" "time" "github.com/mozilla-services/autograph/database" @@ -185,7 +186,7 @@ func (s *ContentSigner) initEE(conf signer.Configuration) error { default: return fmt.Errorf("contentsignaturepki %q: failed to find suitable end-entity: %w", s.ID, err) } - _, _, err = GetX5U(buildHTTPClient(), s.X5U) + _, _, err = GetX5U(http.DefaultClient, s.X5U) if err != nil { return fmt.Errorf("contentsignaturepki %q: failed to verify x5u: %w", s.ID, err) } diff --git a/signer/contentsignaturepki/contentsignature_test.go b/signer/contentsignaturepki/contentsignature_test.go index b196b122f..cd0539e7e 100644 --- a/signer/contentsignaturepki/contentsignature_test.go +++ b/signer/contentsignaturepki/contentsignature_test.go @@ -9,6 +9,8 @@ package contentsignaturepki import ( "crypto/ecdsa" "errors" + "fmt" + "net/http" "strings" "testing" @@ -19,73 +21,75 @@ import ( func TestSign(t *testing.T) { input := []byte("foobarbaz1234abcd") for i, testcase := range PASSINGTESTCASES { - // initialize a signer - s, err := New(testcase.cfg) - if err != nil { - t.Fatalf("testcase %d signer initialization failed with: %v", i, err) - } - if s.Type != testcase.cfg.Type { - t.Fatalf("testcase %d signer type %q does not match configuration %q", i, s.Type, testcase.cfg.Type) - } - if s.ID != testcase.cfg.ID { - t.Fatalf("testcase %d signer id %q does not match configuration %q", i, s.ID, testcase.cfg.ID) - } - if s.PrivateKey != testcase.cfg.PrivateKey { - t.Fatalf("testcase %d signer private key %q does not match configuration %q", i, s.PrivateKey, testcase.cfg.PrivateKey) - } - if s.Mode != testcase.cfg.Mode { - t.Fatalf("testcase %d signer curve %q does not match expected %q", i, s.Mode, testcase.cfg.Mode) - } + t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) { + // initialize a signer + s, err := New(testcase.cfg) + if err != nil { + t.Fatalf("testcase %d signer initialization failed with: %v", i, err) + } + if s.Type != testcase.cfg.Type { + t.Fatalf("testcase %d signer type %q does not match configuration %q", i, s.Type, testcase.cfg.Type) + } + if s.ID != testcase.cfg.ID { + t.Fatalf("testcase %d signer id %q does not match configuration %q", i, s.ID, testcase.cfg.ID) + } + if s.PrivateKey != testcase.cfg.PrivateKey { + t.Fatalf("testcase %d signer private key %q does not match configuration %q", i, s.PrivateKey, testcase.cfg.PrivateKey) + } + if s.Mode != testcase.cfg.Mode { + t.Fatalf("testcase %d signer curve %q does not match expected %q", i, s.Mode, testcase.cfg.Mode) + } - // sign input data - sig, err := s.SignData(input, nil) - if err != nil { - t.Fatalf("testcase %d failed to sign data: %v", i, err) - } - // convert signature to string format - sigstr, err := sig.Marshal() - if err != nil { - t.Fatalf("testcase %d failed to marshal signature: %v", i, err) - } + // sign input data + sig, err := s.SignData(input, nil) + if err != nil { + t.Fatalf("testcase %d failed to sign data: %v", i, err) + } + // convert signature to string format + sigstr, err := sig.Marshal() + if err != nil { + t.Fatalf("testcase %d failed to marshal signature: %v", i, err) + } - // convert string format back to signature - cs, err := verifier.Unmarshal(sigstr) - if err != nil { - t.Fatalf("testcase %d failed to unmarshal signature: %v", i, err) - } + // convert string format back to signature + cs, err := verifier.Unmarshal(sigstr) + if err != nil { + t.Fatalf("testcase %d failed to unmarshal signature: %v", i, err) + } - // make sure we still have the same string representation - sigstr2, err := cs.Marshal() - if err != nil { - t.Fatalf("testcase %d failed to re-marshal signature: %v", i, err) - } - if sigstr != sigstr2 { - t.Fatalf("testcase %d marshalling signature changed its format.\nexpected\t%q\nreceived\t%q", - i, sigstr, sigstr2) - } + // make sure we still have the same string representation + sigstr2, err := cs.Marshal() + if err != nil { + t.Fatalf("testcase %d failed to re-marshal signature: %v", i, err) + } + if sigstr != sigstr2 { + t.Fatalf("testcase %d marshalling signature changed its format.\nexpected\t%q\nreceived\t%q", + i, sigstr, sigstr2) + } - if cs.Len != getSignatureLen(s.Mode) { - t.Fatalf("testcase %d expected signature len of %d, got %d", - i, getSignatureLen(s.Mode), cs.Len) - } - if cs.Mode != s.Mode { - t.Fatalf("testcase %d expected curve name %q, got %q", i, s.Mode, cs.Mode) - } + if cs.Len != getSignatureLen(s.Mode) { + t.Fatalf("testcase %d expected signature len of %d, got %d", + i, getSignatureLen(s.Mode), cs.Len) + } + if cs.Mode != s.Mode { + t.Fatalf("testcase %d expected curve name %q, got %q", i, s.Mode, cs.Mode) + } - // verify the signature using the public key of the end entity - _, certs, err := GetX5U(buildHTTPClient(), s.X5U) - if err != nil { - t.Fatalf("testcase %d failed to get X5U %q: %v", i, s.X5U, err) - } - leaf := certs[0] - key := leaf.PublicKey.(*ecdsa.PublicKey) - if !sig.(*verifier.ContentSignature).VerifyData([]byte(input), key) { - t.Fatalf("testcase %d failed to verify signature", i) - } + // verify the signature using the public key of the end entity + _, certs, err := GetX5U(http.DefaultClient, s.X5U) + if err != nil { + t.Fatalf("testcase %d failed to get X5U %q: %v", i, s.X5U, err) + } + leaf := certs[0] + key := leaf.PublicKey.(*ecdsa.PublicKey) + if !sig.(*verifier.ContentSignature).VerifyData([]byte(input), key) { + t.Fatalf("testcase %d failed to verify signature", i) + } - if leaf.Subject.CommonName != testcase.expectedCommonName { - t.Errorf("testcase %d expected common name %#v, got %#v", i, testcase.expectedCommonName, leaf.Subject.CommonName) - } + if leaf.Subject.CommonName != testcase.expectedCommonName { + t.Errorf("testcase %d expected common name %#v, got %#v", i, testcase.expectedCommonName, leaf.Subject.CommonName) + } + }) } } diff --git a/signer/contentsignaturepki/upload.go b/signer/contentsignaturepki/upload.go index 1e54a859e..f444b1070 100644 --- a/signer/contentsignaturepki/upload.go +++ b/signer/contentsignaturepki/upload.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path" + "path/filepath" "strings" "time" @@ -82,56 +83,57 @@ func writeLocalFile(data, name string, target *url.URL) error { return err } } - // write the file into the target dir - return os.WriteFile(target.Path+name, []byte(data), 0755) -} -// buildHTTPClient returns the default HTTP.Client for fetching X5Us -func buildHTTPClient() *http.Client { - return &http.Client{} + return os.WriteFile(filepath.Join(target.Path, name), []byte(data), 0755) } // GetX5U retrieves a chain file of certs from upload location, parses // and verifies it, then returns a byte slice of the response body and // a slice of parsed certificates. -func GetX5U(client *http.Client, x5u string) (body []byte, certs []*x509.Certificate, err error) { +func GetX5U(client *http.Client, x5u string) ([]byte, []*x509.Certificate, error) { parsedURL, err := url.Parse(x5u) if err != nil { - err = fmt.Errorf("failed to parse chain upload location: %w", err) - return - } - if parsedURL.Scheme == "file" { - t := &http.Transport{} - t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) - client.Transport = t - } - resp, err := client.Get(x5u) - if err != nil { - err = fmt.Errorf("failed to retrieve x5u: %w", err) - return + return nil, nil, fmt.Errorf("failed to parse chain upload location: %w", err) + } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("failed to retrieve x5u from %s: %s", x5u, resp.Status) - return + var bodyReader io.ReadCloser + switch parsedURL.Scheme { + case "https", "http": + resp, err := client.Get(x5u) + if err != nil { + return nil, nil, fmt.Errorf("failed to retrieve x5u from %#v: %w", x5u, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("failed to retrieve x5u from %#v: %s", x5u, resp.Status) + } + bodyReader = resp.Body + + case "file": + bodyReader, err = os.Open(parsedURL.Path) + if err != nil { + return nil, nil, fmt.Errorf("failed to open x5u file:// at %#v: %w", x5u, err) + } + defer bodyReader.Close() + default: + return nil, nil, fmt.Errorf("unsupported x5u scheme: %#v", parsedURL.Scheme) } - body, err = io.ReadAll(resp.Body) + + body, err := io.ReadAll(bodyReader) if err != nil { - err = fmt.Errorf("failed to parse x5u body: %w", err) - return + return nil, nil, fmt.Errorf("failed to parse x5u body from %#v: %w", x5u, err) } - certs, err = csigverifier.ParseChain(body) + certs, err := csigverifier.ParseChain(body) if err != nil { - err = fmt.Errorf("failed to parse x5u : %w", err) - return + + return nil, nil, fmt.Errorf("failed to parse x5u : %w", err) } rootHash := sha2Fingerprint(certs[2]) err = csigverifier.VerifyChain([]string{rootHash}, certs, time.Now()) if err != nil { - err = fmt.Errorf("failed to verify certificate chain: %w", err) - return + return nil, nil, fmt.Errorf("failed to verify certificate chain: %w", err) } - return + return body, certs, nil } func sha2Fingerprint(cert *x509.Certificate) string { diff --git a/signer/contentsignaturepki/x509.go b/signer/contentsignaturepki/x509.go index 894d60f8d..30a32a3e6 100644 --- a/signer/contentsignaturepki/x509.go +++ b/signer/contentsignaturepki/x509.go @@ -7,6 +7,8 @@ import ( "encoding/pem" "fmt" "math/big" + "net/http" + "net/url" "time" "github.com/mozilla-services/autograph/database" @@ -41,9 +43,8 @@ func (s *ContentSigner) findAndSetEE(conf signer.Configuration) (err error) { // makeAndUploadChain makes a certificate using the end-entity public key, // uploads the chain to its destination and creates an X5U download URL -func (s *ContentSigner) makeAndUploadChain() (err error) { - var fullChain, chainName string - fullChain, chainName, err = s.makeChain() +func (s *ContentSigner) makeAndUploadChain() error { + fullChain, chainName, err := s.makeChain() if err != nil { return fmt.Errorf("failed to make chain: %w", err) } @@ -51,13 +52,16 @@ func (s *ContentSigner) makeAndUploadChain() (err error) { if err != nil { return fmt.Errorf("failed to upload chain: %w", err) } - newX5U := s.X5U + chainName - _, _, err = GetX5U(buildHTTPClient(), newX5U) + newX5U, err := url.JoinPath(s.X5U, chainName) + if err != nil { + return fmt.Errorf("failed to join x5u with chain name: %w", err) + } + _, _, err = GetX5U(http.DefaultClient, newX5U) if err != nil { return fmt.Errorf("failed to download new chain: %w", err) } s.X5U = newX5U - return + return nil } // makeChain issues an end-entity certificate using the ca private key and the first diff --git a/tools/autograph-monitor/contentsignaturepki_test.go b/tools/autograph-monitor/contentsignaturepki_test.go index c39d49a19..2bd556bc6 100644 --- a/tools/autograph-monitor/contentsignaturepki_test.go +++ b/tools/autograph-monitor/contentsignaturepki_test.go @@ -167,32 +167,32 @@ func Test_verifyContentSignature(t *testing.T) { })) defer ts.Close() - oneCertChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + oneCertChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, NormandyDevChain2021Intermediate) })) defer oneCertChainTestServer.Close() - rsaLeafChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rsaLeafChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(rsaLeafChain)) })) defer rsaLeafChainTestServer.Close() - testLeafChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testLeafChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(testLeafChain)) })) defer testLeafChainTestServer.Close() - testLeafExpiringSoonChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testLeafExpiringSoonChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(mustCertsToChain([]*x509.Certificate{testLeaf7DaysToExpiration, testInter, testRoot}))) })) defer testLeafExpiringSoonChainTestServer.Close() - testInterExpiringSoonChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testInterExpiringSoonChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(mustCertsToChain([]*x509.Certificate{testLeaf, testInter30DaysToExpiration, testRoot}))) })) defer testInterExpiringSoonChainTestServer.Close() - testRootExpiringSoonChainTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testRootExpiringSoonChainTestServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(mustCertsToChain([]*x509.Certificate{testLeaf, testInter, testRoot16DaysToExpiration}))) })) defer testRootExpiringSoonChainTestServer.Close() @@ -220,7 +220,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "valid csig response", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -237,7 +237,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "valid csig response typed nil notifier ok", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: typedNilNotifier, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -254,7 +254,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "valid csig response notifies", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -279,7 +279,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "valid csig response with invalid root hash but ignored EE ok", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{"invalidroothash"}, ignoredCerts: map[string]bool{"example.content-signature.mozilla.org": true}, @@ -298,7 +298,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "valid csig response with invalid root hash fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{"invalidroothash"}, response: formats.SignatureResponse{ @@ -316,7 +316,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "empty x5u fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{"invalidroothash"}, response: formats.SignatureResponse{ @@ -351,7 +351,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "truncated signature fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -369,7 +369,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "one cert X5U chain fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: oneCertChainTestServer.Client(), notifier: nil, rootHashes: normandyDev2021Roothash, response: formats.SignatureResponse{ @@ -388,7 +388,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "bad EE pubkey fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: rsaLeafChainTestServer.Client(), notifier: nil, rootHashes: normandyDev2021Roothash, response: formats.SignatureResponse{ @@ -407,7 +407,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "invalid data (wrong EE for normandyDev2021Roothash) fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafChainTestServer.Client(), notifier: nil, rootHashes: normandyDev2021Roothash, response: formats.SignatureResponse{ @@ -426,7 +426,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "expiring EE fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testLeafExpiringSoonChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -444,7 +444,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "expiring inter fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testInterExpiringSoonChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot)}, response: formats.SignatureResponse{ @@ -462,7 +462,7 @@ func Test_verifyContentSignature(t *testing.T) { { name: "expiring root fails", args: args{ - x5uClient: &http.Client{}, + x5uClient: testRootExpiringSoonChainTestServer.Client(), notifier: nil, rootHashes: []string{sha2Fingerprint(testRoot16DaysToExpiration)}, response: formats.SignatureResponse{