> For the complete documentation index, see [llms.txt](https://apidoc.tokenpay.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apidoc.tokenpay.me/ko-kr/undefined-1/undefined-3.md).

# 서명 생성

{% hint style="info" %}
팁 (TIP)

가맹점은 아래 단계에 따라 요청 서명을 생성할 수 있으며, 플랫폼은 요청을 받은 후 서명을 검증합니다. 서명 검증에 실패하면 요청이 거부되고 적절한 [상태 코드](/ko-kr/undefined-5/undefined.md)(status code)가 반환됩니다.
{% endhint %}

### 준비 (Preparation)

가맹점은 가맹점 번호를 등록하고 가맹점 백스테이지를 통해 결제 애플리케이션을 생성하여 `APP_ID` 및 `APP_SECRECT`를 획득해야 합니다.

### 조건 (Condition)

이 문서에서 `POST` 메서드의 모든 인터페이스는 서명을 검증해야 하며, 다른 인터페이스는 현재 검증할 필요가 없습니다.

### 생성 (Generation)

서명 문자열은 4줄로 구성되며, 각 줄마다 하나의 매개변수가 있습니다. 줄은 `\n`(줄바꿈, ASCII 인코딩 값 0x0A)으로 끝나며, 마지막 줄에는 `\n`을 추가하지 마십시오. 매개변수 자체가 `\n`으로 끝나는 경우에도 `\n`을 추가로 붙여야 합니다.

```
URL\n
Request timestamp\n
Request random string\n
Request message body
```

[주문 조회](/ko-kr/undefined-3/undefined-1.md)(order inquiry)를 예로 들어 보겠습니다.

첫 번째 단계는 요청의 절대 URL을 가져오고 도메인 이름 부분을 제거하여 참여 서명된 URL을 얻는 것입니다. 요청에 쿼리 매개변수가 있는 경우 URL에 '? '와 해당 쿼리 문자열을 추가해야 합니다.

```
/v1/transaction/query
```

두 번째 단계는 요청이 시작될 때 시스템의 현재 타임스탬프(밀리초)를 가져오는 것입니다. 즉, 1970년 1월 1일 00:00:00 GMT부터 현재까지의 총 초를 요청 타임스탬프로 사용합니다. 플랫폼은 오래전에 이루어진 요청의 처리를 거부하므로 가맹점 자체 시스템의 시간을 정확하게 유지하십시오.

```
1554208460
```

세 번째 단계는 32비트 `무작위 문자열 (random string)`을 생성하는 것입니다.

```
593BEC0C930BF1AFEB40B4A08C8FB242
```

네 번째 단계는 요청(request body)에서 요청 메시지 본문을 가져오는 것입니다.

```json
{
    "app_id": "8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd",
    "mch_id": "12345678",
    "transaction_id": "e98b30294xxxxxxxxxxxx97a9d9e09ce",
    "out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"
}
```

다섯 번째 단계는 다음과 같이 이전 규칙에 따라 요청 서명 문자열을 구성하는 것입니다.

```html
/v1/transaction/query\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
{"app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd","mch_id":"12345678","transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce","out_trade_no":"fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb"}
```

### 암호화 (Encryption)

자바스크립트 암호화 과정을 예로 들어 보겠습니다:

```json
let cryptoJs = require("crypto-js");
let key = CryptoJS.enc.Utf8.parse("9db664697xxxxxxxxxxxx2d27a3c925c") //the secret key of APP
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(Signature string, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.Pkcs7,
}).toString();
```

암호화 후 예시:

```
QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh
```

### 연결 (Concatenate)

서명 정보의 연결 형식은 다음과 같습니다.\
app\_id=`APP_ID`, mch\_id=`가맹점 ID`, nonce\_str=`3단계에서 생성된 무작위 문자열`, timestamp=`2단계에서 생성된 타임스탬프`, signature=`암호화된 서명 문자열`.

### HTTP 헤더 (HTTP header)

문서 API는 HTTP `Authorization` 헤더를 통해 서명을 전달합니다. `Authorization`은 `인증 유형`과 `서명 정보`의 두 부분으로 구성됩니다. 현재 `인증 유형`은 `TTPAY-AES-256-ECB`만 지원합니다.

```
Authorization: authentication type  signature information
```

Authorization 헤더는 다음과 같습니다. (참고: 조판상 줄바꿈이 포함될 수 있으므로 이는 예시이며 실제 데이터는 한 줄에 있어야 합니다.)

```
Authorization: TTPAY-AES-256-ECB app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh"
```

마지막으로 서명이 포함된 HTTP 요청을 생성할 수 있습니다.

```sh
$ curl https://api.tokenpay.me/v1/transaction/query -H "Content-Type: application/json" -H 'Authorization: TTPAY-AES-256-ECB
 app_id=8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd,mch_id=12345678,nonce_str=593BEC0C930BF1AFEB40B4A08C8FB242,timestamp=1554208460,signature=QCwHvoBM9TJ2wokF8hhaoS34P0nkJpYMisBUizpOj5q/77I6+KFPVvFUCaaUiu+KFctisJFU1DfJdCHrLpJIx9CirX5ku3L9TMGihFcEG8MGoh2dwDvunH8JgJOVV9ClSkpXqjad4flSuYMoxPOZqPHr+ktOLZ3pPzs12BMqmbZVNIe+oOezTZsQ8xxxxRgOJzwU/AbouZSl2xto7DcYCjvNSnw7BkuzBFgTfxVXB3+R7e+1SpdeJajuCKGKvYMVTe7slS5j/4LQ4vcr1QqOPhpoemsOV92tPhgQ0iGw3GKpLIEOoDAwy2+ojzP5XERh' -X POST -d '{"out_trade_no": "fb72xxxx-xxxx-xxxx-xxxx-xxxx8a7b52cb", "transaction_id":"e98b30294xxxxxxxxxxxx97a9d9e09ce", "app_id":"8e4b8c2e7cxxxxxxxx1a1cbd3d59e0bd", "mch_id":"12345678" }'
```

### 데모 코드 (Demo code)

{% tabs %}
{% tab title="Java" %}

```java
package com.example.http;

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.sun.istack.internal.NotNull;
import okhttp3.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;

public class HttpApplication {
    private static String SECRET = "AES";
    private static String CIPHER_ALGORITHM = "AES/ECB/PKCS7Padding";
    private static String schema = "TTPAY-AES-256-ECB ";

    public static void main(String[] args) throws Exception {
        String key = "xxxxxxxx";    // AppSecret
        String reqURL = "/v1/transaction/query";    // Request interface

        OkHttpClient client = new OkHttpClient();
        JSONObject params = new JSONObject();

        try {
            params.put("app_id", "xxxxxxxx");           // Application ID
            params.put("mch_id", "xxxxxxxx");           // Merchat ID
            params.put("transaction_id", "xxxxxxxx");   // Platform order number
            params.put("out_trade_no", "xxxxxxxx");     // Merchat order number
        } catch (JSONException e) {
            e.printStackTrace();
        }

        Security.addProvider(new BouncyCastleProvider());

        // Signature
        String auth = auth(reqURL, params, key);

        RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), params.toString());
        Request request = new Request.Builder()
        .header("Authorization", auth)
        .url(reqURL)
        .post(body)
        .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("http request error");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println(response.body().string());
            }
        });
    }

    /**
    * Signature
    * @param reqURL Request URL
    * @param raw Message body
    * @param key AppSecret
    * @return Signature information
    */
    public static String auth(String reqURL, JSONObject raw, String key) throws Exception {
        HttpUrl parseReqURL = HttpUrl.parse(reqURL);
        String url = parseReqURL.encodedPath();
        String appID = raw.getString("app_id");
        String mchID = raw.getString("mch_id");
        long currentTimeMillis = System.currentTimeMillis();
        String timestamp = String.valueOf(currentTimeMillis);
        String nonceStr = randomString(32);
        String message = url + "\n" + timestamp + "\n" + nonceStr + "\n" + raw.toString();
        String aes256ECBPkcs7PaddingEncrypt = aes256ECBPkcs7PaddingEncrypt(message, key);

        return schema + "app_id=" + appID + ",mch_id=" + mchID + ",nonce_str=" + nonceStr + ",timestamp=" + timestamp + ",signature=" + aes256ECBPkcs7PaddingEncrypt;
    }

    /**
    * Generate random string
    * @param len String length
    * @return Random string
    */
    public static String randomString(Integer ...len) {
        int e = len.length <= 0 ? 32 : len[0];
        String str = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
        int strLen = str.length();
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < e; i++) {
            double random = Math.random();
            int v = (int) Math.floor(random * strLen);
            char charAt = str.charAt(v);

            stringBuilder.append(charAt);
        }

        return stringBuilder.toString();
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Cryptographic string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingEncrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));

        return new String(Base64.getEncoder().encode(doFinal));
    }

    /**
    * AES encryption
    * @param str String
    * @param key Secret key
    * @return Decryption string
    * @throws Exception Abnormal information
    */
    public static String aes256ECBPkcs7PaddingDecrypt(String str, String key) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);

        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, SECRET));

        byte[] doFinal = cipher.doFinal(Base64.getDecoder().decode(str));

        return new String(doFinal);
    }
}
```

{% endtab %}

{% tab title="Go" %}

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"time"

	"github.com/forgoer/openssl"
)

const (
	SignatureMessageFormat = "%s\n%d\n%s\n%s" // The original format of the digital signature
	HeaderAuthorizationFormat = "%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s"
)

type QueryReq struct {
	AppID         string `json:"app_id"`
	MchID         string `json:"mch_id"`
	TransactionID string `json:"transaction_id,omitempty"`
	OutTradeNo    string `json:"out_trade_no,omitempty"`
}

func main() {

	appSecret := "**********"           // AppSecret
	reqPath := "/v1/transaction/query"  // Request interface path
	queryReq := QueryReq{
		AppID:         "**********",    // Application ID
		MchID:         "**********",    // Merchat ID
		TransactionID: "**********",    // Platform order number
		OutTradeNo:    "**********",    // Merchat order number
	}

	b, _ := json.Marshal(queryReq)

    // Signature
	authorization, err := GenerateAuthorizationHeader(queryReq.AppID, queryReq.MchID, reqPath, string(b), appSecret)
	if err != nil {
		panic(err)
	}

    // Request interface
	url := "" + reqPath

	payload := bytes.NewBuffer(b)

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", authorization)

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))
}

// Signature
func GenerateAuthorizationHeader(appID, mchID, reqURL, signBody, appSecret string) (string, error) {
	nonceStr := RandomString(32)
	timestamp := time.Now().Unix()
	message := fmt.Sprintf(SignatureMessageFormat, reqURL, timestamp, nonceStr, signBody)

	signatureResult, err := AesECBEncrypt(message, appSecret)
	if err != nil {
		return "", err
	}
	authorization := fmt.Sprintf(
		HeaderAuthorizationFormat, getAuthorizationType(), appID,
		mchID, nonceStr, timestamp, signatureResult,
	)
	return authorization, nil
}

// AES encryption
func AesECBEncrypt(orig, key string) (string, error) {
	dst, _ := openssl.AesECBEncrypt([]byte(orig), []byte(key), openssl.PKCS7_PADDING)
	return base64.StdEncoding.EncodeToString(dst), nil
}

// AES encryption
func AesECBDecrypt(crypted, key string) (string, error) {
	x := len(crypted) * 3 % 4
	switch {
	case x == 2:
		crypted += "=="
	case x == 1:
		crypted += "="
	}
	crytedByte, err := base64.StdEncoding.DecodeString(crypted)
	if err != nil {
		return "", err
	}
	origData, err := openssl.AesECBDecrypt(crytedByte, []byte(key), openssl.PKCS7_PADDING)
	if err != nil {
		return "", err
	}
	return string(origData), err
}

// Generate random character
func RandomString(length int) string {
	str := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	var result []byte
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := 0; i < length; i++ {
		result = append(result, str[rnd.Intn(len(str))])
	}
	return string(result)
}

func getAuthorizationType() string {
	return "TTPAY-AES-256-ECB"
}
```

{% endtab %}

{% tab title="PHP" %}

```php
<?php
$key = "xxxxxxxx";                //  AppSecret
$APP_ID = "xxxxxxxx";              // Application ID
$mchID = "xxxxxxxx";              // Merchat ID
$transactionID = "xxxxxxxx";      // Platform order number
$outTradeNo = "xxxxxxxx";         // Merchat order number
$url = "/v1/transaction/query";   // Interface path
$timestamp = time();              // Current timestamp
$nonce = generateStr(32);         // 32-bit random string

// Generate random string
function generateStr($length) {
  $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  $res ='';

  for ( $i = 0; $i < $length; $i++ ) {
    $res .= $chars[ mt_rand(0, strlen($chars) - 1) ];
  }

  return $res;
}

// Message body
$body = '{"app_id":"'.$APP_ID.'","mch_id":"'.$mchID.'","transaction_id":"'.$transactionID.'","out_trade_no":"'.$outTradeNo.'"}';

// Construct signature string
$message = $url."\n".$timestamp."\n".$nonce."\n".$body;

// Encrypt signature string
$cipher = "aes-256-ecb";
$sign = openssl_encrypt($message, $cipher, $key);

// Split signature information
$schema = 'TTPAY-AES-256-ECB';
$authorization = sprintf('%s app_id=%s,mch_id=%s,nonce_str=%s,timestamp=%d,signature=%s', $schema, $APP_ID, $mchID, $nonce, $timestamp, $sign);

// Set HTTP header
$header[] = "Accept: application/json";
$header[] = "Content-Type: application/json";
$header[] = "Authorization: $authorization";

// Request interface
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_URL, "".$url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);

var_export($result);
```

<br>
{% endtab %}

{% tab title="JavaScript" %}

```javascript
let cryptoJs = require("crypto-js");

let jsonObj = JSON.parse(pm.request.body.raw); //Convert the submitted body string into an object
let jsonStr = JSON.stringify(jsonObj)//Turn the object back into a string to compress, avoiding line breaks in the body that affect signature compression.
let appID = jsonObj.app_id;
let mchID = jsonObj.mch_id;

// Generate the current timestamp
let timestamp = Math.round(new Date() / 1000).toString()

//Define random string
var nonceStr = randomString(); // Random string
function randomString(e) {
    e = e || 32;
    var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    a = t.length,
    n = "";
    for (i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));
    return n
}

//Get the full path
var reqURL = "/" + pm.request.url.path.join("/");

//Splice signature plaintext
message = reqURL + "\n" + timestamp + "\n" + nonceStr + "\n" + jsonStr

let key = CryptoJS.enc.Utf8.parse("********************************") // AppSecret
//AES encryption
let ciphertext = cryptoJs.AES.encrypt(message, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  }).toString();

//Splice on the authorization of Header, TTPAY-{EncryMode)},currently supports only AES-256-ECB
let authorization = 'TTPAY-AES-256-ECB app_id=' + appID + ',mch_id=' + mchID + ',nonce_str=' + nonceStr + ',timestamp=' + timestamp + ',signature=' + ciphertext

pm.request.headers.upsert({ key: "authorization", value: authorization })
```

{% endtab %}
{% endtabs %}
