blob: e1c60e3125ed925aeeb0c283e9740ec229401d6f [file] [log] [blame]
/* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.security.cryptauth.lib.securemessage;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.security.cryptauth.lib.securemessage.CryptoOps.EncType;
import com.google.security.cryptauth.lib.securemessage.CryptoOps.SigType;
import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.Header;
import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBody;
import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.HeaderAndBodyInternal;
import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SecureMessage;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
/**
* Utility class to parse and verify {@link SecureMessage} protos. Verifies the signature on the
* message, and decrypts "signcrypted" messages (while simultaneously verifying the signature).
*
* @see SecureMessageBuilder
*/
public class SecureMessageParser {
private SecureMessageParser() {} // Do not instantiate
/**
* Extracts the {@link Header} component from a {@link SecureMessage} but <em>DOES NOT VERIFY</em>
* the signature when doing so. Callers should not trust the resulting output until after a
* subsequent {@code parse*()} call has succeeded.
*
* <p>The intention is to allow the caller to determine the type of the protocol message and which
* keys are in use, prior to attempting to verify (and possibly decrypt) the payload body.
*/
public static Header getUnverifiedHeader(SecureMessage secmsg)
throws InvalidProtocolBufferException {
if (!secmsg.hasHeaderAndBody()) {
throw new InvalidProtocolBufferException("Missing header and body");
}
if (!HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).hasHeader()) {
throw new InvalidProtocolBufferException("Missing header");
}
Header result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody()).getHeader();
// Check that at least a signature scheme was set
if (!result.hasSignatureScheme()) {
throw new InvalidProtocolBufferException("Missing header field(s)");
}
// Check signature scheme is legal
try {
SigType.valueOf(result.getSignatureScheme());
} catch (IllegalArgumentException e) {
throw new InvalidProtocolBufferException("Corrupt/unsupported SignatureScheme");
}
// Check encryption scheme is legal
if (result.hasEncryptionScheme()) {
try {
EncType.valueOf(result.getEncryptionScheme());
} catch (IllegalArgumentException e) {
throw new InvalidProtocolBufferException("Corrupt/unsupported EncryptionScheme");
}
}
return result;
}
/**
* Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature.
*
* @return the parsed {@link HeaderAndBody} pair (which is fully verified)
* @throws SignatureException if signature verification fails
* @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[])
*/
public static HeaderAndBody parseSignedCleartextMessage(
SecureMessage secmsg, Key verificationKey, SigType sigType)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return parseSignedCleartextMessage(secmsg, verificationKey, sigType, null);
}
/**
* Parses a {@link SecureMessage} containing a cleartext payload body, and verifies the signature.
*
* @param associatedData optional associated data bound to the signature (but not in the message)
* @return the parsed {@link HeaderAndBody} pair (which is fully verified)
* @throws SignatureException if signature verification fails
* @see SecureMessageBuilder#buildSignedCleartextMessage(Key, SigType, byte[])
*/
public static HeaderAndBody parseSignedCleartextMessage(
SecureMessage secmsg, Key verificationKey, SigType sigType, @Nullable byte[] associatedData)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if ((secmsg == null) || (verificationKey == null) || (sigType == null)) {
throw new NullPointerException();
}
return verifyHeaderAndBody(
secmsg,
verificationKey,
sigType,
EncType.NONE,
associatedData,
false /* suppressAssociatedData is always false for signed cleartext */);
}
/**
* Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of
* the payload body and verifying the signature.
*
* @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted)
* @throws SignatureException if signature verification fails
* @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])
*/
public static HeaderAndBody parseSignCryptedMessage(
SecureMessage secmsg,
Key verificationKey,
SigType sigType,
Key decryptionKey,
EncType encType)
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
return parseSignCryptedMessage(secmsg, verificationKey, sigType, decryptionKey, encType, null);
}
/**
* Parses a {@link SecureMessage} containing an encrypted payload body, extracting a decryption of
* the payload body and verifying the signature.
*
* @param associatedData optional associated data bound to the signature (but not in the message)
* @return the parsed {@link HeaderAndBody} pair (which is fully verified and decrypted)
* @throws SignatureException if signature verification fails
* @see SecureMessageBuilder#buildSignCryptedMessage(Key, SigType, Key, EncType, byte[])
*/
public static HeaderAndBody parseSignCryptedMessage(
SecureMessage secmsg,
Key verificationKey,
SigType sigType,
Key decryptionKey,
EncType encType,
@Nullable byte[] associatedData)
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
if ((secmsg == null)
|| (verificationKey == null)
|| (sigType == null)
|| (decryptionKey == null)
|| (encType == null)) {
throw new NullPointerException();
}
if (encType == EncType.NONE) {
throw new SignatureException("Not a signcrypted message");
}
boolean tagRequired =
SecureMessageBuilder.taggedPlaintextRequired(verificationKey, sigType, decryptionKey);
HeaderAndBody headerAndEncryptedBody;
headerAndEncryptedBody = verifyHeaderAndBody(
secmsg,
verificationKey,
sigType,
encType,
associatedData,
tagRequired /* suppressAssociatedData if it is handled by the tag instead */);
byte[] rawDecryptedBody;
Header header = headerAndEncryptedBody.getHeader();
if (!header.hasIv()) {
throw new SignatureException();
}
try {
rawDecryptedBody = CryptoOps.decrypt(
decryptionKey, encType, header.getIv().toByteArray(),
headerAndEncryptedBody.getBody().toByteArray());
} catch (InvalidAlgorithmParameterException e) {
throw new SignatureException();
} catch (IllegalBlockSizeException e) {
throw new SignatureException();
} catch (BadPaddingException e) {
throw new SignatureException();
}
if (!tagRequired) {
// No tag expected, so we're all done
return HeaderAndBody.newBuilder(headerAndEncryptedBody)
.setBody(ByteString.copyFrom(rawDecryptedBody))
.build();
}
// Verify the tag that binds the ciphertext to the header, and remove it
byte[] headerBytes;
try {
headerBytes =
HeaderAndBodyInternal.parseFrom(secmsg.getHeaderAndBody()).getHeader().toByteArray();
} catch (InvalidProtocolBufferException e) {
// This shouldn't happen, but throw it up just in case
throw new SignatureException(e);
}
boolean verifiedBinding = false;
byte[] expectedTag = CryptoOps.digest(CryptoOps.concat(headerBytes, associatedData));
if (rawDecryptedBody.length >= CryptoOps.DIGEST_LENGTH) {
byte[] actualTag = CryptoOps.subarray(rawDecryptedBody, 0, CryptoOps.DIGEST_LENGTH);
if (CryptoOps.constantTimeArrayEquals(actualTag, expectedTag)) {
verifiedBinding = true;
}
}
if (!verifiedBinding) {
throw new SignatureException();
}
int bodyLen = rawDecryptedBody.length - CryptoOps.DIGEST_LENGTH;
return HeaderAndBody.newBuilder(headerAndEncryptedBody)
// Remove the tag and set the plaintext body
.setBody(ByteString.copyFrom(rawDecryptedBody, CryptoOps.DIGEST_LENGTH, bodyLen))
.build();
}
private static HeaderAndBody verifyHeaderAndBody(
SecureMessage secmsg,
Key verificationKey,
SigType sigType,
EncType encType,
@Nullable byte[] associatedData,
boolean suppressAssociatedData /* in case it is in the tag instead */)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (!secmsg.hasHeaderAndBody() || !secmsg.hasSignature()) {
throw new SignatureException("Signature failed verification");
}
byte[] signature = secmsg.getSignature().toByteArray();
byte[] data = secmsg.getHeaderAndBody().toByteArray();
byte[] signedData = suppressAssociatedData ? data : CryptoOps.concat(data, associatedData);
// Try not to leak the specific reason for verification failures, due to security concerns.
boolean verified = CryptoOps.verify(verificationKey, sigType, signature, signedData);
HeaderAndBody result = null;
try {
result = HeaderAndBody.parseFrom(secmsg.getHeaderAndBody());
// Even if declared required, micro proto doesn't throw an exception if fields are not present
if (!result.hasHeader() || !result.hasBody()) {
throw new SignatureException("Signature failed verification");
}
verified &= (result.getHeader().getSignatureScheme() == sigType.getSigScheme());
verified &= (result.getHeader().getEncryptionScheme() == encType.getEncScheme());
// Check that either a decryption operation is expected, or no DecryptionKeyId is set.
verified &= (encType != EncType.NONE) || !result.getHeader().hasDecryptionKeyId();
// If encryption was used, check that either we are not using a public key signature or a
// VerificationKeyId was set (as is required for public key based signature + encryption).
verified &= (encType == EncType.NONE) || !sigType.isPublicKeyScheme() ||
result.getHeader().hasVerificationKeyId();
int associatedDataLength = associatedData == null ? 0 : associatedData.length;
verified &= (result.getHeader().getAssociatedDataLength() == associatedDataLength);
} catch (InvalidProtocolBufferException e) {
verified = false;
}
if (verified) {
return result;
}
throw new SignatureException("Signature failed verification");
}
}