Commits

rimmington  committed 46fd561 Draft

added CMS compressed data type from RFC3274

  • Participants
  • Parent commits 74cfdc5

Comments (0)

Files changed (8)

File crypto/bouncycastle/compression.ceylon

+import ceylon.io.buffer { newByteBuffer }
+
+import java.lang { arrays }
+import java.util.zip { Deflater, Inflater }
+
+import crypto.spec { Compression, Zlib }
+import crypto.spec.util { makeIterable }
+
+shared object javaCompression
+        satisfies Compression<Zlib> {
+    shared actual {Integer+} compress(Zlib algorithm, [Integer+] bytes) =>
+        makeIterable(() {
+            value deflater = Deflater();
+            deflater.setInput(arrays.toByteArray(bytes));
+            deflater.finish();
+            
+            value buf = newByteBuffer(1024);
+            buf.limit = 0;
+            
+            return () {
+                if (buf.hasAvailable) {
+                    return buf.get();
+                } else if (!deflater.needsInput()) {
+                    buf.position = 0;
+                    buf.limit = deflater.deflate(arrays.asByteArray(buf.bytes()));
+                    if (buf.hasAvailable) {
+                        return buf.get();
+                    }
+                }
+                
+                return finished;
+            };
+        });
+
+    shared actual {Integer+} decompress(Zlib algorithm, [Integer+] bytes) =>
+        makeIterable(() {
+            value inflater = Inflater();
+            inflater.setInput(arrays.toByteArray(bytes));
+            
+            value buf = newByteBuffer(1024);
+            buf.limit = 0;
+            
+            return () {
+                if (buf.hasAvailable) {
+                    return buf.get();
+                } else if (!inflater.needsInput()) {
+                    buf.position = 0;
+                    buf.limit = inflater.inflate(arrays.asByteArray(buf.bytes()));
+                    if (buf.hasAvailable) {
+                        return buf.get();
+                    }
+                }
+                
+                return finished;
+            };
+        });
+}

File crypto/bouncycastle/test/run.ceylon

 
 import org.bouncycastle.jce.provider { BouncyCastleProvider }
 
-import crypto.spec { aes256Ofb, sha1 }
-import crypto.bouncycastle { bouncyEcdh }
+import crypto.spec { aes256Ofb, sha1, zlib }
+import crypto.spec.util { codepointsOf, asCodepoints }
+import crypto.spec.asn1 { CompressedData, AlgorithmIdentifier, ArbitraryData, OctetString, EncapsulatedContentInfo, dataContentType, asn1Decodable, ASN1Sequence, asn1ContentInfo }
+import crypto.bouncycastle { bouncyEcdh, javaCompression }
 
 "Run the tests for the module `crypto.bouncycastle`"
 void run() {
     addSecurityProvider(BouncyCastleProvider());
     
     intraop();
+    cmsCompression();
 }
 
 void intraop() {
     value bobSecret = bob.complete(alice.sharedPart, aes256Ofb, sha1);
     
     assert(aliceSecret == bobSecret);
+}
+
+void cmsCompression() {
+    assert(nonempty bytes = codepointsOf("Hello world!").sequence);
+    
+    value arbData = ArbitraryData { content = OctetString(bytes); };
+    assert(nonempty compressed = javaCompression.compress(zlib,
+            arbData.asn1.asBytes).sequence);
+    
+    value contentInfo = CompressedData {
+        compressionAlgorithm = AlgorithmIdentifier {
+            identifier = zlib.identifier;
+            params = null;
+        };
+        encapContentInfo = EncapsulatedContentInfo {
+            contentType = dataContentType;
+            content = OctetString(compressed);
+        };
+    };
+    
+    value asBytes = contentInfo.asn1.asBytes;
+    assert(is ASN1Sequence asn1 = asn1Decodable(asBytes));
+    assert(is CompressedData cms = asn1ContentInfo(asn1));
+    assert(contentInfo == cms);
+    
+    assert(nonempty retrieved = cms.encapContentInfo.content.bytes);
+    assert(nonempty decompressed = javaCompression.decompress(zlib, retrieved)
+            .sequence);
+    
+    assert(is ASN1Sequence retArbDataSeq = asn1Decodable(decompressed));
+    assert(is ArbitraryData retArbData = asn1ContentInfo(retArbDataSeq));
+    assert(arbData == retArbData);
+    
+    assert(is OctetString codepoints = retArbData.content);
+    assert(String(asCodepoints(codepoints.bytes)) == "Hello world!");
 }

File crypto/spec/algorithms.ceylon

     shared actual ObjectIdentifier hmacIdentifier => nothing;
 }
 
-shared object whirlpool extends Whirlpool() {}
+shared object whirlpool extends Whirlpool() {}
+
+shared interface CompressionAlgorithm
+        satisfies NamedAlgorithm {}
+
+shared abstract class Zlib()
+        of zlib
+        satisfies CompressionAlgorithm {
+    // id-alg-zlibCompress OBJECT IDENTIFIER ::= { iso(1) member-body(2)
+    //    us(840) rsadsi(113549) pkcs(1) pkcs-9(9) smime(16) alg(3) 8 }
+    shared actual ObjectIdentifier identifier =
+        ObjectIdentifier([1, 2, 840, 113549, 1, 9, 16, 3, 8]);
+}
+
+shared object zlib extends Zlib() {}

File crypto/spec/asn1/asn1.ceylon

         contextTag.or(derTags.tagged).or(derTags.constructed);
 
     shared actual Integer[] body => wrappedObject.asBytes;
+    
+    shared actual String string => "ExplicitTag(``contextTag``)(``wrappedObject``)";
 }
 
 shared class ImplicitTag(contextTag, wrappedObject)
     
     shared actual Integer tag => contextTag.or(derTags.tagged);
     shared actual Integer[] body => wrappedObject.body;
+    
+    shared actual String string => "ImplicitTag(``contextTag``)(``wrappedObject``)";
 }
 
 shared abstract class OpaqueImplicitTag(contextTag)

File crypto/spec/asn1/cms.ceylon

-import ceylon.math.whole { wholeNumber }
+import ceylon.math.whole { wholeNumber, zero }
 
 shared abstract class ContentType([Integer+] identifier)
-    of dataContentType|signedDataContentType|envelopedDataContentType
+    of dataContentType|signedDataContentType|envelopedDataContentType|
+       compressedDataContentType
     extends ObjectIdentifier(identifier) {}
 
 ObjectIdentifier pkcs7 = ObjectIdentifier([1, 2, 840, 113549, 1, 7]);
 //id-data OBJECT IDENTIFIER ::= { iso(1) member-body(2)
 //         us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 }
-object dataContentType extends ContentType(pkcs7.sequence.withTrailing(1)) {}
+shared object dataContentType extends ContentType(pkcs7.sequence.withTrailing(1)) {}
 //id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
 //         us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
-object signedDataContentType extends ContentType(pkcs7.sequence.withTrailing(2)) {}
+shared object signedDataContentType extends ContentType(pkcs7.sequence.withTrailing(2)) {}
 //id-envelopedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
 //          us(840) rsadsi(113549) pkcs(1) pkcs7(7) 3 }
-object envelopedDataContentType extends ContentType(pkcs7.sequence.withTrailing(3)) {}
+shared object envelopedDataContentType extends ContentType(pkcs7.sequence.withTrailing(3)) {}
+//id-ct-compressedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
+//         us(840) rsadsi(113549) pkcs(1) pkcs-9(9) smime(16) ct(1) 9 }
+shared object compressedDataContentType extends ContentType([1, 2, 840, 113549, 1, 9, 16, 1, 9]) {}
 
 shared abstract class ContentInfo(contentType)
-        of ArbitraryData|SignedData|EnvelopedData
+        of ArbitraryData|SignedData|EnvelopedData|CompressedData
         satisfies ASN1Representable {
     shared ContentType contentType;
     shared formal ASN1Representable content;
         encryptionAlgorithm.asn1,
         ImplicitTag(0, content)
     ]);
+    
+    shared actual Boolean equals(Object other) {
+        if (is EncryptedContentInfo other) {
+            return contentType == other.contentType &&
+                encryptionAlgorithm == other.encryptionAlgorithm &&
+                content == other.content;
+        } else {
+            return false;
+        }
+    }
 }
 
-class EncapsulatedContentInfo(contentInfo)
+shared class EncapsulatedContentInfo(contentType, content)
         satisfies ASN1Representable {
-    shared ContentInfo contentInfo;
+    shared ContentType contentType;
+    shared OctetString content;
+
+    shared actual ASN1Sequence asn1 => ASN1Sequence([
+        contentType,
+        ExplicitTag(0, content)
+    ]);
     
-    shared actual ASN1Sequence asn1 => ASN1Sequence([
-        contentInfo.contentType,
-        ExplicitTag(0, OctetString(contentInfo.content.asn1.asBytes))
-    ]);
+    shared actual Boolean equals(Object other) {
+        if (is EncapsulatedContentInfo other) {
+            return contentType == other.contentType &&
+                content == other.content;
+        } else {
+            return false;
+        }
+    }
 }
 
 shared class ArbitraryData(content)
         extends ContentInfo(dataContentType) {
     shared actual ASN1Representable content;
+    
+    shared actual Boolean equals(Object other) {
+        if (is ArbitraryData other) {
+            return content == other.content;
+        } else {
+            return false;
+        }
+    }
 }
 
 class SignedData(digestAlgorithms, encapContentInfo, signerInfos)
     // calculated as per http://tools.ietf.org/html/rfc5652#section-5.1
     shared Integer version {
         if (signerInfos.any((SignerInfo e) => e.version == 3) ||
-                encapContentInfo.contentInfo.contentType != dataContentType) {
+                encapContentInfo.contentType != dataContentType) {
             return 3;
         } else {
             return 1;
     shared actual ASN1Sequence asn1 => ASN1Sequence([keyIdentifier]);
 }
 
+"As defined in RFC3274: Compressed Data Content Type for Cryptographic Message
+ Syntax (CMS)."
+shared class CompressedData(compressionAlgorithm, encapContentInfo)
+        extends ContentInfo(compressedDataContentType) {
+    shared Integer version = 0;
+    shared AlgorithmIdentifier compressionAlgorithm;
+    shared EncapsulatedContentInfo encapContentInfo;
+
+    shared actual ASN1Sequence content => ASN1Sequence([
+        ASN1Integer(wholeNumber(version)),
+        compressionAlgorithm.asn1,
+        encapContentInfo.asn1
+    ]);
+    
+    shared actual Boolean equals(Object other) {
+        if (is CompressedData other) {
+            return version == other.version &&
+                compressionAlgorithm == other.compressionAlgorithm &&
+                encapContentInfo == other.encapContentInfo;
+        } else {
+            return false;
+        }
+    }
+}
+
 ContentType checkContentType(ObjectIdentifier identifier) {
-    if (dataContentType == identifier) {
-        return dataContentType;
-    } else if (signedDataContentType == identifier) {
-        return signedDataContentType;
-    } else if (envelopedDataContentType == identifier) {
-        return envelopedDataContentType;
-    } else {
-        throw Exception("Found non-content-type identifier " + identifier.string);
+    for (contentType in [dataContentType, signedDataContentType,
+            envelopedDataContentType, compressedDataContentType]) {
+        if (contentType == identifier) {
+            return contentType;
+        }
     }
+
+    throw Exception("Found non-content-type identifier " + identifier.string);
 }
 
 EncapsulatedContentInfo asn1EncapsulatedContentInfo(ASN1Sequence asn1) {
     assert(asn1.sequence.size == 2);
     assert(is ObjectIdentifier identifier = asn1.sequence.first);
     assert(is ExplicitTag wrappedEncContent = asn1.sequence.last);
-    assert(wrappedEncContent.tag == 0);
+    assert(wrappedEncContent.contextTag == 0);
     assert(is OctetString encContentString = wrappedEncContent.wrappedObject);
-    assert(nonempty encContent = encContentString.bytes);
-    value content = asn1Decodable(encContent);
     
-    return EncapsulatedContentInfo(createContentInfo(identifier, content));
+    return EncapsulatedContentInfo(checkContentType(identifier), encContentString);
 }
 
 SignerIdentifier asn1SignerIdentifier(ExplicitTag tag) {
     return EnvelopedData(recipientInfos, encryptedContentInfo);
 }
 
+CompressedData asn1CompressedData(ASN1Sequence asn1) {
+    assert(asn1.sequence.size == 3);
+    assert(is ASN1Integer version = asn1.sequence.first);
+    assert(version.whole == zero);
+    
+    assert(is ASN1Sequence algorithmSeq = asn1.sequence[1]);
+    value compressionAlgorithm = asn1AlgorithmIdentifier(algorithmSeq);
+    
+    assert(is ASN1Sequence encapContentInfoSeq = asn1.sequence[2]);
+    value encapContentInfo = asn1EncapsulatedContentInfo(encapContentInfoSeq);
+    
+    return CompressedData(compressionAlgorithm, encapContentInfo);
+}
+
 ContentInfo createContentInfo(ObjectIdentifier identifier, ASN1Representable content) {
     assert(is ASN1Decodable content);
     switch (checkContentType(identifier))
         assert(is ASN1Sequence content);
         return asn1EnvelopedData(content);
     }
+    case (compressedDataContentType) {
+        assert(is ASN1Sequence content);
+        return asn1CompressedData(content);
+    }
 }
 
 shared ContentInfo asn1ContentInfo(ASN1Sequence asn1) {

File crypto/spec/asn1/x509.ceylon

 shared class AlgorithmIdentifier(identifier, params)
         satisfies ASN1Representable {
     shared ObjectIdentifier identifier;
-    shared ASN1Representable params;
+    shared ASN1Representable? params;
 
-    shared actual ASN1BasicObject asn1 => ASN1Sequence([identifier, params.asn1]);
+    shared actual ASN1BasicObject asn1 {
+        if (exists params) {
+            return ASN1Sequence([identifier, params.asn1]);
+        } else {
+            return ASN1Sequence([identifier]);
+        }
+    }
+    
+    shared actual Boolean equals(Object other) {
+        if (is AlgorithmIdentifier other) {
+            if (exists params) {
+                if (exists otherParams = other.params) {
+                    return identifier == other.identifier &&
+                        params == otherParams;
+                } else {
+                    return false;
+                }
+            } else {
+                return identifier == other.identifier;
+            }
+        } else {
+            return false;
+        }
+    }
 }
 
 shared AlgorithmIdentifier asn1AlgorithmIdentifier(ASN1Sequence seq) {
-    assert(seq.sequence.size == 2);
     assert(is ObjectIdentifier identifier = seq.sequence.first);
-    assert(is ASN1Representable params = seq.sequence[1]);
-    return AlgorithmIdentifier(identifier, params);
+    switch (seq.sequence.size)
+    case (1) {
+        return AlgorithmIdentifier(identifier, null);
+    }
+    case (2) {
+        assert(is ASN1Representable params = seq.sequence[1]);
+        return AlgorithmIdentifier(identifier, params);
+    }
+    else {
+        throw Exception("Bad algorithm identifier ASN1 sequence length: " +
+            seq.sequence.size.string);
+    }
 }
 
 shared class SubjectPublicKeyInfo(algorithmIdentifier, keyData)

File crypto/spec/compression.ceylon

+shared interface Compression<SupportedAlgorithms>
+        given SupportedAlgorithms satisfies CompressionAlgorithm {
+    shared formal {Integer+} compress(SupportedAlgorithms algorithm,
+        [Integer+] bytes);
+    shared formal {Integer+} decompress(SupportedAlgorithms algorithm,
+        [Integer+] bytes);
+}

File crypto/spec/util/iterable.ceylon

-Iterable<Element, Absent> makeIterable<Element, Absent>(Callable<Element|Finished, []>() generator)
+shared Iterable<Element, Absent> makeIterable<Element, Absent>(Callable<Element|Finished, []>() generator)
         given Absent satisfies Null {
     object iterable satisfies {Element+} {
         shared actual Iterator<Element> iterator() {