195 lines
6.5 KiB
Go
195 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"runtime/debug"
|
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
func signingKey() []byte {
|
|
if k := os.Getenv("JWT_KEY"); k != "" {
|
|
return []byte(k)
|
|
}
|
|
return []byte("demo-only-not-for-real-use")
|
|
}
|
|
|
|
type versionInfo struct {
|
|
Service string `json:"service"`
|
|
Commit string `json:"commit"`
|
|
GoVersion string `json:"go_version"`
|
|
Deps map[string]string `json:"deps"`
|
|
JWTLibrary string `json:"jwt_library"`
|
|
}
|
|
|
|
func main() {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", index)
|
|
mux.HandleFunc("/version", version)
|
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintln(w, "ok")
|
|
})
|
|
mux.HandleFunc("/verify", verifyToken)
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
log.Printf("demo-secure listening on :%s", port)
|
|
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func index(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
jwtLib := "unknown"
|
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
|
for _, d := range bi.Deps {
|
|
if d.Path == "github.com/dgrijalva/jwt-go" || d.Path == "github.com/golang-jwt/jwt/v5" {
|
|
jwtLib = d.Path + "@" + d.Version
|
|
break
|
|
}
|
|
}
|
|
}
|
|
vulnerable := jwtLib == "github.com/dgrijalva/jwt-go@v3.2.0+incompatible"
|
|
pillClass := "pill-good"
|
|
pillText := "patched"
|
|
if vulnerable {
|
|
pillClass = "pill-bad"
|
|
pillText = "vulnerable"
|
|
}
|
|
fmt.Fprintf(w, `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>demo-secure · RHADS supply-chain demo</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1115; --panel: #1a1d24; --line: #2a2f3a;
|
|
--fg: #e6e8eb; --muted: #9aa3ad; --accent: #ee0000;
|
|
--good: #2ecc71; --bad: #e74c3c;
|
|
}
|
|
* { box-sizing: border-box }
|
|
body {
|
|
margin: 0; background: var(--bg); color: var(--fg);
|
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
line-height: 1.5;
|
|
}
|
|
.wrap { max-width: 56rem; margin: 0 auto; padding: 3rem 1.25rem 4rem }
|
|
header { border-bottom: 1px solid var(--line); padding-bottom: 1.25rem; margin-bottom: 1.75rem }
|
|
.brand { display: flex; align-items: baseline; gap: .75rem }
|
|
h1 { margin: 0; font-size: 2rem; letter-spacing: -.02em }
|
|
.brand .accent { color: var(--accent); font-weight: 700 }
|
|
.lede { color: var(--muted); margin: .5rem 0 0 }
|
|
.pills { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0 0 }
|
|
.pill {
|
|
font-size: .75rem; text-transform: uppercase; letter-spacing: .08em;
|
|
padding: .2rem .55rem; border-radius: 999px; border: 1px solid var(--line);
|
|
background: var(--panel); color: var(--muted);
|
|
}
|
|
.pill-good { color: #0b1; border-color: rgba(46,204,113,.4); background: rgba(46,204,113,.08) }
|
|
.pill-bad { color: #e85; border-color: rgba(231,76,60,.4); background: rgba(231,76,60,.10) }
|
|
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(16rem,1fr)) }
|
|
.card {
|
|
background: var(--panel); border: 1px solid var(--line); border-radius: .6rem;
|
|
padding: 1.1rem 1.2rem;
|
|
}
|
|
.card h2 { margin: 0 0 .35rem; font-size: 1.05rem; letter-spacing: -.01em }
|
|
.card p { margin: 0; color: var(--muted); font-size: .95rem }
|
|
.card a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--muted) }
|
|
.card a:hover { border-bottom-color: var(--accent); color: #fff }
|
|
.code {
|
|
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
background: #0a0c10; border: 1px solid var(--line); border-radius: .35rem;
|
|
padding: .15rem .45rem; font-size: .85em;
|
|
}
|
|
footer { color: var(--muted); font-size: .85rem; margin-top: 2rem; text-align: center }
|
|
.dep { margin-top: 1rem; font-size: .9rem; color: var(--muted) }
|
|
.dep strong { color: var(--fg) }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<div class="brand">
|
|
<h1>demo-<span class="accent">secure</span></h1>
|
|
<span class="pill">go service</span>
|
|
</div>
|
|
<p class="lede">A minimal HTTP service threaded through the Red Hat Advanced Developer Suite: RHDH golden-path scaffolding → Tekton build → RHTAS keyless signing → RHTPA SBOM scanning.</p>
|
|
<div class="pills">
|
|
<span class="pill pill-good">image signed (cosign + Fulcio)</span>
|
|
<span class="pill pill-good">SBOM attested (CycloneDX)</span>
|
|
<span class="pill %s">jwt-go %s</span>
|
|
</div>
|
|
<p class="dep">Active JWT library: <span class="code">%s</span></p>
|
|
</header>
|
|
|
|
<section class="grid">
|
|
<div class="card">
|
|
<h2><a href="/version">GET /version</a></h2>
|
|
<p>Build info plus the full dependency tree. Proves at runtime which jwt-go version is shipped.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h2><a href="/healthz">GET /healthz</a></h2>
|
|
<p>Liveness probe endpoint. Returns 200 OK while the process is up.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h2><span class="code">POST /verify</span></h2>
|
|
<p>Send <span class="code">Authorization: Bearer <jwt></span> to exercise the JWT library — this is the call path RHTPA sees as in-use.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
Built and signed in the Hibachi Ninja lab. Verify with <span class="code">scripts/show-signature.sh</span> on the bootstrap repo.
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>`, pillClass, pillText, jwtLib)
|
|
}
|
|
|
|
func version(w http.ResponseWriter, _ *http.Request) {
|
|
info := versionInfo{
|
|
Service: "demo-secure",
|
|
Commit: os.Getenv("GIT_COMMIT"),
|
|
Deps: map[string]string{},
|
|
}
|
|
if bi, ok := debug.ReadBuildInfo(); ok {
|
|
info.GoVersion = bi.GoVersion
|
|
for _, d := range bi.Deps {
|
|
info.Deps[d.Path] = d.Version
|
|
if d.Path == "github.com/dgrijalva/jwt-go" || d.Path == "github.com/golang-jwt/jwt/v5" {
|
|
info.JWTLibrary = fmt.Sprintf("%s@%s", d.Path, d.Version)
|
|
}
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(info)
|
|
}
|
|
|
|
func verifyToken(w http.ResponseWriter, r *http.Request) {
|
|
tok := r.Header.Get("Authorization")
|
|
if len(tok) > 7 && tok[:7] == "Bearer " {
|
|
tok = tok[7:]
|
|
}
|
|
if tok == "" {
|
|
http.Error(w, "missing bearer token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
parsed, err := jwt.Parse(tok, func(t *jwt.Token) (interface{}, error) {
|
|
return signingKey(), nil
|
|
})
|
|
if err != nil || !parsed.Valid {
|
|
http.Error(w, "invalid token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(parsed.Claims)
|
|
}
|