Commit 5a573083 authored by rajashree's avatar rajashree
Browse files

Remove use of saml library's middleware code

Remove middleware use
parent 48e414a5
Showing with 245 additions and 158 deletions
+245 -158
......@@ -73,7 +73,6 @@ func (p *adProvider) testAndApply(actionName string, action *types.Action, reque
return err
}
fmt.Printf("\nuserPrincipal: %v\n", userPrincipal)
//if this works, save adConfig CR adding enabled flag
config.Enabled = configApplyInput.Enabled
err = p.saveActiveDirectoryConfig(config)
......
......@@ -76,7 +76,7 @@ func Configure(ctx context.Context, mgmt *config.ScaledContext) {
providersByType[client.FreeIpaConfigType] = p
providersByType[publicclient.FreeIpaProviderType] = p
p = saml.Configure(ctx, mgmt, userMGR, saml.PingName)
p = saml.Configure(ctx, mgmt, userMGR, tokenMGR, saml.PingName)
providers[saml.PingName] = p
providersByType[client.PingConfigType] = p
providersByType[publicclient.PingProviderType] = p
......
......@@ -85,7 +85,6 @@ func (h *loginHandler) createLoginToken(request *types.APIContext) (v3.Token, st
var userPrincipal v3.Principal
var groupPrincipals []v3.Principal
providerInfo := make(map[string]string)
fmt.Printf("\nHERE for creating login request for :%v\n", request.Type)
logrus.Debugf("Create Token Invoked")
bytes, err := ioutil.ReadAll(request.Request.Body)
......@@ -145,7 +144,10 @@ func (h *loginHandler) createLoginToken(request *types.APIContext) (v3.Token, st
return v3.Token{}, "", err
}
} else {
saml.PerformAuthRedirect(providerName, request)
err := saml.PerformAuthRedirect(providerName, request, h.tokenMGR)
if err != nil {
return v3.Token{}, "saml", nil
}
return v3.Token{}, "saml", nil
}
......
......@@ -57,7 +57,10 @@ func (s *Provider) configureTest(actionName string, action *types.Action, reques
annotations["configured"] = "true"
samlConfig.Annotations = annotations
s.saveSamlConfig(samlConfig)
s.SamlClient.config = samlConfig
p := SamlProviders[s.Name]
p.SamlClient.config = samlConfig
p.Request = request
request.WriteResponse(http.StatusOK, data)
return nil
......@@ -82,13 +85,14 @@ func (s *Provider) testAndApply(actionName string, action *types.Action, request
}
samlConfig = samlConfigApplyInput.SamlConfig
s.Request = request
p := SamlProviders[s.Name]
p.SamlClient.config = &samlConfig
p.Request = request
redirectURL := s.formSamlRedirectURL(&samlConfig)
http.Redirect(request.Response, request.Request, redirectURL, http.StatusFound)
fmt.Printf("\n\nYES it's here to set\n\n")
samlConfig.Enabled = samlConfigApplyInput.Enabled
//samlConfig.Enabled = samlConfigApplyInput.Enabled
err := s.saveSamlConfig(&samlConfig)
if err != nil {
return httperror.NewAPIError(httperror.ServerError, fmt.Sprintf("SAML: Failed to save saml config: %v", err))
......@@ -96,41 +100,3 @@ func (s *Provider) testAndApply(actionName string, action *types.Action, request
return nil
}
//func (s *Provider) testAndApply(actionName string, action *types.Action, request *types.APIContext) error {
// var samlConfig v3.SamlConfig
// samlConfigApplyInput := &v3.SamlConfigApplyInput{}
//
// if err := json.NewDecoder(request.Request.Body).Decode(samlConfigApplyInput); err != nil {
// return httperror.NewAPIError(httperror.InvalidBodyContent,
// fmt.Sprintf("SAML: Failed to parse body: %v", err))
// }
//
// samlConfig = samlConfigApplyInput.SamlConfig
// samlLogin := &v3public.CodeBasedLogin{
// Code: samlConfigApplyInput.Code,
// }
//
// //Call provider to testLogin
// userPrincipal, groupPrincipals, providerInfo, err := s.loginUser(samlLogin, &samlConfig, true)
// if err != nil {
// if httperror.IsAPIError(err) {
// return err
// }
// return fmt.Errorf("SAML: server error while authenticating: %v", err)
// }
//
// //if this works, save samlConfig CR adding enabled flag
// user, err := s.UserMGR.SetPrincipalOnCurrentUser(request, userPrincipal)
// if err != nil {
// return err
// }
//
// samlConfig.Enabled = samlConfigApplyInput.Enabled
// err = s.saveSamlConfig(&samlConfig)
// if err != nil {
// return httperror.NewAPIError(httperror.ServerError, fmt.Sprintf("SAML: Failed to save saml config: %v", err))
// }
//
// return tokens.CreateTokenAndSetCookie(user.Name, userPrincipal, groupPrincipals, providerInfo, 0, "Token via Saml Configuration", request)
//}
......@@ -16,6 +16,7 @@ import (
"time"
"github.com/crewjam/saml"
"github.com/crewjam/saml/logger"
"github.com/crewjam/saml/samlsp"
"github.com/gorilla/mux"
"github.com/rancher/rancher/pkg/auth/tokens"
......@@ -23,10 +24,10 @@ import (
log "github.com/sirupsen/logrus"
)
//SamlClient implements a client for the saml library
type SamlClient struct {
//Client implements a client for the saml library
type Client struct {
config *v3.SamlConfig
//SamlSP *samlsp.Middleware
SamlSP *saml.ServiceProvider
}
type IDPMetadata struct {
......@@ -140,15 +141,17 @@ func InitializeSamlClient(configToSet *v3.SamlConfig, name string) error {
return fmt.Errorf("SAML: error in parsing URL")
}
samlspInstance, err := samlsp.New(samlsp.Options{
IDPMetadataURL: nil,
URL: *actURL,
Key: privKey,
Certificate: cert,
})
if err != nil {
log.Errorf("SAML: Error initializing SAML SP instance from the config %v, error %v", configToSet, err)
metadataURL := *actURL
metadataURL.Path = metadataURL.Path + "/saml/metadata"
acsURL := *actURL
acsURL.Path = acsURL.Path + "/saml/acs"
sp := saml.ServiceProvider{
Key: privKey,
Certificate: cert,
MetadataURL: metadataURL,
AcsURL: acsURL,
Logger: logger.DefaultLogger,
}
// XML unmarshal throws an error for IdP Metadata cacheDuration field, as it's of type xml Duration. Using a separate struct for unmarshaling for now
......@@ -162,12 +165,12 @@ func InitializeSamlClient(configToSet *v3.SamlConfig, name string) error {
if err != nil {
return fmt.Errorf("SAML: cannot initialize saml SP, cannot get IDP Metadata from the url %v, error %v", idpURL, err)
}
samlspInstance.ServiceProvider.IDPMetadata = &saml.EntityDescriptor{}
sp.IDPMetadata = &saml.EntityDescriptor{}
if err := xml.NewDecoder(resp.Body).Decode(idm); err != nil {
return fmt.Errorf("SAML: cannot initialize saml SP, cannot decode IDP Metadata xml from the config %v, error %v", configToSet, err)
}
} else if configToSet.IDPMetadataContent != "" {
samlspInstance.ServiceProvider.IDPMetadata = &saml.EntityDescriptor{}
sp.IDPMetadata = &saml.EntityDescriptor{}
if err := xml.NewDecoder(strings.NewReader(configToSet.IDPMetadataContent)).Decode(idm); err != nil {
return fmt.Errorf("SAML: cannot initialize saml SP, cannot decode IDP Metadata content from the config %v, error %v", configToSet, err)
}
......@@ -177,35 +180,30 @@ func InitializeSamlClient(configToSet *v3.SamlConfig, name string) error {
return fmt.Errorf("SAML: cannot initialize saml SP, cannot read IDP Metadata file from the config %v, error %v", configToSet, err)
}
metadataReader := bufio.NewReader(file)
samlspInstance.ServiceProvider.IDPMetadata = &saml.EntityDescriptor{}
sp.IDPMetadata = &saml.EntityDescriptor{}
if err := xml.NewDecoder(metadataReader).Decode(idm); err != nil {
return fmt.Errorf("SAML: cannot initialize saml SP, cannot decode IDP Metadata xml from the config %v, error %v", configToSet, err)
}
}
samlspInstance.ServiceProvider.IDPMetadata.XMLName = idm.XMLName
samlspInstance.ServiceProvider.IDPMetadata.ValidUntil = idm.ValidUntil
samlspInstance.ServiceProvider.IDPMetadata.EntityID = idm.EntityID
samlspInstance.ServiceProvider.IDPMetadata.SPSSODescriptors = idm.SPSSODescriptors
samlspInstance.ServiceProvider.IDPMetadata.IDPSSODescriptors = idm.IDPSSODescriptors
//binding := saml.HTTPRedirectBinding
//bindingLocation := samlspInstance.ServiceProvider.GetSSOBindingLocation(binding)
//if bindingLocation == "" {
// binding = saml.HTTPPostBinding
// bindingLocation = samlspInstance.ServiceProvider.GetSSOBindingLocation(binding)
//}
//
//if p, ok := SamlProviders[name]; ok {
// if p != nil {
// p.RedirectURL = bindingLocation
// }
//}
sp.IDPMetadata.XMLName = idm.XMLName
sp.IDPMetadata.ValidUntil = idm.ValidUntil
sp.IDPMetadata.EntityID = idm.EntityID
sp.IDPMetadata.SPSSODescriptors = idm.SPSSODescriptors
sp.IDPMetadata.IDPSSODescriptors = idm.IDPSSODescriptors
provider.SamlClient.SamlSP = &sp
cookieStore := samlsp.ClientCookies{
ServiceProvider: &sp,
Name: "token",
Domain: actURL.Host,
}
provider.ClientState = &cookieStore
if name == PingName {
Root.Get("PingLogin").Handler(samlspInstance.RequireAccount(http.HandlerFunc(provider.HandleSamlPost)))
Root.Get("PingACS").Handler(samlspInstance)
Root.Get("PingMetadata").Handler(samlspInstance)
Root.Get("PingLogin").HandlerFunc(provider.HandleSamlLogin)
Root.Get("PingACS").HandlerFunc(provider.ServeHTTP)
Root.Get("PingMetadata").HandlerFunc(provider.ServeHTTP)
}
return nil
}
......@@ -220,6 +218,7 @@ func PingHandlers() *mux.Router {
if provider == nil {
return Root
}
storedSamlConfig, err := provider.getSamlConfig()
if err != nil {
log.Errorf("SAML(PingHandlers): error in getting config: %v", err)
......@@ -232,7 +231,7 @@ func PingHandlers() *mux.Router {
return Root
}
func (samlClient *SamlClient) getSamlIdentities(samlData map[string][]string) ([]Account, error) {
func (samlClient *Client) getSamlIdentities(samlData map[string][]string) ([]Account, error) {
//look for saml attributes set in the config
var samlAccts []Account
......@@ -269,34 +268,29 @@ func (samlClient *SamlClient) getSamlIdentities(samlData map[string][]string) ([
return samlAccts, nil
}
//HandleSamlPost handles the SAML Post
func (s *Provider) HandleSamlPost(w http.ResponseWriter, r *http.Request) {
// HandleSamlAssertion processes/handles the assertion obtained by the POST to /saml/acs from IdP
func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) {
var groupPrincipals []v3.Principal
var userPrincipal v3.Principal
log.Debugf("SAML: HandleSamlPost: request url is %v", r.URL.String())
cookie, _ := r.Cookie("token")
log.Debugf("SAML: token cookie: %v", cookie)
_, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
//failed to get the url query parameters
log.Errorf("SAML: HandleSamlPost failed to parse query params with error %v", err)
w.WriteHeader(500)
w.Write([]byte("Server error while authenticating"))
return
if relayState := r.Form.Get("RelayState"); relayState != "" {
// delete the cookie
s.ClientState.DeleteState(w, r, relayState)
}
// SAML library middleware, on a request with valid session, returns the assertion attributes within request's context
samlData := make(map[string][]string)
authToken := samlsp.Token(r.Context())
if authToken == nil {
log.Errorf("SAML: No assertions returned by IdP %v", err)
w.WriteHeader(403)
w.Write([]byte("No assertions returned by IdP"))
return
for _, attributeStatement := range assertion.AttributeStatements {
for _, attr := range attributeStatement.Attributes {
attrName := attr.FriendlyName
if attrName == "" {
attrName = attr.Name
}
for _, value := range attr.Values {
samlData[attrName] = append(samlData[attrName], value.Value)
}
}
}
samlData = authToken.Attributes
config, err := s.getSamlConfig()
if err != nil {
......@@ -347,7 +341,20 @@ func (s *Provider) HandleSamlPost(w http.ResponseWriter, r *http.Request) {
return
}
tokens.CreateTokenAndSetCookie(user.Name, userPrincipal, groupPrincipals, map[string]string{}, 0, "Token via Saml Configuration", s.Request)
config.Enabled = true
err = s.saveSamlConfig(config)
if err != nil {
log.Errorf("SAML: Error saving SAML config %v", err)
w.WriteHeader(500)
w.Write([]byte(" Error saving SAML config"))
return
}
isSecure := false
if r.URL.Scheme == "https" {
isSecure = true
}
setRancherToken(w, r, s.TokenMGR, user.Name, userPrincipal, groupPrincipals, isSecure)
return
}
......@@ -363,48 +370,30 @@ func (s *Provider) HandleSamlPost(w http.ResponseWriter, r *http.Request) {
return
}
rToken, err := tokens.NewLoginToken(user.Name, userPrincipal, groupPrincipals, map[string]string{}, 0, "")
if p, ok := SamlProviders[s.Name]; ok {
setRancherToken(w, r, p.PublicTokenMGR, user.Name, userPrincipal, groupPrincipals, true)
}
return
}
func setRancherToken(w http.ResponseWriter, r *http.Request, tokenMGR *tokens.Manager, userID string, userPrincipal v3.Principal,
groupPrincipals []v3.Principal, isSecure bool) {
rToken, err := tokenMGR.NewLoginToken(userID, userPrincipal, groupPrincipals, map[string]string{}, 0, "")
if err != nil {
log.Errorf("Failed creating token with error: %v", err)
w.WriteHeader(500)
w.Write([]byte("Server error while authenticating"))
return
}
tokenCookie := &http.Cookie{
Name: "R_SESS",
Value: rToken.ObjectMeta.Name + ":" + rToken.Token,
Secure: true,
Secure: isSecure,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, tokenCookie)
w.WriteHeader(http.StatusOK)
return
}
////HandleSamlPost handles the SAML Post
//func (s *Provider) HandleSamlPost(w http.ResponseWriter, r *http.Request) {
// log.Debugf("SAML: HandleSamlPost: request url is %v", r.URL.String())
// cookie, _ := r.Cookie("token")
// log.Debugf("SAML: token cookie: %v", cookie)
//
// query, err := url.ParseQuery(r.URL.RawQuery)
// if err != nil {
// //failed to get the url query parameters
// log.Errorf("SAML: HandleSamlPost failed to parse query params with error %v", err)
// w.WriteHeader(500)
// w.Write([]byte("SAML: Failed to get auth query parameters"))
// return
// }
//
// // SAML library middleware, on a request with valid session, returns the assertion attributes within request's context
// samlData := make(map[string][]string)
// samlData = samlsp.Token(r.Context()).Attributes
// log.Debugf("SAML: HandleSamlPost: Received a SAML POST data %v", samlData)
// mapB, err := json.Marshal(samlData)
// if err != nil {
// //failed to get the saml data
// log.Debugf("SAML: HandleSamlPost failed to unmarshal saml data with error %v", err)
// w.WriteHeader(500)
// w.Write([]byte(err.Error()))
// return
// }
//
// redirectURL := s.getSamlRedirectURL(query.Get(redirectBackBase), query.Get(redirectBackPath))
// w.Header().Add("code", string(mapB))
// log.Debugf("SAML: redirecting the user to %v", redirectURL)
// http.Redirect(w, r, redirectURL, http.StatusFound)
//}
package saml
import (
"crypto/x509"
"encoding/base64"
"encoding/xml"
"net/http"
"github.com/crewjam/saml"
"github.com/dgrijalva/jwt-go"
)
// ServeHTTP is the handler for /saml/metadata and /saml/acs endpoints
func (s *Provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
serviceProvider := s.SamlClient.SamlSP
if r.URL.Path == serviceProvider.MetadataURL.Path {
buf, _ := xml.MarshalIndent(serviceProvider.Metadata(), "", " ")
w.Header().Set("Content-Type", "application/samlmetadata+xml")
w.Write(buf)
return
}
if r.URL.Path == serviceProvider.AcsURL.Path {
r.ParseForm()
assertion, err := s.SamlClient.SamlSP.ParseResponse(r, s.getPossibleRequestIDs(r))
if err != nil {
if parseErr, ok := err.(*saml.InvalidResponseError); ok {
serviceProvider.Logger.Printf("RESPONSE: ===\n%s\n===\nNOW: %s\nERROR: %s",
parseErr.Response, parseErr.Now, parseErr.PrivateErr)
}
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
s.HandleSamlAssertion(w, r, assertion)
return
}
http.NotFoundHandler().ServeHTTP(w, r)
}
func (s *Provider) getPossibleRequestIDs(r *http.Request) []string {
rv := []string{}
serviceProvider := s.SamlClient.SamlSP
for _, value := range s.ClientState.GetStates(r) {
jwtParser := jwt.Parser{
ValidMethods: []string{jwt.SigningMethodHS256.Name},
}
token, err := jwtParser.Parse(value, func(t *jwt.Token) (interface{}, error) {
secretBlock := x509.MarshalPKCS1PrivateKey(serviceProvider.Key)
return secretBlock, nil
})
if err != nil || !token.Valid {
serviceProvider.Logger.Printf("... invalid token %s", err)
continue
}
claims := token.Claims.(jwt.MapClaims)
rv = append(rv, claims["id"].(string))
}
return rv
}
// HandleSamlLogin is the endpoint for /saml/login endpoint
func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) {
serviceProvider := s.SamlClient.SamlSP
if r.URL.Path == serviceProvider.AcsURL.Path {
panic("don't wrap Middleware with RequireAccount")
}
binding := saml.HTTPRedirectBinding
bindingLocation := serviceProvider.GetSSOBindingLocation(binding)
if bindingLocation == "" {
binding = saml.HTTPPostBinding
bindingLocation = serviceProvider.GetSSOBindingLocation(binding)
}
req, err := serviceProvider.MakeAuthenticationRequest(bindingLocation)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// relayState is limited to 80 bytes but also must be integrety protected.
// this means that we cannot use a JWT because it is way to long. Instead
// we set a cookie that corresponds to the state
relayState := base64.URLEncoding.EncodeToString(randomBytes(42))
secretBlock := x509.MarshalPKCS1PrivateKey(serviceProvider.Key)
state := jwt.New(jwt.SigningMethodHS256)
claims := state.Claims.(jwt.MapClaims)
claims["id"] = req.ID
claims["uri"] = r.URL.String()
signedState, err := state.SignedString(secretBlock)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.ClientState.SetState(w, r, relayState, signedState)
if binding == saml.HTTPRedirectBinding {
redirectURL := req.Redirect(relayState)
w.Header().Add("Location", redirectURL.String())
w.WriteHeader(http.StatusFound)
return
}
if binding == saml.HTTPPostBinding {
w.Header().Add("Content-Security-Policy", ""+
"default-src; "+
"script-src 'sha256-AjPdJSbZmeWHnEc5ykvJFay8FTWeTeRbs9dutfZ0HqE='; "+
"reflected-xss block; referrer no-referrer;")
w.Header().Add("Content-type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><body>`))
w.Write(req.Post(relayState))
w.Write([]byte(`</body></html>`))
return
}
}
func randomBytes(n int) []byte {
rv := make([]byte, n)
if _, err := saml.RandReader.Read(rv); err != nil {
panic(err)
}
return rv
}
......@@ -12,6 +12,7 @@ import (
"github.com/rancher/norman/httperror"
"github.com/rancher/norman/types"
"github.com/rancher/rancher/pkg/auth/providers/common"
"github.com/rancher/rancher/pkg/auth/tokens"
"github.com/rancher/types/apis/management.cattle.io/v3"
"github.com/rancher/types/apis/management.cattle.io/v3public"
"github.com/rancher/types/client/management/v3"
......@@ -21,6 +22,7 @@ import (
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/crewjam/saml/samlsp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
......@@ -28,25 +30,28 @@ import (
const PingName = "ping"
type Provider struct {
Ctx context.Context
AuthConfigs v3.AuthConfigInterface
UserMGR user.Manager
SamlClient *SamlClient
Name string
UserType string
GroupType string
RedirectURL string
Request *types.APIContext
Ctx context.Context
AuthConfigs v3.AuthConfigInterface
UserMGR user.Manager
TokenMGR *tokens.Manager
PublicTokenMGR *tokens.Manager
SamlClient *Client
Name string
UserType string
GroupType string
Request *types.APIContext
ClientState samlsp.ClientState
}
var SamlProviders = make(map[string]*Provider)
func Configure(ctx context.Context, mgmtCtx *config.ScaledContext, userMGR user.Manager, name string) common.AuthProvider {
samlClient := &SamlClient{}
func Configure(ctx context.Context, mgmtCtx *config.ScaledContext, userMGR user.Manager, tokenMGR *tokens.Manager, name string) common.AuthProvider {
samlClient := &Client{}
samlp := &Provider{
Ctx: ctx,
AuthConfigs: mgmtCtx.Management.AuthConfigs(""),
UserMGR: userMGR,
TokenMGR: tokenMGR,
SamlClient: samlClient,
Name: name,
UserType: name + "_user",
......@@ -82,21 +87,21 @@ func (s *Provider) AuthenticateUser(input interface{}) (v3.Principal, []v3.Princ
return s.loginUser(login, nil, false)
}
func PerformAuthRedirect(name string, apiContext *types.APIContext) string {
func PerformAuthRedirect(name string, apiContext *types.APIContext, publicTokenMGR *tokens.Manager) error {
var redirectURL string
if provider, ok := SamlProviders[name]; ok {
provider.PublicTokenMGR = publicTokenMGR
savedConfig, err := provider.getSamlConfig()
if err != nil {
fmt.Errorf("\nERROR!!: %v\n", err)
return fmt.Errorf("error in redirecting: %v", err)
}
redirectURL = provider.formSamlRedirectURL(savedConfig)
}
http.Redirect(apiContext.Response, apiContext.Request, redirectURL, http.StatusFound)
return ""
return nil
}
func (s *Provider) loginUser(samlCredential *v3public.CodeBasedLogin, config *v3.SamlConfig, test bool) (v3.Principal, []v3.Principal, map[string]string, error) {
......
......@@ -29,10 +29,10 @@ github.com/heptio/authenticator d282f87a19728018b67d80754bcb3e9b04
github.com/smartystreets/go-aws-auth 8ef1316913ee4f44bc48c2456e44a5c1c68ea53b
github.com/mcuadros/go-version 6d5863ca60fa6fe914b5fd43ed8533d7567c5b0b
github.com/rancher/rdns-server f79428ee317c10fa75f6bf2846aa6b88bff081ef
github.com/rancher/norman aecae32b4ae6b73b9945cdedef5a5b0dafa11973
github.com/rancher/norman 388faeb89eac2636101af8e279fb6a36967a192a
github.com/rancher/kontainer-engine 111ab41f43c979578f02d86667c8e5a821fc045e
github.com/rancher/rke 74b81a8766b7316c70f1d6dadcf794263539e60e
github.com/rancher/types 6aafbf88951b5af708ccf7e31c430c5f70656279 https://github.com/mrajashree/types.git
github.com/rancher/rke a75e3d693cd4395d74b2f92da47b1c1e17124342
github.com/rancher/types 197cc9660174384724d7a7a8e046466e8110239b https://github.com/mrajashree/types.git
gopkg.in/ldap.v2 v2.5.0
gopkg.in/asn1-ber.v1 v1.1
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment