diff --git a/server/bindata.go b/server/bindata.go
index bf3c03639259752681498fc2a8dff7a64584feee..7e9bbcceea385cad1ec65a89c5ae34a86407b9f2 100644
--- a/server/bindata.go
+++ b/server/bindata.go
@@ -74,7 +74,7 @@ func staticCssBootstrapMinCss() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 140936, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/css/bootstrap.min.css", size: 140936, mode: os.FileMode(420), modTime: time.Unix(1550305824, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -147,7 +147,7 @@ func staticCssSigninCss() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/css/signin.css", size: 1009, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/css/signin.css", size: 1009, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -170,7 +170,7 @@ func staticJsBootstrap413MinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/bootstrap-4.1.3.min.js", size: 51039, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/js/bootstrap-4.1.3.min.js", size: 51039, mode: os.FileMode(420), modTime: time.Unix(1550305766, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -189,7 +189,7 @@ func staticJsJquery331MinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/jquery-3.3.1.min.js", size: 86927, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/js/jquery-3.3.1.min.js", size: 86927, mode: os.FileMode(420), modTime: time.Unix(1516469204, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -243,7 +243,7 @@ func staticJsLogoutJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/logout.js", size: 1005, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/js/logout.js", size: 1005, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -265,7 +265,7 @@ func staticJsPopper1143MinJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/popper-1.14.3.min.js", size: 20337, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/js/popper-1.14.3.min.js", size: 20337, mode: os.FileMode(420), modTime: time.Unix(1526549114, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1030,7 +1030,7 @@ func staticJsU2fApiJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1512325237, 0)}
+	info := bindataFileInfo{name: "static/js/u2f-api.js", size: 20880, mode: os.FileMode(420), modTime: time.Unix(1535013418, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1099,7 +1099,7 @@ func staticJsU2fJs() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "static/js/u2f.js", size: 1274, mode: os.FileMode(420), modTime: time.Unix(1560696660, 0)}
+	info := bindataFileInfo{name: "static/js/u2f.js", size: 1274, mode: os.FileMode(420), modTime: time.Unix(1541228751, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1154,7 +1154,7 @@ func templatesLogin_otpHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_otp.html", size: 956, mode: os.FileMode(420), modTime: time.Unix(1561757406, 0)}
+	info := bindataFileInfo{name: "templates/login_otp.html", size: 956, mode: os.FileMode(420), modTime: time.Unix(1561884470, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1225,7 +1225,7 @@ func templatesLogin_passwordHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_password.html", size: 1432, mode: os.FileMode(420), modTime: time.Unix(1561757427, 0)}
+	info := bindataFileInfo{name: "templates/login_password.html", size: 1432, mode: os.FileMode(420), modTime: time.Unix(1561884470, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1282,7 +1282,7 @@ func templatesLogin_u2fHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_u2f.html", size: 908, mode: os.FileMode(420), modTime: time.Unix(1561757448, 0)}
+	info := bindataFileInfo{name: "templates/login_u2f.html", size: 908, mode: os.FileMode(420), modTime: time.Unix(1561884470, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1342,7 +1342,7 @@ func templatesLogoutHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/logout.html", size: 1063, mode: os.FileMode(420), modTime: time.Unix(1561757528, 0)}
+	info := bindataFileInfo{name: "templates/logout.html", size: 1063, mode: os.FileMode(420), modTime: time.Unix(1548600535, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1353,8 +1353,8 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
-    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
-    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
+    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css"{{SRI "/static/css/bootstrap.min.css"}}>
+    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css"{{SRI "/static/css/signin.css"}}>
     {{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
     <title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
   </head>
@@ -1366,15 +1366,15 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
 {{define "footer"}}
     </div>
 
-    <script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
-    <script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
-    <script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"></script>
+    <script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js"{{SRI "/static/js/jquery-3.3.1.min.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js"{{SRI "/static/js/popper-1.14.3.min.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js"{{SRI "/static/js/bootstrap-4.1.3.min.js"}}></script>
 {{if .U2FSignRequest}}
-    <script src="{{.URLPrefix}}/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script>
-    <script src="{{.URLPrefix}}/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script>
+    <script src="{{.URLPrefix}}/static/js/u2f-api.js"{{SRI "/static/js/u2f-api.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/u2f.js"{{SRI "/static/js/u2f.js"}}></script>
 {{end}}
 {{if .IncludeLogoutScripts}}
-    <script src="{{.URLPrefix}}/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script>
+    <script src="{{.URLPrefix}}/static/js/logout.js"{{SRI "/static/js/logout.js"}}></script>
 {{end}}
   </body>
 </html>
@@ -1391,7 +1391,7 @@ func templatesPageHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/page.html", size: 1865, mode: os.FileMode(420), modTime: time.Unix(1561757493, 0)}
+	info := bindataFileInfo{name: "templates/page.html", size: 1476, mode: os.FileMode(420), modTime: time.Unix(1576422396, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
diff --git a/server/http.go b/server/http.go
index f4412f1a9bb762ab1b90cb4cccb1b9cd1bef9587..cc299c23683548e6e0f71ebab900e77f1d385bf4 100644
--- a/server/http.go
+++ b/server/http.go
@@ -1,6 +1,6 @@
 package server
 
-//go:generate python sri.py templates/*.html
+//go:generate go run scripts/sri.go --package server --output sri_map.go static
 //go:generate go-bindata --nocompress --pkg server static/... templates/...
 
 import (
@@ -362,6 +362,7 @@ func (h *Server) Handler() http.Handler {
 func parseEmbeddedTemplates() *template.Template {
 	root := template.New("").Funcs(template.FuncMap{
 		"json": toJSON,
+		"SRI":  sriIntegrity,
 	})
 	files, err := AssetDir("templates")
 	if err != nil {
@@ -422,3 +423,13 @@ func intersectGroups(a, b []string) []string {
 	}
 	return out
 }
+
+// Return an integrity= attribute for the given URI (which should be
+// supplied without an eventual prefix).
+func sriIntegrity(uri string) template.HTML {
+	sri, ok := sriMap[uri]
+	if !ok {
+		return template.HTML("")
+	}
+	return template.HTML(fmt.Sprintf(" integrity=\"%s\"", sri))
+}
diff --git a/server/scripts/sri.go b/server/scripts/sri.go
new file mode 100644
index 0000000000000000000000000000000000000000..251ddddb768a534d2a4e09d27d751b8234a28bc0
--- /dev/null
+++ b/server/scripts/sri.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+	"crypto/sha512"
+	"encoding/base64"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+var (
+	outputPath  = flag.String("output", "", "output `file` name")
+	packageName = flag.String("package", "", "`name` of the package for generated code")
+	stripPrefix = flag.String("strip", "", "prefix to strip from `path`s to generate URLs")
+)
+
+func computeChecksum(path string) (string, error) {
+	data, err := ioutil.ReadFile(path)
+	if err != nil {
+		return "", err
+	}
+	sha := sha512.Sum384(data)
+	return "sha384-" + base64.StdEncoding.EncodeToString(sha[:]), nil
+}
+
+func urlForPath(path string) string {
+	path = strings.TrimPrefix(path, *stripPrefix)
+	if !strings.HasPrefix(path, "/") {
+		path = "/" + path
+	}
+	return path
+}
+
+func mkSRIMap(m map[string]string, dir string) error {
+	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		// Only match files with the desired extensions.
+		if info.IsDir() || !match(info.Name()) {
+			return nil
+		}
+
+		if cksum, err := computeChecksum(path); err == nil {
+			m[urlForPath(path)] = cksum
+		}
+		return nil
+	})
+}
+
+func match(name string) bool {
+	switch filepath.Ext(name) {
+	case ".js", ".json", ".css":
+		return true
+	default:
+		return false
+	}
+}
+
+// nolint: errcheck
+func codegen(w io.Writer, m map[string]string) {
+	fmt.Fprintf(w, "package %s\n", *packageName)
+	io.WriteString(w, `
+var sriMap = map[string]string{
+`)
+	for k, v := range m {
+		fmt.Fprintf(w, "\t%q: %q,\n", k, v)
+	}
+	io.WriteString(w, "}\n")
+}
+
+func main() {
+	log.SetFlags(0)
+	flag.Parse()
+
+	m := make(map[string]string)
+	for _, path := range flag.Args() {
+		if err := mkSRIMap(m, path); err != nil {
+			log.Println(err)
+		}
+	}
+
+	var w io.Writer = os.Stdout
+	if *outputPath != "" {
+		var err error
+		w, err = os.Create(*outputPath)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	codegen(w, m)
+}
diff --git a/server/sri.py b/server/sri.py
deleted file mode 100755
index 725dd07c6a5ffdbdcd04743aa1d41289130d94f4..0000000000000000000000000000000000000000
--- a/server/sri.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/python
-#
-# Automatically fix Subresource Integrity links in the HTML templates.
-#
-# Pass templates as command-line arguments. Expects to be run from the
-# base resource directory.
-#
-
-import glob
-import re
-import sys
-from hashlib import sha384
-
-
-script_rx = re.compile(r'<(?:script|link rel="stylesheet")[^>]*(?:src|href)="(?:{{.URLPrefix}})?([^"]+)"[^>]*>')
-integrity_rx = re.compile(r' +integrity="[^"]*"')
-
-
-def compute_checksum(src):
-    if src[0] == '/':
-        src = src[1:]
-    with open(src) as fd:
-        return 'sha384-' + sha384(fd.read()).digest().encode('base64').strip()
-
-
-def replace_checksum(m):
-    src = m.group(1)
-    checksum = compute_checksum(src)
-
-    script = m.group(0)
-    script = integrity_rx.sub('', script)
-    script = '%s integrity="%s">' % (script[:-1], checksum)
-
-    return script
-
-
-def fix_sri(path):
-    with open(path) as fd:
-        data = fd.read()
-    result = script_rx.sub(replace_checksum, data)
-    if result != data:
-        print >>sys.stderr, 'updating %s' % path
-        with open(path, 'w') as fd:
-            fd.write(result)
-
-
-if __name__ == '__main__':
-    for arg in sys.argv[1:]:
-        for path in glob.glob(arg):
-            try:
-                fix_sri(path)
-            except Exception as e:
-                print >>sys.stderr, "Error fixing %s: %s" % (path, e)
-
diff --git a/server/sri_map.go b/server/sri_map.go
new file mode 100644
index 0000000000000000000000000000000000000000..9fb8c6dcd4649f467d4982b55009d78647142196
--- /dev/null
+++ b/server/sri_map.go
@@ -0,0 +1,12 @@
+package server
+
+var sriMap = map[string]string{
+	"/static/css/bootstrap.min.css": "sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO",
+	"/static/css/signin.css": "sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o",
+	"/static/js/bootstrap-4.1.3.min.js": "sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy",
+	"/static/js/jquery-3.3.1.min.js": "sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT",
+	"/static/js/logout.js": "sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD",
+	"/static/js/popper-1.14.3.min.js": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
+	"/static/js/u2f-api.js": "sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR",
+	"/static/js/u2f.js": "sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe",
+}
diff --git a/server/templates/page.html b/server/templates/page.html
index 18401f5c983b6ae622554e8a5fa17452f16ab396..e1ec50fe7730f505471cfa1da9285590208fd647 100644
--- a/server/templates/page.html
+++ b/server/templates/page.html
@@ -4,8 +4,8 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
-    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
-    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
+    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css"{{SRI "/static/css/bootstrap.min.css"}}>
+    <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css"{{SRI "/static/css/signin.css"}}>
     {{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
     <title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
   </head>
@@ -17,15 +17,15 @@
 {{define "footer"}}
     </div>
 
-    <script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js" integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT"></script>
-    <script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"></script>
-    <script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"></script>
+    <script src="{{.URLPrefix}}/static/js/jquery-3.3.1.min.js"{{SRI "/static/js/jquery-3.3.1.min.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/popper-1.14.3.min.js"{{SRI "/static/js/popper-1.14.3.min.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/bootstrap-4.1.3.min.js"{{SRI "/static/js/bootstrap-4.1.3.min.js"}}></script>
 {{if .U2FSignRequest}}
-    <script src="{{.URLPrefix}}/static/js/u2f-api.js" integrity="sha384-9ChevE6pp8ArGK03HgolnFjZbF3webZQtYkwcabzbcI28Lx1/2x2j2fbaAWD4cgR"></script>
-    <script src="{{.URLPrefix}}/static/js/u2f.js" integrity="sha384-7zZy25ajTABErGlCQgcyRDpQDS9QVZv9o+95IfvCjWftQe20f411F1a39Ge5xmCe"></script>
+    <script src="{{.URLPrefix}}/static/js/u2f-api.js"{{SRI "/static/js/u2f-api.js"}}></script>
+    <script src="{{.URLPrefix}}/static/js/u2f.js"{{SRI "/static/js/u2f.js"}}></script>
 {{end}}
 {{if .IncludeLogoutScripts}}
-    <script src="{{.URLPrefix}}/static/js/logout.js" integrity="sha384-lChVngGLNFXetIJTSxc+scDpi1vsBL+7Xa4r2uZpQFP/6Y2z9eCDXe/Y4IUdklRD"></script>
+    <script src="{{.URLPrefix}}/static/js/logout.js"{{SRI "/static/js/logout.js"}}></script>
 {{end}}
   </body>
 </html>