/** * Copyright (c) 2013, Joyent, Inc. All rights reserved. */ package io.urmia.auth.joyent; import com.google.api.client.http.HttpRequest; import io.urmia.auth.CryptoException; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.util.encoders.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileReader; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.*; /** * Joyent HTTP authorization signer. This adheres to the specs of the node-http-signature spec. * * @author Yunong Xiao */ public class HttpVerifier { private static final Logger LOG = LoggerFactory.getLogger(HttpVerifier.class); private static final String AUTHZ_SIGNING_STRING = "date: %s"; private static final String AUTHZ_PATTERN = "signature=\""; static final String SIGNING_ALGORITHM = "SHA256WithRSAEncryption"; static { Security.addProvider(new BouncyCastleProvider()); } /** * Returns a new {@link io.urmia.auth.joyent.HttpVerifier} instance that can be used to sign and verify requests according to the * joyent-http-signature spec. * * @see <a href="http://github.com/joyent/node-http-signature/blob/master/http_signing.md">node-http-signature</a> * @param keyPath * The path to the rsa key on disk. * @return An instance of {@link io.urmia.auth.joyent.HttpVerifier}. * @throws java.io.IOException * If the key is invalid. */ public static HttpVerifier newInstance(String keyPath) throws IOException { return new HttpVerifier(keyPath); } private final PublicKey publicKey; /** * @param keyPath * @throws java.io.IOException */ private HttpVerifier(String keyPath) throws IOException { LOG.debug("initializing HttpSigner with keypath: {}", keyPath); publicKey = readPublicKey(keyPath); } private PublicKey readPublicKey(String publicKeyPath) throws IOException { FileReader fileReader = new FileReader(publicKeyPath); PEMParser keyReader = new PEMParser(fileReader); JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); Object keyPair = keyReader.readObject(); keyReader.close(); return converter.getPublicKey((SubjectPublicKeyInfo)keyPair); } public final boolean verifyRequest(HttpRequest request) throws CryptoException { LOG.debug("verifying request: " + request.getHeaders()); String date = request.getHeaders().getDate(); if (date == null) { throw new CryptoException("no date header in request"); } date = String.format(AUTHZ_SIGNING_STRING, date); try { Signature verify = Signature.getInstance(SIGNING_ALGORITHM); verify.initVerify(publicKey); String authzHeader = request.getHeaders().getAuthorization(); int startIndex = authzHeader.indexOf(AUTHZ_PATTERN); if (startIndex == -1) { throw new CryptoException("invalid authorization header " + authzHeader); } String encodedSignedDate = authzHeader.substring(startIndex + AUTHZ_PATTERN.length(), authzHeader.length() - 1); byte[] signedDate = Base64.decode(encodedSignedDate.getBytes("UTF-8")); verify.update(date.getBytes("UTF-8")); return verify.verify(signedDate); } catch (NoSuchAlgorithmException e) { throw new CryptoException("invalid algorithm", e); } catch (InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (SignatureException e) { throw new CryptoException("invalid signature", e); } catch (UnsupportedEncodingException e) { throw new CryptoException("invalid encoding", e); } } }