add apparmor hat transitions

This commit is contained in:
ゆめ 2022-11-11 23:58:05 -05:00
parent d8ec9bb883
commit 0fb7171198
14 changed files with 336 additions and 33 deletions

16
.vscode/c_cpp_properties.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu17",
"cppStandard": "gnu++17",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}

View file

@ -1,5 +1,5 @@
<h2>お姫様の成績がアップデートされました!</h2> <h2>お姫様の成績がアップデートされました!</h2>
<h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</h2> <h2><a href="https://yoake.yumechi.jp/#canvas-grades">最近の採点</a></h2>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -17,10 +17,9 @@
<tr> <tr>
<td>{{ .SubmissionUserName }}</td> <td>{{ .SubmissionUserName }}</td>
<td>{{ .CourseCode }}</td> <td>{{ .CourseCode }}</td>
<td><a href="{{.AssignmentURL}}"">{{ .Name }}</a></td> <td><a href="{{.AssignmentURL}}">{{ .Name }}</a></td>
<td>{{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}</td> <td>{{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}</td>
<td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" <td>{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" }} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
}} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}</td>
<td>{{ .GradedAt }} </td> <td>{{ .GradedAt }} </td>
<td>{{ .PostedAt }}</td> <td>{{ .PostedAt }}</td>
</tr> </tr>

View file

@ -3,8 +3,10 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"os"
"github.com/eternal-flame-AD/yoake/config" "github.com/eternal-flame-AD/yoake/config"
"github.com/eternal-flame-AD/yoake/internal/apparmor"
"github.com/eternal-flame-AD/yoake/internal/comm" "github.com/eternal-flame-AD/yoake/internal/comm"
"github.com/eternal-flame-AD/yoake/internal/db" "github.com/eternal-flame-AD/yoake/internal/db"
"github.com/eternal-flame-AD/yoake/server" "github.com/eternal-flame-AD/yoake/server"
@ -37,10 +39,45 @@ func init() {
} }
} }
} }
func changeHat() {
profile := config.Config().Listen.AppArmor.Serve
if profile != "" {
token, err := apparmor.GetMagicToken()
if err != nil {
log.Panicf("failed to get apparmor magic token: %v", err)
}
if err := apparmor.ChangeHat(profile, token); err != nil {
log.Panicf("failed to change apparmor hat: %v", err)
} else {
log.Printf("changed apparmor hat to %s", profile)
}
}
}
func main() { func main() {
listen := config.Config().Listen listen := config.Config().Listen
if listen.Ssl.Use { if listen.Ssl.Use {
log.Fatalln(server.Server.StartTLS(listen.Addr, listen.Ssl.Cert, listen.Ssl.Key)) var sslCertBytes, sslKeyBytes []byte
apparmor.ExecuteInHat(listen.AppArmor.SSL, func() {
var err error
sslCertBytes, err = os.ReadFile(listen.Ssl.Cert)
if err != nil {
log.Panicf("failed to read ssl cert: %v", err)
}
sslKeyBytes, err = os.ReadFile(listen.Ssl.Key)
if err != nil {
log.Panicf("failed to read ssl key: %v", err)
}
}, true)
if listen.AppArmor.SSL != "" {
// defensive programming, try read ssl key
if _, err := os.ReadFile(listen.Ssl.Key); err == nil {
log.Panicf("AppArmor profile set for SSL but I could still read %v!", listen.Ssl.Key)
}
}
log.Fatalln(server.Server.StartTLS(listen.Addr, sslCertBytes, sslKeyBytes))
} else { } else {
log.Fatalln(server.Server.Start(listen.Addr)) log.Fatalln(server.Server.Start(listen.Addr))
} }

View file

@ -1,5 +1,7 @@
listen: listen:
addr: :80 addr: :80
apparmor:
ssl: ssl
ssl: ssl:
use: off use: off
cert: "fullchain.pem" cert: "fullchain.pem"

View file

@ -9,6 +9,10 @@ type C struct {
Hosts map[string]string Hosts map[string]string
Listen struct { Listen struct {
Addr string Addr string
AppArmor struct {
Serve string
SSL string
}
Ssl struct { Ssl struct {
Use bool Use bool
Cert string Cert string

View file

@ -6,12 +6,13 @@ make verify
make build make build
or exit 2 or exit 2
if ! diff etc/yoake-server.service /etc/systemd/system/yoake-server.service
sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service
or exit 2 or exit 2
sudo systemctl daemon-reload sudo systemctl daemon-reload
or exit 2 or exit 2
end
sudo systemctl stop yoake-server.service sudo systemctl stop yoake-server.service

65
internal/apparmor/api.go Normal file
View file

@ -0,0 +1,65 @@
package apparmor
import (
// #cgo LDFLAGS: -lapparmor
// #include "./apparmor.h"
"C"
)
import (
"runtime"
"syscall"
"unsafe"
)
func ChangeHat(subprofile string, magicToken uint64) error {
var ret uintptr
if subprofile != "" {
subProfileC := C.CString(subprofile)
defer C.free(unsafe.Pointer(subProfileC))
ret = uintptr(C.go_aa_change_hat(subProfileC, C.ulong(magicToken)))
} else {
ret = uintptr(C.go_aa_change_hat(nil, C.ulong(magicToken)))
}
if ret != 0 {
return syscall.Errno(ret)
}
return nil
}
func ExecuteInHat(subprofile string, fn func(), lockThread bool) error {
if subprofile == "" {
fn()
return nil
}
if lockThread {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
}
token, err := GetMagicToken()
if err != nil {
return err
}
if err := ChangeHat(subprofile, token); err != nil {
return err
}
fn()
return ChangeHat("", token)
}
func ChangeProfile(subprofile string) error {
var ret uintptr
if subprofile != "" {
subProfileC := C.CString(subprofile)
defer C.free(unsafe.Pointer(subProfileC))
ret = uintptr(C.go_aa_change_profile(subProfileC))
} else {
ret = uintptr(C.go_aa_change_profile(nil))
}
if ret != 0 {
return syscall.Errno(ret)
}
return nil
}

View file

@ -0,0 +1,21 @@
#include "./apparmor.h"
int go_aa_change_hat(const char *hat, unsigned long magic)
{
int ret = aa_change_hat(hat, magic);
if (ret < 0)
{
return errno;
}
return 0;
}
int go_aa_change_profile(const char *profile)
{
int ret = aa_change_profile(profile);
if (ret < 0)
{
return errno;
}
return 0;
}

View file

@ -0,0 +1,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/apparmor.h>
int go_aa_change_hat(const char *hat, unsigned long magic);
int go_aa_change_profile(const char *profile);

View file

@ -0,0 +1,17 @@
package apparmor
import (
"crypto/rand"
"fmt"
)
func GetMagicToken() (uint64, error) {
var buf [64 / 8]byte
if _, err := rand.Read(buf[:]); err != nil {
return 0, fmt.Errorf("failed to generate magic token: %v", err)
}
return uint64(buf[0])<<56 | uint64(buf[1])<<48 |
uint64(buf[2])<<40 | uint64(buf[3])<<32 |
uint64(buf[4])<<24 | uint64(buf[5])<<16 |
uint64(buf[6])<<8 | uint64(buf[7]), nil
}

View file

@ -1,7 +1,9 @@
package auth package auth
import ( import (
"crypto/rand"
"crypto/subtle" "crypto/subtle"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -9,6 +11,7 @@ import (
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"github.com/eternal-flame-AD/yoake/config" "github.com/eternal-flame-AD/yoake/config"
"github.com/eternal-flame-AD/yoake/internal/echoerror"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
@ -16,6 +19,20 @@ import (
const AuthSessionName = "auth_session" const AuthSessionName = "auth_session"
var dummyHash string
func init() {
var dummyPassword [16]byte
_, err := rand.Read(dummyPassword[:])
if err != nil {
panic(err)
}
dummyHash, err = argon2id.CreateHash(string(dummyPassword[:]), Argon2IdParams)
if err != nil {
panic(err)
}
}
type RequestAuth struct { type RequestAuth struct {
Present bool Present bool
Valid bool Valid bool
@ -118,6 +135,8 @@ func issueSession(c echo.Context, period time.Duration, roles []string) error {
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: config.Config().Listen.Ssl.Use,
} }
if period == 0 { if period == 0 {
period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute
@ -139,6 +158,8 @@ type LoginForm struct {
OtpResponse string `json:"otp_response" form:"otp_response"` OtpResponse string `json:"otp_response" form:"otp_response"`
} }
var errInvalidUserPass = echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid username or password"))
func Register(g *echo.Group) (err error) { func Register(g *echo.Group) (err error) {
g.GET("/auth.json", func(c echo.Context) error { g.GET("/auth.json", func(c echo.Context) error {
return c.JSON(http.StatusOK, GetRequestAuth(c)) return c.JSON(http.StatusOK, GetRequestAuth(c))
@ -181,6 +202,7 @@ func Register(g *echo.Group) (err error) {
return echo.NewHTTPError(http.StatusBadRequest, "password required") return echo.NewHTTPError(http.StatusBadRequest, "password required")
} }
if user, ok := config.Config().Auth.Users[form.Username]; ok { if user, ok := config.Config().Auth.Users[form.Username]; ok {
if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match {
if len(user.PublicKeyId) > 0 { if len(user.PublicKeyId) > 0 {
if verifiedOtpPubId == "" { if verifiedOtpPubId == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "otp required") return echo.NewHTTPError(http.StatusUnauthorized, "otp required")
@ -196,15 +218,15 @@ func Register(g *echo.Group) (err error) {
return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error") return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error")
} }
if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match {
issueSession(c, 0, user.Roles) issueSession(c, 0, user.Roles)
c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true}) c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true})
return nil return nil
} else { } else {
return echo.NewHTTPError(http.StatusUnauthorized, "incorrect password") return errInvalidUserPass
} }
} }
return echo.NewHTTPError(http.StatusUnauthorized, "invalid username") argon2id.ComparePasswordAndHash(form.Password, dummyHash)
return errInvalidUserPass
}, loginRateLimiter) }, loginRateLimiter)
g.DELETE("/login", func(c echo.Context) error { g.DELETE("/login", func(c echo.Context) error {
return issueSession(c, -1, nil) return issueSession(c, -1, nil)

View file

@ -2,6 +2,7 @@ package utilapi
import ( import (
"errors" "errors"
"os"
"time" "time"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
@ -36,6 +37,12 @@ func Register(g *echo.Group) (err error) {
Store: limiterStore, Store: limiterStore,
})) }))
} }
g.GET("/tryopen", func(c echo.Context) error {
if _, err := os.ReadFile(c.QueryParam("path")); err != nil {
return err
}
return c.String(200, c.QueryParam("path"))
}, auth.RequireMiddleware(auth.RoleAdmin))
return nil return nil
} }

View file

@ -3,6 +3,8 @@ package server
import ( import (
"strings" "strings"
"github.com/eternal-flame-AD/yoake/config"
"github.com/eternal-flame-AD/yoake/internal/apparmor"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -16,7 +18,21 @@ var Server = echo.New()
var hosts = map[string]*Host{} var hosts = map[string]*Host{}
func init() { func init() {
hatChanged := false
Server.Any("/*", func(c echo.Context) (err error) { Server.Any("/*", func(c echo.Context) (err error) {
if !hatChanged {
appArmor := config.Config().Listen.AppArmor
if appArmor.Serve != "" {
if key, err := apparmor.GetMagicToken(); err != nil {
return err
} else {
if err := apparmor.ChangeHat(appArmor.Serve, key); err != nil {
return err
}
}
}
hatChanged = true
}
req := c.Request() req := c.Request()
res := c.Response() res := c.Response()
host := hosts[strings.ToLower(req.Host)] host := hosts[strings.ToLower(req.Host)]

View file

@ -6,9 +6,9 @@
<div class="row p-2"> <div class="row p-2">
<div class="col"> <div class="col">
<div class="card border"> <div class="card border">
<div class="card-header">Quote</div> <div class="card-header">Welcome</div>
<div class="card-body"> <div class="card-body text-center">
<blockquote class="blockquote text-center"> <blockquote class="blockquote">
<p> <p>
夜明け前が一番暗い 夜明け前が一番暗い
</p> </p>
@ -17,6 +17,42 @@
</p> </p>
</blockquote> </blockquote>
<hr>
<div id="welcome">
<p>
Welcome to yoake.yumechi.jp, Yumechi's <a target="_blank"
href="https://en.wikipedia.org/wiki/Personal_information_manager">
<abbr title="Personal Information Manager" class="initialism">PIM</abbr></a>.
</p>
<p>
Built with
<abbr title="Echo HTTP Framework"><a target="_blank" rel="nofollow noreferer"
href="https://echo.labstack.com/">Echo</a></abbr>,
<abbr title="Bootstrap CSS Framework"><a target="_blank" rel="nofollow noreferer"
href="https://getbootstrap.com/">Bootstrap</a></abbr>,
<abbr title="jQuery JavaScript Library"><a href="https://jquery.com/" target="_blank"
rel="nofollow noreferer">jQuery</a></abbr>,
and
<abbr title="Golang Standard Library html/template"><a target="_blank"
rel="nofollow noreferer"
href="https://pkg.go.dev/html/template">html/template</a></abbr>.
Intended to be a parody of the
<abbr title="Terumo BCT Trima Accel(R)"><a href="https://www.terumobct.com/trima"
target="_blank" rel="nofollow noreferer">Trima Accel (R)</a></abbr> UI.
</p>
<p>
{{ if not $auth.Valid }}
This website welcomes guests as well!
Click around and I hope you cound have some fun!
{{ else }}
{{ if eq (index $auth.Roles 0) "admin" }}
Welcome Home!
{{ else }}
Hello, my friend! You know the drill!
{{ end }}
{{ end }}
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -96,7 +132,59 @@
</p> </p>
<button class="btn btn-primary" onclick="signin()">Sign In</button> <button class="btn btn-primary" onclick="signin()">Sign In</button>
{{ end }} {{ end }}
</div> <table class="table text-center">
<thead>
<tr>
<th>Tab Status Icon</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src={{ trima_img "icon_t_vista_procedure_optimal.gif" "url" }} />
</td>
<td class="align-middle">
Tab Selected (Active)
</td>
</tr>
<tr>
<td>
<img src={{ trima_img "icon_t_vista_procedure_valid.gif" "url" }} />
</td>
<td class="align-middle">
Tab Available
</td>
</tr>
<tr>
<td>
<img src={{ trima_img "icon_t_vista_procedure_ineligible.gif" "url" }} />
</td>
<td class="align-middle">
Addn. Authorization Required
</td>
</tr>
<tr>
<td>
<img src={{ trima_img "icon_t_vista_procedure_questionable.gif" "url" }} />
</td>
<td class="align-middle">
Unknown
</td>
</tr>
<tr>
<td>
<img src={{ trima_img "icon_t_vista_procedure_invalid.gif" "url" }} />
</td>
<td class="align-middle">
Access Denied
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>