Initial release

This commit is contained in:
some 2024-09-30 19:02:10 -04:00
commit 2c6c508643
Signed by: some
GPG Key ID: 65D0589220B9BFC8
10 changed files with 561 additions and 0 deletions

35
.editorconfig Normal file
View File

@ -0,0 +1,35 @@
root = true
[*]
charset = utf-8
end_of_line = LF
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.sh]
indent_style = space
indent_size = 4
[{*.html,*.js,*.css,*.scss}]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 8
[{{*.,}[Dd]ockerfile{.*,},{*.,}[Cc]ontainerfile{.*,}}]
indent_style = space
indent_size = 4
[*.proto]
indent_style = space
indent_size = 2
[{*.go,go.mod}]
indent_style = tab
indent_size = 8

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2024, some
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

106
authentication/session.go Normal file
View File

@ -0,0 +1,106 @@
package authentication
import (
"crypto/rand"
"fmt"
"time"
"somehole.com/common/security/identity"
)
const (
RetryCreateSessionToken = 3
MaxSessionAge = 1 * time.Hour
)
type SessionToken [8]byte
func NewSessionToken() (st SessionToken) {
var stBytes = make([]byte, 8)
rand.Read(stBytes)
st = SessionToken(stBytes)
return
}
type NextToken [4]byte
func NewNextToken() (nt NextToken) {
var ntBytes = make([]byte, 4)
rand.Read(ntBytes)
nt = NextToken(ntBytes)
return
}
type Session struct {
identity.Identity
nextTokens []NextToken
expiration time.Time
}
func NewSession(identity identity.Identity) (s *Session) {
s = &Session{
Identity: identity,
nextTokens: make([]NextToken, 0),
expiration: time.Now().Add(MaxSessionAge),
}
return
}
func (s *Session) Active() (ok bool) {
ok = time.Now().Before(s.expiration)
return
}
func (s *Session) NewNextToken() (nt NextToken) {
nt = NewNextToken()
s.nextTokens = append(s.nextTokens, nt)
return
}
func (s *Session) NextTokenIsCurrent(nt NextToken) (ok bool) {
ok = s.nextTokens[len(s.nextTokens)-1] == nt
return
}
type SessionService struct {
sessions map[SessionToken]*Session
}
func NewSessionService() (srv *SessionService) {
srv = &SessionService{
sessions: make(map[SessionToken]*Session),
}
return
}
func (srv *SessionService) NewSessionToken() (st SessionToken, err error) {
for i := 0; i < RetryCreateSessionToken; i++ {
st = NewSessionToken()
if _, exists := srv.sessions[st]; exists {
err = fmt.Errorf("could only creat colliding session tokens in %d tries", RetryCreateSessionToken)
continue
}
err = nil
break
}
return
}
func (srv *SessionService) AddSession(st SessionToken, session *Session) (err error) {
if _, exists := srv.sessions[st]; exists {
err = fmt.Errorf("session already exists")
return
}
srv.sessions[st] = session
return
}
func (srv *SessionService) GetSession(sessionToken SessionToken) (session *Session, err error) {
var exists bool
session, exists = srv.sessions[sessionToken]
if !exists {
err = fmt.Errorf("session not found")
return
}
return
}

22
authorization/audit.go Normal file
View File

@ -0,0 +1,22 @@
package authorization
import (
"time"
"somehole.com/common/security/identity"
)
type record struct {
Verb
Noun
time.Time
}
type AuditService struct {
records map[identity.Id][]record
}
func NewAuditService() (srv *AuditService) {
srv = &AuditService{}
return
}

View File

@ -0,0 +1,43 @@
package authorization
import "somehole.com/common/security/identity"
type Verb int
const (
_ Verb = iota
Create
Read
Watch
Update
Patch
Delete
)
type Noun int
const (
_ Noun = iota
Identity
Command
Log
)
type AuthorizationService struct {
*AuditService
}
func NewAuthorizationService(auditService *AuditService) (srv *AuthorizationService) {
if auditService == nil {
auditService = NewAuditService()
}
srv = &AuthorizationService{
AuditService: auditService,
}
return
}
func (srv *AuthorizationService) Authorized(identity identity.Identity, verb Verb, noun Noun) (authorized bool, err error) {
authorized = true
return
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module somehole.com/common/security
go 1.23.1
require github.com/cloudflare/circl v1.4.0
require (
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d // indirect
golang.org/x/sys v0.10.0 // indirect
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d h1:LiA25/KWKuXfIq5pMIBq1s5hz3HQxhJJSu/SUGlD+SM=
golang.org/x/crypto v0.11.1-0.20230711161743-2e82bdd1719d/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

120
identity/identity.go Normal file
View File

@ -0,0 +1,120 @@
package identity
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"somehole.com/common/security/signature"
)
const (
RetryCreateId = 3
)
type identity interface {
GetId() []byte
GetPublicKey() []byte
GetPublicKeySignature() []byte
}
type Id [8]byte
func (i Id) String() (out string) {
out = base64.StdEncoding.EncodeToString(i[:])
return
}
type Identity struct {
Id Id
PublicKey signature.PublicKey
PublicKeySignature signature.Signature
}
func NewIdentity(ident identity) (i Identity, err error) {
id := ident.GetId()
if len(id) != 8 {
err = errors.New("wrong size id")
return
}
pubKey := ident.GetPublicKey()
if len(pubKey) != 96 {
err = errors.New("wrong size public key")
return
}
pubKeySig := ident.GetPublicKeySignature()
if len(pubKeySig) != 48 {
err = errors.New("wrong size public key signature")
return
}
i = Identity{
Id: Id(id),
PublicKey: signature.PublicKey(pubKey),
PublicKeySignature: signature.Signature(pubKeySig),
}
return
}
func (i Identity) String() (out string) {
b := make([]byte, 0)
b = append(b, i.Id[:]...)
b = append(b, i.PublicKey[:]...)
b = append(b, i.PublicKeySignature[:]...)
out = base64.StdEncoding.EncodeToString(b)
return
}
func ParseIdentityString(in string) (i Identity, err error) {
var b []byte
b, err = base64.StdEncoding.DecodeString(in)
if err != nil {
return
}
if len(b) != 8+96+48 {
err = errors.New("wrong for size identity data in string")
return
}
i = Identity{
Id: Id(b[0:8]),
PublicKey: signature.PublicKey(b[8:104]),
PublicKeySignature: signature.Signature(b[104:152]),
}
return
}
type IdentityService struct {
identities map[Id]Identity
}
func NewIdentityService() (srv *IdentityService) {
srv = &IdentityService{
identities: make(map[Id]Identity),
}
return
}
func (srv *IdentityService) NewId() (id Id, err error) {
var idBytes = make([]byte, 8)
for i := 0; i < RetryCreateId; i++ {
rand.Read(idBytes)
if _, exists := srv.identities[id]; exists {
err = fmt.Errorf("could only creat colliding ids in %d tries", RetryCreateId)
continue
}
id = Id(idBytes)
err = nil
break
}
return
}
func (srv *IdentityService) AddIdentity(id Id, identity Identity) (err error) {
_, exists := srv.identities[id]
if exists {
err = fmt.Errorf("failed to add identity")
return
}
srv.identities[id] = identity
return
}

45
security.go Normal file
View File

@ -0,0 +1,45 @@
package security
import (
"somehole.com/common/security/authentication"
"somehole.com/common/security/authorization"
"somehole.com/common/security/identity"
"somehole.com/common/security/signature"
)
type Config struct {
Signer signature.Keypair
}
type SecurityService struct {
config Config
*authentication.SessionService
*authorization.AuditService
*authorization.AuthorizationService
*identity.IdentityService
*signature.SignatureService
}
func NewSecurityService(config ...Config) (srv *SecurityService) {
cfg := Config{}
if len(config) == 1 {
cfg = config[0]
}
signatureService, err := signature.NewSignatureService(&cfg.Signer)
if err != nil {
return
}
identityService := identity.NewIdentityService()
sessionService := authentication.NewSessionService()
auditService := authorization.NewAuditService()
authorizationService := authorization.NewAuthorizationService(auditService)
srv = &SecurityService{
config: cfg,
SessionService: sessionService,
AuditService: auditService,
AuthorizationService: authorizationService,
IdentityService: identityService,
SignatureService: signatureService,
}
return
}

149
signature/signature.go Normal file
View File

@ -0,0 +1,149 @@
package signature
import (
"crypto/rand"
"encoding/base64"
"errors"
"github.com/cloudflare/circl/sign/bls"
)
type PrivateKey [32]byte
func (pk PrivateKey) Bls() (privKey *bls.PrivateKey[bls.KeyG2SigG1], err error) {
privKey = &bls.PrivateKey[bls.KeyG2SigG1]{}
err = privKey.UnmarshalBinary(pk[:])
if err != nil {
return
}
return
}
func (pk PrivateKey) Sign(msg []byte) (sig Signature, err error) {
privKey, err := pk.Bls()
if err != nil {
return
}
sig = Signature(bls.Sign(privKey, msg))
return
}
func (pk PrivateKey) SignKey(key PublicKey) (sig Signature, err error) {
pubKey := &bls.PublicKey[bls.KeyG2SigG1]{}
err = pubKey.UnmarshalBinary(key[:])
if err != nil {
return
}
return pk.Sign(key[:])
}
func (pk PrivateKey) String() (out string) {
out = base64.StdEncoding.EncodeToString(pk[:])
return
}
type PublicKey [96]byte
func (pk PublicKey) Bls() (pubKey *bls.PublicKey[bls.KeyG2SigG1], err error) {
pubKey = &bls.PublicKey[bls.KeyG2SigG1]{}
err = pubKey.UnmarshalBinary(pk[:])
if err != nil {
return
}
return
}
func (pk PublicKey) Verify(msg []byte, sig Signature) (ok bool, err error) {
pubKey, err := pk.Bls()
if err != nil {
return
}
ok = bls.Verify(pubKey, msg, sig[:])
return
}
func (pk PublicKey) String() (out string) {
out = base64.StdEncoding.EncodeToString(pk[:])
return
}
type Signature [48]byte
type Keypair struct {
PrivateKey
PublicKey
}
func NewKeypair() (kp *Keypair, err error) {
var (
ikm = make([]byte, 64)
salt = make([]byte, 32)
privKey *bls.PrivateKey[bls.KeyG2SigG1]
pubKey *bls.PublicKey[bls.KeyG2SigG1]
privKeyBytes = make([]byte, 32)
pubKeyBytes = make([]byte, 96)
)
_, err = rand.Read(ikm)
if err != nil {
return
}
_, err = rand.Read(salt)
if err != nil {
return
}
privKey, err = bls.KeyGen[bls.KeyG2SigG1](ikm, salt, []byte(""))
if err != nil {
return
}
privKeyBytes, err = privKey.MarshalBinary()
if err != nil {
return
}
pubKey = privKey.PublicKey()
pubKeyBytes, err = pubKey.MarshalBinary()
if err != nil {
return
}
kp = &Keypair{
PrivateKey: PrivateKey(privKeyBytes),
PublicKey: PublicKey(pubKeyBytes),
}
return
}
func (kp *Keypair) validate() (ok bool, err error) {
var testMessage [128]byte
_, err = rand.Read(testMessage[:])
if err != nil {
return
}
sig, _ := kp.PrivateKey.Sign(testMessage[:])
ok, _ = kp.PublicKey.Verify(testMessage[:], sig)
return
}
type SignatureService struct {
*Keypair
}
func NewSignatureService(signer *Keypair) (srv *SignatureService, err error) {
if signer == nil {
var ok bool
signer, err = NewKeypair()
if err != nil {
return
}
ok, err = signer.validate()
if err != nil {
return
}
if !ok {
err = errors.New("failed to validate new signer keypair")
return
}
}
srv = &SignatureService{
Keypair: signer,
}
return
}