WEB/Security

OAuth 2.0 Resource Server - 검증 기초

Tony Lim 2022. 12. 23. 09:50
728x90

JCA(java crpytography architecture) & JCE(java cryptography extension) - 소개 및 구조

provider가 여러개 존재하고 provider 저장소에서 해당 알고리즘의 적합한 구현체 클래스를 찾아 클래스 인스턴스를 생성할수 있다.

 

Message Digest 는 원본 파일이 그대로인지 파악하는 무결성 검사이다.
MD알고리즘은 단방향이고 다양한 길이의 원본 값을 고정 길이 해시값으로 출력한다.
보통 대칭키에서 활용된다.

Signature는 초기화 시 제공받은 키를 사용해서 데이터를 서명하고 전자 서명의 유효성을 검증 하는데 사용된다.

서명 = Signature 객체는 개인키로 서명하기위해 초기화되고 서명할 원본 data가 제공된다.
Signature 의 sign() 은 개인 키로 원본 data를 서명하면 해시된 데이터를 암호화한 Signature Bytes를 반환한다.

검증 = 검증이 필요한 경우 검증을 위해 Signature 객체를 생성 및 초기화하고 개인키와 쌍을 이루는 해당 공개 키를 제공한다.
원본 data와 Signature Bytes가 검증 Signature 객체에 전달되고 verify()를 실행하면 공개키로 Signature Bytes의 해시데이터를 추출하고 원본데이터를 해시한 값과 비교해서 일치하면 Signature객체가 성공을 보고한다.

public class MessageDigestTest {
    public static void messageDigest(String message) throws Exception {
        createMd5(message);

        validateMd5(message);
    }

    private static void createMd5(String message) throws Exception{
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[10];
        random.nextBytes(salt);

        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
        messageDigest.update(salt);
        messageDigest.update(message.getBytes("UTF-8"));

        byte[] digest = messageDigest.digest();

        FileOutputStream fileOutputStream = new FileOutputStream("/home/tony/vscode/security/spring-security-oauth2/test");
        fileOutputStream.write(salt);
        fileOutputStream.write(digest);
        fileOutputStream.close();
    }

    private static void validateMd5(String message) throws Exception{
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream("/home/tony/vscode/security/spring-security-oauth2/test");
        int theByte = 0;
        while ((theByte = fis.read()) != -1)
            byteArrayOutputStream.write(theByte);
        fis.close();
        byte[] hashedMessage = byteArrayOutputStream.toByteArray();
        byteArrayOutputStream.reset();

        byte[] salt = new byte[10];
        System.arraycopy(hashedMessage,0,salt,0,10);
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(salt);
        md.update(message.getBytes("UTF-8"));
        byte[] digest = md.digest();

        byte[] digestInFile = new byte[hashedMessage.length - 10];
        System.arraycopy(hashedMessage,10,digestInFile,0,hashedMessage.length-10);

        if (Arrays.equals(digest,digestInFile))
            System.out.println("message matches.");
        else
            System.out.println("message does not matches");
    }
}

단방향이지만 일일이 노가다를 하면 원본을 알아낼수있기에 salt를 추가한것이다. 그리고 validation에서 쓸 수 있게 test파일에 salt + digest(salt와 message가 해시된값) 을 올려둔다.
validation 시점에 앞에 salt를 뺴오고 동일한 해싱을 시도한다. 
digest가 validation에서 hash(salt + message) 한 값이고 digestInFile은 createMd5에서 salt + digest(salt와 message가 해시된값) 주엥서 앞에 salt를 뺀 digest 값을 가져온것이고 이 둘을 비교해서 같은지 확인하는것이다.

public class SignatureTest {
    public static void signature(String message) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.genKeyPair();

        //전자 서명
        byte[] data = message.getBytes("UTF-8");
        Signature signature = Signature.getInstance("UTF-8");
        signature.initSign(keyPair.getPrivate());
        signature.update(data);

        byte[] sign = signature.sign();

        // 검증
        signature.initVerify(keyPair.getPublic());
        signature.update(data);

        boolean verified = false;

        try {
            verified = signature.verify(sign);
        }
        catch (SignatureException e) {
            System.out.println("전자서명 실행 중 오류발생");
            e.printStackTrace();
        }
        if(verified)
            System.out.println("전자서명 검증 성공");
        else
            System.out.println("전자서명 검증 실패");

    }
}

비대칭 키인 private , public key를 만든다. 

서명은 private key로 data를 사인하여 sign bytes을 만들어내고 검증시에는 public key로 와 원본 data로 verify를 시도하여 실제 auth server의 private key로 하였는지 확인하는 것이다.


대칭키 & 비대칭 키

대칭키

암호화 복호화에 같은 암호 키를 쓰는 알고리즘을 의미함 즉 서로 공유하고 있음

비대칭키 암호화 에 비해 속도가 빠르다는 장점

MAC(Message Authentication Code)

변조 되었는지 검증할 수 있도록 데이터에 덧붙이는 코드이다.

해시 값을 생성한다는 점에서 Message Digest 와 유사하지만 초기화시 대칭키를 요구한다는 점에서 다르다.
또한 Message Digest는 누구든지 무결성 검사가 가능하지만 MAC는 동일한 대칭키를 가진 쪽에서만 무결성을 검사할 수 있다.

HMAC은 JWT를 암호화할 때 사용되고 Message Digest알고리즘과 공유된 대칭키를 사용한다.

 

비대칭 키

외부에 절대 노출되어서는 안되는 개인키 + 공개적으로 개방되어있는 공개키를 쌍으로 이룬 형태이다.
A의 privatekey로 암호화된 data는 A의 public key로만 복호화가 가능하다.

2가지 암호학적 문제를 해결
1. 데이터 보안 = 송신자 공개키로 암호화 -> 송신자 개인키로 복호화 , 중간에 가로채도 개인키가 없으므로 아무 소용없음
2. 인증 = 송신자 개인키로 암호화 -> 송신자 공개키로 복호화를 통해 메시지를 인증가능함(송신자가 암호화한게 맞다!!)

RSA는 해시알고리즘으로 data를 먼저암호화 한후에 private key로 한번더 암호화 한다.
검증 단계에서는 동일하게 기존 data를 같은 해시알고리즘으로 암호화 하고 Digest 생성
전달받은 암호문을 public key로 복호화 한 결과값(Digest)을 비교하게 된다.

 

public class MacTest {
    public static void hmac(String data) throws Exception {
        hamcBase64("secretKey", data, "HmacMD5");
        hamcBase64("secretKey",data,"HmacSHA256");

    }

    private static void hamcBase64(String secret, String data, String algorithms) throws Exception{
        SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes("utf-8"), algorithms);
        Mac mac = Mac.getInstance(algorithms);
        mac.init(secretKey);
        byte[] hash = mac.doFinal(data.getBytes());
        String encodedStr = Base64.getEncoder().encodeToString(hash);
        System.out.println(algorithms + ": " + encodedStr);
    }
}

Hmac에서 암호화 알고리즘 + 대칭키를 사용하는것을 확인 할 수 있다.

public class RSAGen {
    public static KeyPair genKeyPair() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(1024, new SecureRandom());
        return gen.genKeyPair();
    }

    public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        byte[] bytePlain = cipher.doFinal(plainText.getBytes());
        return Base64.getEncoder().encodeToString(bytePlain);
    }

    public static String decrypt(String encrypted, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        byte[] byteEncrypted = Base64.getDecoder().decode(encrypted.getBytes());

        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] bytePlain = cipher.doFinal(byteEncrypted);
        return new String(bytePlain, "utf-8");
    }

    public static PublicKey getPublicKeyFromKeySpec(String base64PublicKey)  throws Exception {
        byte[] decodedBase64PubKey = Base64.getDecoder().decode(base64PublicKey);
        return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decodedBase64PubKey));
    }

    public static PrivateKey getPrivateKeyFromKeySpec(String base64PrivateKey) throws Exception {
        byte[] decodedBase64PrivateKey = Base64.getDecoder().decode(base64PrivateKey);
        return KeyFactory.getInstance("RSA").generatePrivate(new X509EncodedKeySpec(decodedBase64PrivateKey));
    }
}
public class RSATest {

    public static void rsa(String message) throws Exception {

        KeyPair keyPair = RSAGen.genKeyPair();
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        String encrypted = RSAGen.encrypt(message, publicKey);
        String decrypted = RSAGen.decrypt(encrypted, privateKey);

        System.out.println("message = " + message);
        System.out.println("decrypted = " + decrypted);

        // key spec 전환하기
        byte[] bytePublicKey = publicKey.getEncoded();
        String base64PublicKey = Base64.getEncoder().encodeToString(bytePublicKey);
        byte[] bytePrivateKey = privateKey.getEncoded();
        String base64PrivateKey = Base64.getEncoder().encodeToString(bytePrivateKey);

        // X.509 표준형식
        PublicKey X509PublicKey = RSAGen.getPublicKeyFromKeySpec(base64PublicKey);
        String encrypted2 = RSAGen.encrypt(message, X509PublicKey);
        String decrypted2 = RSAGen.decrypt(encrypted2, privateKey);

        System.out.println("message = " + message);
        System.out.println("decrypted2 = " + decrypted2);
        
        // PKCS8 표준형식
        PrivateKey PKCS8PrivateKey = RSAGen.getPrivateKeyFromKeySpec(base64PrivateKey);
        String decrypted3 = RSAGen.decrypt(encrypted2, PKCS8PrivateKey);

        System.out.println("message = " + message);
        System.out.println("decrypted3 = " + decrypted3);
    }
}

JWT

JOSE( JSON Object Signing and Encryption) = JSON 데이터의 컨텐츠를 암호화 또는 서명의 형태로 나타내기 위해 IETF에서 표준화 한 소프트웨어 기술 세트

JWT(JSON Web Token) = 클레임 기반 보안값을 나타내는 방법으로 두 당사자간에 안전하게 전달되는 클레임을 표현하기 위한 개방형 표준

JWS (JSON Web Signature) = JSON을 사용하여 디지털 서명 또는 MAC으로 보안된 콘텐츠를 표현하는 방법 , 보통 이방식으로 JWT를 구현한다.

JWE(JSON Web Encrpytion) = JSON을 사용하여 의도한 수신자만 읽을 수 있도록 암호화 데이터(토큰)를 나타내는 형식

JWK (JSON Web Key) = HMAC이나 타원 곡선 또는 RSA알고리즘을 사용하여 공개 키 세트를 JSON 객체로 나타내는 JSON 구조

JWA(JSON Web Algorithm) = JWS , JWK 및 JWE 에 필요한 알고리즘 목록으로 JWS 헤더 및 JWS 페이로드의 내용을 서명하는데 사용된다.

payload의 클레임값을 변조하여 토큰을 생성한 후 전달하더라도 서명에서 해시된 값과 변조된 값의 해시된 값이 서로 일치하지 않기 때문에 검증이 실패하여 데이터의 안전성을 보장

secretKey를 탈취당했을 경우에는 중요한 정보가 도난당할 수 있는 취약점이 있기에 key rotation 을 할 필요가 있다.


JWK 이해 및 활용하기

JWK = 암호화 키를 저장하는 방식으로 인가서버에서 발행하는 JWT 토큰의 암호화 및 서명에 필요한 암호화 키의 다양한 정보를 담은 JSON 객체 표준이다.

JwkSetUri 정보를 설정하면 인가서버로부터 JWK형태의 정보를 다운로드 할 수 있고 JWT를 검증할 수 있다.

public class JWKTest {

    public static void jwk() throws JOSEException, NoSuchAlgorithmException {

        // 비대칭키 JWK
        KeyPairGenerator rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
        rsaKeyPairGenerator.initialize(2048);

        KeyPair keyPair = rsaKeyPairGenerator.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        RSAKey rsaKey1 = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyUse(KeyUse.SIGNATURE)
                .algorithm(JWSAlgorithm.RS256)
                .keyID("rsa-kid1")
                .build();


        RSAKey rsaKey2 = new RSAKeyGenerator(2048)
                .keyID("rsa-kid2")
                .keyUse(KeyUse.SIGNATURE)
                .keyOperations(Set.of(KeyOperation.SIGN))
                .algorithm(JWSAlgorithm.RS512)
                .generate();

        // 대칭키 JWK
        SecretKey secretKey = new SecretKeySpec(
                Base64.getDecoder().decode("bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4="), "AES");

        OctetSequenceKey octetSequenceKey1 = new OctetSequenceKey.Builder(secretKey)
                .keyID("secret-kid1")
                .keyUse(KeyUse.SIGNATURE)
                .keyOperations(Set.of(KeyOperation.SIGN))
                .algorithm(JWSAlgorithm.HS256)
                .build();

        OctetSequenceKey octetSequenceKey2 = new OctetSequenceKeyGenerator(256)
                .keyID("secret-kid2")
                .keyUse(KeyUse.SIGNATURE)
                .keyOperations(Set.of(KeyOperation.SIGN))
                .algorithm(JWSAlgorithm.HS384)
                .generate();


        String kId;
//        kId = rsaKey1.getKeyID();
//        kId = rsaKey2.getKeyID();
        kId = octetSequenceKey1.getKeyID();
//        kId = octetSequenceKey2.getKeyID();

        JWSAlgorithm alg;
//        alg = (JWSAlgorithm)rsaKey1.getAlgorithm();
//        alg = (JWSAlgorithm)rsaKey2.getAlgorithm();
        alg = (JWSAlgorithm)octetSequenceKey1.getAlgorithm();
//        alg = (JWSAlgorithm)octetSequenceKey2.getAlgorithm();
//
        KeyType type;
        type = KeyType.RSA;
//        type = KeyType.OCT;

        jwkSet(kId,alg,type,rsaKey1,rsaKey2,octetSequenceKey1,octetSequenceKey2);
    }

    private static void jwkSet(String kid, JWSAlgorithm alg,KeyType type,JWK ...jwk) throws KeySourceException {

        JWKSet jwkSet = new JWKSet(List.of(jwk));
        JWKSource<SecurityContext> jwkSource =(jwkSelector, securityContext) -> jwkSelector.select(jwkSet);

        JWKMatcher jwkMatcher = new JWKMatcher.Builder()
                .keyType(type)
                .keyID(kid)
                .keyUses(KeyUse.SIGNATURE)
                .algorithms(alg)
                .build();

        JWKSelector jwkSelector = new JWKSelector(jwkMatcher);
        List<JWK> jwks = jwkSource.get(jwkSelector, null);

        if(!jwks.isEmpty()){

            JWK jwk1 = jwks.get(0);

            KeyType keyType = jwk1.getKeyType();
            System.out.println("keyType = " + keyType);

            String keyID = jwk1.getKeyID();
            System.out.println("keyID = " + keyID);

            Algorithm algorithm = jwk1.getAlgorithm();
            System.out.println("algorithm = " + algorithm);

        }

        System.out.println("jwks = " + jwks);
    }
}

대칭키 와 비대칭키를 Builder와 Generator로 만든것이다.

 

 

728x90