From cfa498a548a23914f4ff3a9289122a04d9b70923 Mon Sep 17 00:00:00 2001 From: oiweiwei <168575193+oiweiwei@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:35:17 +0200 Subject: [PATCH] feat: ssp: accept security context for krb5/ntlm/spnego (#111) * feat: ssp: accept security context for krb5/ntlm/spnego * fix tests * fix ntlm signature * ssp: capabilities: override only when empty --- README.md | 9 +- examples/helpers/{ => nthash}/nt_hash.go | 0 examples/helpers/{ => pac}/pac.go | 0 midl/file.go | 12 +- midl/idl/h/guiddef.h | 205 ++++++++++++++++++++++ midl/types_helper.go | 2 +- ssp/credential/credential.go | 20 +++ ssp/credential/database.go | 52 ++++++ ssp/gssapi/context.go | 156 ++++++++++++++--- ssp/gssapi/credential.go | 56 ++++++ ssp/gssapi/gssapi.go | 26 +++ ssp/gssapi/mechanism.go | 2 + ssp/krb5/authentifier.go | 126 +++++++++++++- ssp/krb5/config.go | 50 ++++++ ssp/krb5/crypto/aes_cts_hmac_sha1.go | 66 +++++-- ssp/krb5/crypto/asn1.go | 34 ++-- ssp/krb5/crypto/crypto.go | 7 +- ssp/krb5/crypto/des.go | 60 ++++++- ssp/krb5/crypto/rc4_hmac.go | 60 ++++++- ssp/krb5/gssapi.go | 138 ++++++++++++--- ssp/name/name.go | 78 +++++++++ ssp/netlogon/gssapi.go | 4 + ssp/ntlm/authentifier.go | 171 ++++++++++++++++++- ssp/ntlm/config.go | 44 +++++ ssp/ntlm/gssapi.go | 132 +++++++++++++- ssp/ntlm/ntlm.go | 7 +- ssp/ntlm/signature.go | 2 + ssp/ntlm/v1.go | 66 ++++++- ssp/ntlm/v2.go | 81 +++++++++ ssp/ntlm/verifier.go | 54 ++++++ ssp/spnego/authentifier.go | 209 +++++++++++++++++++++-- ssp/spnego/gssapi.go | 20 ++- 32 files changed, 1818 insertions(+), 131 deletions(-) rename examples/helpers/{ => nthash}/nt_hash.go (100%) rename examples/helpers/{ => pac}/pac.go (100%) create mode 100644 midl/idl/h/guiddef.h create mode 100644 ssp/credential/database.go create mode 100644 ssp/name/name.go create mode 100644 ssp/ntlm/verifier.go diff --git a/README.md b/README.md index d6a6baf4..e53e748a 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Examples rely on following environment variables: | ---- | ----------- | ------- | | **USERNAME** | The Domain\Username | `"MSAD2.COM\User"` | | **PASSWORD** | The password | `"password"` | -| **PASSWORD_MD4** | The password hash (use [go run examples/helpers/nt_hash.go -d $PASSWORD](./examples/helpers/nt_hash.go) to generate the hash) | `"f077ca4b7d73486a45e75dcdd74cd5bd"` | +| **PASSWORD_MD4** | The password hash (use [go run examples/helpers/nthash/nt_hash.go -d $PASSWORD](./examples/helpers/nthash/nt_hash.go) to generate the hash) | `"f077ca4b7d73486a45e75dcdd74cd5bd"` | | **WORKSTATION** | The workstation name | `"Ubuntu"` | | **SERVER** | The server FQDN or IP | `"192.168.0.22"` | | **SERVER_NAME** | The server NetBIOS name | `"WIN2019"` | @@ -273,6 +273,13 @@ The set of changes includes: | [MS-WMI](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wmi) | Windows Management Instrumentation Remote Protocol | [github.com/oiweiwei/msrpc/wmi](./msrpc/dcom/wmi) | | [MS-WSRM](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wsrm) | Windows System Resource Manager (WSRM) Protocol | [github.com/oiweiwei/msrpc/wsrm](./msrpc/dcom/wsrm) | +### Other Stubs + +| Code | Description | Package | +| ------- | -------- | ------- | +| [MIMICOM](https://gist.github.com/gentilkiwi/e3d9c92b93ed4bb48f7956492c1d335a) | Mimikatz COM Interface | [github.com/oiweiwei/msrpc/mimicom](./msrpc/mimicom) | + + ### Documentation The codegen package also generates the documentation for the generated code diff --git a/examples/helpers/nt_hash.go b/examples/helpers/nthash/nt_hash.go similarity index 100% rename from examples/helpers/nt_hash.go rename to examples/helpers/nthash/nt_hash.go diff --git a/examples/helpers/pac.go b/examples/helpers/pac/pac.go similarity index 100% rename from examples/helpers/pac.go rename to examples/helpers/pac/pac.go diff --git a/midl/file.go b/midl/file.go index 1a8c425f..6ee1e541 100644 --- a/midl/file.go +++ b/midl/file.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "sort" "strings" "sync" @@ -92,7 +93,16 @@ func FileStore(p string, f *File) { } func FindPath() []string { - return strings.Split(os.Getenv("MSIDLPATH"), ":") + var paths []string + _, p, _, ok := runtime.Caller(0) + if ok { + paths = append(paths, filepath.Join(filepath.Dir(p), "idl")) + } + + if p := os.Getenv("MSIDLPATH"); p != "" { + paths = append(paths, strings.Split(p, ":")...) + } + return paths } func LookupType(n string) *Type { diff --git a/midl/idl/h/guiddef.h b/midl/idl/h/guiddef.h new file mode 100644 index 00000000..612e88ef --- /dev/null +++ b/midl/idl/h/guiddef.h @@ -0,0 +1,205 @@ +//+--------------------------------------------------------------------------- +// +// Microsoft Windows +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// File: guiddef.h +// +// Contents: GUID definition +// +//---------------------------------------------------------------------------- + +//#ifndef GUID_DEFINED +//#define GUID_DEFINED +//#if defined(__midl) +//typedef struct { +// unsigned long Data1; +// unsigned short Data2; +// unsigned short Data3; +// byte Data4[ 8 ]; +//} GUID; +//#else +typedef struct _GUID { + unsigned long Data1; + unsigned short Data2; + unsigned short Data3; + unsigned char Data4[ 8 ]; +} GUID; +//#endif +//#endif +// +//#ifndef FAR +//#ifdef _WIN32 +//#define FAR +//#else +//#define FAR _far +//#endif +//#endif +// +//#ifndef DECLSPEC_SELECTANY +//#if (_MSC_VER >= 1100) +//#define DECLSPEC_SELECTANY __declspec(selectany) +//#else +//#define DECLSPEC_SELECTANY +//#endif +//#endif +// +//#ifndef EXTERN_C +//#ifdef __cplusplus +//#define EXTERN_C extern "C" +//#else +//#define EXTERN_C extern +//#endif +//#endif +// +//#ifdef DEFINE_GUID +//#undef DEFINE_GUID +//#endif +// +//#ifdef INITGUID +//#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ +// EXTERN_C const GUID DECLSPEC_SELECTANY name \ +// = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } +//#else +//#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ +// EXTERN_C const GUID FAR name +//#endif // INITGUID +// +//#define DEFINE_OLEGUID(name, l, w1, w2) DEFINE_GUID(name, l, w1, w2, 0xC0,0,0,0,0,0,0,0x46) +// +//#ifndef _GUIDDEF_H_ +//#define _GUIDDEF_H_ +// +//#ifndef __LPGUID_DEFINED__ +//#define __LPGUID_DEFINED__ +typedef GUID *LPGUID; +//#endif +// +//#ifndef __LPCGUID_DEFINED__ +//#define __LPCGUID_DEFINED__ +typedef const GUID *LPCGUID; +//#endif +// +//#ifndef __IID_DEFINED__ +//#define __IID_DEFINED__ + +typedef GUID IID; +typedef IID *LPIID; +//#define IID_NULL GUID_NULL +//#define IsEqualIID(riid1, riid2) IsEqualGUID(riid1, riid2) +typedef GUID CLSID; +typedef CLSID *LPCLSID; +//#define CLSID_NULL GUID_NULL +//#define IsEqualCLSID(rclsid1, rclsid2) IsEqualGUID(rclsid1, rclsid2) +typedef GUID FMTID; +typedef FMTID *LPFMTID; +//#define FMTID_NULL GUID_NULL +//#define IsEqualFMTID(rfmtid1, rfmtid2) IsEqualGUID(rfmtid1, rfmtid2) +// +//#ifdef __midl_proxy +//#define __MIDL_CONST +//#else +//#define __MIDL_CONST const +//#endif +// +//#ifndef _REFGUID_DEFINED +//#define _REFGUID_DEFINED +//#ifdef __cplusplus +//#define REFGUID const GUID & +//#else +//#define REFGUID const GUID * __MIDL_CONST +//#endif +//#endif +// +//#ifndef _REFIID_DEFINED +//#define _REFIID_DEFINED +//#ifdef __cplusplus +//#define REFIID const IID & +//#else +//#define REFIID const IID * __MIDL_CONST +//#endif +//#endif +// +//#ifndef _REFCLSID_DEFINED +//#define _REFCLSID_DEFINED +//#ifdef __cplusplus +//#define REFCLSID const IID & +//#else +//#define REFCLSID const IID * __MIDL_CONST +//#endif +//#endif +// +//#ifndef _REFFMTID_DEFINED +//#define _REFFMTID_DEFINED +//#ifdef __cplusplus +//#define REFFMTID const IID & +//#else +//#define REFFMTID const IID * __MIDL_CONST +//#endif +//#endif +// +//#endif // !__IID_DEFINED__ +// +//#if !defined (__midl) +//#if !defined (_SYS_GUID_OPERATORS_) +//#define _SYS_GUID_OPERATORS_ +//#include +// +//// Faster (but makes code fatter) inline version...use sparingly +//#ifdef __cplusplus +//__inline int InlineIsEqualGUID(REFGUID rguid1, REFGUID rguid2) +//{ +// return ( +// ((unsigned long *) &rguid1)[0] == ((unsigned long *) &rguid2)[0] && +// ((unsigned long *) &rguid1)[1] == ((unsigned long *) &rguid2)[1] && +// ((unsigned long *) &rguid1)[2] == ((unsigned long *) &rguid2)[2] && +// ((unsigned long *) &rguid1)[3] == ((unsigned long *) &rguid2)[3]); +//} +// +//__inline int IsEqualGUID(REFGUID rguid1, REFGUID rguid2) +//{ +// return !memcmp(&rguid1, &rguid2, sizeof(GUID)); +//} +// +//#else // ! __cplusplus +// +//#define InlineIsEqualGUID(rguid1, rguid2) \ +// (((unsigned long *) rguid1)[0] == ((unsigned long *) rguid2)[0] && \ +// ((unsigned long *) rguid1)[1] == ((unsigned long *) rguid2)[1] && \ +// ((unsigned long *) rguid1)[2] == ((unsigned long *) rguid2)[2] && \ +// ((unsigned long *) rguid1)[3] == ((unsigned long *) rguid2)[3]) +// +//#define IsEqualGUID(rguid1, rguid2) (!memcmp(rguid1, rguid2, sizeof(GUID))) +// +//#endif // __cplusplus +// +//#ifdef __INLINE_ISEQUAL_GUID +//#undef IsEqualGUID +//#define IsEqualGUID(rguid1, rguid2) InlineIsEqualGUID(rguid1, rguid2) +//#endif +// +//// Same type, different name +// +//#define IsEqualIID(riid1, riid2) IsEqualGUID(riid1, riid2) +//#define IsEqualCLSID(rclsid1, rclsid2) IsEqualGUID(rclsid1, rclsid2) +// +// +//#if !defined _SYS_GUID_OPERATOR_EQ_ && !defined _NO_SYS_GUID_OPERATOR_EQ_ +//#define _SYS_GUID_OPERATOR_EQ_ +//// A couple of C++ helpers +// +//#ifdef __cplusplus +//__inline bool operator==(REFGUID guidOne, REFGUID guidOther) +//{ +// return !!IsEqualGUID(guidOne,guidOther); +//} +// +//__inline bool operator!=(REFGUID guidOne, REFGUID guidOther) +//{ +// return !(guidOne == guidOther); +//} +//#endif +//#endif // _SYS_GUID_OPERATOR_EQ_ +//#endif // _SYS_GUID_OPERATORS_ +//#endif // __midl +//#endif // _GUIDDEF_H_ diff --git a/midl/types_helper.go b/midl/types_helper.go index 980a398f..74f97a16 100644 --- a/midl/types_helper.go +++ b/midl/types_helper.go @@ -716,7 +716,7 @@ func (t *ExprTree) Expression(opts ...any) string { switch t.Op { case TERNARY: if t.Cond.Op == UMUL || t.Cond.Op == IDENT { - ret = fmt.Sprintf(t.Lval.Expression(opts...)) + ret = fmt.Sprint(t.Lval.Expression(opts...)) } else { ret = fmt.Sprintf("(%s?%s:%s)", t.Cond.Expression(opts...), t.Lval.Expression(opts...), t.Rval.Expression(opts...)) } diff --git a/ssp/credential/credential.go b/ssp/credential/credential.go index 51849361..7a6f118e 100644 --- a/ssp/credential/credential.go +++ b/ssp/credential/credential.go @@ -62,11 +62,31 @@ func DomainName(un string) string { return dn } +func IsAnonymous(cred Credential) bool { + if cred == nil { + return true + } + p, ok := (any)(cred).(*passwordCred) + if !ok { + return false + } + return p.allowEmpty && p.userName == "" +} + // Anonymous function returns the anonymous password credentials. func Anonymous() Password { return &passwordCred{allowEmpty: true} } +func New(un string, opts ...Option) Credential { + dn, un, wkst := parseDomainUserWorkstation(un, opts...) + return &userCred{ + userName: un, + domainName: dn, + workstation: wkst, + } +} + func NewFromString(s string, opts ...Option) Password { ss := strings.Split(s, "%") if len(ss) > 1 { diff --git a/ssp/credential/database.go b/ssp/credential/database.go new file mode 100644 index 00000000..6f7bf9d9 --- /dev/null +++ b/ssp/credential/database.go @@ -0,0 +1,52 @@ +package credential + +import ( + "context" + "strings" +) + +// Database is an interface for storing and retrieving credentials. +type Database interface { + // Add adds a credential to the database. + Add(Credential) + // Lookup looks up a credential in the database. + Lookup(context.Context, Credential) (Credential, bool) +} + +type localDatabase struct { + credentials map[string]Credential +} + +// NewLocalDatabase creates a new in-memory database for credentials. +func NewLocalDatabase() Database { + return &localDatabase{ + credentials: make(map[string]Credential), + } +} + +// Add adds a credential to the local database. +func (db *localDatabase) Add(cred Credential) { + if cred != nil { + db.credentials[strings.ToLower(cred.UserName())] = cred + if cred.DomainName() != "" { + db.credentials[strings.ToLower(cred.DomainName()+"\\"+cred.UserName())] = cred + } + } +} + +// Lookup looks up a credential in the local database. +func (db *localDatabase) Lookup(ctx context.Context, cred Credential) (Credential, bool) { + if cred == nil { + return nil, false + } + if cred.DomainName() != "" { + name := strings.ToLower(cred.DomainName() + "\\" + cred.UserName()) + if cred, ok := db.credentials[name]; ok { + return cred, true + } + } + if cred, ok := db.credentials[strings.ToLower(cred.UserName())]; ok { + return cred, true + } + return nil, false +} diff --git a/ssp/gssapi/context.go b/ssp/gssapi/context.go index 3c2e594b..7aca16b5 100644 --- a/ssp/gssapi/context.go +++ b/ssp/gssapi/context.go @@ -3,6 +3,8 @@ package gssapi import ( "context" "fmt" + + "github.com/oiweiwei/go-msrpc/ssp/name" ) type ChannelBindings interface { @@ -46,6 +48,13 @@ type SecurityContext struct { MechanismStore *MechanismStore // Local context credentials storage. CredentialStore *CredentialStore + // The credential database for the server to + // validate incoming client credentials. + CredentialDatabase CredentialDatabase + // The list of acceptable server names. + ServerNames []*name.Name + // DefaultOptions for the context. + DefaultOptions []Option } // The security context key. @@ -100,8 +109,9 @@ func WithMechanismFactory(value MechanismFactory, defaultConfig ...MechanismConf func NewSecurityContext(ctx context.Context, opts ...ContextOption) context.Context { var ( - creds *CredentialStore - mechs *MechanismStore + creds *CredentialStore + mechs *MechanismStore + defaultOpts []Option ) // fill-in the credentials and mechanisms local to the current @@ -118,10 +128,16 @@ func NewSecurityContext(ctx context.Context, opts ...ContextOption) context.Cont mechs = new(MechanismStore) } mechs.AddMechanism(o) + case Option: + defaultOpts = append(defaultOpts, o) } } - return context.WithValue(ctx, ctxKey{}, &SecurityContext{MechanismStore: mechs, CredentialStore: creds}) + return context.WithValue(ctx, ctxKey{}, &SecurityContext{ + MechanismStore: mechs, + CredentialStore: creds, + DefaultOptions: defaultOpts, + }) } // ResetSecurityContext to it's initial state. @@ -130,7 +146,11 @@ func ResetSecurityContext(ctx context.Context) context.Context { if cc == nil { return ctx } - return context.WithValue(ctx, ctxKey{}, &SecurityContext{MechanismStore: cc.MechanismStore, CredentialStore: cc.CredentialStore}) + return context.WithValue(ctx, ctxKey{}, &SecurityContext{ + MechanismStore: cc.MechanismStore, + CredentialStore: cc.CredentialStore, + DefaultOptions: cc.DefaultOptions, + }) } // FromContext retrieves the Security Context. @@ -143,6 +163,7 @@ func FromContext(ctx context.Context) SecurityContext { c.MechanismConfigs = nil // guard from changing the state. c.Attributes = nil // guard from changing the state. c.MechanismStore, c.CredentialStore = nil, nil // guard from changing the state. + c.DefaultOptions = nil return c } return SecurityContext{} @@ -201,7 +222,7 @@ func InitSecurityContext(ctx context.Context, tok *Token, opts ...Option) (*Toke // initalize context with parameters. - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) cc.Compatibility = cfg.Compatibility cc.QoP = cfg.QoP @@ -257,6 +278,81 @@ func InitSecurityContext(ctx context.Context, tok *Token, opts ...Option) (*Toke return &Token{}, nil } +// Accept inbound security context. +func AcceptSecurityContext(ctx context.Context, tok *Token, opts ...Option) (*Token, error) { + + cc := fromContext(ctx) + if cc == nil { + return nil, ErrNoContext + } + + var err error + + if cc.Status == NoContext { + + // initalize context with parameters. + + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) + + cc.Compatibility = cfg.Compatibility + cc.QoP = cfg.QoP + cc.Capabilities = cfg.Capabilities + cc.ContextTTL = cfg.ContextTTL + cc.TargetName = cfg.TargetName + cc.MechanismConfigs = cfg.MechanismConfigs + cc.CredentialDatabase = cfg.CredentialDatabase + cc.ServerNames = cfg.ServerNames + + f := GetMechanism(ctx, cfg.MechanismType) + if f == nil { + return nil, ContextError(ctx, BadMech, ErrBadMech) + } + + // get stored credentials. + cc.Credential = GetCredential(ctx, cfg.TargetName, f.Type(), InitiateOnly) + + // optionally validate credential. + if cc.Credential != nil { + if validator, ok := (any)(cc.Credential.Value()).(interface{ Validate() error }); ok { + if err = validator.Validate(); err != nil { + return nil, ContextError(ctx, Failure, err) + } + } + } + + // initiator is a server. + cc.IsServer = true + + if cc.Mechanism, err = f.New(ctx); err != nil { + return nil, ContextError(ctx, Failure, err) + } + } + + // handle error. + if tok, err = cc.Mechanism.Accept(ctx, tok); err != nil { + // set proper error if not set. + if cc.Status == NoContext || cc.Error == nil { + return tok, withContextStatus(ctx, Failure, err) + } + return tok, err + } + + if caps := cc.Mechanism.Capabilities(ctx); caps != 0 { + cc.Capabilities = caps + } + + // set proper status. (if nil is returned.) + if cc.Status == NoContext { + cc.Status = Complete + } + + if tok != nil { + return tok, nil + } + + return &Token{}, nil +} + // The maximum message size for the given limit. func WrapSizeLimit(ctx context.Context, sz int, opts ...Option) int { @@ -269,7 +365,7 @@ func WrapSizeLimit(ctx context.Context, sz int, opts ...Option) int { return sz } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) return cc.Mechanism.WrapSizeLimit(ctx, sz, cfg.Capabilities.IsSet(Confidentiality)) } @@ -285,13 +381,15 @@ func Wrap(ctx context.Context, tok *MessageToken, opts ...Option) (*MessageToken return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tok.QoP = cfg.QoP } - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } return cc.Mechanism.Wrap(ctx, tok) } @@ -309,14 +407,16 @@ func WrapEx(ctx context.Context, tokEx *MessageTokenEx, opts ...Option) (*Messag return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tokEx.QoP = cfg.QoP } for _, tok := range tokEx.Payloads { - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } } mechEx, ok := (interface{})(cc.Mechanism).(MechanismEx) @@ -340,13 +440,15 @@ func Unwrap(ctx context.Context, tok *MessageToken, opts ...Option) (*MessageTok return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tok.QoP = cfg.QoP } - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } return cc.Mechanism.Unwrap(ctx, tok) } @@ -364,14 +466,16 @@ func UnwrapEx(ctx context.Context, tokEx *MessageTokenEx, opts ...Option) (*Mess return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tokEx.QoP = cfg.QoP } for _, tok := range tokEx.Payloads { - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } } mechEx, ok := (interface{})(cc.Mechanism).(MechanismEx) @@ -395,13 +499,15 @@ func MakeSignature(ctx context.Context, tok *MessageToken, opts ...Option) (*Mes return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tok.QoP = cfg.QoP } - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } return cc.Mechanism.MakeSignature(ctx, tok) } @@ -419,14 +525,16 @@ func MakeSignatureEx(ctx context.Context, tokEx *MessageTokenEx, opts ...Option) return nil, ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tokEx.QoP = cfg.QoP } for _, tok := range tokEx.Payloads { - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } } mechEx, ok := (interface{})(cc.Mechanism).(MechanismEx) @@ -450,13 +558,15 @@ func VerifySignature(ctx context.Context, tok *MessageToken, opts ...Option) err return ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tok.QoP = cfg.QoP } - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } return cc.Mechanism.VerifySignature(ctx, tok) } @@ -472,14 +582,16 @@ func VerifySignatureEx(ctx context.Context, tokEx *MessageTokenEx, opts ...Optio return ErrUnavailable } - cfg := MakeOptions(opts...) + cfg := MakeOptions(append(cc.DefaultOptions, opts...)...) if cfg.QoP >= 0 { tokEx.QoP = cfg.QoP } for _, tok := range tokEx.Payloads { - tok.Capabilities |= cfg.Capabilities + if tok.Capabilities == 0 { + tok.Capabilities |= cfg.Capabilities + } } mechEx, ok := (interface{})(cc.Mechanism).(MechanismEx) diff --git a/ssp/gssapi/credential.go b/ssp/gssapi/credential.go index 066ac319..dc87ee8a 100644 --- a/ssp/gssapi/credential.go +++ b/ssp/gssapi/credential.go @@ -148,3 +148,59 @@ func GetCredentialValue(ctx context.Context, name string, mechanismType OID, usa } return nil } + +type CredentialDatabase interface { + // Value returns the actual database. + Value() any + // AllowGuest returns true if the database allows guest access. + AllowGuest() bool + // AllowAnonymous returns true if the database allows anonymous access. + AllowAnonymous() bool +} + +type credentialDatabase struct { + value any + allowGuest bool + allowAnonymous bool +} + +// Value returns the actual database value. +func (c *credentialDatabase) Value() any { + return c.value +} + +// AllowGuest returns true if the database allows guest access. +func (c *credentialDatabase) AllowGuest() bool { + return c.allowGuest +} + +// AllowAnonymous returns true if the database allows anonymous access. +func (c *credentialDatabase) AllowAnonymous() bool { + return c.allowAnonymous +} + +// CredentialDatabaseOption is a function that modifies the credential database options. +type CredentialDatabaseOption func(*credentialDatabase) + +// AllowGuest sets the guest access option for the credential database. +func AllowGuest(allow bool) CredentialDatabaseOption { + return func(db *credentialDatabase) { + db.allowGuest = allow + } +} + +// AllowAnonymous sets the anonymous access option for the credential database. +func AllowAnonymous(allow bool) CredentialDatabaseOption { + return func(db *credentialDatabase) { + db.allowAnonymous = allow + } +} + +// NewCredentialDatabase creates a new credential database with the given value and options. +func NewCredentialDatabase(value any, opts ...CredentialDatabaseOption) CredentialDatabase { + db := credentialDatabase{value: value} + for _, opt := range opts { + opt(&db) + } + return &db +} diff --git a/ssp/gssapi/gssapi.go b/ssp/gssapi/gssapi.go index ba79757f..0a27b7a1 100644 --- a/ssp/gssapi/gssapi.go +++ b/ssp/gssapi/gssapi.go @@ -3,6 +3,8 @@ package gssapi import ( "context" "encoding/asn1" + + "github.com/oiweiwei/go-msrpc/ssp/name" ) // Cap is a capability used both for request and response. @@ -135,6 +137,10 @@ type Config struct { // The flag that indicates whether it's a server // handle. IsServer bool + // The credential database. + CredentialDatabase CredentialDatabase + + ServerNames []*name.Name } // MakeOption function is used to build the option structure. @@ -214,6 +220,21 @@ func WithMechanismType(oid OID) Option { } } +// WithCredentialDatabase returns the option for the +// credential database. +func WithCredentialDatabase(db CredentialDatabase) Option { + return func(o *Config) { + o.CredentialDatabase = db + } +} + +// WithServerNames returns the option for the server names. +func WithServerNames(names ...*name.Name) Option { + return func(o *Config) { + o.ServerNames = append(o.ServerNames, names...) + } +} + type MessageToken struct { // The quality-of-protection. QoP int @@ -227,6 +248,11 @@ type MessageToken struct { Signature []byte } +func (m *MessageToken) Raw() []byte { + // The raw token is the concatenation of the payload and signature. + return append(m.Signature, m.Payload...) +} + // Per-Message Security Service Availability. type SecurityService interface { // MakeSignature function accepts the payload and returns the diff --git a/ssp/gssapi/mechanism.go b/ssp/gssapi/mechanism.go index 9f978248..0422b539 100644 --- a/ssp/gssapi/mechanism.go +++ b/ssp/gssapi/mechanism.go @@ -59,6 +59,8 @@ type Mechanism interface { Type() OID // The security context init call. Init(ctx context.Context, token *Token) (*Token, error) + // The capabilities of the token are defined by the mechanism. + Capabilities(ctx context.Context) Cap // The security context accept call. Accept(ctx context.Context, token *Token) (*Token, error) // The maximum message size for the given limit. (and flag determining if diff --git a/ssp/krb5/authentifier.go b/ssp/krb5/authentifier.go index 16ab342f..90e50312 100644 --- a/ssp/krb5/authentifier.go +++ b/ssp/krb5/authentifier.go @@ -261,11 +261,99 @@ func (a *Authentifier) APRequest(ctx context.Context) ([]byte, error) { return b, nil } +func (a *Authentifier) VerifyAPRequest(ctx context.Context, b []byte) ([]byte, error) { + + if len(b) == 0 { + return nil, fmt.Errorf("krb5: verify apreq: empty token") + } + + var err error + + if a.service, err = a.makeService(ctx); err != nil { + return nil, fmt.Errorf("krb5: verify apreq: make service: %w", err) + } + + if b[0] == 0x60 { + tok := &spnego.KRB5Token{} + if err := tok.Unmarshal(b); err != nil { + return nil, fmt.Errorf("krb5: verify apreq: unmarshal spnego: %w", err) + } + a.APReq = &tok.APReq + } else { + // dce style token. + a.APReq = &messages.APReq{} + if err := a.APReq.Unmarshal(b); err != nil { + return nil, fmt.Errorf("krb5: verify apreq: unmarshal: %w", err) + } + } + + if ok, _, err := service.VerifyAPREQ(a.APReq, a.service); err != nil { + return nil, fmt.Errorf("krb5: verify apreq: verify apreq: %w", err) + } else if !ok { + return nil, fmt.Errorf("krb5: verify apreq: invalid apreq") + } + + a.SessionKey = a.APReq.Ticket.DecryptedEncPart.Key + + a.Config.Accept(a.APReq.Authenticator.Cksum.Checksum, a.APReq.APOptions) + + if err := a.makeSecurityService(ctx); err != nil { + return nil, fmt.Errorf("krb5: verify apreq: make security service: %w", err) + } + + if !a.Config.DCEStyle && !a.Config.FlagIsSet(gssapi.MutualAuthn) { + // no mutual authn, no aprep. + return nil, nil + } + + encPart := messages.NewEncAPRepPart(a.APReq.Authenticator.SeqNumber) + encPart.Subkey = a.SessionKey + + ap, err := messages.NewAPRep(a.SessionKey, encPart) + if err != nil { + return nil, fmt.Errorf("krb5: verify apreq: aprep: new aprep: %w", err) + } + + b, err = ap.Marshal() + if err != nil { + return nil, fmt.Errorf("krb5: verify apreq: aprep: marshal: %w", err) + } + + if err := a.makeSecurityService(ctx); err != nil { + return nil, fmt.Errorf("krb5: accept: make security service: %w", err) + } + + a.APRep = &ap + + return b, nil +} + +func (a *Authentifier) VerifyAPReply(ctx context.Context, b []byte) error { + + if !a.Config.DCEStyle { + tok := &spnego.KRB5Token{} + if err := tok.Unmarshal(b); err != nil { + return fmt.Errorf("krb5: verify: aprep: unmarshal: dcestyle: %w", err) + } + a.APRep = &tok.APRep + } else { + if err := a.APRep.Unmarshal(b); err != nil { + return fmt.Errorf("krb5: verify: aprep: unmarshal: %w", err) + } + } + + if err := a.APRep.DecryptEncPart(a.SessionKey); err != nil { + return fmt.Errorf("krb5: verify: aprep: decrypt enc part: %w", err) + } + + return nil +} + func (a *Authentifier) APReply(ctx context.Context, b []byte) ([]byte, error) { if len(b) == 0 { if err := a.makeSecurityService(ctx); err != nil { - return nil, fmt.Errorf("krb5: init: aprep: make security service: %w", err) + return nil, fmt.Errorf("krb5: init: aprep: b0: make security service: %w", err) } return nil, nil } @@ -305,6 +393,14 @@ func (a *Authentifier) APReply(ctx context.Context, b []byte) ([]byte, error) { return b, nil } +func (a *Authentifier) ParseInboundSignature(ctx context.Context, payload []byte) ([]byte, []byte, error) { + sgn, payload, err := a.state.InboundCipher.ParseSignature(ctx, payload) + if err != nil { + return nil, nil, fmt.Errorf("krb5: parse inbound signature: %w", err) + } + return sgn, payload, nil +} + func (a *Authentifier) MakeOutboundSignature(ctx context.Context, forSign [][]byte) ([]byte, error) { sgn, err := a.state.OutboundCipher.MakeSignature(ctx, a.state.OutboundSequenceNumber, forSign) if err != nil { @@ -323,8 +419,17 @@ func (a *Authentifier) MakeInboundSignature(ctx context.Context, forSign [][]byt return sgn, nil } -func (a *Authentifier) WrapOutboundPayload(ctx context.Context, forSign, forSeal [][]byte) ([]byte, error) { - sgn, err := a.state.OutboundCipher.Wrap(ctx, a.state.OutboundSequenceNumber, forSign, forSeal) +func (a *Authentifier) WrapOutboundPayload(ctx context.Context, payload []byte, conf bool) ([]byte, error) { + sgn, err := a.state.OutboundCipher.Wrap(ctx, a.state.OutboundSequenceNumber, payload, conf) + if err != nil { + return nil, fmt.Errorf("krb5: wrap outbound payload: %w", err) + } + a.state.OutboundSequenceNumber++ + return sgn, nil +} + +func (a *Authentifier) WrapExOutboundPayload(ctx context.Context, forSign, forSeal [][]byte) ([]byte, error) { + sgn, err := a.state.OutboundCipher.WrapEx(ctx, a.state.OutboundSequenceNumber, forSign, forSeal) if err != nil { return nil, fmt.Errorf("krb5: wrap outbound payload: %w", err) } @@ -333,8 +438,8 @@ func (a *Authentifier) WrapOutboundPayload(ctx context.Context, forSign, forSeal return sgn, nil } -func (a *Authentifier) UnwrapInboundPayload(ctx context.Context, forSign, forSeal [][]byte, sgn []byte) (bool, error) { - ok, err := a.state.InboundCipher.Unwrap(ctx, a.state.InboundSequenceNumber, forSign, forSeal, sgn) +func (a *Authentifier) UnwrapExInboundPayload(ctx context.Context, forSign, forSeal [][]byte, sgn []byte) (bool, error) { + ok, err := a.state.InboundCipher.UnwrapEx(ctx, a.state.InboundSequenceNumber, forSign, forSeal, sgn) if err != nil { return ok, fmt.Errorf("krb5: unwrap inbound payload: %w", err) } @@ -344,6 +449,17 @@ func (a *Authentifier) UnwrapInboundPayload(ctx context.Context, forSign, forSea return ok, nil } +func (a *Authentifier) UnwrapInboundPayload(ctx context.Context, payload []byte, sgn []byte) ([]byte, bool, error) { + sgn, ok, err := a.state.InboundCipher.Unwrap(ctx, a.state.InboundSequenceNumber, payload, sgn) + if err != nil { + return sgn, ok, fmt.Errorf("krb5: unwrap inbound payload: %w", err) + } + if ok { + a.state.InboundSequenceNumber++ + } + return sgn, ok, nil +} + func (a *Authentifier) OutboundSignatureSize(ctx context.Context, conf bool) int { // use outbound cipher as we wrap the outbound data. return a.state.OutboundCipher.Size(ctx, conf) diff --git a/ssp/krb5/config.go b/ssp/krb5/config.go index 15e0c0bf..a43f07d5 100644 --- a/ssp/krb5/config.go +++ b/ssp/krb5/config.go @@ -1,15 +1,19 @@ package krb5 import ( + "encoding/binary" "os" "strings" "time" v8_config "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gofork/encoding/asn1" + "github.com/oiweiwei/gokrb5.fork/v9/client" "github.com/oiweiwei/gokrb5.fork/v9/config" "github.com/oiweiwei/gokrb5.fork/v9/iana/etypeID" + "github.com/oiweiwei/gokrb5.fork/v9/iana/flags" "github.com/oiweiwei/gokrb5.fork/v9/service" "github.com/oiweiwei/gokrb5.fork/v9/types" @@ -83,6 +87,52 @@ type Config struct { AnyServiceClassSPN bool } +func (c *Config) Accept(ff []byte, apOptions asn1.BitString) { + if len(ff) > 24 { + c.Flags = append(c.Flags, int(gssapi.Delegation)) + } + if len(ff) >= 24 { + f := binary.LittleEndian.Uint32(ff[20:24]) + + if f&uint32(gssapi.Anonymity) != 0 { + c.Flags = append(c.Flags, int(gssapi.Anonymity)) + } + + if f&uint32(gssapi.Confidentiality) != 0 { + c.Flags = append(c.Flags, int(gssapi.Confidentiality)) + } + + if f&uint32(gssapi.Integrity) != 0 { + c.Flags = append(c.Flags, int(gssapi.Integrity)) + } + + if f&uint32(gssapi.Identify) != 0 { + c.Flags = append(c.Flags, int(gssapi.Identify)) + } + + if f&uint32(gssapi.Datagram) != 0 { + c.Flags = append(c.Flags, int(gssapi.Datagram)) + } + + if f&uint32(gssapi.ReplayDetection) != 0 { + c.Flags = append(c.Flags, int(gssapi.ReplayDetection)) + } + + if f&uint32(gssapi.Sequencing) != 0 { + c.Flags = append(c.Flags, int(gssapi.Sequencing)) + } + + if f&uint32(gssapi.DCEStyle) != 0 { + c.Flags = append(c.Flags, int(gssapi.DCEStyle)) + c.DCEStyle = true + } + } + + if apOptions.At(flags.APOptionMutualRequired) != 0 { + c.APOptions = append(c.APOptions, flags.APOptionMutualRequired) + } +} + func (c *Config) GetKRB5Config() *config.Config { switch conf := (any)(c.KRB5Config).(type) { diff --git a/ssp/krb5/crypto/aes_cts_hmac_sha1.go b/ssp/krb5/crypto/aes_cts_hmac_sha1.go index a38bb150..f26e5dce 100644 --- a/ssp/krb5/crypto/aes_cts_hmac_sha1.go +++ b/ssp/krb5/crypto/aes_cts_hmac_sha1.go @@ -79,19 +79,28 @@ func (c *AESCTSHMACSHA1) DeriveEncryptionKey() ([]byte, error) { return c.setting.Type.DeriveKey(c.setting.Key.KeyValue, common.GetUsageKe(uint32(ecU))) } -func (c *AESCTSHMACSHA1) Wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { - b, err := c.wrap(ctx, seqNum, forSign, forSeal) +func (c *AESCTSHMACSHA1) Wrap(ctx context.Context, seqNum uint64, payload []byte, conf bool) ([]byte, error) { + var forSeal [][]byte + if conf { + forSeal = [][]byte{payload} + } + + b, err := c.wrap(ctx, seqNum, [][]byte{payload}, forSeal, false) if err != nil { return nil, fmt.Errorf("aes-cts-hmac-sha1: wrap: %w", err) } return b, nil } -func isSinglePayload(forSign, forSeal [][]byte) bool { - return len(forSign) == 1 && len(forSeal) == 1 && bytes.Equal(forSign[0], forSeal[0]) +func (c *AESCTSHMACSHA1) WrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { + b, err := c.wrap(ctx, seqNum, forSign, forSeal, true) + if err != nil { + return nil, fmt.Errorf("aes-cts-hmac-sha1: wrap_ex: %w", err) + } + return b, nil } -func (c *AESCTSHMACSHA1) wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { +func (c *AESCTSHMACSHA1) wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, isEx bool) ([]byte, error) { eB, hdr := bytes.NewBuffer(nil), c.WrapHeader(ctx, seqNum) @@ -103,7 +112,7 @@ func (c *AESCTSHMACSHA1) wrap(ctx context.Context, seqNum uint64, forSign, forSe ec := EC - if isSinglePayload(forSign, forSeal) /* it's a Wrap call, compute EC according to RFC 4121, section 4.3.2. */ { + if !isEx /* it's a Wrap call, compute EC according to RFC 4121, section 4.3.2. */ { sz, block := 0, c.setting.Type.GetMessageBlockByteSize() for _, b := range forSeal { sz += len(b) @@ -115,7 +124,7 @@ func (c *AESCTSHMACSHA1) wrap(ctx context.Context, seqNum uint64, forSign, forSe // gen ec. ecB := bytes.Repeat([]byte{0xFF}, ec) - // set ec value (16). (pad = 1, block_size = 16). + // set ec value. binary.BigEndian.PutUint16(hdr[4:6], uint16(ec)) iH, err := c.IntegrityHash() @@ -156,15 +165,50 @@ func (c *AESCTSHMACSHA1) wrap(ctx context.Context, seqNum uint64, forSign, forSe return append(hdr, sgn...), nil } -func (c *AESCTSHMACSHA1) Unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { - ok, err := c.unwrap(ctx, seqNum, forSign, forSeal, sgn) +func (c *AESCTSHMACSHA1) Unwrap(ctx context.Context, seqNum uint64, payload []byte, sgn []byte) ([]byte, bool, error) { + + var err error + + if len(sgn) == 0 { + if sgn, payload, err = c.ParseSignature(ctx, payload); err != nil { + return nil, false, fmt.Errorf("aes-cts-hmac-sha1: unwrap: parse token: %w", err) + } + } + + ok, err := c.unwrapEx(ctx, seqNum, [][]byte{payload}, [][]byte{payload}, sgn) if err != nil { - return ok, fmt.Errorf("aes-cts-hmac-sha1: unwrap: %w", err) + return nil, false, fmt.Errorf("aes-cts-hmac-sha1: unwrap: %w", err) + } + return sgn, ok, nil +} + +func (c *AESCTSHMACSHA1) UnwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { + ok, err := c.unwrapEx(ctx, seqNum, forSign, forSeal, sgn) + if err != nil { + return ok, fmt.Errorf("aes-cts-hmac-sha1: unwrap_ex: %w", err) } return ok, nil } -func (c *AESCTSHMACSHA1) unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { +// ParseSignature parses the header of the signature and returns the header and the remaining of the signature. +func (c *AESCTSHMACSHA1) ParseSignature(ctx context.Context, payload []byte) ([]byte, []byte, error) { + + if len(payload) < 16 { + return nil, nil, fmt.Errorf("invalid payload size: %d < 16", len(payload)) + } + + rrc, ec := int(binary.BigEndian.Uint16(payload[6:])), int(binary.BigEndian.Uint16(payload[4:])) + + sgn := ec + rrc + 16 /* hdr */ + c.setting.Type.GetConfounderByteSize() + if len(payload) < sgn { + return nil, nil, fmt.Errorf("invalid payload size: %d < %d", len(payload), sgn) + } + + return payload[:sgn], payload[sgn:], nil + +} + +func (c *AESCTSHMACSHA1) unwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { // buffer for decryption. eB, hdr := bytes.NewBuffer(nil), sgn[:16] diff --git a/ssp/krb5/crypto/asn1.go b/ssp/krb5/crypto/asn1.go index 4c7093ac..e688b7bc 100644 --- a/ssp/krb5/crypto/asn1.go +++ b/ssp/krb5/crypto/asn1.go @@ -68,17 +68,17 @@ func EncodeASN1ValueWithExtraSize(b []byte, oid asn1.ObjectIdentifier, extra ... // ParseASN1Value function parses the given byte slice as an ASN.1 value. The // function returns the remaining data, the object identifier and an error if // any. -func ParseASN1Value(b []byte, extra [][]byte) ([]byte, asn1.ObjectIdentifier, error) { +func ParseASN1Value(b []byte, extra [][]byte) ([]byte, int, asn1.ObjectIdentifier, error) { var oid asn1.ObjectIdentifier if !IsASN1Value(b) { - return b, oid, nil + return b, 0, oid, nil } - b, sz, ok := DecodeASN1Header(b) + b, oft, sz, ok := DecodeASN1Header(b) if !ok { - return nil, oid, fmt.Errorf("invalid asn1 size header") + return nil, 0, oid, fmt.Errorf("invalid asn1 size header") } extraL := uint32(0) @@ -95,10 +95,10 @@ func ParseASN1Value(b []byte, extra [][]byte) ([]byte, asn1.ObjectIdentifier, er // trim oid. b, err := asn1.Unmarshal(b, &oid) if err != nil { - return nil, oid, fmt.Errorf("unmarshal oid: %w", err) + return nil, 0, oid, fmt.Errorf("unmarshal oid: %w", err) } - return b, oid, nil + return b, oft + (int(sz) - len(b)), oid, nil } // EncodeASN1Header function encodes the given size into an ASN.1 header and @@ -148,35 +148,37 @@ func EncodeASN1HeaderTo(b []byte, sz uint32) (int, error) { // DecodeASN1Header function decodes the given byte slice as an ASN.1 header and // returns the remaining data, the size and a boolean value indicating if the // header is valid. -func DecodeASN1Header(b []byte) ([]byte, uint32, bool) { +func DecodeASN1Header(b []byte) ([]byte, int, uint32, bool) { + + oft := 0 if len(b) == 0 || b[0] != 0x60 { // not an asn1 value. - return b, uint32(len(b)), true + return b, 0, uint32(len(b)), true } - if b = b[1:]; len(b) < 1 { + if b, oft = b[1:], oft+1; len(b) < 1 { // trim asn1 app tag. - return nil, 0, false + return nil, oft, 0, false } var sz uint32 - if b, sz = b[1:], uint32(b[0]); sz <= 0x7F { + if b, sz, oft = b[1:], uint32(b[0]), oft+1; sz <= 0x7F { // trim primitive size. - return b, sz, true + return b, oft, sz, true } if sz&0x80 != 0x80 || (sz&0x0F) > uint32(len(b)) { // check number of bytes. - return nil, 0, false + return nil, 0, 0, false } sz &= 0x0F var cnt int - switch cnt = int(sz); cnt { + switch cnt, oft = int(sz), oft+int(sz); cnt { case 1: b, sz = b[cnt:], uint32(b[0]) case 2: @@ -186,8 +188,8 @@ func DecodeASN1Header(b []byte) ([]byte, uint32, bool) { case 4: b, sz = b[cnt:], uint32(b[0])<<24|uint32(b[1])<<16|uint32(b[2])<<8|uint32(b[3]) default: - return nil, 0, false + return nil, oft, 0, false } - return b, sz, true + return b, oft, sz, true } diff --git a/ssp/krb5/crypto/crypto.go b/ssp/krb5/crypto/crypto.go index 70e9a88b..82512141 100644 --- a/ssp/krb5/crypto/crypto.go +++ b/ssp/krb5/crypto/crypto.go @@ -10,8 +10,11 @@ import ( ) type Cipher interface { - Wrap(context.Context, uint64, [][]byte, [][]byte) ([]byte, error) - Unwrap(context.Context, uint64, [][]byte, [][]byte, []byte) (bool, error) + Wrap(context.Context, uint64, []byte, bool) ([]byte, error) + WrapEx(context.Context, uint64, [][]byte, [][]byte) ([]byte, error) + UnwrapEx(context.Context, uint64, [][]byte, [][]byte, []byte) (bool, error) + Unwrap(context.Context, uint64, []byte, []byte) ([]byte, bool, error) + ParseSignature(context.Context, []byte) ([]byte, []byte, error) MakeSignature(context.Context, uint64, [][]byte) ([]byte, error) Size(context.Context, bool) int } diff --git a/ssp/krb5/crypto/des.go b/ssp/krb5/crypto/des.go index f7a5a00d..f034a5dd 100644 --- a/ssp/krb5/crypto/des.go +++ b/ssp/krb5/crypto/des.go @@ -17,7 +17,39 @@ func NewDESCipher(ctx context.Context, setting CipherSetting) (Cipher, error) { return &DES{setting: setting}, nil } -func (c *DES) Wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { +// ParseHeader extracts the header and payload from the token. +func (c *DES) ParseSignature(ctx context.Context, payload []byte) ([]byte, []byte, error) { + + _, oft, _, err := ParseASN1Value(payload, [][]byte{payload}) + if err != nil { + return nil, nil, fmt.Errorf("des: parse header: %w", err) + } + + if len(payload) < oft+4 { + return nil, nil, fmt.Errorf("des: parse header: invalid payload size: %d < %d", len(payload), oft) + } + + switch payload[oft+4] { + case 0x02 /* wrap token */ : + oft += 32 + case 0x01 /* mic token */ : + oft += 24 + default: + return nil, nil, fmt.Errorf("des: parse header: invalid token type: %d", payload[oft+4]) + } + + return payload[:oft], payload[oft:], nil +} + +func (c *DES) Wrap(ctx context.Context, seqNum uint64, payload []byte, conf bool) ([]byte, error) { + var forSeal [][]byte + if conf { + forSeal = [][]byte{payload} + } + return c.WrapEx(ctx, seqNum, [][]byte{payload}, forSeal) +} + +func (c *DES) WrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { b, err := c.wrap(ctx, seqNum, forSign, forSeal) if err != nil { return nil, fmt.Errorf("des: wrap: %w", err) @@ -62,15 +94,33 @@ func (c *DES) wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte return EncodeASN1Value(tok.Marshal(), KRB5OID, true /* always use dce-style */, forSign) } -func (c *DES) Unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { - ok, err := c.unwrap(ctx, seqNum, forSign, forSeal, sgn) +func (c *DES) Unwrap(ctx context.Context, seqNum uint64, payload []byte, sgn []byte) ([]byte, bool, error) { + + var err error + + if len(sgn) == 0 { + // if sgn is empty, it's encoded as part of the payload. + if sgn, payload, err = c.ParseSignature(ctx, payload); err != nil { + return nil, false, fmt.Errorf("des: unwrap: parse token: %w", err) + } + } + ok, err := c.unwrapEx(ctx, seqNum, [][]byte{payload}, [][]byte{payload}, sgn) + if err != nil { + return nil, false, fmt.Errorf("des: unwrap: %w", err) + } + + return sgn, ok, err +} + +func (c *DES) UnwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { + ok, err := c.unwrapEx(ctx, seqNum, forSign, forSeal, sgn) if err != nil { return ok, fmt.Errorf("des: unwrap: %w", err) } return ok, nil } -func (c *DES) unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { +func (c *DES) unwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { key := c.setting.Key @@ -78,7 +128,7 @@ func (c *DES) unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]by expTok.SetSequenceNumber(uint32(seqNum), c.setting.IsLocal) // trim asn1 header. - sgn, _, err := ParseASN1Value(sgn, forSign) + sgn, _, _, err := ParseASN1Value(sgn, forSign) if err != nil { return false, gssapi.ErrDefectiveToken } diff --git a/ssp/krb5/crypto/rc4_hmac.go b/ssp/krb5/crypto/rc4_hmac.go index 590f2907..177a976c 100644 --- a/ssp/krb5/crypto/rc4_hmac.go +++ b/ssp/krb5/crypto/rc4_hmac.go @@ -17,7 +17,38 @@ func NewRC4Cipher(ctx context.Context, setting CipherSetting) (Cipher, error) { return &RC4HMAC{setting: setting}, nil } -func (c *RC4HMAC) Wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { +func (c *RC4HMAC) ParseSignature(ctx context.Context, payload []byte) ([]byte, []byte, error) { + + _, oft, _, err := ParseASN1Value(payload, [][]byte{payload}) + if err != nil { + return nil, nil, fmt.Errorf("des: parse header: %w", err) + } + + if len(payload) < oft+4 { + return nil, nil, fmt.Errorf("des: parse header: invalid payload size: %d < %d", len(payload), oft) + } + + switch payload[oft+4] { + case 0x02 /* wrap token */ : + oft += 32 + case 0x01 /* mic token */ : + oft += 24 + default: + return nil, nil, fmt.Errorf("des: parse header: invalid token type: %d", payload[oft+4]) + } + + return payload[:oft], payload[oft:], nil +} + +func (c *RC4HMAC) Wrap(ctx context.Context, seqNum uint64, payload []byte, conf bool) ([]byte, error) { + var forSeal [][]byte + if conf { + forSeal = [][]byte{payload} + } + return c.WrapEx(ctx, seqNum, [][]byte{payload}, forSeal) +} + +func (c *RC4HMAC) WrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte) ([]byte, error) { b, err := c.wrap(ctx, seqNum, forSign, forSeal) if err != nil { return nil, fmt.Errorf("rc4-hmac: wrap: %w", err) @@ -63,15 +94,34 @@ func (c *RC4HMAC) wrap(ctx context.Context, seqNum uint64, forSign, forSeal [][] return EncodeASN1Value(tok.Marshal(), KRB5OID, true /* always use dce-style */, forSign) } -func (c *RC4HMAC) Unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { - ok, err := c.unwrap(ctx, seqNum, forSign, forSeal, sgn) +func (c *RC4HMAC) Unwrap(ctx context.Context, seqNum uint64, payload []byte, sgn []byte) ([]byte, bool, error) { + + var err error + + if len(sgn) == 0 { + // if sgn is empty, it's encoded as part of the payload. + if sgn, payload, err = c.ParseSignature(ctx, payload); err != nil { + return nil, false, fmt.Errorf("rc4-hmac: unwrap: parse token: %w", err) + } + } + + ok, err := c.unwrapEx(ctx, seqNum, [][]byte{payload}, [][]byte{payload}, sgn) + if err != nil { + return nil, false, fmt.Errorf("rc4-hmac: unwrap: %w", err) + } + + return sgn, ok, nil +} + +func (c *RC4HMAC) UnwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { + ok, err := c.unwrapEx(ctx, seqNum, forSign, forSeal, sgn) if err != nil { return ok, fmt.Errorf("rc4-hmac: unwrap: %w", err) } return ok, nil } -func (c *RC4HMAC) unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { +func (c *RC4HMAC) unwrapEx(ctx context.Context, seqNum uint64, forSign, forSeal [][]byte, sgn []byte) (bool, error) { key := c.setting.Key.KeyValue @@ -79,7 +129,7 @@ func (c *RC4HMAC) unwrap(ctx context.Context, seqNum uint64, forSign, forSeal [] expTok.SetSequenceNumber(uint32(seqNum), c.setting.IsLocal) // trim asn1 header. - sgn, _, err := ParseASN1Value(sgn, forSign) + sgn, _, _, err := ParseASN1Value(sgn, forSign) if err != nil { return false, gssapi.ErrDefectiveToken } diff --git a/ssp/krb5/gssapi.go b/ssp/krb5/gssapi.go index a14b9fcc..58213fb3 100644 --- a/ssp/krb5/gssapi.go +++ b/ssp/krb5/gssapi.go @@ -165,9 +165,87 @@ func (m *Mechanism) Init(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, return &gssapi.Token{Payload: b}, gssapi.ContextContinueNeeded(ctx) } +func (m *Mechanism) Capabilities(ctx context.Context) gssapi.Cap { + + caps := gssapi.Cap(0) + + if m.Config.FlagIsSet(gssapi.Anonymity) { + caps |= gssapi.Anonymity + } + if m.Config.FlagIsSet(gssapi.Identify) { + caps |= gssapi.Identify + } + if m.Config.FlagIsSet(gssapi.MutualAuthn) { + caps |= gssapi.MutualAuthn + } + if m.Config.FlagIsSet(gssapi.ReplayDetection) { + caps |= gssapi.ReplayDetection + } + if m.Config.FlagIsSet(gssapi.Sequencing) { + caps |= gssapi.Sequencing + } + if m.Config.FlagIsSet(gssapi.Integrity) { + caps |= gssapi.Integrity + } + if m.Config.FlagIsSet(gssapi.Confidentiality) { + caps |= gssapi.Confidentiality + } + if m.Config.FlagIsSet(gssapi.DCEStyle) || m.Config.DCEStyle { + caps |= gssapi.DCEStyle + } + + for _, o := range m.Config.APOptions { + if o == flags.APOptionMutualRequired { + caps |= gssapi.MutualAuthn + } + } + + return caps + +} + // The security context accept call. func (m *Mechanism) Accept(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, error) { - return nil, gssapi.ContextError(ctx, gssapi.Unavailable, gssapi.ErrUnavailable) + + if m.APRep != nil { + + if err := m.VerifyAPReply(ctx, tok.Payload); err != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + gssapi.SetAttribute(ctx, gssapi.AttributeSessionKey, m.ExportedSessionKey) + gssapi.SetAttribute(ctx, gssapi.AttributeTarget, m.Config.SName) + + return &gssapi.Token{}, gssapi.ContextComplete(ctx) + } + + if m.APReq != nil { + + b, err := m.APReply(ctx, tok.Payload) + if err != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + gssapi.SetAttribute(ctx, gssapi.AttributeSessionKey, m.ExportedSessionKey) + gssapi.SetAttribute(ctx, gssapi.AttributeTarget, m.Config.SName) + + return &gssapi.Token{Payload: b}, gssapi.ContextComplete(ctx) + } + + b, err := m.VerifyAPRequest(ctx, tok.Payload) + if err != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + if !m.Config.DCEStyle && !m.Config.FlagIsSet(gssapi.MutualAuthn) /* for non-dce style, there will be no APReply */ { + + gssapi.SetAttribute(ctx, gssapi.AttributeSessionKey, m.ExportedSessionKey) + gssapi.SetAttribute(ctx, gssapi.AttributeTarget, m.Config.SName) + + return &gssapi.Token{Payload: b}, gssapi.ContextComplete(ctx) + } + + return &gssapi.Token{Payload: b}, gssapi.ContextContinueNeeded(ctx) } // The maximum message size for the given limit. (and flag determining if @@ -192,7 +270,7 @@ func (m *Mechanism) WrapEx(ctx context.Context, tokEx *gssapi.MessageTokenEx) (* var err error - tokEx.Signature, err = m.WrapOutboundPayload(ctx, forSign, forSeal) + tokEx.Signature, err = m.WrapExOutboundPayload(ctx, forSign, forSeal) if err != nil { return nil, gssapi.ContextError(ctx, gssapi.Failure, err) } @@ -214,7 +292,7 @@ func (m *Mechanism) UnwrapEx(ctx context.Context, tokEx *gssapi.MessageTokenEx) } } - ok, err := m.UnwrapInboundPayload(ctx, forSign, forSeal, tokEx.Signature) + ok, err := m.UnwrapExInboundPayload(ctx, forSign, forSeal, tokEx.Signature) if err != nil { return nil, gssapi.ContextError(ctx, gssapi.Failure, err) } @@ -230,21 +308,18 @@ func (m *Mechanism) UnwrapEx(ctx context.Context, tokEx *gssapi.MessageTokenEx) // Wrap token. func (m *Mechanism) Wrap(ctx context.Context, tok *gssapi.MessageToken) (*gssapi.MessageToken, error) { - tokEx, err := m.WrapEx(ctx, &gssapi.MessageTokenEx{ - QoP: tok.QoP, - Payloads: []*gssapi.PayloadEx{ - {Capabilities: tok.Capabilities, Payload: tok.Payload}, - }, - }) + var err error + + tok.Signature, err = m.WrapOutboundPayload(ctx, tok.Payload, tok.Capabilities.IsSet(gssapi.Confidentiality)) if err != nil { - return nil, err + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) } return &gssapi.MessageToken{ - QoP: tokEx.QoP, - Capabilities: tokEx.Payloads[0].Capabilities, - Payload: tokEx.Payloads[0].Payload, - Signature: tokEx.Signature, + QoP: tok.QoP, + Capabilities: tok.Capabilities, + Payload: tok.Payload, + Signature: tok.Signature, }, nil } @@ -252,22 +327,25 @@ func (m *Mechanism) Wrap(ctx context.Context, tok *gssapi.MessageToken) (*gssapi // Unwrap token. func (m *Mechanism) Unwrap(ctx context.Context, tok *gssapi.MessageToken) (*gssapi.MessageToken, error) { - tokEx, err := m.UnwrapEx(ctx, &gssapi.MessageTokenEx{ - QoP: tok.QoP, - Payloads: []*gssapi.PayloadEx{ - {Capabilities: tok.Capabilities, Payload: tok.Payload}, - }, - Signature: tok.Signature, - }) + sgn, ok, err := m.UnwrapInboundPayload(ctx, tok.Payload, tok.Signature) if err != nil { - return nil, err + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + if !ok { + return nil, gssapi.ContextError(ctx, gssapi.BadMIC, gssapi.ErrBadMIC) + } + + if len(tok.Signature) == 0 && len(sgn) > 0 { + tok.Payload = tok.Payload[len(sgn):] + tok.Signature = sgn } return &gssapi.MessageToken{ - QoP: tokEx.QoP, - Capabilities: tokEx.Payloads[0].Capabilities, - Payload: tokEx.Payloads[0].Payload, - Signature: tokEx.Signature, + QoP: tok.QoP, + Capabilities: tok.Capabilities, + Payload: tok.Payload, + Signature: tok.Signature, }, nil } @@ -312,6 +390,14 @@ func (m *Mechanism) MakeSignatureEx(ctx context.Context, tokEx *gssapi.MessageTo // VerifySignature token. func (m *Mechanism) VerifySignature(ctx context.Context, tok *gssapi.MessageToken) error { + var err error + + if len(tok.Signature) == 0 { + if tok.Signature, tok.Payload, err = m.ParseInboundSignature(ctx, tok.Payload); err != nil { + return gssapi.ContextError(ctx, gssapi.Failure, err) + } + } + expSgn, err := m.MakeInboundSignature(ctx, [][]byte{tok.Payload}) if err != nil { return gssapi.ContextError(ctx, gssapi.Failure, err) diff --git a/ssp/name/name.go b/ssp/name/name.go new file mode 100644 index 00000000..ebf5bb85 --- /dev/null +++ b/ssp/name/name.go @@ -0,0 +1,78 @@ +package name + +import "strings" + +// NameType represents the type of naming convention used. +type NameType int + +const ( + UnknownNameType NameType = iota + // DNSNameType represents a DNS name, e.g. host.domain.tld + DNSNameType + // NetBIOSNameType represents a NetBIOS name, e.g. DOMAIN\host + NetBIOSNameType +) + +// Name represents a computer name with optional domain and forest components. +type Name struct { + // Type indicates the naming convention used (DNS or NetBIOS). + Type NameType + // Name is the primary identifier, either the hostname or NetBIOS name. + Name string + // Domain is the domain part of the name, if applicable. + Domain string + // Forest is an optional higher-level grouping, often used in Active Directory contexts. + Forest string +} + +type NameOption func(*Name) + +// Forest sets the Forest field of the Name struct. +func Forest(forest string) NameOption { + return func(n *Name) { + n.Forest = forest + } +} + +// Domain sets the Domain field of the Name struct. +func Domain(domain string) NameOption { + return func(n *Name) { + n.Domain = domain + } +} + +// NetBIOS creates a Name instance using the NetBIOS naming convention. +func NetBIOS(name string, o ...NameOption) *Name { + + n := &Name{Type: NetBIOSNameType, Name: name} + + if n.Name = strings.TrimLeft(n.Name, "\\"); strings.Contains(n.Name, "\\") { + parts := strings.SplitN(n.Name, "\\", 2) + n.Name = parts[1] + n.Domain = parts[0] + } + + for _, opt := range o { + opt(n) + } + + return n +} + +// DNS creates a Name instance using the DNS naming convention. +func DNS(name string, o ...NameOption) *Name { + + n := &Name{Type: DNSNameType, Name: name} + + if strings.Contains(n.Name, ".") { + parts := strings.SplitN(n.Name, ".", 2) + n.Name = parts[0] + n.Domain = parts[1] + } + + for _, opt := range o { + opt(n) + } + + return n +} diff --git a/ssp/netlogon/gssapi.go b/ssp/netlogon/gssapi.go index a54907c9..3b18661d 100644 --- a/ssp/netlogon/gssapi.go +++ b/ssp/netlogon/gssapi.go @@ -110,6 +110,10 @@ func (m *Mechanism) Init(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, return &gssapi.Token{Payload: b}, gssapi.ContextContinueNeeded(ctx) } +func (m *Mechanism) Capabilities(ctx context.Context) gssapi.Cap { + return gssapi.Cap(0) +} + // The security context accept call. func (m *Mechanism) Accept(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, error) { return nil, gssapi.ContextError(ctx, gssapi.Unavailable, gssapi.ErrUnavailable) diff --git a/ssp/ntlm/authentifier.go b/ssp/ntlm/authentifier.go index 84a5e427..a27223f5 100644 --- a/ssp/ntlm/authentifier.go +++ b/ssp/ntlm/authentifier.go @@ -5,7 +5,9 @@ import ( "context" "fmt" "hash" + "time" + "github.com/oiweiwei/go-msrpc/ssp/credential" "github.com/oiweiwei/go-msrpc/ssp/crypto" ) @@ -196,7 +198,7 @@ func (a *Authentifier) Authenticate(ctx context.Context, b []byte) ([]byte, erro return nil, fmt.Errorf("ntlm: init: authenticate: compute mic: %w", err) } // copy mic value. - copy(b[micidx:], am.MIC) + copy(b[MICOffset:], am.MIC) // reset the mic buffer. a.mic.Reset() } @@ -204,8 +206,161 @@ func (a *Authentifier) Authenticate(ctx context.Context, b []byte) ([]byte, erro return b, nil } +func (a *Authentifier) Challenge(ctx context.Context, b []byte) ([]byte, error) { + + var err error + + nm := &NegotiateMessage{} + + if err := nm.Unmarshal(ctx, b); err != nil { + return nil, fmt.Errorf("ntlm: init: challenge: unmarshal negotiate message: %w", err) + } + + // accept the negotiated flags. + a.Config.Accept(nm.Negotiate) + + a.mic.Write(b) + + cm := &ChallengeMessage{ + Negotiate: nm.Negotiate | RequestTarget | NegotiateNTLM | NegotiateAlwaysSign, + TargetInfo: AttrValues{}, + } + + if cm.Negotiate.IsSet(NegotiateUnicode) { + cm.Negotiate = cm.Negotiate.Unset(NegotiateOEM) + } else { + cm.Negotiate = cm.Negotiate.Set(NegotiateOEM) + } + + if nm.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { + cm.Negotiate = cm.Negotiate.Unset(NegotiateLMKey) + } + + if cm.Negotiate.IsSet(TargetTypeDomain) || a.Config.NetBIOSDomainName != "" { + cm.TargetName = a.Config.NetBIOSDomainName + cm.Negotiate = cm.Negotiate.Set(TargetTypeDomain).Unset(TargetTypeServer) + } else { + cm.TargetName = a.Config.NetBIOSComputerName + cm.Negotiate = cm.Negotiate.Set(TargetTypeServer).Unset(TargetTypeDomain) + } + + cm.Negotiate = cm.Negotiate.Set(NegotiateTargetInfo).Set(RequestTarget) + + // if err := a.Config.Verify(cm.Negotiate); err != nil { + // return nil, fmt.Errorf("ntlm: init: challenge: %w", err) + // } + + if a.Config.NetBIOSComputerName != "" { + cm.TargetInfo[AttrNetBIOSComputerName] = &Value{NetBIOSComputerName: a.Config.NetBIOSComputerName} + } + + if a.Config.NetBIOSDomainName != "" { + cm.TargetInfo[AttrNetBIOSDomainName] = &Value{NetBIOSDomainName: a.Config.NetBIOSDomainName} + } + + if a.Config.DNSDomainName != "" { + cm.TargetInfo[AttrDNSDomainName] = &Value{DNSDomainName: a.Config.DNSDomainName} + } + + if a.Config.DNSComputerName != "" { + cm.TargetInfo[AttrDNSComputerName] = &Value{DNSComputerName: a.Config.DNSComputerName} + } + + if a.Config.DNSForestName != "" { + cm.TargetInfo[AttrDNSTreeName] = &Value{DNSTreeName: a.Config.DNSForestName} + } + + if a.Config.RequestMIC { + cm.TargetInfo[AttrTimestamp] = &Value{Timestamp: TimeToFiletime(time.Now())} + } + + if cm.ServerChallenge, err = crypto.Nonce(8); err != nil { + return nil, fmt.Errorf("ntlm: init: challenge: generate server challenge: %w", err) + } + + if cm.Negotiate.IsSet(NegotiateVersion) { + if cm.Version = a.Config.Version; cm.Version.IsZero() { + cm.Version = DefaultVersion + } + } + + if b, err = cm.Marshal(ctx); err != nil { + return nil, fmt.Errorf("ntlm: init: challenge: marshal challenge message: %w", err) + } + + a.makeSecurityParameters(ctx, cm) + + a.mic.Write(b) + + return b, nil +} + +func (a *Authentifier) VerifyAuthenticate(ctx context.Context, b []byte) error { + + am := &AuthenticateMessage{} + + if err := am.Unmarshal(ctx, b); err != nil { + return fmt.Errorf("ntlm: init: verify authenticate: unmarshal authenticate message: %w", err) + } + + if len(am.MIC) == 0 && a.Config.RequestMIC { + return fmt.Errorf("ntlm: init: verify authenticate: mic is required but not provided") + } + + copy(b[MICOffset:], make([]byte, 16)) + + a.mic.Write(b) + + var cred credential.Credential + + cred = credential.NewFromPassword(am.UserName, "", + credential.Domain(am.DomainName), + credential.Workstation(am.Workstation), + ) + + nonce := a.session.ServerChallenge + + resp, err := a.Config.Verifier.VerifyChallenge(ctx, a.session, cred, nonce, am) + if err != nil { + if am.UserName == "" { + return fmt.Errorf("ntlm: init: verify authenticate: verify challenge response: %w", err) + } + cred = credential.New(am.UserName, + credential.Workstation(am.Workstation), + ) + if resp, err = a.Config.Verifier.VerifyChallenge(ctx, a.session, cred, nonce, am); err != nil { + return fmt.Errorf("ntlm: init: verify authenticate: verify challenge response: %w", err) + } + } + + var exportedKey []byte + + if a.session.KeyExchange { + if exportedKey, err = RC4K(ctx, resp.KeyExchangeKey, am.EncryptedRandomSessionKey); err != nil { + return fmt.Errorf("ntlm: local provider: rc4k failed: %w", err) + } + } else { + exportedKey = resp.KeyExchangeKey + } + + expectedMIC, err := crypto.HMACMD5(exportedKey, a.mic.Bytes()) + if err != nil { + return fmt.Errorf("ntlm: init: authenticate: compute mic: %w", err) + } + + if !bytes.Equal(am.MIC, expectedMIC) { + return fmt.Errorf("ntlm: init: verify authenticate: mic mismatch") + } + + if err = a.makeSecurityService(ctx, exportedKey); err != nil { + return fmt.Errorf("ntlm: init: verify authenticate: make security service: %w", err) + } + + return nil +} + // The MIC index in the output buffer. -const micidx = 72 +const MICOffset = 72 var ( // client-to-server seal. @@ -283,7 +438,7 @@ func (a *Authentifier) makeSecurityService(ctx context.Context, key []byte) erro } // makeSignKey creates the key for the signature. -func (a *Authentifier) makeSignKey(ctx context.Context, key []byte, magic []byte) ([]byte, error) { +func (a *Authentifier) makeSignKey(_ context.Context, key []byte, magic []byte) ([]byte, error) { if !a.session.ExtendedSessionSecurity { return nil, nil } @@ -299,10 +454,11 @@ type SecurityParameters struct { KeySize int ServerName string DomainName string + ServerChallenge []byte } // makeHashFunc creates the hash function. -func (a *Authentifier) makeHashFunc(ctx context.Context, key []byte) func(uint32) hash.Hash { +func (a *Authentifier) makeHashFunc(_ context.Context, key []byte) func(uint32) hash.Hash { if a.session.ExtendedSessionSecurity { return HMACMD5(key) } @@ -310,7 +466,7 @@ func (a *Authentifier) makeHashFunc(ctx context.Context, key []byte) func(uint32 } // makeSealKey creates the key for the encryption. -func (a *Authentifier) makeSealKey(ctx context.Context, key []byte, magic []byte) ([]byte, error) { +func (a *Authentifier) makeSealKey(_ context.Context, key []byte, magic []byte) ([]byte, error) { if a.session.ExtendedSessionSecurity { switch a.session.KeySize { @@ -339,7 +495,7 @@ const ( SignatureVersion = 0x01 ) -func (a *Authentifier) makeSecurityParameters(ctx context.Context, cm *ChallengeMessage) { +func (a *Authentifier) makeSecurityParameters(_ context.Context, cm *ChallengeMessage) { a.session = &SecurityParameters{ ExtendedSessionSecurity: cm.Negotiate.IsSet(NegotiateExtendedSessionSecurity), UseLMKey: cm.Negotiate.IsSet(NegotiateLMKey), @@ -347,6 +503,7 @@ func (a *Authentifier) makeSecurityParameters(ctx context.Context, cm *Challenge KeyExchange: cm.Negotiate.IsSet(NegotiateKeyExchange), Datagram: cm.Negotiate.IsSet(NegotiateDatagram), KeySize: 40, + ServerChallenge: cm.ServerChallenge, } if cm.Negotiate.IsSet(TargetTypeDomain) { @@ -408,7 +565,7 @@ func (a *Authentifier) MakeOutboundChecksum(ctx context.Context, b [][]byte) ([] return chk, nil } -func (a *Authentifier) makeChecksum(ctx context.Context, cipher *Cipher, seqNum uint32, b ...[]byte) ([]byte, error) { +func (a *Authentifier) makeChecksum(_ context.Context, cipher *Cipher, seqNum uint32, b ...[]byte) ([]byte, error) { if !a.Config.Integrity { return nil, nil } diff --git a/ssp/ntlm/config.go b/ssp/ntlm/config.go index 8e53a2a6..e4328d94 100644 --- a/ssp/ntlm/config.go +++ b/ssp/ntlm/config.go @@ -117,6 +117,25 @@ type Config struct { // The flag that indicates whether all the input buffers must be used to // build a signature. (DO NOT USE IT). NoSignAllBuffers bool + + ServerConfigFlags Flag + // The NetBIOS computer name of the server. + NetBIOSComputerName string + // The NetBIOS domain name of the server. + NetBIOSDomainName string + // The DNS computer name of the server. + DNSComputerName string + // The DNS domain name of the server. + DNSDomainName string + // The DNS forest name of the server. + DNSForestName string + + // Allow the use of the null session. + AllowAnonymous bool + // Allow the use of the guest account. + AllowGuest bool + // The credential verifier. + Verifier Verifier } func IsCredentialEmpty(cred any) bool { @@ -212,6 +231,31 @@ func (c *Config) Verify(f Flag) error { return nil } +func (c *Config) Accept(flags Flag) { + + c.Integrity = true + + if flags.IsSet(NegotiateSign) { + c.Integrity = true + } + + if flags.IsSet(NegotiateSeal) { + c.Confidentiality = true + } + + if flags.IsSet(Anonymous) { + c.Anonymity = true + } + + if flags.IsSet(NegotiateIdentify) { + c.Identify = true + } + + if flags.IsSet(NegotiateDatagram) { + c.Datagram = true + } +} + // Negotiate function returns the set of negotiated flags. func (c *Config) Negotiate() Flag { diff --git a/ssp/ntlm/gssapi.go b/ssp/ntlm/gssapi.go index f15cf578..21373e49 100644 --- a/ssp/ntlm/gssapi.go +++ b/ssp/ntlm/gssapi.go @@ -6,7 +6,9 @@ import ( "errors" "time" + "github.com/oiweiwei/go-msrpc/ssp/credential" "github.com/oiweiwei/go-msrpc/ssp/gssapi" + "github.com/oiweiwei/go-msrpc/ssp/name" ) var ( @@ -23,6 +25,37 @@ func (Config) Type() gssapi.OID { return MechanismType } +func (c *Config) Capabilities() gssapi.Cap { + + var caps gssapi.Cap + + if c.Integrity { + caps |= gssapi.Integrity | gssapi.ReplayDetection | gssapi.Sequencing + } + + if c.Confidentiality { + caps |= gssapi.Confidentiality + } + + if c.Identify { + caps |= gssapi.Identify + } + + if c.Anonymity { + caps |= gssapi.Anonymity + } + + if c.Datagram { + caps |= gssapi.Datagram + } + + if c.Anonymity { + caps |= gssapi.Anonymity + } + + return caps +} + // Copy function returns the copy of the configuration. func (c *Config) Copy() gssapi.MechanismConfig { @@ -61,11 +94,65 @@ func (Mechanism) New(ctx context.Context) (gssapi.Mechanism, error) { // config should have been populated. return nil, gssapi.ContextError(ctx, gssapi.NoContext, gssapi.ErrNoContext) } - if cc.Credential != nil { + + c.IsServer = cc.IsServer + + if !c.IsServer && cc.Credential != nil { if c.Credential, ok = cc.Credential.Value().(Credential); !ok || !IsValidCredential(c.Credential) { return nil, gssapi.ContextError(ctx, gssapi.DefectiveCredential, gssapi.ErrDefectiveCredential) } } + + if cc.CredentialDatabase != nil { + if db, ok := cc.CredentialDatabase.Value().(credential.Database); ok { + if cc.CredentialDatabase.AllowAnonymous() { + c.AllowAnonymous = true + } + if cc.CredentialDatabase.AllowGuest() { + c.AllowGuest = true + } + c.Verifier = &LocalVerifier{ + Config: c, + Database: db, + } + } + } else { + if c.Verifier == nil { + c.Verifier = &LocalVerifier{ + Config: c, + Database: credential.NewLocalDatabase(), + } + } + } + + for _, n := range cc.ServerNames { + switch n.Type { + case name.DNSNameType: + if n.Name != "" { + c.DNSComputerName = n.Name + } + if n.Domain != "" { + c.DNSDomainName = n.Domain + } + if n.Forest != "" { + c.DNSForestName = n.Forest + } + case name.NetBIOSNameType: + if n.Name != "" { + c.NetBIOSComputerName = n.Name + } + if n.Domain != "" { + c.NetBIOSDomainName = n.Domain + } + } + } + + if cc.ChannelBindings != nil { + if c.ChannelBindings, err = cc.ChannelBindings.Marshal(); err != nil { + return nil, gssapi.ContextError(ctx, gssapi.BadBindings, gssapi.ErrBadBindings) + } + } + if cc.Capabilities.IsSet(gssapi.MutualAuthn) { return nil, gssapi.ContextError(ctx, gssapi.Unavailable, ErrMutualAuthnNotSupported) } @@ -96,11 +183,6 @@ func (Mechanism) New(ctx context.Context) (gssapi.Mechanism, error) { if cc.TargetNameFromUntrustedSource { c.UnverifiedTargetName = cc.TargetNameFromUntrustedSource } - if cc.ChannelBindings != nil { - if c.ChannelBindings, err = cc.ChannelBindings.Marshal(); err != nil { - return nil, gssapi.ContextError(ctx, gssapi.BadBindings, gssapi.ErrBadBindings) - } - } return &Mechanism{ Authentifier: &Authentifier{Config: c}, }, nil @@ -139,9 +221,30 @@ func (m *Mechanism) Init(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, return &gssapi.Token{Payload: b}, gssapi.ContextContinueNeeded(ctx) } +func (m *Mechanism) Capabilities(ctx context.Context) gssapi.Cap { + return m.Config.Capabilities() +} + // The security context accept call. func (m *Mechanism) Accept(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, error) { - return nil, gssapi.ContextError(ctx, gssapi.Unavailable, gssapi.ErrUnavailable) + + if m.session != nil { + err := m.VerifyAuthenticate(ctx, tok.Payload) + if err != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + gssapi.SetAttribute(ctx, gssapi.AttributeSessionKey, m.SessionKey()) + + return &gssapi.Token{}, gssapi.ContextComplete(ctx) + } + + b, err := m.Challenge(ctx, tok.Payload) + if err != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + return &gssapi.Token{Payload: b}, gssapi.ContextContinueNeeded(ctx) } // The maximum message size for the given limit. (and flag determining if @@ -285,6 +388,14 @@ func (m *Mechanism) Unwrap(ctx context.Context, tok *gssapi.MessageToken) (*gssa tok.Capabilities &= ^gssapi.Confidentiality } + if len(tok.Signature) == 0 { + if len(tok.Payload) < SignatureSize { + return nil, gssapi.ContextError(ctx, gssapi.DefectiveToken, gssapi.ErrDefectiveToken) + } + // compute signature and checksum from the payload if signature is not provided. + tok.Signature, tok.Payload = tok.Payload[:SignatureSize], tok.Payload[SignatureSize:] + } + // decrypt the payload. if tok.Capabilities.IsSet(gssapi.Confidentiality) { if err := m.ApplyInboundCipher(ctx, tok.Payload); err != nil { @@ -369,6 +480,13 @@ func (m *Mechanism) MakeSignatureEx(ctx context.Context, tokEx *gssapi.MessageTo // VerifySignature token. func (m *Mechanism) VerifySignature(ctx context.Context, tok *gssapi.MessageToken) error { + if len(tok.Signature) == 0 { + if len(tok.Payload) < 16 { + return gssapi.ContextError(ctx, gssapi.DefectiveToken, gssapi.ErrDefectiveToken) + } + tok.Payload, tok.Signature = tok.Payload[:len(tok.Payload)-16], tok.Payload[len(tok.Payload)-16:] + } + expChk, err := m.MakeInboundChecksum(ctx, [][]byte{tok.Payload}) if err != nil { return gssapi.ContextError(ctx, gssapi.Failure, err) diff --git a/ssp/ntlm/ntlm.go b/ssp/ntlm/ntlm.go index b5ccae6e..1ce517ae 100644 --- a/ssp/ntlm/ntlm.go +++ b/ssp/ntlm/ntlm.go @@ -37,6 +37,9 @@ type NTLMVersion interface { // ChallengeResponse function computes the response for the server challenge message // based on client challenge `nonce` parameter and set of negotiated flags. ChallengeResponse(context.Context, Credential, *ChallengeMessage, []byte) (*ChallengeResponse, error) + // AuthenticateResponse function computes the response for the server authenticate message + // based on the server challenge `nonce` parameter. + AuthenticateResponse(context.Context, Credential, *AuthenticateMessage, []byte) (*ChallengeResponse, error) // KeyExchangeKey function returns the key used to protect the session key that is // generated by the client. KeyExchangeKey(context.Context, *ChallengeMessage, *ChallengeResponse) ([]byte, error) @@ -780,9 +783,9 @@ func (m *AuthenticateMessage) Unmarshal(ctx context.Context, b []byte) error { e.ReadData(ctx, &m.Version) // mic. (decode nt_challenge_response av_pairs). - if m.NTChallengeResponseLen > 24 { // at position 24 av_pairs start. + if m.NTChallengeResponseLen >= 44 { attrs := AttrValues{} - if err := (&attrs).Unmarshal(ctx, b[m.NTChallengeResponseOffset+24:]); err != nil { + if err := (&attrs).Unmarshal(ctx, b[m.NTChallengeResponseOffset+44:]); err != nil { return err } if attrs.Flag().IsSet(MICProvided) { diff --git a/ssp/ntlm/signature.go b/ssp/ntlm/signature.go index 17d9a98f..b2ae7c9f 100644 --- a/ssp/ntlm/signature.go +++ b/ssp/ntlm/signature.go @@ -12,6 +12,8 @@ type Signature struct { SeqNum uint32 } +const SignatureSize = 16 + func (s *Signature) Marshal(ctx context.Context) ([]byte, error) { e := internal.NewCodec(ctx, nil) diff --git a/ssp/ntlm/v1.go b/ssp/ntlm/v1.go index 536d34a3..51bdf223 100644 --- a/ssp/ntlm/v1.go +++ b/ssp/ntlm/v1.go @@ -146,7 +146,7 @@ func (v1 *V1) ChallengeResponse(ctx context.Context, cred Credential, respNT, respLM := new(NTLMv1Response), new(LMv1Response) - if c.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { + if v1.ExtendedSessionSecurity || c.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { if respNT.Response, err = crypto.MD5(c.ServerChallenge, nonce); err != nil { return nil, fmt.Errorf("v1: compute response: %w", err) } @@ -161,6 +161,10 @@ func (v1 *V1) ChallengeResponse(ctx context.Context, cred Credential, } } + if resp.SessionBaseKey, err = crypto.MD4(resp.KeyNT); err != nil { + return nil, fmt.Errorf("v1: compute session base key: %v", err) + } + if resp.NT, err = respNT.Marshal(ctx); err != nil { return nil, fmt.Errorf("v1: marshal nt response: %v", err) } @@ -169,10 +173,64 @@ func (v1 *V1) ChallengeResponse(ctx context.Context, cred Credential, return nil, fmt.Errorf("v1: marshal lm response: %v", err) } + return resp, nil +} + +func (v1 *V1) AuthenticateResponse(ctx context.Context, cred Credential, a *AuthenticateMessage, nonce []byte) (*ChallengeResponse, error) { + + if len(a.NTChallengeResponse) == 0 && bytes.Equal(a.LMChallengeResponse, []byte{0}) { + return &ChallengeResponse{ + LM: []byte{0}, + IsAnonymous: true, + SessionBaseKey: make([]byte, 16), + }, nil + } + + var ( + resp = new(ChallengeResponse) + err error + ) + + if resp.KeyNT, err = v1.NTOWF(ctx, cred); err != nil { + return nil, err + } + + if resp.KeyLM, err = v1.LMOWF(ctx, cred); err != nil { + return nil, err + } + + if v1.ExtendedSessionSecurity || a.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { + if resp.NT, err = crypto.MD5(nonce, a.LMChallengeResponse); err != nil { + return nil, fmt.Errorf("v1: compute response: %w", err) + } + resp.NT = crypto.DESL(resp.KeyNT, resp.NT[:8]) + resp.LM = a.LMChallengeResponse + } else { + resp.NT = crypto.DESL(resp.KeyNT, nonce) + resp.LM = crypto.DESL(resp.KeyLM, nonce) + } + if resp.SessionBaseKey, err = crypto.MD4(resp.KeyNT); err != nil { return nil, fmt.Errorf("v1: compute session base key: %v", err) } + if !bytes.Equal(a.NTChallengeResponse, resp.NT) { + return nil, fmt.Errorf("v1: authenticate response: nt challenge response mismatch") + } + + if len(a.LMChallengeResponse) > 0 { + if !bytes.Equal(a.LMChallengeResponse, resp.LM) && !bytes.Equal(a.LMChallengeResponse, resp.NT) { + return nil, fmt.Errorf("v1: authenticate response: lm challenge response mismatch") + } + } + + if resp.KeyExchangeKey, err = v1.KeyExchangeKey(ctx, &ChallengeMessage{ + Negotiate: a.Negotiate, + ServerChallenge: v1.ServerChallenge, + }, resp); err != nil { + return nil, fmt.Errorf("v1: compute key exchange key: %v", err) + } + return resp, nil } @@ -188,7 +246,7 @@ func (v1 *V1) KeyExchangeKey(ctx context.Context, c *ChallengeMessage, chal *Cha return nil, errors.New("v1: key exchange key: challenge response lm is empty") } - if c.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { + if v1.ExtendedSessionSecurity || c.Negotiate.IsSet(NegotiateExtendedSessionSecurity) { return crypto.HMACMD5(chal.SessionBaseKey, c.ServerChallenge, chal.LM[:8]) } @@ -196,7 +254,7 @@ func (v1 *V1) KeyExchangeKey(ctx context.Context, c *ChallengeMessage, chal *Cha return nil, errors.New("v1: key exchange key: challenge response key lm is empty") } - if c.Negotiate.IsSet(NegotiateLMKey) { + if v1.SecurityParameters.UseLMKey || c.Negotiate.IsSet(NegotiateLMKey) { buf := bytes.NewBuffer(nil) @@ -212,7 +270,7 @@ func (v1 *V1) KeyExchangeKey(ctx context.Context, c *ChallengeMessage, chal *Cha return buf.Bytes(), nil } - if c.Negotiate.IsSet(RequestNonNTSessionKey) { + if v1.SecurityParameters.NonNTSessionKey || c.Negotiate.IsSet(RequestNonNTSessionKey) { return append(chal.KeyLM[:8], bytes.Repeat([]byte{0}, 8)...), nil } diff --git a/ssp/ntlm/v2.go b/ssp/ntlm/v2.go index 20265630..ced9b812 100644 --- a/ssp/ntlm/v2.go +++ b/ssp/ntlm/v2.go @@ -1,6 +1,7 @@ package ntlm import ( + "bytes" "context" "errors" "fmt" @@ -344,6 +345,86 @@ func (v2 *V2) ChallengeResponse(ctx context.Context, cred Credential, c *Challen return resp, nil } +func (v2 *V2) AuthenticateResponse(ctx context.Context, cred Credential, a *AuthenticateMessage, nonce []byte) (*ChallengeResponse, error) { + + var ( + resp = new(ChallengeResponse) + err error + ) + + if len(a.NTChallengeResponse) == 0 && bytes.Equal(a.LMChallengeResponse, []byte{0}) { + return &ChallengeResponse{ + LM: []byte{0}, + IsAnonymous: true, + SessionBaseKey: make([]byte, 16), + }, nil + } + + // compute nt key. + if resp.KeyNT, err = v2.NTOWF(ctx, cred); err != nil { + return nil, err + } + + // "compute" lm key. + resp.KeyLM = resp.KeyNT + + respNT, respLM := new(NTLMv2Response), new(LMv2Response) + + if err := respNT.Unmarshal(ctx, a.NTChallengeResponse); err != nil { + return nil, fmt.Errorf("v2: unmarshal nt challenge response: %w", err) + } + + resp.Tmp = a.NTChallengeResponse[16:] + + if resp.NT, err = crypto.HMACMD5(resp.KeyNT, nonce, resp.Tmp); err != nil { + return nil, fmt.Errorf("v2: compute response nt: %v", err) + } + + if resp.SessionBaseKey, err = crypto.HMACMD5(resp.KeyNT, resp.NT); err != nil { + return nil, fmt.Errorf("v2: compute session base key: %v", err) + } + + resp.NT = append(resp.NT, resp.Tmp...) + + if !isZero(a.LMChallengeResponse) { + if err := respLM.Unmarshal(ctx, a.LMChallengeResponse); err != nil { + return nil, fmt.Errorf("v2: unmarshal lm challenge response: %w", err) + } + if resp.LM, err = crypto.HMACMD5(resp.KeyLM, nonce, respNT.NTLMv2ClientChallenge.ChallengeFromClient); err != nil { + return nil, fmt.Errorf("v2: compute response lm: %v", err) + } + resp.LM = append(resp.LM, respNT.NTLMv2ClientChallenge.ChallengeFromClient...) + } + + if !bytes.Equal(a.NTChallengeResponse, resp.NT) { + return nil, fmt.Errorf("v2: authenticate response: nt challenge response mismatch") + } + + if !isZero(a.LMChallengeResponse) { + if !bytes.Equal(a.LMChallengeResponse, resp.LM) { + return nil, fmt.Errorf("v2: authenticate response: lm challenge response mismatch") + } + } + + if resp.KeyExchangeKey, err = v2.KeyExchangeKey(ctx, &ChallengeMessage{ + Negotiate: a.Negotiate, + ServerChallenge: v2.ServerChallenge, + }, resp); err != nil { + return nil, fmt.Errorf("v2: compute key exchange key: %w", err) + } + + return resp, nil +} + +func isZero(b []byte) bool { + for _, v := range b { + if v != 0 { + return false + } + } + return true +} + // KeyExchangeKey function returns the key used to protect the session key that is // generated by the client. func (v2 *V2) KeyExchangeKey(ctx context.Context, c *ChallengeMessage, chal *ChallengeResponse) ([]byte, error) { diff --git a/ssp/ntlm/verifier.go b/ssp/ntlm/verifier.go new file mode 100644 index 00000000..f6ac306b --- /dev/null +++ b/ssp/ntlm/verifier.go @@ -0,0 +1,54 @@ +package ntlm + +import ( + "context" + "fmt" + + "github.com/oiweiwei/go-msrpc/ssp/credential" +) + +type Verifier interface { + VerifyChallenge(context.Context, *SecurityParameters, Credential, []byte, *AuthenticateMessage) (*ChallengeResponse, error) +} + +type LocalVerifier struct { + Config *Config + Database credential.Database +} + +func (v *LocalVerifier) Lookup(ctx context.Context, cred Credential) (Credential, bool) { + if credential.IsAnonymous(cred) && v.Config.AllowAnonymous { + return credential.Anonymous(), true + } + return v.Database.Lookup(ctx, cred) +} + +func (v *LocalVerifier) VerifyChallenge(ctx context.Context, session *SecurityParameters, cred Credential, nonce []byte, am *AuthenticateMessage) (*ChallengeResponse, error) { + + var ( + ntlm NTLMVersion + ) + + ntlm = &V1{Config: v.Config, SecurityParameters: session} + + if len(am.NTChallengeResponse) > 24 { + ntlm = &V2{Config: v.Config, SecurityParameters: session} + } + + cred, ok := v.Lookup(ctx, cred) + if !ok { + return nil, fmt.Errorf("ntlm: local verifyer: credential not found for %s", cred.DomainName()+"\\"+cred.UserName()) + } + + resp, err := ntlm.AuthenticateResponse(ctx, cred, am, nonce) + if err != nil { + return nil, fmt.Errorf("ntlm: local provider: authenticate verification failed: %w", err) + } + + resp.KeyExchangeKey, err = ntlm.KeyExchangeKey(ctx, &ChallengeMessage{ServerChallenge: nonce}, resp) + if err != nil { + return nil, fmt.Errorf("ntlm: local provider: key exchange key failed: %w", err) + } + + return resp, nil +} diff --git a/ssp/spnego/authentifier.go b/ssp/spnego/authentifier.go index 363382d6..c2d1e2c4 100644 --- a/ssp/spnego/authentifier.go +++ b/ssp/spnego/authentifier.go @@ -9,6 +9,7 @@ import ( ) type Config struct { + IsServer bool // The services available. Capabilities gssapi.Cap // The list of negotiated mechanisms. @@ -55,6 +56,180 @@ func (a *Authentifier) Negotiate(ctx context.Context) ([]byte, error) { return b, nil } +func (a *Authentifier) Reject(ctx context.Context) ([]byte, error) { + + resp := &NegTokenResp{ + State: Reject, + } + + b, err := resp.Marshal(ctx) + if err != nil { + return nil, fmt.Errorf("spnego: reject: marshal neg token resp: %w", err) + } + + return b, nil +} + +const Init2HintName = "not_defined_in_RFC4178@please_ignore" + +func (a *Authentifier) ServerRespond(ctx context.Context, b []byte) ([]byte, error) { + + var err error + + if len(b) == 0 { + + init2 := &NegTokenInit{ + MechTypes: a.MakeMechanismList(ctx), + HintName: Init2HintName, + } + + a.RetrievedMechanismList = init2.MechTypes + + if a.Mechanism, err = a.SelectMechanism(ctx); err != nil { + return nil, err + } + + if a.Mechanism == nil { + return nil, fmt.Errorf("spnego: init: no mechanism selected") + } + + tok, err := a.Mechanism.Accept(ctx, &gssapi.Token{}) + if err != nil { + return nil, fmt.Errorf("spnego: init: mechanism accept: %w", err) + } + + init2.MechToken = tok.Payload + + b, err := init2.Marshal(ctx) + if err != nil { + return nil, fmt.Errorf("spnego: init: marshal neg token init: %w", err) + } + + return b, nil + } + + var resp = new(NegTokenResp) + + if a.IsNegTokenInit(ctx, b) { + + init := &NegTokenInit{} + + if err := init.Unmarshal(ctx, b); err != nil { + return nil, fmt.Errorf("spnego: init: unmarshal neg token init: %w", err) + } + + a.RetrievedMechanismList = init.MechTypes + + if a.Mechanism, err = a.SelectMechanism(ctx); err != nil { + return nil, fmt.Errorf("spnego: init: select mechanism: %w", err) + } + + if a.Mechanism == nil { + return nil, fmt.Errorf("spnego: init: no mechanism selected") + } + + tok, err := a.Mechanism.Accept(ctx, &gssapi.Token{Payload: init.MechToken}) + if err != nil { + return nil, fmt.Errorf("spnego: init: mechanism accept: %w", err) + } + + resp = &NegTokenResp{ + SupportedMech: (asn1.ObjectIdentifier)(a.Mechanism.Type()), + ResponseToken: tok.Payload, + State: AcceptIncomplete, + } + + if gssapi.IsComplete(ctx) { + + // create mechanism list mic. + sgn, err := a.ComputeSignature(ctx, init.MechTokenMIC) + if err != nil { + return nil, fmt.Errorf("spnego: init: compute mech list mic: %w", err) + } + + resp.MechListMIC = sgn + resp.State = AcceptCompleted + } + + b, err := resp.Marshal(ctx) + if err != nil { + return nil, fmt.Errorf("spnego: init: marshal neg token resp: %w", err) + } + + return b, nil + } + + if err := resp.Unmarshal(ctx, b); err != nil { + return nil, fmt.Errorf("spnego: init: unmarshal neg token resp: %w", err) + } + + mechTok, err := a.Mechanism.Accept(ctx, &gssapi.Token{Payload: resp.ResponseToken}) + if err != nil { + return nil, fmt.Errorf("spnego: init: mechanism accept: %w", err) + } + + micTok := resp.MechListMIC + + resp = &NegTokenResp{ + SupportedMech: (asn1.ObjectIdentifier)(a.Mechanism.Type()), + ResponseToken: mechTok.Payload, + State: AcceptIncomplete, + } + + if a.Config.RequireMechanismListMIC { + resp.State = RequestMIC + } + + if gssapi.IsComplete(ctx) { + if len(micTok) == 0 && a.Config.RequireMechanismListMIC { + return nil, fmt.Errorf("spnego: init: mechanism list mic required but not provided") + } + // verify the mechanism list mic. + resp.MechListMIC, err = a.ComputeSignature(ctx, micTok) + if err != nil { + return nil, fmt.Errorf("spnego: init: compute mech list mic: %w", err) + } + resp.State = AcceptCompleted + } + + b, err = resp.Marshal(ctx) + if err != nil { + return nil, fmt.Errorf("spnego: init: marshal neg token resp: %w", err) + } + + return b, nil +} + +func (a *Authentifier) ComputeSignature(ctx context.Context, tok []byte) ([]byte, error) { + + // create mechanism list mic. + + b, err := asn1.Marshal(a.MakeMechanismList(ctx)) + if err != nil { + return nil, fmt.Errorf("spnego: init: marshal mech list: %w", err) + } + + if len(tok) > 0 { + if err := a.Mechanism.VerifySignature(ctx, &gssapi.MessageToken{Payload: b, Signature: tok}); err != nil { + return nil, fmt.Errorf("spnego: init: verify mech list mic: %w", err) + } + } + + sgn, err := a.Mechanism.MakeSignature(ctx, &gssapi.MessageToken{Payload: b}) + if err != nil { + return nil, fmt.Errorf("spnego: init: make mech list signature: %w", err) + } + + rst, ok := (any)(a.Mechanism).(interface{ ResetSecurityService(context.Context) error }) + if ok { + if err := rst.ResetSecurityService(ctx); err != nil { + return nil, fmt.Errorf("spnego: init: reset security service: %w", err) + } + } + + return sgn.Signature, nil +} + func (a *Authentifier) Respond(ctx context.Context, b []byte) ([]byte, error) { var err error @@ -71,17 +246,8 @@ func (a *Authentifier) Respond(ctx context.Context, b []byte) ([]byte, error) { a.RetrievedMechanismList = init.MechTypes - loop: - // select the mechanism from the retrieved list. - for i := range a.MechanismsList { - for _, rm := range a.RetrievedMechanismList { - if a.MechanismsList[i].Type().Equal((gssapi.OID)(rm)) { - if a.Mechanism, err = a.MechanismsList[i].New(ctx); err != nil { - continue - } - break loop - } - } + if a.Mechanism, err = a.SelectMechanism(ctx); err != nil { + return nil, gssapi.ErrFailure } if a.Mechanism == nil { @@ -169,6 +335,7 @@ func (a *Authentifier) Respond(ctx context.Context, b []byte) ([]byte, error) { return nil, fmt.Errorf("spnego: init: reset security service: %w", err) } } + } b, err = resp.Marshal(ctx) @@ -180,7 +347,7 @@ func (a *Authentifier) Respond(ctx context.Context, b []byte) ([]byte, error) { } func (a *Authentifier) IsNegTokenInit(ctx context.Context, b []byte) bool { - return len(b) > 0 && b[0] == Application|0 + return len(b) > 0 && b[0] == 0x60 // Application|0 } func (a *Authentifier) MakeMechanismList(ctx context.Context) []asn1.ObjectIdentifier { @@ -198,9 +365,21 @@ func (a *Authentifier) MakeMechanismList(ctx context.Context) []asn1.ObjectIdent return mechTypes } -func (a *Authentifier) SelectMechanism(ctx context.Context, oid gssapi.OID) gssapi.Mechanism { +func (a *Authentifier) SelectMechanism(ctx context.Context) (gssapi.Mechanism, error) { - // select mechanism based on oid or first entry if default... - return nil + var err error + // select the mechanism from the retrieved list. + for i := range a.MechanismsList { + for _, rm := range a.RetrievedMechanismList { + if a.MechanismsList[i].Type().Equal((gssapi.OID)(rm)) { + if a.Mechanism, err = a.MechanismsList[i].New(ctx); err != nil { + continue + } + return a.Mechanism, nil + } + } + } + + return nil, err } diff --git a/ssp/spnego/gssapi.go b/ssp/spnego/gssapi.go index 4f2f9131..d2f6e231 100644 --- a/ssp/spnego/gssapi.go +++ b/ssp/spnego/gssapi.go @@ -63,6 +63,7 @@ func (Mechanism) New(ctx context.Context) (gssapi.Mechanism, error) { // set capabilities. c.Capabilities = cc.Capabilities + c.IsServer = cc.IsServer // check that mechanism list is not empty. if len(c.MechanismsList) == 0 { @@ -110,8 +111,25 @@ func (m *Mechanism) Init(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, } +func (m *Mechanism) Capabilities(ctx context.Context) gssapi.Cap { + return m.Mechanism.Capabilities(ctx) +} + func (m *Mechanism) Accept(ctx context.Context, tok *gssapi.Token) (*gssapi.Token, error) { - return nil, nil + + b, err := m.ServerRespond(ctx, tok.Payload) + if err != nil { + b, err1 := m.Reject(ctx) + if err1 != nil { + return nil, gssapi.ContextError(ctx, gssapi.Failure, err) + } + return &gssapi.Token{Payload: b}, gssapi.ContextError(ctx, gssapi.Failure, err) + } + + return &gssapi.Token{ + Payload: b, + }, nil + } func (m *Mechanism) WrapSizeLimit(ctx context.Context, sz int, conf bool) int {