<* M2EXTENSIONS+ *>

IMPLEMENTATION MODULE WRSA;

        (********************************************************)
        (*                                                      *)
        (*                     RSA signing                      *)
        (*                                                      *)
        (*  Programmer:         P. Moylan                       *)
        (*  Started:            8 August 2023                   *)
        (*  Last edited:        8 January 2025                  *)
        (*  Status:             OK                              *)
        (*                                                      *)
        (*  This is an adaptation of the sshRSA module in       *)
        (*  my SSH implementation, retaining only those         *)
        (*  features needed for DKIM calculations.              *)
        (*                                                      *)
        (********************************************************)


IMPORT RSAKeys, HashAlgs, BigNum, VarStrings, RSA, Strings, Base64, ASN1;

FROM SYSTEM IMPORT CARD8, ADR, FILL;

FROM VarStrings IMPORT
    (* type *)  ByteStr, VarStringPtr, ByteStringPtr,
    (* proc *)  MakeBS, DiscardBS;

FROM BigNum IMPORT
    (* type *)  BN;

FROM TransLog IMPORT
    (* type *)  TransactionLogID,
    (* proc *)  LogTransactionL;

FROM FileOps IMPORT
    (* const*)  NoSuchChannel,
    (* type *)  ChanId,
    (* proc *)  OpenOldFile, ReadLine, CloseFile;

FROM MiscFuncs IMPORT
    (* proc *)  HexEncodeArray, HeadMatch;

FROM Inet2Misc IMPORT
    (* proc *)  Swap4;

FROM STextIO IMPORT
    (* proc *)  WriteChar, WriteString, WriteLn;

FROM LowLevel IMPORT
    (* proc *)  Copy, EVAL;

FROM Storage IMPORT
    (* proc *)  ALLOCATE, DEALLOCATE;

(************************************************************************)
(*               EXTRACTING THE COMPONENTS OF A PUBLIC KEY              *)
(************************************************************************)

(*
PROCEDURE TestPublicKey (publickey: RSAKeys.RSAKeyType);

    (* Debug check: checks that the public and private key are a        *)
    (* matched pair.  Of course this works only in a local test, where  *)
    (* the public key is our public key.                                *)

    VAR N, k: CARDINAL;  A, B, C: BN;
        original, decrypted: ARRAY [0..63] OF CHAR;
        binoriginal, bindecrypted: ARRAY [0..63] OF CARD8;

    BEGIN
        original := "The quick brown fox etc.";
        N := Strings.Length (original) + 1;
        Copy (ADR(original), ADR(binoriginal), N);

        (* Convert original text to a BigNum, then encrypt it with  *)
        (* the private key.                                         *)

        A := BigNum.BinToBN (binoriginal, 0, N);
        B := RSA.PrivKeyEncode (A, RSAKey);

        (* Decrypt with public key, convert back to string. *)

        C := RSA.PubKeyEncode (B, publickey);
        k := BigNum.BNtoBytes (C, bindecrypted);
        Copy (ADR(bindecrypted), ADR(decrypted), k);
        WriteString ("Decrypted result is ");
        WriteString (decrypted);  WriteLn;

        BigNum.Discard (B);  BigNum.Discard (C);

        (* For completeness, do it again in the opposite order. *)

        B := RSA.PubKeyEncode (A, publickey);
        C := RSA.PrivKeyEncode (B, RSAKey);
        Copy (ADR(bindecrypted), ADR(decrypted), k);
        WriteString ("Decrypted result is ");
        WriteString (decrypted);  WriteLn;

        BigNum.Discard (B);  BigNum.Discard (C);
        BigNum.Discard (A);

    END TestPublicKey;
*)

(************************************************************************)

PROCEDURE LoadPublicKey (VAR (*IN*) bindata: ARRAY OF CARD8): RSAKeys.RSAKeyType;

    (* Converts a binary string, in the format required by DKIM, to *)
    (* an RSA public key.                                           *)

    VAR N: CARDINAL;
        result: RSAKeys.RSAKeyType;
        bskey: ByteStr;
        part: ARRAY [0..1] OF ByteStr;

    BEGIN
        result := RSAKeys.InitKey();
        N := ASN1.ExtractParts (bindata, part);
        IF N <> 2 THEN
            RETURN result;
        END (*IF*);

        (* part[0] is the object ID, which I won't bother to check. *)
        (* part[1] is an ASN.1 BIT STRING, which I want to          *)
        (* interpret as a sequence of two integers.                 *)

        (* NOTE: ExtractParts generates pointers into the original  *)
        (* bindata array, so we don't have to deallocate the "part" *)
        (* results.  The caller just has to deallocate bindata.     *)
        (* CopyContent does, however, create a copy, so we need to  *)
        (* discard the data in bskey after using it.                *)

        bskey := ASN1.CopyContent (part[1].data^);
        EVAL (ASN1.ExtractParts (bskey.data^, part));

        (* Modulus (n). *)

        result.n := ASN1.GetInteger (part[0].data^);

        (* Exponent (public). *)

        result.public := ASN1.GetInteger (part[1].data^);

        DiscardBS (bskey);
        (*TestPublicKey (result);*)
        RETURN result;
    END LoadPublicKey;

(************************************************************************)
(*                        GENERATING A SIGNATURE                        *)
(************************************************************************)

PROCEDURE PKCS1_v1_5 (hashf: HashAlgs.HashAlgorithm;
                                digest: ARRAY OF CARD8;  k: CARDINAL): BN;

    (* Implements the EMSA-PKCS1-v1_5-ENCODE operation as specified in  *)
    (* section 9.2 (page 45) of RFC 8017.   This is the initial step    *)
    (* needed in creating a signature.  The result, as a byte string,   *)
    (* must be k bytes long.                                            *)

    (* Added later: The DKIM standard RFC 6376 specifies using the      *)
    (* obsolete standard RFC 3447 for signing, instead of the newer     *)
    (* RFC 8017.  I now need to check whether this makes a difference.  *)
    (* The revision history in RFC 8017 seems to suggest that the main  *)
    (* change was to allow more hashing algorithms, and we don't use    *)
    (* those newer ones.  The algorithms for the EMSA version of v1.5   *)
    (* of the algorithm show no difference, except for the mention of   *)
    (* the signature blob. I still need to track down where that blob   *)
    (* is mentioned.                                                    *)

    (* Further comment: RFC 3447 says that SSL/TLS handshake uses       *)
    (* RSAES-PKCS1-v1_5. While this is irrelevant to DKIM, I'll need    *)
    (* reread that while debugging my TLS implementation.  THE RSAES    *)
    (* version seems to rely on pseudo-random padding, while the EMSA   *)
    (* variant uses a constant fill.  Unhelpfully, the DKIM standard    *)
    (* fails to distinguish between these, falling back on the fiction  *)
    (* that there is a single algorithm called PKCS #1 version 1.5.     *)

    (* This differs from the v1.5 standard in that we don't do a hash   *)
    (* of the message. Instead, we assume that the caller has already   *)
    (* done that step, and that the "message" supplied to this          *)
    (* procedure is in fact a hash of the message.                      *)

    (* There is obvious scope here to do the job more efficiently, but  *)
    (* until this is known to be working it's best to stay with the     *)
    (* step-by-step following of the standard.                          *)

    TYPE
        asn1str1 = ARRAY [0..14] OF CARD8;
        asn1str2 = ARRAY [0..18] OF CARD8;

    CONST
        asn1start1 = asn1str1 {30H, 21H, 30H, 09H, 06H, 05H, 2BH, 0EH,
                               03H, 02H, 1AH, 05H, 00H, 04H, 14H};

        asn1start2 = asn1str2 {30H, 31H, 30H, 0DH, 06H, 09H, 60H, 86H,
                               48H, 01H, 65H, 03H, 04H, 02H, 01H, 05H,
                               00H, 04H, 20H};

        asn1start3 = asn1str2 {30H, 51H, 30H, 0DH, 06H, 09H, 60H, 86H,
                               48H, 01H, 65H, 03H, 04H, 02H, 03H, 05H,
                               00H, 04H, 40H};

    VAR j, L, PSLen, tLen: CARDINAL;
        T: ARRAY [0..82] OF CARD8;
        EM: ByteStr;
        M: BN;

    BEGIN
        (* Step 1: caller has already computed the hash.  *)

        (* hLen = 20 to 64 bytes after SHA encoding.    *)
        (* emLen = parameter k = length of RSAKey.n     *)

        (* Step 2: ASN1-encode the digest.  This only requires  *)
        (* appending the hash to the asn1start string.          *)

        CASE hashf OF
            HashAlgs.sha1:
                FOR j := 0 TO 14 DO
                    T[j] := asn1start1[j];
                END (*FOR*);
                j := 15;
                PSLen := k - 38;  tLen := 35;

          | HashAlgs.sha2_256:
                FOR j := 0 TO 18 DO
                    T[j] := asn1start2[j];
                END (*FOR*);
                j := 19;
                PSLen := k - 54;  tLen := 51;

          | HashAlgs.sha2_512:
                FOR j := 0 TO 18 DO
                    T[j] := asn1start3[j];
                END (*FOR*);
                j := 19;
                PSLen := k - 86;  tLen := 83;

        END (*CASE*);

        L := HashAlgs.HashLength(hashf);
        Copy (ADR(digest), ADR(T[j]), L);

        (* We require k >= tLen + 11.  The length of PS (see standard)  *)
        (* is k - tLen - 3 bytes, and this must be at least 8.  For the *)
        (* hashing algorithms I am currently using, the numbers are:    *)
        (*                    tLen     PSLen   min k    min bits        *)
        (*   sha1           15+20=35   k-38     46        368           *)
        (*   sha2_256       19+32=51   k-54     62        496           *)
        (*   sha2_512       19+64=83   k-86     94        752           *)

        (* PS = emLen - tLen - 3 bytes of 0FFH. *)
        (* This is k - 38 bytes.                *)

        (* Precede T with some padding. *)

        MakeBS (EM, k);
        EM.data^[0] := 0;
        EM.data^[1] := 1;
        FILL (ADR(EM.data^[2]), 0FFH, PSLen);
        EM.data^[k-tLen-1] := 0;

        (* Move T to the tail of EM.  *)

        Copy (ADR(T), ADR(EM.data^[k-tLen]), tLen);

        (* Convert EM to a Bignum. *)

        M := BigNum.BinToBN (EM.data^, 0, EM.size);
        DiscardBS (EM);
        RETURN M;

    END PKCS1_v1_5;

(************************************************************************)

PROCEDURE Sign (privatekey: RSAKeys.RSAKeyType;  hashf: HashAlgs.HashAlgorithm;
                    VAR (*IN*) digest: ARRAY OF CARD8): ByteStr;

    (* Creates an RSA signature using the specified hash function and   *)
    (* our private key.  Uses RSASSA-PKCS1-v1_5 signature method        *)
    (* as specified in RFC 8017 section 8.2 (page 35).                  *)

    VAR M, V: BN;
        k: CARDINAL;
        result: ByteStr;

    BEGIN
        (* Let k = length in bytes of the RSA modulus n.  This will     *)
        (* also be the length of the result.                            *)

        k := BigNum.NbytesNLZ (privatekey.n);

        (* Initial encoding of the digest. *)

        M := PKCS1_v1_5 (hashf, digest, k);

        (* Process with our private key.  *)

        V := RSA.PrivKeyEncode (M, privatekey);
        BigNum.Discard (M);

        (* Convert V to ByteStr form.  This must occupy exactly k   *)
        (* bytes, by inserting leading zeroes if necessary.         *)

        MakeBS (result, k);
        BigNum.BNtoBytesExact (V, k, result.data^);
        BigNum.Discard (V);

        RETURN result;

    END Sign;

(************************************************************************)

PROCEDURE CheckSignature (hashf: HashAlgs.HashAlgorithm;
                            VAR (*IN*) digest: ARRAY OF CARD8;
                              publickey: RSAKeys.RSAKeyType;
                               VAR (*INOUT*) signature: ByteStr;
                                logID: TransactionLogID): BOOLEAN;

    (* Checks that signature is a valid signature of the digest,        *)
    (* using the given public key.  The verification method is          *)
    (* specified in RFC 8017 section 8.2.2 (page 37).                   *)
    (* During processing the signature is discarded.                    *)

    CONST bufsize = 2048;

    VAR k: CARDINAL;
        S, V, M: BN;
        match: BOOLEAN;

    BEGIN
        (* Encrypt the signature using the public key.  This is         *)
        (* effectively a decryption, because the signature as we have   *)
        (* received it is the version after encryption with the         *)
        (* sender's private key.                                        *)

        k := signature.size;
        S := BigNum.BinToBN (signature.data^, 0, k);
        VarStrings.DiscardBS (signature);
        V := RSA.PubKeyEncode (S, publickey);

        BigNum.Discard (S);

        (* Encode the original digest. *)

        M := PKCS1_v1_5 (hashf, digest, k);

        (* Now M should be equal to V. *)

        match := BigNum.Cmp (M,V) = 0;

        (*
        IF NOT match THEN
            LogTransactionL (logID, "Mismatch of signature content, dumping to debug file");
            DebugDump.DebugMes ("Mismatch of signature content.  Signature as supplied:");
            BigNum.ToHex (V, buffer, bufsize);
            DebugDump.DebugMes (buffer);
            DebugDump.DebugMes ("Our encoding for comparison:");
            BigNum.ToHex (M, buffer, bufsize);
            DebugDump.DebugMes (buffer);
        END (*IF*);
        *)

        BigNum.Discard (V);
        BigNum.Discard (M);
        RETURN match;

    END CheckSignature;

(************************************************************************)

(*
PROCEDURE ExtractDigest (VAR (*IN*) binsig: ARRAY OF CARD8;
                            publickey: RSAKeys.RSAKeyType);

    (* For debugging only.  Decodes binsig to reveal the digest *)
    (* that was used in creating the signature.                 *)

    CONST bufsize = 2048;

    VAR k: CARDINAL;  S, V: BN;
        buffer: ARRAY [0..bufsize-1] OF CHAR;

    BEGIN
        k := BigNum.NbytesNLZ (publickey.n);
        S := BigNum.BinToBN (binsig, 0, k);
        V := RSA.PubKeyEncode (S, publickey);

        (* V is the decoded version of binsig.  Turn it into a binary string. *)

        BigNum.ToHex (V, buffer, bufsize);
        WriteString ("The decoded signature is");  WriteLn;
        WriteString (buffer);  WriteLn;

    END ExtractDigest;
*)

(************************************************************************)
(*                          LOAD RSA PRIVATE KEY                        *)
(************************************************************************)

PROCEDURE LoadRSAPrivateKey (filename: ARRAY OF CHAR): RSAKeys.RSAKeyType;

    (* Gets an RSA private key from a file.  Returns a nil key on error. *)

    CONST
        Nul = CHR(0);
        MaxChars = 32768;
        MaxBytes = 32768;

    VAR cid: ChanId;
        pos: CARDINAL;
        found: BOOLEAN;
        line: ARRAY [0..255] OF CHAR;
        pstring: VarStringPtr;
        pbytes: ByteStringPtr;
        numbers: ARRAY [0..9] OF BN;
        key: RSAKeys.RSAKeyType;

    BEGIN
        cid := OpenOldFile (filename, FALSE, FALSE);
        IF cid = NoSuchChannel THEN
            RETURN RSAKeys.InitKey();
        END (*IF*);

        (* Skip any lines before the "BEGIN" line. *)

        REPEAT
            ReadLine (cid, line);
        UNTIL HeadMatch (line, "-----BEGIN");

        (* Skip that line too. *)

        ReadLine (cid, line);

        (* Then there are optionally some header lines.  I also choose  *)
        (* to skip those, but I might have to change that decision if   *)
        (* I implement password protection.                             *)

        LOOP
            Strings.FindNext (':', line, 0, found, pos);
            IF NOT found THEN EXIT(*LOOP*) END(*IF*);
            ReadLine (cid, line);
        END (*LOOP*);

        (* Skip any blank lines. *)

        WHILE line[0] = Nul DO
            ReadLine (cid, line);
        END (*WHILE*);

        (* Read the Base64 string. *)

        ALLOCATE (pstring, MaxChars);
        pstring^[0] := CHR(0);
        WHILE line[0] <> '-' DO
            Strings.Append (line, pstring^);
            ReadLine (cid, line);
        END (*WHILE*);
        CloseFile (cid);

        ALLOCATE (pbytes, MaxBytes);
        EVAL (Base64.Decode (pstring^, pbytes^));
        DEALLOCATE (pstring, MaxChars);

        EVAL (ASN1.GetNumericArray (pbytes^, numbers));
        DEALLOCATE (pbytes, MaxBytes);

        (* Assign the values to the fields of our key. *)

        BigNum.Discard(numbers[0]);
        key.n       := numbers[1];
        key.public  := numbers[2];
        key.private := numbers[3];
        key.p       := numbers[4];
        key.q       := numbers[5];
        key.dp      := numbers[6];
        key.dq      := numbers[7];
        key.qinv    := numbers[8];

        RETURN key;

    END LoadRSAPrivateKey;

(************************************************************************)

END WRSA.

