funcrsaSignKey()(*rsa.PrivateKey,error){privKeyPath:=filepath.Join(utils.GetRootPath(),"key folder","private key file")signBytes,err:=ioutil.ReadFile(privKeyPath)iferr!=nil{returnnil,err}signKey,err:=jwt.ParseRSAPrivateKeyFromPEM(signBytes)iferr!=nil{returnnil,err}returnsignKey,nil}funcmain(){signKey,err:=rsaSignKey()now:=time.Now()jwtToken:=jwt.NewWithClaims(jwt.GetSigningMethod("RS256"),&jwt.RegisteredClaims{ExpiresAt:jwt.NewNumericDate(now.Add(1*time.Hour*24)),IssuedAt:jwt.NewNumericDate(now),NotBefore:jwt.NewNumericDate(now),ID:"id",Issuer:"jaejin",Subject:"1",})str,err:=jwtToken.SignedString(signKey)iferr!=nil{log.Fatalf("can not sign jwt %s",err)}log.Printf("%s\n",str)}
golang에서 예전에 token인증을 위해 작성했던 코드이다. 기존엔 private key를 파일로 읽어서 SignedString function에 파라미터로 넣어주고 jwt pacakage 내부의 Sign fucntion으로 인해 jwt token값을 바로 받을 수 있었다.
typeSigningMethodinterface{Verify(signingString,signaturestring,keyinterface{})error// Returns nil if signature is valid
Sign(signingStringstring,keyinterface{})(string,error)// Returns encoded signature or error
Alg()string// returns the alg identifier for this method (example: 'HS256')
}func(m*SigningMethodRSA)Alg()string{returnm.Name}// Verify implements token verification for the SigningMethod
// For this signing method, must be an *rsa.PublicKey structure.
func(m*SigningMethodRSA)Verify(signingString,signaturestring,keyinterface{})error{varerrerror// Decode the signature
varsig[]byteifsig,err=base64.RawURLEncoding.DecodeString(signature);err!=nil{returnerr}varrsaKey*rsa.PublicKeyvarokboolifrsaKey,ok=key.(*rsa.PublicKey);!ok{returnErrInvalidKeyType}// Create hasher
if!m.Hash.Available(){returnErrHashUnavailable}hasher:=m.Hash.New()hasher.Write([]byte(signingString))// Verify the signature
returnrsa.VerifyPKCS1v15(rsaKey,m.Hash,hasher.Sum(nil),sig)}// Sign implements token signing for the SigningMethod
// For this signing method, must be an *rsa.PrivateKey structure.
func(m*SigningMethodRSA)Sign(signingStringstring,keyinterface{})(string,error){varrsaKey*rsa.PrivateKeyvarokbool// Validate type of key
ifrsaKey,ok=key.(*rsa.PrivateKey);!ok{return"",ErrInvalidKey}// Create the hasher
if!m.Hash.Available(){return"",ErrHashUnavailable}hasher:=m.Hash.New()hasher.Write([]byte(signingString))// Sign the string and return the encoded bytes
ifsigBytes,err:=rsa.SignPKCS1v15(rand.Reader,rsaKey,m.Hash,hasher.Sum(nil));err==nil{returnbase64.RawURLEncoding.EncodeToString(sigBytes),nil}else{return"",err}}
KMS를 이용하면 따로 private key를 받는 것이 아닌 KMS쪽으로 api를 날려서 인증을 받아야하기 때문에 이 interface를 재 정의하고 KMS쪽으로 인증을 받아오는 방식으로 구현하면 될 것 같다.
varpubkeyCache=newPubKeyCache()typepubKeyCachestruct{pubKeysmap[string]crypto.PublicKeymutexsync.RWMutex}funcnewPubKeyCache()*pubKeyCache{return&pubKeyCache{pubKeys:make(map[string]crypto.PublicKey),}}func(c*pubKeyCache)Add(keyIDstring,keycrypto.PublicKey){c.mutex.Lock()deferc.mutex.Unlock()c.pubKeys[keyID]=key}func(c*pubKeyCache)Get(keyIDstring)crypto.PublicKey{c.mutex.RLock()deferc.mutex.RUnlock()returnc.pubKeys[keyID]}func(m*RSASigningMethod)Verify(signingString,signaturestring,keyConfiginterface{})error{kmsConfig,ok:=keyConfig.(*KMSConfig)if!ok{returnjwt.ErrInvalidKeyType}sig,err:=base64.RawURLEncoding.DecodeString(signature)iferr!=nil{returnfmt.Errorf("decoding signature: %w",err)}if!m.hash.Available(){returnjwt.ErrHashUnavailable}hasher:=m.hash.New()hasher.Write([]byte(signingString))returnverifyRSA(kmsConfig,m.hash,hasher.Sum(nil),sig)}funcverifyRSA(kmsConfig*KMSConfig,hashcrypto.Hash,hashedSigningString[]byte,sig[]byte)error{varrsaPublicKey*rsa.PublicKeycachedKey:=pubkeyCache.Get(kmsConfig.keyID)ifcachedKey==nil{getPubKeyOutput,err:=kmsConfig.Client.GetPublicKey(kmsConfig.ctx,&kms.GetPublicKeyInput{KeyId:aws.String(kmsConfig.keyID),})iferr!=nil{returnfmt.Errorf("getting public key: %w",err)}cachedKey,err=x509.ParsePKIXPublicKey(getPubKeyOutput.PublicKey)iferr!=nil{returnfmt.Errorf("parsing public key: %w",err)}pubkeyCache.Add(kmsConfig.keyID,cachedKey)}rsaPublicKey,ok:=cachedKey.(*rsa.PublicKey)if!ok{returnerrors.New("invalid key type for key")}iferr:=rsa.VerifyPKCS1v15(rsaPublicKey,hash,hashedSigningString,sig);err!=nil{returnfmt.Errorf("verifying signature locally: %w",err)}returnnil}
verify 부분은 public key가 사용되고 KMS에서 생성된 key의 public key를 가져와 이용한다.
var(SigningMethodRS256*RSASigningMethod)funcmain(){SigningMethodRS256=&RSASigningMethod{name:"RS256",hash:crypto.SHA256,}jwt.RegisterSigningMethod(SigningMethodRS256.Alg(),func()jwt.SigningMethod{returnSigningMethodRS256})ctx:=context.TODO()cfg,err:=config.LoadDefaultConfig(ctx,config.WithRegion("ap-northeast-2"),)iferr!=nil{log.Fatal(err)}// sts assume role
stsclient:=sts.NewFromConfig(cfg)awsCfg,err:=config.LoadDefaultConfig(context.Background(),config.WithRegion("ap-northeast-2"),config.WithCredentialsProvider(aws.NewCredentialsCache(stscreds.NewAssumeRoleProvider(stsclient,"{role arn}",)),))now:=time.Now()jwtToken:=jwt.NewWithClaims(SigningMethodRS256,&jwt.RegisteredClaims{ExpiresAt:jwt.NewNumericDate(now.Add(1*time.Hour*24)),IssuedAt:jwt.NewNumericDate(now),NotBefore:jwt.NewNumericDate(now),ID:generateRandomString(40),Issuer:"jaejin",Subject:"1",})kmsClient:=kms.NewFromConfig(awsCfg)kmsConfig:=NewKMSConfig(kmsClient,keyID)str,err:=jwtToken.SignedString(kmsConfig)iferr!=nil{log.Fatalf("can not sign jwt %s",err)}log.Printf("%s\n",str)claims:=jwt.RegisteredClaims{}_,err=jwt.ParseWithClaims(str,&claims,func(token*jwt.Token)(interface{},error){returnkmsConfig,nil})iferr!=nil{log.Fatalf("%s",err)}log.Printf("%v",claims)}
AWS 인증을 STS Assume role을 이용해서 한번 더 임시 token을 발급해서 사용하기 위해 sts client와 role arn을 추가 적으로 설정하는 부분이 있었고
verify하는 부분에 jwt.ParseWithClaims 내부에서 등록된 jwt signing method를 처리하는 로직이 존재하여서 초반부분에 새로 구성한 RSASigningMethod을 등록한 부분까지 확인 할 수 있다.