テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

S3認証をコード調査する

AmazonS3Clientが何をしているのか

S3のクライアントを実装する時, AmazonS3Clientというクラスを使う. このクラスは認証(Credential)を食う. そして, 認証に関する一切の事柄は隠蔽される. 認証周辺のコードは, v2のみのコードからv4のコードを混ぜていったからだと思うが, 設計がぐちゃぐちゃであり, 一度リファクタリングが必要だと思う. (後から追加したv4の設計はまぁまぁクリーンだが, v2のコードが中途半端にv4の概念に合わせようとしているのでうんこになってる)

SingerFactoryは, タグと, 対応したSignerクラスを食っている.

public class AmazonS3Client extends AmazonWebServiceClient implements AmazonS3 {

    static {
        // Enable S3 specific predefined request metrics.
        AwsSdkMetrics.addAll(Arrays.asList(S3ServiceMetric.values()));

        // Register S3-specific signers.
        SignerFactory.registerSigner(S3_SIGNER, S3Signer.class);
        SignerFactory.registerSigner(S3_V4_SIGNER, AWSS3V4Signer.class);
    }

    /**
     * Returns a "complete" S3 specific signer, taking into the S3 bucket, key,
     * and the current S3 client configuration into account.
     */
    protected Signer createSigner(final Request<?> request,
                                  final String bucketName,
                                  final String key) {
        final Signer signer = getSignerByURI(request.getEndpoint());

AmazonS3Clientの中ではgetSignerByURIを呼んで, URIからv2/v4の判定を行っているわけだが, その処理自体は親クラスにある.

    /**
     * Returns the signer based on the given URI and the current AWS client
     * configuration. Currently only the SQS client can have different region on
     * a per request basis. For other AWS clients, the region remains the same
     * on a per AWS client level.
     * <p>
     * Note, however, the signer returned for S3 is incomplete at this stage as
     * the information on the S3 bucket and key is not yet known.
     */
    public Signer getSignerByURI(URI uri) {
        return computeSignerByURI(uri, signerRegionOverride, true);
    }

    private Signer computeSignerByURI(URI uri, String signerRegionOverride,
            boolean isRegionIdAsSignerParam) {
        if (uri == null) {
            throw new IllegalArgumentException(
                    "Endpoint is not set. Use setEndpoint to set an endpoint before performing any request.");
        }
        String service = getServiceNameIntern();
        String region = AwsHostNameUtils.parseRegionName(uri.getHost(), service);
        return computeSignerByServiceRegion(
                service, region, signerRegionOverride, isRegionIdAsSignerParam);
    }

    private Signer computeSignerByServiceRegion(
            String serviceName, String regionId,
            String signerRegionOverride,
            boolean isRegionIdAsSignerParam) {
        String signerType = clientConfiguration.getSignerOverride();
        Signer signer = signerType == null
             ? SignerFactory.getSigner(serviceName, regionId)
             : SignerFactory.getSignerByTypeAndService(signerType, serviceName)
             ;

ここまでが, ふつう(非プリサイン)認証の分岐に関して.

v4の認証

v4の認証はAWSS3V4Signerで行われる. v2/v4という認証自体はAWS一般の概念なのだが, S3の認証はやや特殊であり, クラス階層を作ることによって対処されている(FUCK!!!!)

/**
 * AWS4 signer implementation for AWS S3
 */
public class AWSS3V4Signer extends AWS4Signer {
    // v4ではPresignする時, payloadをUNSIGNED-PAYLOADという文字列で固定する
    @Override
    protected String calculateContentHashPresign(SignableRequest<?> request){
        return "UNSIGNED-PAYLOAD";
    }

AWS4Singerというのはv4一般の認証方法である. (典型的なテンプレートパターンの)AbstractAWSSignerは, 通常の認証(sign)を実装するものであり,

public class AWS4Signer extends AbstractAWSSigner implements
        ServiceAwareSigner, RegionAwareSigner, Presigner {

   // signatureを計算してから, AUTHORIZATIONヘッダを足している
   public void sign(SignableRequest<?> request, AWSCredentials credentials) {
        request.addHeader(
                AUTHORIZATION,
                buildAuthorizationHeader(request, signature,
                        sanitizedCredentials, signerParams));

        processRequestPayload(request, signature, signingKey,
                signerParams);
    }

    public void presignRequest(SignableRequest<?> request, AWSCredentials credentials,
            Date userSpecifiedExpirationDate) {


        request.addParameter(X_AMZ_SIGNATURE, BinaryUtils.toHex(signature));
    }

Presignerは, プリサインするものである. ただしこのクラスは, S3のv2認証では使われていない(FUCK!!!!).

public interface Presigner {
    /**
     * Signs the request by adding the signature to the URL rather than as a
     * header. This method is expected to modify the passed-in request to
     * add the signature.
     *
     * @param request      The request to sign.
     * @param credentials  The credentials to sign it with.
     * @param expiration   The time when this presigned URL will expire.
     */
    public void presignRequest(SignableRequest<?> request,
                               AWSCredentials credentials,
                               Date expiration);
}

v2のプリサインは?

v2のふつう認証は同じように実装されているが, プリサインについては別パスとなっている.

以下のコードはAmazonS3Clientから

    public URL generatePresignedUrl(GeneratePresignedUrlRequest req) {
        Signer signer = createSigner(request, bucketName, key);

        if (signer instanceof Presigner) {
            // If we have a signer which knows how to presign requests,
            // delegate directly to it.
            ((Presigner) signer).presignRequest(
                request,
                awsCredentialsProvider.getCredentials(),
                req.getExpiration()
            );
        } else {
            // Otherwise use the default presigning method, which is hardcoded
            // to use QueryStringSigner.
            presignRequest(
                request,
                req.getMethod(),
                bucketName,
                key,
                req.getExpiration(),
                null
            );
        }

これは何を言ってるかというと,

  • プリサインしようとした
  • Signerを取り出した(ふつう認証と同様)
  • そいつがPresignerのサブクラスだったらv4とみなす
  • そうじゃなかったらv2とみなす

本来ならばこのif分岐は消滅して, v2もPresignerで実装されるべきだと思うが, そうなっていない. (私の勘では, v2自体はobsoleteに近いので, 将来的にコードから消すつもりなんだろう. だからわざわざサポートする必要がない. Presignerはおそらく, あとになって追加されたinterfaceなのだと思う)

presignedRequestは, S3QueryStringSignerを使っている. これがS3のv2認証を実装しているクラスである.

    protected <T> void presignRequest(Request<T> request, HttpMethod methodName,
            String bucketName, String key, Date expiration, String subResource) {
        ....
        new S3QueryStringSigner(methodName.toString(), resourcePath, expiration)
            .sign(request, credentials);

Request/SignableRequest型

これらの型はコメントによると, internal use onlyであり, 外野は使うなということになっている. なぜだ.

S3サーバで認証をする方法としては,

  1. リクエストを解析してシグネチャを自分で計算する: AWS SDKを通して送られてきたリクエストは完全であるため, シグネチャの計算においては, クライアントサイドよりはストレートにいける?
  2. DefaultRequest(これもinternal use only)にリクエストの情報をコピーして, AUTORIZATIONヘッダを再追加させる
  3. S3サーバ上のリクエスト型に対してSignableRequestのアダプタを作り(ry

これら3つが考えられるが, コメントによると1しか許容されていないことになる. 糞以外の表現方法がない. 本来ならば, シグネチャを計算する部分をファクタアウトすべきである. さてさてどうしようかということで悩んでいる.