Table of Contents

 Team-Fly
Next Section
Cryptography in C and C++
Cryptography in C and C++
by Michael Welschenbach ISBN:189311595x
Apress ?2001 (432 pages)

A behind-the-scenes examination of binary cryptography, from the simple "modulo" and shift operations to complex algorithms like RSA.

Table of Contents
 | Cryptography in C and C++
 | Preface to | - | the Translation of the Second Edition
 | Preface to | - | the First Edition
 | Part I - Arithmetic and Number Theory in C
 | Chapter 1 | - | Introduction
 | Chapter 2 | - | Number Formats: The Representation of Large Numbers in C
 | Chapter 3 | - | Interface Semantics
 | Chapter 4 | - | The Fundamental Operations
 | Chapter 5 | - | Modular Arithmetic: Calculating with Residue Classes
 | Chapter 6 | - | Where All Roads Meet: Modular Exponentiation
 | Chapter 7 | - | Bitwise and Logical Functions
 | Chapter 8 | - | Input, Output, Assignment, Conversion
 | Chapter 9 | - | Dynamic Registers
 | Chapter 10 | - | Basic Number-Theoretic Functions
 | Chapter 11 | - | Large Random Numbers
 | Chapter 12 | - | Strategies for Testing LINT
 | Part II - Arithmetic in C++ with the Class LINT
 | Chapter 13 | - | Let C++ Simplify Your Life
 | Chapter 14 | - | The LINT Public Interface: Members and Friends
 | Chapter 15 | - | Error Handling
 | Chapter 16 | - | An Application Example: The RSA Cryptosystem
 | Chapter 17 | - | Do It Yourself: Test LINT
 | Chapter 18 | - | Approaches for Further Extensions
 | Chapter 19 | - | Rijndael: A Successor to the Data Encryption Standard
 | Part III - Appendices
 | Appendix A | - | Directory of C Functions
 | Appendix B | - | Directory of C++ Functions
 | Appendix C | - | Macros
 | Appendix D | - | Calculation Times
 | Appendix E | - | Notation
 | Appendix F | - | Arithmetic and Number-Theoretic Packages
 | References
 | Index
 | List of Figures
 | List of Tables
 | List of Listings
Team-Fly Next Section


BackCover

 Team-Fly
Back Cover

  • Detailed treatment of public key cryptography with detailed coverage of the RSA algorithm that is now in the public domain
  • Explains and includes an implementation of Rijndael, the new American Encryption Standard in both C and C++
  • Not toy code(tm), but a practical treatment of modern cryptographic methods

Cryptography in C and C++ mainly focuses on the practical aspects involved in implementing public key cryptography methods, such as the RSA algorithm that was recently released from patent protection. It also gives both a technical overview and an implementation of the Rijndael algorithm that was selected as the Advanced Encryption Standard by the U.S. government. Welschenbach avoids complexities by explaining cryptography and its mathematical basis in terms a programmer can easily understand.

This book offers a comprehensive, yet relentlessly practical overview of the fundamentals of modern cryptography. It includes a wide-ranging library of code in C and C++, including the RSA algorithm, completed by an extensive Test Suite that proves that the code works correctly. Readers will learn, step by step, how to implement a platform independent library for the all-important multi-precision arithmetic used in modern cryptography. This is followed by an implementation of the cryptographic algorithms themselves.

About the Author

Michael Welschenbach currently works for SRC Security Research & Consulting GmbH in Bonn, Germany. He graduated with a Master's Degree in Mathematics from the University of Cologne and has gained extensive experience in cryptological research over the years. Currently, his favorite programming languages are C and C++. When not working, he enjoys spending time with his wife and two sons, programming, reading and music.

Team-Fly


Cryptography in C and C++

MICHAEL WELSCHENBACH
Translated by DAVID KRAMER

Copyright © 2001 by Michael Welschenbach

All rights reserved. No part of this work may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval system, without the prior written permission of the copyright owner and the publisher.

ISBN (pbk): 1-893115-95-X

Trademarked names may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, we use the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark.

Editorial Directors: Dan Appleman, Gary Cornell, Karen Watterson

Translator and Compositor: David Kramer

Proofreader: Anne Friedman

TEX Support: Fred Bartlett and Arthur Ogawa

Cover Designer: Karl Miyajima

Distributed to the book trade in the United States by Springer-Verlag New York, Inc.,175 Fifth Avenue, New York, NY, 10010 and outside the United States by Springer-Verlag GmbH & Co. KG, Tiergartenstr. 17, 69112 Heidelberg, Germany

In the United States, phone 1-800-SPRINGER;
<orders@springer-ny.com>;
http://www.springer-ny.com
Outside the United States, contact <orders@springer.de>;
http://www.springer.de; fax +49 6221 345229

For information on translations, please contact Apress directly at 901 Grayson Street, Suite 204, Berkeley, CA, 94710

Phone: 510-549-5937; Fax: 510-549-5939; <info@apress.com>; http://www.apress.com

The information in this book is distributed on an "as is" basis, without warranty. Although every precaution has been taken in the preparation of this work, neither the author nor Apress shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in this work.

For Helga, Daniel, and Lukas, from whom I borrowed the time for writing this book


Warning

 Team-Fly

Preface to the Translation of the Second Edition

When I have to wrestle with figures, I feel I'd like to stuff myself into a hole in the ground, so I can't see anything. If I raise my eyes and see the sea, or a tree, or a woman—even if she's an old 'un—damme if all the sums and figures don't go to blazes. They grow wings and I have to chase 'em.

—Nikos Kazanzakis, Zorba the Greek

THE SECOND EDITION has been revised and expanded in a number of ways. Additional examples of cryptographic algorithms have been added, such as the procedures of Rabin and El Gamal, and in the realization of the RSA procedure the hash function RIPEMD-160 and formatting according to PKCS #1 have been adopted. There is also a discussion of possible sources of error that could lead to a weakening of the procedure. The text has been expanded or clarified at a number of points, and errors have been corrected. Additionally, certain didactic strategies have been strengthened, with the result that some of the programs on the CD-ROM differ in certain details from those presented in the book. Not all technical details are of equal importance, and the desire for fast and efficient code is not always compatible with attractive and easy-to-read programs.

And speaking of efficiency, in Appendix D running times are compared to those for certain functions in the GNU Multiprecision Library. In this comparison the FLINT/C exponentiation routine did not do at all badly. As a further extension, Appendix F provides references to some arithmetic and number-theoretic packages.

The software has been expanded by several functions and in places has been significantly overhauled, and in the process a number of errors and points of imprecision were removed. Additional test functions were developed and existing test functions expanded. A security mode was implemented, whereby security-critical variables in the individual functions are deleted by being overwritten. All C and C++ functions are now clearly cited and annotated in the appendices.

Since current compilers represent varying stages of development of the C++ standard, the C++ modules of the FLINT/C package have been set up in such a way that both traditional C++ header files of the form xxxxx.h and the new ANSI header files can be used. For the same reason the use of the operator new() has been checked, as always, as to whether the null pointer is returned. This type of error handling does not make use of the ANSI standard exceptions, but it nonetheless functions with current compilers, while the method that conforms to the standard, by which new() generates an error via throw(), is not universally available.

Although the focus of this book is the fundamentals of asymmetric cryptography, the recent nomination of Rijndael by the American National Institute of Standards and Technology (NIST) to be the advanced encryption standard (AES) encouraged me to include a final chapter (Chapter 19) with an extensive description of this algorithm. I am indebted to Gary Cornell, at Apress, for bringing up the subject and convincing me that this would be a worthwhile complement to the topics of this book. I would like to thank Vincent Rijmen, Antoon Bosselaers, Paulo Barreto, and Brian Gladman for their kind permission to include the source code for their Rijndael implementations on the CD-ROM that accompanies this book.

I wish to thank all the readers of the first edition, particularly those who called errors to my attention, made comments, or suggested improvements. All their communications were most welcome. As always, the author assumes all responsibility for errors that may yet remain in the text or the software, as well as for any new errors that may have crept in.

I offer my heartfelt thanks to Gary Cornell, at Apress, and again to Hermann Engesser, Dorothea Glaunsinger, and Ulrike Stricker, at Springer-Verlag, for their unstinting commitment and friendly collaboration.

I am deeply grateful to my translator, David Kramer, who has contributed with distinguished expertise and indefatigable dedication many valuable hints, which have been incorporated into the German edition of this book as well.

Warning

Before making use of the programs contained in this book please refer to the manuals and technical introductions for the relevant software and computers. Neither the author nor the publisher accepts any responsibility for losses due to improper execution of the instructions and programs contained in this book or due to errors in the text or in the programs that despite careful checking may remain. The programs on the accompanying CD-ROM are protected by copyright and may not be reproduced without permission of the publisher.


Disclaimer

In this book frequent use is made of the term "leading zeros." The use of this term is in no way to be construed as alluding to any person or persons, in public or private life, living or dead, and any such correspondence is entirely coincidental.


Translator's Note

The pleasant task of translating this excellent book (Übersetzungsvergnügung) was made even more pleasant by the helpful and friendly collaboration of the author. Whenever I found myself entangled in the briar patch of German syntax, I simply launched a query across the reaches of cyberspace, and soon thereafter I received a message from Michael Welschenbach providing the necessary decryption algorithm.


Preface to the First Edition

Mathematics is the queen of the sciences, and number theory is the queen of mathematics. Frequently, she deigns to assist astronomy and other of the natural sciences, but primacy is due her under all circumstances.

—Carl Friedrich Gauss

WHY DO WE NEED a book on cryptography whose principal focus is the arithmetic of whole numbers—the integers—and its application to computer programming? Is this not a rather insignificant subject in comparison to the important problems with which computer science generally involves itself? So long as one confines oneself to the range of numbers that can be represented by the standard numerical types of a programming language, arithmetic is a rather simple affair, and the familiar arithmetic operations make their traditional appearances in programs accompanied by the familiar symbols +, −, /, *.

But if one requires results whose length far exceeds what can be expressed in 16 or 32 bits, then the situation begins to get interesting. Even the basic arithmetic operations are no longer available for such numbers, and one gets nowhere without first investing considerable effort in solving problems that never even seemed like problems before. Anyone who investigates problems in number theory, whether professionally or as a hobby, in particular the topic of contemporary cryptography, is familiar with such issues: The techniques of doing arithmetic that we learned in school now demand renewed attention, and we find ourselves sometimes dealing with incredibly involved processes.

The reader who wishes to develop programs in these areas and is not inclined to reinvent the wheel will find included with this book a suite of functions that will serve as an extension of C and C++ for calculating with large integers. We are not talking about "toy" examples that say, "this is how it works in principle," but a complete collection of functions and methods that satisfy the professional requirements of stability, performance, and a sound theoretical basis.

Making the connection between theory and practice is the goal of this book, that is, to close the gap between the theoretical literature and practical programming problems. In the chapters ahead we shall develop step by step the fundamental calculational principles for large natural numbers, arithmetic in finite rings and fields, and the more complex functions of elementary number theory, and we shall elucidate the many and various possibilities for applying these principles to modern cryptography. The mathematical fundamentals will be explained to the extent necessary for understanding the programs that are presented here, and for those interested in pursuing these matters further there are extensive references to the literature. The functions that we develop will then be brought together and extensively tested, resulting in a useful and comprehensive programming interface.

Beginning with the representation of large numbers, in the following chapters we shall first deal with the fundamentals of computation. For addition, subtraction, multiplication, and division of large numbers we shall create powerful basic functions. Building on these, we shall explain modular arithmetic in residue classes and implement the relevant operations in library functions. A separate chapter is devoted to the time-intensive process of exponentiation, where we develop and program various specialized algorithms for a number of applications in modular arithmetic.

After extensive preparation, which includes input and output of large numbers and their conversion into various bases, we study algorithms of elementary number theory using the basic arithmetic functions, and we then develop programs, beginning with the calculation of the greatest common divisor of large numbers. We shall then move on to such problems as calculating the Legendre and Jacobi symbols, and inverses and square roots in finite rings, and we shall also become familiar with the Chinese remainder theorem and its applications.

In connection with this we shall go into some detail about the principles of identifying large prime numbers, and we shall program a powerful multistage primality test.

A further chapter is devoted to the generation of large random numbers, in which a cryptographically useful bit generator is developed and tested with respect to its statistical properties.

To end the first part we shall concern ourselves with testing arithmetic and other functions. To do this we shall derive special test methods from the mathematical rules of arithmetic, and we shall consider the implementation of efficient external tools.

The subject of the second part is the step-by-step construction of the C++ class LINT (Large INTegers), in the course of which we shall embed the C functions of the first part into the syntax and semantics of the object-oriented programming language C++. We shall put particular weight on formatted input and output of LINT objects with flexible stream functions and manipulators, as well as error handling with exceptions. The elegance with which algorithms can be formulated in C++ is particularly impressive when the boundaries between standard types and large numbers as LINT objects begin to dissolve, resulting in the syntactic closeness to the implemented algorithms and in great clarity and transparency.

Finally, we shall demonstrate the application of the methods we have developed by implementing an extensive RSA cryptosystem for encryption and the creation of digital signatures. In the process we shall explain the theory of the RSA procedure and its operation as the most prominent representative of asymmetric cryptosystems, and in a self-contained example we shall develop an extensible kernel for applications of this ultramodern cryptographic process according to the object-oriented principles of the programming language C++.

We shall round all of this off with a glimpse of further possible extensions of the software library. As a small highlight at the end we shall present four functions in 80×86 assembly language for multiplication and division, which will improve the performance of our software. Appendix D contains a table of typical calculation times with and without the assembler supplement.

All readers of this book are heartily invited to join me on this path, or perhaps—depending on individual interest—to focus on particular sections or chapters and try out the functions presented there. The author hopes that it will not be taken amiss that he refers to his readers, together with himself, as "we." He hopes thereby to encourage them to take an active role in this journey through a cutting-edge area of mathematics and computer science, to figure things out for themselves and take from this book what is of greatest benefit. As for the software, let the reader not be lacking in ambition to extend the scope or speed of one or more functions through new implementations.

I wish to thank Springer-Verlag and particularly Hermann Engesser, Dorothea Glaunsinger, and Ulrike Stricker for their interest in the publication of this book and for their friendly and active collaboration. The manuscript was reviewed by Jörn Garbers, Josef von Helden, Brigitte Nebelung, Johannes Ueberberg, and Helga Welschenbach. I offer them my heartfelt thanks for their critical suggestions and improvements, and above all for their care and patience. If despite all of our efforts some errors remain in the text or in the software, the author alone bears the responsibility. I am extremely grateful to my friends and colleagues Robert Hammelrath, Franz-Peter Heider, Detlef Kraus, and Brigitte Nebelung for their insights into the connections between mathematics and computer science over many years of collaboration that have meant a great deal to me.


Part Overview

 Team-Fly

Part I: Arithmetic and Number Theory in C

Chapter List

Chapter 1: Introduction
Chapter 2: Number Formats: The Representation of Large Numbers in C
Chapter 3: Interface Semantics
Chapter 4: The Fundamental Operations
Chapter 5: Modular Arithmetic: Calculating with Residue Classes
Chapter 6: Where All Roads Meet: Modular Exponentiation
Chapter 7: Bitwise and Logical Functions
Chapter 8: Input, Output, Assignment, Conversion
Chapter 9: Dynamic Registers
Chapter 10: Basic Number-Theoretic Functions
Chapter 11: Large Random Numbers
Chapter 12: Strategies for Testing LINT

Part Overview

How necessary arithmetic and the entire art of mathematics are can be easily measured, in that nothing can be created that is not connected with precise number and measurement, and no independent art can exist without its measures and proportions.

—Adam Ries: Book of Calculation, 1574

Typographical rules for manipulating numerals are actually arithmetical rules for operating on numbers.

—D. R. Hofstadter: Gödel, Escher, Bach: An Eternal Golden Braid

The human brain would no longer be burdened with anything that needed to be calculated! Gifted people would again be able to think instead of scribbling numbers.

—Sten Nadolny: The Discovery of Slowness, trans. Ralph Freedman


Chapter 1: Introduction

Overview

God created the integers. All the rest is the work of man.

—Leopold Kronecker

If you look at zero you see nothing; but look through it and you will see the world.

—Robert Kaplan, The Nothing That Is: A Natural History of Zero

TO BE INVOLVED WITH MODERN cryptography is to dive willy-nilly into number theory, that is, the study of the natural numbers, one of the most beautiful areas of mathematics. However, we have no intention of becoming deep-sea divers who raise sunken treasure from the mathematical ocean floor, which in any case is unnecessary for cryptographic applications. Our goals are much more modest. On the other hand, there is no limit to the depth of involvement of number theory with cryptography, and many significant mathematicians have made important contributions to this area.

The roots of number theory reach back to antiquity. The Pythagoreans—the Greek mathematician and philosopher Pythagoras and his school—were already deeply involved in the sixth century B.C.E. with relations among the integers, and they achieved significant mathematical results, for example the famed Pythagorean theorem, which is a part of every school child's education. With religious zeal they took the position that all numbers should be commensurate with the natural numbers, and they found themselves on the horns of a serious dilemma when they discovered the existence of "irrational" numbers such as [image] , which cannot be expressed as the quotient of two integers. This discovery threw the world view of the Pythagoreans into disarray, to the extent that they sought to suppress knowledge of the irrational numbers, a futile form of behavior oft repeated throughout human history.

Two of the oldest number-theoretic algorithms, which have been passed down to us from the Greek mathematicians Euclid (third century B.C.E.) and Eratosthenes (276–195 B.C.E.), are closely related to the most contemporary encryption algorithms that we use every day to secure communication across the Internet. The "Euclidean algorithm" and the "sieve of Eratosthenes" are both quite up-to-date for our work, and we shall discuss their theory and application in Sections 10.1 and 10.5 of this book.

Among the most important founders of modern number theory are to be counted Pierre de Fermat (1601–1665), Leonhard Euler (1707–1783), Adrien Marie Legendre (1752–1833), Carl Friedrich Gauss (1777–1855), and Ernst Eduard Kummer (1810–1893). Their work forms the basis for the modern development of this area of mathematics and in particular the interesting application areas such as cryptography, with its asymmetric procedures for encryption and the generation of digital signatures (cf. Chapter 16). We could mention many more names of important contributors to this field, who continue to this day to be involved in often dramatic developments in number theory, and to those interested in a thrilling account of the history of number theory and its protagonists, I heartily recommend the book Fermats Last Theorem, by Simon Singh.

Considering that already as children we learned counting as something to be taken for granted and that we were readily convinced of such facts as that two plus two equals four, we must turn to surprisingly abstract thought constructs to derive the theoretical justification for such assertions. For example, set theory allows us to derive the existence and arithmetic of the natural numbers from (almost) nothing. This "almost nothing" is the empty (or null) set ø := { }, that is, the set that has no elements. If we consider the empty set to correspond to the number 0, then we are able to construct additional sets as follows. The successor 0+ of 0 is associated with the set 0+ := { 0 } = { ø }, which contains a single element, namely the null set. We give the successor of 0 the name 1, and for this set as well we can determine a successor, namely 1+ := { ø, { ø }}. The successor of 1, which contains 0 and 1 as its elements, is given the name 2. The sets thus constructed, which we have rashly given the names 0, 1, and 2, we identify—not surprisingly—with the well-known natural numbers 0, 1, and 2.

This principle of construction, which to every number x associates a successor x+ := x È { x } by adjoining x to the previous set, can be continued to produce additional numbers. Each number thus constructed, with the exception of 0, is itself a set whose elements constitute its predecessors. Only 0 has no predecessor. To ensure that this process continues ad infinitum, set theory formulates a special rule, called the axiom of infinity: There exists a set that contains 0 as well as the successor of every element that it contains.

From this postulated existence of (at least) one so-called successor set, which, beginning with 0, contains all successors, set theory derives the existence of a minimal successor set [image] , which is itself a subset of every successor set. This minimal and thus uniquely determined successor set [image] is called the set of natural numbers, in which we expressly include zero as an element.[1]

The natural numbers can be characterized by means of the axioms of Giuseppe Peano (1858–1932), which coincide with our intuitive understanding of the natural numbers:

  1. The successors of two unequal natural numbers are unequal: From n ≠ m, it follows that n+ ≠ m+, for all n, m Î [image] .

  2. Every natural number, with the exception of 0, has a predecessor: = [image] \ {0}.

  3. The principle of complete induction: If S ⊂ [image] , 0 Î S, and n Î S always imply n+ Î S, then S = [image] .

The principle of complete induction makes it possible to derive the arithmetic operations with natural numbers in which we are interested. The fundamental operations of addition and multiplication can be defined recursively as follows. We begin with addition:

For every natural number n Î [image] there exists a function sn from [image] to [image] such that

  1. sn (0) = n,

  2. sn (x+) = (sn(x))+ for all natural numbers x Î [image] .

The value of the function sn(x) is called the sum n + x of n and x.

The existence of such functions sn for all natural numbers n Î [image] must, however, be proved, since the infinitude of natural numbers does not a priori justify such an assumption. The existence proof goes back to the principle of complete induction, corresponding to Peano's third axiom above (see [Halm], Chapters 11–13). For multiplication one proceeds analogously:

For every natural number n Î [image] there exists a function pn from [image] to [image] such that

  1. pn(0) = 0,

  2. pn (x+) = pn(x) + n for all natural numbers x Î [image] .

The value of the function pn(x) is called the product n · x of n and x.

As expected, multiplication is defined in terms of addition. For the arithmetic operations thus defined one can prove, through repeated application of complete induction on x in accordance with Axiom III, such well-known arithmetic laws as associativity, commutativity, and distributivity (cf. [Halm], Chapter 13). Although we usually use these laws without further ado, we shall help ourselves to them as much as we please in testing our FLINT functions (see Chapters 12 and 17).

In a similar way we obtain a definition of exponentiation, which we give here in view of the importance of this operation in what follows.

For every natural number n Î [image] there exists a function en from [image] to [image] such that

  1. en(0) = 1,

  2. en (x+) = en(x) · n for every natural number x Î [image] .

The value of the function en(x) is called the xth power nx of n.

With complete induction we can prove the power law

Click To expand

to which we shall return in Chapter 6.

In addition to the calculational operations, the set [image] of natural numbers has defined on it an order relation "<" that makes it possible to compare two elements n, m Î [image] . Although this fact is worthy of our great attention from a set-theoretic point of view, here we shall content ourselves with noting that the order relation has precisely those properties that we know about and use in our everyday lives.

Now that we have begun with establishing the empty set as the sole fundamental building block of the natural numbers, we now proceed to consider the materials with which we shall be concerned in what follows. Although number theory generally considers the natural numbers and the integers as given and goes on to consider their properties without excessive beating about the bush, it is nonetheless of interest to us to have at least once taken a glance at a process of "mathematical cell division," a process that produces not only the natural numbers, but also the arithmetic operations and rules with which we shall be deeply involved from here on.

[1]It was not decisive for this choice that according to standard DIN 5473 zero belongs to the natural numbers. From the point of view of computer science, however, it is practical to begin counting at zero instead of 1, which is indicative of the important role played by zero as the neutral element for addition (additive identity).


About the Software

The software described in this book constitutes in its entirety a package, a so-called function library, to which frequent reference will be made. This library has been given the name FLINT/C, which is an acronym for "functions for large integers in number theory and cryptography."

The FLINT/C library contains, among other items, the following modules, which can be found as source code on the accompanying CD-ROM:

Table 1.1: Arithmetic and Number Theory in C in Directory flint/src
flint.h | header file for using functions from flint.c
flint.c | arithmetic and number-theoretic functions in C
kmul.h,c | functions for Karatsuba multiplication and squaring
ripemd.h,c | implementation of the hash function RIPEMD-1606
Table 1.2: Arithmetic and Number Theory in C++ in Directory flint/src
flintpp.h | header file for the use of functions from flintpp.cpp
flintpp.cpp | arithmetic and number-theoretic functions in C++. The module uses the functions in flint.c
Table 1.3: Arithmetic Module in 80×86 Assembler (see Chapter 18) in Directory flint/src/asm
mult.asm | multiplication, replaces the C function mult() in flint.c
umul.asm | multiplication, replaces the C function umul()
sqr.asm | squaring, replaces the C function sqr()
div.asm | division, replaces the C function div_l()
Table 1.4: Arithmetic Libraries in 80×86 Assembler (see Chapter 18) in Directory flint/lib
flinta.lib | library with assembler functions in OMF (object module format)
flintavc.lib | library with assembler functions in COFF (common object file format)
flinta.a | archive with assembler functions for emx/gcc under OS/2
libflint.a | archive with assembler functions for use under LINUX
Table 1.5: Tests (see Section 12.2 and Chapter 17) in Directory flint/test

testxxx.c[pp]

test programs in C and C++

Table 1.6: RSA Implementation (see Chapter 16) in Directory flint/rsa
rsakey.h | header file for the RSA classes
rsakey.cpp | implementation of the RSA classes RSAkey and RSApub
rsademo.cpp | example application of the RSA classes and their functions

A list of the individual components of the FLINT/C software can be found in the file readme.doc on the CD-ROM. The software has been tested with the indicated development tools on the following platforms:

  • GNU gcc under Linux, SunOS 4.1, and Sun Solaris

  • GNU/EMX gcc under OS/2 Warp, DOS, and Windows (9x, NT)

  • lcc-win32 under Windows (9x, NT, 2000)

  • Cygnus cygwin B20 under Windows (9x, NT, 2000)

  • IBM VisualAge under OS/2 Warp and Windows (9x, NT, 2000)

  • Microsoft C under DOS, OS/2 Warp, and Windows (9x, NT)

  • Microsoft Visual C/C++ under Windows (9x, NT, 2000)

  • Watcom C/C++ under DOS, OS/2 Warp, and Windows (3.1, 9x, NT)

The assembler programs can be translated with Microsoft MASM[2] or with Watcom WASM. They are contained on the CD-ROM in translated form as libraries in OMF (object module format) and COFF (common object file format), respectively, as well as in the form of a LINUX archive, and are used instead of the corresponding C functions when in translating C programs the macro FLINT_ASM is defined and the assembler object modules from the libraries, respectively archives, are linked.

A typical compiler call, here for the GNU compiler gcc, looks something like the following (with the paths to the source directories suppressed):

gcc -O2 -DFLINT_ASM -o rsademo rsademo.cpp rsakey.cpp
flintpp.cpp flint.c ripemd.c -lflint -lstdc++

The C++ header files following the ANSI standard are used when in compilation the macro FLINTPP_ANSI is defined; otherwise, the traditional header files xxxxx.h are used.

Depending on the computer platform, there may be deviations with regard to the compiler switches; but to achieve maximum performance the options for speed optimization should always be turned on. Because of the demands on the stack, in many environments and applications it will have to be adjusted.[3] Regarding the necessary stack size for particular applications, one should note the suggestion about the exponentiation functions in Chapter 6 and in the overview on page 115. The stack requirements can be lessened by using the exponentiation function with dynamic stack allocation as well as by the implementation of dynamic registers (see Chapter 9).

The C functions and constants have been provided with the macros

  • __FLINT_API

    Qualifier for C functions

    __FLINT_API_A

    Qualifier for assembler functions

    __FLINT_API_DATA

    Qualifier for constants

as in

  extern int __FLINT_API add_l(CLINT, CLINT, CLINT);
  extern USHORT __FLINT_API_DATA smallprimes[];

or, respectively, in the use of the assembler functions

  extern int __FLINT_API_A div_l (CLINT, CLINT, CLINT, CLINT);

These macros are generally defined as empty comments /**/. With their aid, using the appropriate definitions, compiler- and linker-specific instructions to functions and data can be made. If the assembler modules are used and not the GNU compiler gcc, the macro __FLINT_API_A is defined by __cdecl, and some compilers understand this as an instruction that the assembler functions corresponding to the C name and calling conventions are to be called.

For modules that import FLINT/C functions and constants from a dynamic link library (DLL) under Microsoft Visual C/C++, in translation the macros -D__FLINT_API=__cdecl and -D__FLINT_API_DATA= __declspec (dllimport) must be defined. This has already been taken into account in flint.h, and it suffices in this case to define the macro FLINT_USEDLL for compilation. For other development environments analogous definitions should be employed.

The small amount of work involved in initializing a FLINT/C DLL is taken care of by the function FLINTInit_l(), which provides initial values for the random number generator[4] and generates a set of dynamic registers (see Chapter 9). The complementary function FLINTExit_l() deallocates the dynamic registers. Sensibly enough, the initialization is not handed over to every individual process that uses the DLL, but is executed once at the start of the DLL. As a rule, a function with creator-specific signature and calling convention should be used, which is executed automatically when the DLL is loaded by the run-time system. This function can take over the FLINT/C initialization and use the two functions mentioned above. All of this should be considered when a DLL is created.

Some effort was made to make the software usable in security-critical applications. To this end, in security mode local variables in functions, in particular CLINT and LINT objects, are deleted after use by being overwritten with zeros. For the C functions this is accomplished with the help of the macro PURGEVARS_L() and the associated function purgevars_l(). For the C++ functions the destructor ~LINT() is similarly equipped. The assembler functions overwrite their working memory. The deletion of variables that are passed as arguments to functions is the responsibility of the calling functions.

If the deletion of variables, which requires a certain additional expenditure of time, is to be omitted, then in compilation the macro FLINT_UNSECURE must be defined. At run time the function char* verstr_l() gives information about the modes set at compile time, in which additionally to the version label X.x, the letters "a" for assembler support and "s" for security mode are output in a character string if these modes have been turned on.

[2]Call : ml /Cx /c /Gd <filename>.

[3]With modern computers with virtual memory, except in the case of DOS, one usually does not have to worry about this point, in particular with Unix or Linux systems.

[4]The initial values are made up of 32-bit numbers taken from the system clock. For applications in which security is critical it is advisable to use suitable random values from a sufficiently large interval as initial values.


Legal Conditions for Using the Software

The software is exclusively for private use. For such purposes the software may be used, altered, and distributed under the following conditions:

  1. The copyright notice may not be altered or deleted.

  2. All changes must be annotated by means of comment lines. Any other use, in particular the use of the software for commercial purposes, requires written permission from the publisher or the author.

The software has been created and tested with the greatest possible care. Since errors can never be completely eliminated, neither the author nor the publisher can take responsibility for direct or indirect damages that may arise from the use or unusability of the software, regardless of the purpose to which it has been put.


Contacting the Author

The author would be glad to receive information about errors or any other helpful criticism or comment. Please write to him at <cryptography@welschenbach.com>.


Chapter 2: Number Formats: The Representation of Large Numbers in C

So I have made up my own system for writing large numbers and I am going to use this chapter as a chance to explain it

—Isaac Asimov, Adding a Dimension

The process that has led to the higher organization of this form could also be imagined differently

—J. Weber, Form, Motion, Color

ONE OF THE FIRST STEPS in creating a function library for calculating with large numbers is to determine how large numbers are to represented in the computer's main memory. It is necessary to plan carefully, since decisions made at this point will be difficult to revise at a later time. Changes to the internal structure of a software library are always possible, but the user interface should be kept as stable as possible in the sense of "upward compatibility."

It is necessary to determine the order of magnitude of the numbers to be processed and the data type to be used for coding these numerical values.

The basic function of all routines in the FLINT/C library is the processing of natural numbers of several hundred digits, which far exceeds the capacity of standard data types. We thus require a logical ordering of a computer's memory units by means of which large numbers can be expressed and operated on. In this regard one might imagine structures that automatically create sufficient space for the values to be represented, but no more than is actually needed. One would like to maintain such economically organized housekeeping with respect to main memory by means of dynamic memory management for large numbers that allocates or releases memory according to need in the course of arithmetic operations. Although such can certainly be realized (see, for example, [Skal]), memory management has a price in computation time, for which reason the representation of integers in the FLINT/C package gives preference to the simpler definition of static length.

For representing large natural numbers one might use vectors whose elements are a standard data type. For reasons of efficiency an unsigned data type is to be preferred, which allows the results of arithmetic operations to be stored in this type without loss as unsigned long (defined in flint.h as ULONG), which is the largest arithmetic standard C data type (see [Harb], Section 5.1.1). A ULONG variable can usually be represented exactly with a complete register word of the CPU.

Our goal is that operations on large numbers be reducible by the compiler as directly as possible to the register arithmetic of the CPU, for those are the parts that the computer calculates "in its head," so to speak. For the FLINT/C package the representation of large integers is therefore by means of the type unsigned short int (in the sequel USHORT). We assume that the type USHORT is represented by 16 bits and that the type ULONG can fully accept results of arithmetic operations with USHORT types, which is to say that the informally formulated size relationship USHORT × USHORT ≤ ULONG holds.

Whether these assumptions hold for a particular compiler can be deduced from the ISO header file limits.h (cf. [Harb], Sections 2.7.1 and 5.1). For example, in the file limits.h for the GNU C/C++ compiler (cf. [Stlm]) the following appears:

#define UCHAR_MAX 0×ffU
#define USHRT_MAX 0×ffffU
#define UINT_MAX 0×ffffffffU
#define ULONG_MAX 0×ffffffffUL

One should note that with respect to the number of binary places there are actually only three sizes that are distinguished. The type USHRT (respectively USHORT in our notation) can be represented in a 16-bit register; the type ULONG fills the word length of a CPU with 32-bit registers. The type ULONG_MAX determines the value of the largest unsigned whole numbers representable by scalar types (cf. [Harb], page 110).[1] The value of the product of two numbers of type USHRT is at most 0×ffff * 0×ffff = 0×fffe0001 and is thus representable by a ULONG type, where the least-significant 16 bits, in our example the value 0×0001, can be isolated by a cast operation into the type USHRT. The implementation of the basic arithmetic functions of the FLINT/C package is based on the above-discussed size relationship between the types USHORT and ULONG.

An analogous approach, one that used data types with 32-bit and 64-bit lengths in the role of USHORT and ULONG in the present implementation, would reduce the calculation time for multiplication, division, and exponentiation by about 25 percent. Such possibilities are realizable with functions written in assembler with direct access to 64-bit results of machine instructions for multiplication and division or with processors with 64-bit registers that would also allow to C implementations the lossless storage of such results in a ULONG type. The FLINT/C package contains some examples whose gain in speed results from the use of arithmetic assembler functions (see Chapter 18).

The next question is that of the ordering of the USHORT digits within a vector. We can imagine two possibilities: from left to right, with a descending evaluation of digits from lower to higher memory addresses, or the other way round, with an ascending evaluation of digits from lower to higher memory addresses. The latter arrangement, which is the reverse of our usual notation, has the advantage that changes in the size of numbers at constant addresses can take place with the simple allocation of additional digits, without the numbers having to be relocated in memory. Thus the choice is clear: The evaluation of digits of our numerical representation increases with increasing memory address or vector index.

As a further element of the representation the number of digits will be appended and stored in the first element of the vector. The representation of long numbers in memory thus has the format

Click To expand

where B denotes the base of the numerical representation; for the FLINT/C package we have B := 216 = 65536. The value of B will be our companion from here on and will appear continually in what follows. The constant CLINTMAXDIGIT represents the maximal number of digits of a CLINT object.

Zero is represented by the length l = 0. The value n of a number that is represented by a FLINT/C variable n_l is calculated as

If n > 0, then the least-significant digit of n to the base B is given by n_l[1], and the most-significant digit by n_l[n_l[0]]. The number of digits of n_l[0] will be read in what follows by the macro DIGITS_L (n_l) and set to l by the macro SETDIGITS_L (n_l, l). Likewise, access to the least-significant and most-significant digits of n_l will be conveyed by LSDPTR_L(n_l) and MSDPTR_L(n_l), each of which returns a pointer to the digit in question. The use of the macros defined in flint.h yields independence from the actual representation of the number.

Since we have no need of a sign for natural numbers, we now have all the required elements for the representation of such numbers. We define the corresponding data type by

typedef unsigned short clint;
typedef clint CLINT[CLINTMAXDIGIT + 1];

In accordance with this, a large number will be declared by

CLINT n_l;

The declaration of function parameters of type CLINT can follow from the instruction CLINT n_l in the function header.[2] The definition of a pointer myptr_l to a CLINT object occurs via CLINTPTR myptr_l or clint *myptr_l.

FLINT/C functions can, depending on the setting of the constant CLINTMAXDIGIT in flint.h, process numbers up to 4096 bits in length, which corresponds to 1233 decimal digits or 256 digits to the base 216. By changing CLINTMAXDIGIT the maximal length can be adjusted as required. The definitions of other constants depend on this parameter; for example, the number of USHORTs in a CLINT object is specified by

#define CLINTMAXSHORT CLINTMAXDIGIT + 1;

and the maximal number of processable binary digits is defined by

#define CLINTMAXBIT CLINTMAXDIGIT << 4;

Since the constants CLINTMAXDIGIT and CLINTMAXBIT are used frequently, yet are rather unwieldy from a typographical point of view, we shall denote these constants by abbreviations MAXB and MAX2 (with the exception of program code, where the constants will appear in their normal form).

With this definition it follows that CLINT objects can assume whole-number values in the interval [0, BMAXB − 1] respectively [0, 2MAX2− 1]. We denote the value BMAXB − 1] = 2MAX2 − 1, the largest natural number that can be represented by a CLINT object, by Nmax.

For some functions it is necessary to process numbers that have more digits than can be accommodated by a CLINT object. For these cases the variants of the CLINT type are defined by

typedef unsigned short CLINTD[1+(CLINTMAXDIGIT<<1)];

and

typedef unsigned short CLINTQ[1+(CLINTMAXDIGIT<<2)];

which can hold double, respectively four times, the number of digits.

As support personnel to aid in programming, the module flint.c defines the constants nul_l, one_l, and two_l, which represent the numbers 0, 1, and 2 in CLINT format; and in flint.h there are corresponding macros SETZERO_L(), SETONE_L(), and SETTWO_L(), which set CLINT objects to the corresponding values.

[1]Without taking into account such practical nonstandard types as unsigned long long in GNU C/C++ and certain other C compilers.

[2]In this regard compare Chapters 4 and 9 of the extremely readable book [Lind], where there is an extensive explanation of when vectors and pointers in C are equivalent, and above all, when this is not the case and what types of errors can arise from a misunderstanding of these issues.


Chapter 3: Interface Semantics

When people hear some words, they normally believe that there's some thought behind them.

—Goethe, Faust, Part 1

IN THE FOLLOWING WE SHALL set some fundamental properties that relate to the behavior of the interface and the use of FLINT/C functions. First we shall consider the textual representation of CLINT objects and FLINT/C functions, but primarily we wish to clarify some fundamentals of the implementation that are important to the use of the functions.

The functions of the FLINT/C package are identified with the convention that their names end with "_l"; for example, add_l denotes the addition function. Designators of CLINT objects likewise end with an underscore and an appended l. For the sake of simplicity we shall equate from now on, when conditions permit, a CLINT object n_l with the value that it represents.

The representation of a FLINT/C function begins with a header, which contains the syntactic and semantic description of the function interface. Such function headers typically look something like the following.

Start Sidebar
Function: | a brief description of the function
Syntax: | int f_l (CLINT a_l, CLINT b_l, CLINT c_l);
Input: | a_l, b_l (operands)
Output: | c_l (result)
Return: | 0 if all is ok otherwise, a warning or error message
End Sidebar

Here we distinguish, among other things, between output and return value: While output refers to the values that are stored by the function in the passed arguments, by return we mean the values that the function returns via a return command. Except for a few cases (for example, the functions ld_l(), Section 10.3, and twofact_l(), Section 10.4.1), the return value consists of status information or error messages.

Parameters other than those involved with output are not changed by the function. Calls of the form f_l(a_l, b_l, a_l), where a_l and b_l are used as arguments and a_l is overwritten with the return value at the end of the computation, are always possible, since the return variable is written to with the return value only after the complete execution of the operation. From assembler programming one says in this case that the variable a_l is used as an accumulator. This modus operandi is supported by all FLINT/C functions.

A CLINT object n_l possesses leading zeros if for a value l one has

(DIGITS_L (n_l) == l) && (l > 0) && (n_l[l] == 0);

Leading zeros are redundant, since although they lengthen the representation of a number, they have no effect on its value. However, leading zeros are allowed in the notation of a number, for which reason we should not simply ignore this option. The acceptance of leading zeros is, to be sure, a burdensome implementation detail, but it leads to increased tolerance of input from external sources and thus contributes to the stability of all the functions. CLINT numbers with leading zeros are thus accepted by all FLINT/C functions, but they are not generated by them.

A further setting is related to the behavior of arithmetic functions in the case of overflow, which occurs when the result of an arithmetic operation is too large to be represented in the result type. Although in some publications on C the behavior of a program in the case of arithmetic overflow is said to be implementation-dependent, the C standard nonetheless governs precisely the case of overflow in arithmetic operations with unsigned integer types: There it is stated that arithmetic modulo 2n should be executed when the data type can represent integers of n-bit length (see [Harb], Section 5.1.2). Accordingly, in the case of overflow the basic arithmetic functions described below reduce their results modulo (Nmax + 1), which means that the remainder after whole-number division by Nmax + 1 is output as the result (see Section 4.3 and Chapter 5). In the case of underflow, which occurs when the result of an operation is negative, a positive residue modulo (Nmax + 1) is output. The FLINT/C functions thus behave in conformity with arithmetic according to the C standard.

If an overflow or underflow is detected, the arithmetic functions return the appropriate error code. This and all other error codes in Table 3.1 are defined in the header file flint.h.

Table 3.1: FLINT/C error codes
Error Code | Interpretation
E_CLINT_BOR | invalid basis in str2clint_l() (see Chapter 8)
E_CLINT_DBZ | division by zero
E_CLINT_MAL | error in memory allocation
E_CLINT_MOD | nonodd (even) modulus in Montgomery multiplication
E_CLINT_NOR | register unavailable (see Chapter 9)
E_CLINT_NPT | null pointer passed as argument
E_CLINT_OFL | overflow
E_CLINT_UFL | underflow


Chapter 4: The Fundamental Operations

Overview

Thus calculation can be seen as the basis and foundation of all the arts.

—Adam Ries, Book of Calculation

And you, poor creature, you are completely useless. Look at me. Everyone needs me.

—Aesop, "The Fir and the Blackberry Bush"

There is one small prerequisite for mastering the mathemagic tricks in this chapter—you need to know the multiplication tables through 10 … backward and forward.

—Arthur Benjamin, Michael B. Shermer, Mathemagics

THE FUNDAMENTAL BUILDING BLOCKS OF any software package for computer arithmetic are the functions that carry out the basic operations of addition, subtraction, multiplication, and division. The efficiency of the entire package hangs on the last two of these, and for that reason great care must be taken in the selection and implementation of the associated algorithms. Fortunately, volume 2 of Donald Knuth's classic The Art of Computer Programming contains most of what we need for this portion of the FLINT/C functions.

In anticipation of their representation to come, the functions developed in the following sections use the operations cpy_l(), which copies one CLINT object to another in the sense of an allocation, and cmp_l(), which makes a comparison of the sizes of two CLINT values. For a more precise description see Section 7.4 and Chapter 8.

Let us mention at this point that for the sake of clarity, in this chapter the functions for the fundamental arithmetic operations are developed all of a piece, while in Chapter 5 it will prove practical to split some of the functions into their respective "core" operations and from there develop additional steps such as the elimination of leading zeros and the handling of overflow and underflow, where, however, the syntax and semantics of the functions are kept intact. For an understanding of the relations described in this chapter this is irrelevant, so that for now we can forget about these more difficult issues.


4.1 Addition and Subtraction

This notion of "further counting" means, "to the integer n1 add the integer n2," and the integer s at which one arrives by this further counting is called "the result of addition" or the "sum of n1 and n2" and is represented by n1 + n2.

—Leopold Kronecker, On the Idea of Number

Since addition and subtraction are in principle the same operation with differing signs, the underlying algorithms are equivalent, and we can deal with them together in this section. We consider operands a and b with representations

Click To expand

where we assume a ≥ b. For addition this condition represents no restriction, since it can always be achieved by interchanging the two summands. For subtraction it means that the difference is positive or zero and therefore can be represented as a CLINT object without reduction modulo (Nmax + 1).

Addition consists essentially of the following steps.

Algorithm for the addition a + b

  1. Set i ← 0 and c ← 0.

  2. Set t ← ai + bi + c, si ← t mod B, and c ← ⌊t/B⌋.

  3. Set i ← i + 1; if i ≤ n − 1, go to step 2.

  4. Set t ← ai + c, si ← t mod B, and c ← ⌊t/B⌋.

  5. Set i ← i + 1; if i ≤ m − 1, go to step 4.

  6. Set sm ← c.

  7. Output s = (smsm−1 … s0)B.

The digits of the summands, together with the carry, are added in step 2, with the less-significant part stored as a digit of the sum, while the more-significant part is carried to the next digit. If the most-significant digit of one of the summands is reached, then in step 4 any remaining digits of the other summand are added to any remaining carries one after the other. Until the last summand digit is processed, the less-significant part is stored as a digit of the sum, and the more-significant part is used as a carry to the next digit. Finally, if there is a leftover carry at the end, it is stored as the most-significant digit of the sum. The output of this digit is suppressed if it has the value zero.

Steps 2 and 4 of the algorithm appear in a similar form in the case of subtraction, multiplication, and division. The associated code, which is illustrated by the following lines, is typical for arithmetic functions:[1]

s = (USHORT)(carry = (ULONG)a + (ULONG)b + (ULONG)(USHORT)(carry >> BITPERDGT));

The intermediate value t that appears in the algorithm is represented by the variable carry, of type ULONG, which holds the sum of the digits ai and bi as well as the carry of the previous operation. The new summation digit si is stored in the less-significant part of carry, from where it is taken by means of a cast as a USHORT. The resulting carry from this operation is held in the more-significant part of carry for the next operation.

The implementation of this algorithm by our function add_l() deals with a possible overflow of the sum, where in this case a reduction of the sum modulo (Nmax + 1) is carried out.

Start Sidebar
Function: | addition
Syntax: | int add_l (CLINT aa_l, CLINT bb_l, CLINT ss_l);
Input: | aa_l, bb_l (summands)
Output: | ss_l (sum)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL in the case of overflow
End Sidebar
int
add_l (CLINT aa_l, CLINT bb_l, CLINT ss_l)
{
  clint ss_l[CLINTMAXSHORT + 1];
  clint *msdptra_l, *msdptrb_l;
  clint *aptr_l, *bptr_l, *sptr_l = LSDPTR_L (ss_l);
  ULONG carry = 0L;
  int OFL = E_CLINT_OK;
Start Sidebar

The pointers for the addition loop are set. Here it is checked which of the two summands has the greater number of digits. The pointers aptr_l and msdaptr_l are initialized such that they point respectively to the least-significant and most-significant digits of the summand that has the most digits, or to those digits of a_l if both summands are of the same length. This holds analogously for the pointers bptr_l and msdbptr_l, which point to the least-significant and most-significant digits of the shorter summand, or to those digits of b_l. The initialization is carried out with the help of the macro LSDPTR_L() for the least-significant digits and MSDPTR_L() for the most-significant digits of a CLINT object. The macro DIGITS_L (a_l) specifies the number of digits of the CLINT object a_l, and with SETDIGITS_L(a_l, n) the number of digits of a_l is set to the value n.

End Sidebar
 if (DIGITS_L (a_l) < DIGITS_L (b_l))
   {
     aptr_l = LSDPTR_L (b_l);
     bptr_l = LSDPTR_L (a_l);
     msdptra_l = MSDPTR_L (b_l);
     msdptrb_l = MSDPTR_L (a_l);
     SETDIGITS_L (ss_l, DIGITS_L (b_l));
   }
 else
   {
     aptr_l = LSDPTR_L (a_l);
     bptr_l = LSDPTR_L (b_l);
     msdptra_l = MSDPTR_L (a_l);
     msdptrb_l = MSDPTR_L (b_l);
     SETDIGITS_L (ss_l, DIGITS_L (a_l));
   }
Start Sidebar

In the first loop of add_l the digits of a_l and b_l are added and stored in the result variable ss_l. Any leading zeros cause no problem, and they are simply used in the calculation and filtered out when the result is copied to s_l. The loop runs from the least-significant digit of b_l to the most-significant digit. This corresponds exactly to the process of pencil-and-paper addition as learned at school. As promised, here is the implementation of the carry.

End Sidebar
 while (bptr_l <= msdptrb_l)
   {
     *sptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++
                 + (ULONG)*bptr_l++ + (ULONG)(USHORT)(carry >> BITPERDGT));
   }
Start Sidebar

The two USHORT values *aptr and *bptr are copied via a cast to ULONG representation and added. To this the carry from the last interation is added. The result is a ULONG value that contains the carry from the addition step in its higher-valued word. This value is allocated to the variable carry and there reserved for the next iteration. The value of the resulting digit is taken from the lower-valued word of the addition result via a cast to the type USHORT. The carry saved in the higher-valued word of carry is included in the next iteration by a shift to the right by the number BITPERDGT of bits used for the representation of USHORT and a cast to USHORT.

In the second loop only the remaining digits of a_l are added to a possible existing carry and stored in s_l.

End Sidebar
 while (aptr_l <= msdptra_l)
   {
     *sptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++
                         + (ULONG)(USHORT)(carry >> BITPERDGT));
   }
Start Sidebar

If after the second loop there is a carry, the result is one digit longer than a_l. If it is determined that the result exceeds the maximal value Nmax representable by the CLINT type, then the result is reduced modulo (Nmax + 1) (see Chapter 5), analogously to the treatment of standard unsigned types. In this case the status announcement of the error code E_CLINT_OFL is returned.

End Sidebar
  if (carry & BASE)
    {
      *sptr_l = 1;
      SETDIGITS_L (ss_l, DIGITS_L (ss_l) + 1);
    }

  if (DIGITS_L (ss_l) > (USHORT)CLINTMAXDIGIT)     /* overflow? */
    {
      ANDMAX_L (ss_l);     /* reduce modulo (Nmax + 1) */
      OFL = E_CLINT_OFL;
    }

  cpy_l (s_l, ss_l);

  return OFL;
}

The run time t of all the procedures given here for addition and subtraction is t = O(n), and thus proportional to the number of digits of the larger of the two operands.

Now that we have seen addition, we shall present the algorithm for subtraction of two numbers a and b with representations

Click To expand

to base B.

Algorithm for the subtraction a − b

  1. Set i ← 0 and c ← 1.

  2. If c = 1, set t ← B + ai − bi; otherwise, set t ← B − 1 + ai − bi.

  3. Set si ← t mod B and c ← ⌊t/B⌋.

  4. Set i ← i + 1; if i ≤ n − 1, go to step 2.

  5. If c = 1, set t ← B + ai; otherwise, set t ← B − 1 + ai.

  6. Set si ← t mod B and c ← ⌊t/B⌋.

  7. Set i ← i + 1; if i ≤ m − 1, go to step 5.

  8. Output s = (sm − 1sm − 2 … s0)B.

The implementation of subtraction is identical to that of addition, with the following exceptions:

  • The ULONG variable carry is used to "borrow" from the next-higher digit of the minuend if a digit of the minuend is smaller than the corresponding digit of the subtrahend.

  • Instead of an overflow one must be on the lookout for a possible underflow, in which case the result of the subtraction would actually be negative; however, since CLINT is an unsigned type, there will be a reduction modulo (Nmax + 1) (see Chapter 5). The function returns the error code E_CLINT_UFL to indicate this situation.

  • Finally, any existing leading zeros are eliminated.

Thus we obtain the following function, which subtracts a CLINT number b_l from a number a_l.

Start Sidebar
Function: | subtraction
Syntax: | int sub_l (CLINT aa_l, CLINT bb_l, CLINT d_l);
Input: | aa_l (minuend), bb_l (subtrahend)
Output: | d_l (difference)
Return: | E_CLINT_OK if all is ok. E_CLINT_UFL in the case of underflow
End Sidebar
int
sub_l (CLINT aa_l, CLINT bb_l, CLINT d_l)
{
  CLINT b_l;
  clint a_l[CLINTMAXSHORT + 1]; /* allow 1 additional digit in a_l */
  clint *msdptra_l, *msdptrb_l;
  clint *aptr_l = LSDPTR_L (a_l);
  clint *bptr_l = LSDPTR_L (b_l);
  clint *dptr_l = LSDPTR_L (d_l);
  ULONG carry = 0L;
  int UFL = E_CLINT_OK;

  cpy_l (a_l, aa_l);
  cpy_l (b_l, bb_l);
  msdptra_l = MSDPTR_L (a_l);
  msdptrb_l = MSDPTR_L (b_l);
Start Sidebar

In the following the case a_l < b_l is considered, in which b_l is subtracted not from a_l, but from the largest possible value, Nmax. Later, the value (minuend+1) is added to this difference, so that altogether the calculation is carried out modulo (Nmax + 1). To generate the value Nmax the auxiliary function setmax_l() is used.

End Sidebar
 if (LT_L (a_l, b_l))
   {
     setmax_l (a_l);
     msdptra_l = a_l + CLINTMAXDIGIT;
     SETDIGITS_L (d_l, CLINTMAXDIGIT);
     UFL = E_CLINT_UFL;
   }
 else
   {
     SETDIGITS_L (d_l, DIGITS_L (a_l));
   }

 while (bptr_l <= msdptrb_l)
   {
     *dptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++
                - (ULONG)*bptr_l++ - ((carry & BASE) >> BITPERDGT));
   }

 while (aptr_l <= msdptra_l)
   {
     *dptr_l++ = (USHORT)(carry = (ULONG)*aptr_l++
                   - ((carry & BASE) >> BITPERDGT));
   }

 RMLDZRS_L (d_l);
Start Sidebar

The required addition of (minuend + 1) to the difference Nmax − b_l stored in d_l is carried out before the output of d_l.

End Sidebar
  if (UFL)
    {
      add_l (d_l, aa_l, d_l);
      inc_l (d_l);
    }

  return UFL;
}

In addition to the functions add_l() and sub_l() two special functions for addition and subtraction are available, which operate on a USHORT as the second argument instead of a CLINT. These are called mixed functions and identified by a function name with a prefixed "u," as in the functions uadd_l() and usub_l() to follow. The use of the function u2clint_l() for converting a USHORT value into a CLINT object follows in anticipation of its discussion in Chapter 8.

Start Sidebar
Function: | mixed addition of a CLINT type and a USHORT type
Syntax: | int uadd_l (CLINT a_l, USHORT b, CLINT s_l);
Input: | a_l, b (summands)
Output: | s_l (sum)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
uadd_l (CLINT a_l, USHORT b, CLINT s_l)
{
  int err;
  CLINT tmp_l;

  u2clint_l (tmp_l, b);
  err = add_l (a_l, tmp_l, s_l);
  return err;
}
Start Sidebar
Function: | subtraction of a USHORT type from a CLINT type
Syntax: | int usub_l (CLINT a_l, USHORT b, CLINT d_l);
Input: | a_l (minuend), b (subtrahend)
Output: | d_l (difference)
Return: | E_CLINT_OK if all is ok E_CLINT_UFL if underflow
End Sidebar
int
usub_l (CLINT a_l, USHORT b, CLINT d_l)
{
  int err;
  CLINT tmp_l;
  u2clint_l (tmp_l, b);
  err = sub_l (a_l, tmp_l, d_l);
  return err;
}

Two further useful special cases of addition and subtraction are realized in the functions inc_l() and dec_l(), which increase or decrease a CLINT value by 1. These functions are designed as accumulator routines: The operand is overwritten with the return value, which has proved practical in the implementation of many algorithms.

It is not surprising that the implementations of inc_l() and dec_l() are similar to those of the functions add_l() and sub_l(). They test for overflow and underflow, respectively, and return the corresponding error codes E_CLINT_OFL and E_CLINT_UFL.

Start Sidebar
Function: | increment a CLINT object by 1
Syntax: | int inc_l (CLINT a_l);
Input: | a_l (summand)
Output: | a_l (sum)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
inc_l (CLINT a_l)
{
  clint *msdptra_l, *aptr_l = LSDPTR_L (a_l);
  ULONG carry = BASE;
  int OFL = E_CLINT_OK;
  msdptra_l = MSDPTR_L (a_l);

  while ((aptr_l <= msdptra_l) && (carry & BASE))
    {
      *aptr_l = (USHORT)(carry = 1UL + (ULONG)*aptr_l);
      aptr_l++;
    }

  if ((aptr_l > msdptra_l) && (carry & BASE))
    {
      *aptr_l = 1;
      SETDIGITS_L (a_l, DIGITS_L (a_l) + 1);
      if (DIGITS_L (a_l) > (USHORT) CLINTMAXDIGIT)  /* overflow ? */
         {
           SETZERO_L (a_l);      /* reduce modulo (Nmax + 1) */
           OFL = E_CLINT_OFL;
         }
    }
  return OFL;
}
Start Sidebar
Function: | decrement a CLINT object by 1
Syntax: | int dec_l (CLINT a_l);
Input: | a_l (minuend)
Output: | a_l (difference)
Return: | E_CLINT_OK if all is ok E_CLINT_UFL if underflow
End Sidebar
int
dec_l (CLINT a_l)
{
  clint *msdptra_l, *aptr_l = LSDPTR_L (a_l);
  ULONG carry = DBASEMINONE;

  if (EQZ_L (a_l))     /* underflow ? */
    {
      setmax_l (a_l);      /* reduce modulo max_l */
      return E_CLINT_UFL;
    }

  msdptra_l = MSDPTR_L (a_l);

  while ((aptr_l <= msdptra_l) && (carry & (BASEMINONEL << BITPERDGT)))
    {
      *aptr_l = (USHORT)(carry = (ULONG)*aptr_l - 1L);
      aptr_l++;
    }

  RMLDZRS_L (a_l);
  return E_CLINT_OK;
}

[1]The C expression in this compact form is due to my colleague Robert Hammelrath.


4.2 Multiplication

If the individual summands n1, n2, n3, …, nr are all equal to one and the same integer n, then one calls the addition "multiplication of the integer n by the multiplier r" and sets n1 + n2 + n3 + ··· + nr = rn.

—Leopold Kronecker, On the Idea of Number

Multiplication is one of the most critical functions of the entire FLINT/C package due to the computation time required for its execution, since together with division it determines the execution time of many algorithms. In contrast to our experience heretofore with addition and subtraction, the classical algorithms for multiplication and division have execution times that are quadratic in the number of digits of the arguments, and it is not for nothing that Donald Knuth asks in one of his chapter headings, "How fast can we multiply?"

In the literature there have been published various procedures for rapid multiplication of large and very large integers, among which are some rather difficult methods. An example of this is the procedure developed by A. Schönhage and V. Strassen for multiplying large numbers by application of fast Fourier transforms over finite fields. The running time in terms of the number of digits n in the arguments is bounded above by O(n log n log log n) (see [Knut], Section 4.3.3). These techniques encompass the fastest known multiplication algorithms, but their advantage in speed over the classical O (n2) methods comes into play only when the number of binary digits is in the range 8,000–10,000. Based on the demands of cryptographic systems, such numbers, at least for the present, are far beyond the range envisioned in the application domain of our functions.

For our realization of multiplication for the FLINT/C package we would like first to use as a basis the grade school method based on "Algorithm M" given by Knuth (see [Knut], Section 4.3.1), and we shall make an effort to achieve as efficient an implementation of this procedure as possible. Then we shall occupy ourselves in a close examination of the calculation of squares, which offers great potential for savings, and for both cases we shall finally look at the multiplication procedure of Karatsuba, which is asymptotically better than O (n2).[2] The Karatsuba multiplication arouses our curiosity, since it seems simple, and one could pleasantly occupy a (preferably rainy) Sunday afternoon trying it out. We shall see whether this procedure has anything to contribute to the FLINT/C library.

4.2.1 The Grade School Method

We are considering multiplication of two numbers a and b with representations

Click To expand

to the base B. According to the procedure that we learned in school, the product ab can be computed for m = n = 3 as shown in Figure 4.1.

First, the partial products (a2a1a0)B · bj for j = 0, 1, 2, are calculated: The values aibj are the least-significant digits of the terms (aibj + carry) with the inner products aibj, and the c2j are the more-significant digits of p2j. The partial products are summed at the end to form the product p = (p5p4p3p2p1p0)B.

In the general case the product p = ab has the value

The result of a multiplication of two operands with m and n digits has at least m + n − 1 and at most m + n digits. The number of elementary multiplication steps (that is, multiplications by factors smaller than the base B) is mn.

A multiplication function that followed exactly the schema outlined above would first calculate all partial products, store these values, and then sum them up, each provided with the appropriate scaling factor. This school method is quite suitable for calculating with pencil and paper, but for the possibilities of a computer program it is somewhat cumbersome. A more efficient alternative consists in adding the inner products aibj at once to the accumulated values in the result digit pi+j, to which are added the carries c from previous steps. The resulting value for each pair (i, j) is assigned to a variable t:

Click To expand
Figure 4.1: Calculations for multiplication

where t can be represented as

Click To expand

and we then have

Click To expand

The current value of the result digit is taken by the assignment pi+j ← l from this representation of t. As the new carry we set c ← k.

The multiplication algorithm thus consists entirely of an outer loop for calculating the partial products ai(bn − 1 bn − 2 … b0)B and an inner loop for calculating the inner products aibj, j = 0, …, n − 1, and the values t and pi + j. The algorithm then appears as follows.

Algorithm for multiplication

  1. Set pi ← 0 for i = 0, …, n − 1.

  2. Set i ← 0.

  3. Set j ← 0 and c ← 0.

  4. Set t ← pi+j + aibj + c, pi+j ← t mod B, and c ← ⌊t/B⌋.

  5. Set j ← j + 1; if j ≤ n − 1, go to step 4.

  6. Set pi+n ← c.

  7. Set i ← i + 1; if i ≤ m − 1, go to step 3.

  8. Output p = (pm+n−1pm+n−2…p0)B.

The following implementation of multiplication contains at its core this main loop. Corresponding to the above estimate, in step 4 the lossless representation of a value less than B2 in the variable t is required. Analogously to how we proceeded in the case of addition, the inner products t are thus represented as ULONG types. The variable t is nonetheless not used explicitly, and the setting of the result digits pi+j and the carry c occurs rather within a single expression, analogous to the process already mentioned in connection with the addition function (see page 23). For initialization a more efficient procedure will be chosen than the one shown in step 1 of the algorithm.

Start Sidebar
Function: | multiplication
Syntax: | int mul_l (CLINT f1_l, CLINT f2_l, CLINT pp_l);
Input: | f1_l, f2_l (factors)
Output: | pp_l (product)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
mul_l (CLINT f1_l, CLINT f2_l, CLINT pp_l)
{
  register clint *pptr_l, *bptr_l;
  CLINT aa_l, bb_l;
  CLINTD p_l;
  clint *a_l, *b_l, *aptr_l, *csptr_l, *msdptra_l, *msdptrb_l;
  USHORT av;
  ULONG carry;
  int OFL = E_CLINT_OK;
Start Sidebar

First the variables are declared; p_l will hold the result and thus is of double length. The ULONG variable carry will hold the carry. In the first step the case is dealt with in which one of the factors, and therefore the product, is zero. Then the factors are copied into the workspaces a_l and b_l, and leading zeros are purged.

End Sidebar
 if (EQZ_L (f1_l) || EQZ_L (f2_l))
   {
     SETZERO_L (pp_l);
     return E_CLINT_OK;
   }
 cpy_l (aa_l, f1_l);
 cpy_l (bb_l, f2_l);
Start Sidebar

According to the declarations the pointers a_l and b_l are given the addresses of aa_l and bb_l, where a logical transposition occurs if the number of digits of aa_l is smaller than that of bb_l. The pointer a_l always points to the operand with the larger number of digits.

End Sidebar
   if (DIGITS_L (aa_l) < DIGITS_L (bb_l))
     {
        a_l = bb_l;
        b_l = aa_l;
     }
   else
     {
        a_l = aa_l;
        b_l = bb_l;
     }

   msdptra_l = a_l + *a_l;
   msdptrb_l = b_l + *b_l;
Start Sidebar

To save time in the computation, instead of the initialization required above, the partial product (bn − 1bn − 2 … b0)B · a0 is calculated in a loop and stored in pn, pn − 1, …, p0.

End Sidebar
carry = 0;
  av = *LSDPTR_L (a_l);
  for (bptr_l = LSDPTR_L (b_l), pptr_l = LSDPTR_L (p_l);
                 bptr_l <= msdptrb_l; bptr_l++, pptr_l++)
    {
      *pptr_l = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l +
                       (ULONG)(USHORT)(carry >> BITPERDGT));
    }

  *pptr_l = (USHORT)(carry >> BITPERDGT);
Start Sidebar

Next follows the nested multiplication loop, beginning with the digit a_l[2] of a_l.

End Sidebar
   for (csptr_l = LSDPTR_L (p_l) + 1, aptr_l = LSDPTR_L (a_l) + 1;
                 aptr_l <= msdptra_l; csptr_l++, aptr_l++)
     {
       carry = 0;
       av = *aptr_l;

       for (bptr_l = LSDPTR_L (b_l), pptr_l = csptr_l;
                bptr_l <= msdptrb_l; bptr_l++, pptr_l++)
       {
          *pptr_l = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l +
              (ULONG)*pptr_l + (ULONG)(USHORT)(carry >> BITPERDGT));
       }

     *pptr_l = (USHORT)(carry >> BITPERDGT);
   }
Start Sidebar

The largest possible length of the result is the sum of the numbers of digits of a_l and b_l. If the result has one digit fewer, this is determined by the macro RMLDZRS_L.

End Sidebar
 SETDIGITS_L (p_l, DIGITS_L (a_l) + DIGITS_L (b_l));
 RMLDZRS_L (p_l);
Start Sidebar

If the result is larger than can be accommodated in a CLINT object, it is reduced, and the error flag OFL is set to the value E_CLINT_OFL. Then the reduced result is assigned to the object pp_l.

End Sidebar
  if (DIGITS_L (p_l) > (USHORT)CLINTMAXDIGIT)   /* overflow ? */
    {
      ANDMAX_L (p_l);   /* reduce modulo (Nmax + 1) */
      OFL = E_CLINT_OFL;
    }

  cpy_l (pp_l, p_l);
  return OFL;
}

With t = O(mn) the run time t for the multiplication is proportional to the product of the numbers of digits m and n of the operands. For multiplication, too, the analogous mixed function is implemented, which processes a CLINT type and as second argument a USHORT type. This short version of CLINT multiplication requires O(n) CPU multiplications, which is the result not of any particular refinement of the algorithm, but of the shortness of the USHORT argument. Later, we shall set this function implicitly within a special exponentiation routine for USHORT bases (see Chapter 6, the function wmexp_l()).

For the implementation of the umul_l() function we return primarily to a code segment of the mul_l() function and reuse it with a few modifications.

Start Sidebar
Function: | multiplication of a CLINT type by a USHORT
Syntax: | int umul_l (CLINT aa_l, USHORT b, CLINT pp_l);
Input: | aa_l, b (factors)
Output: | pp_l (product)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
umul_l (CLINT aa_l, USHORT b, CLINT pp_l)
{
  register clint *aptr_l, *pptr_l;
  CLINT a_l;
  CLINTD p_l;
  clint *msdptra_l;
  ULONG carry;
  int OFL = E_CLINT_OK;

  cpy_l (a_l, aa_l);
  if (EQZ_L (a_l) || 0 == b)
    {
      SETZERO_L (pp_l);
      return E_CLINT_OK;
    }
Start Sidebar

After these preliminaries, the LINT factor is multiplied in a pass through a loop by the USHORT factor, and at the end the carry is stored in the most-significant USHORT digit of the CLINT value.

End Sidebar
 msdptra_l = MSDPTR_L (a_l);
 carry = 0;

 for (aptr_l = LSDPTR_L (a_l), pptr_l = LSDPTR_l (p_l);
                 aptr_l <= msdptra_l; aptr_l++, pptr_l++)
   {
     *pptr_l = (USHORT)(carry = (ULONG)b * (ULONG)*aptr_l +
                    (ULONG)(USHORT)(carry >> BITPERDGT));
   }
 *pptr_l = (USHORT)(carry >> BITPERDGT);
   SETDIGITS_L (p_l, DIGITS_L (a_l) + 1);
   RMLDZRS_L (p_l);

   if (DIGITS_L (p_l) > (USHORT)CLINTMAXDIGIT)   /* overflow ? */
     {
       ANDMAX_L (p_l);      /* reduce modulo (Nmax + 1) */
       OFL = E_CLINT_OFL;
     }

   cpy_l (pp_l, p_l);
   return OFL;
}

4.2.2 Squaring Is Faster

The calculation of a large square is accomplished with significantly fewer multiplications than in the case of the multiplication of large numbers. This is a result of the symmetry in the multiplication of identical operands. This observation is very important, since when it comes to exponentiation, which involves not one, but hundreds, of squarings, we shall be able to achieve considerable savings in speed. We again look at the well-known multiplication schema, this time with two identical factors (a2a1a0)B (see Figure 4.2).

We recognize that the inner products aiaj for i = j appear once (in boldface in Figure 4.2) and twice for i ≠ j (in boxes in the figure). Thus we can save three out of nine multiplications by multiplying the sum aiajBi+j for i < j by 2. The sum of the inner products of a square can then be written as

Click To expand

The number of required elementary multiplications is thus reduced with respect to the school method from n2 to n(n + 1)/2.

Click To expand
Figure 4.2: Calculations for squaring

A natural algorithmic representation of squaring calculates the above expression with the two summands in two nested loops.

Algorithm 1 for squaring

  1. Set pi ← 0 for i = 0, …, n − 1.

  2. Set i ← 0.

  3. Set t ← p2i + [image] , p2i ← t mod B, and c ← ⌊t/B⌋.

  4. Set j ← i + 1.

  5. Set t ← pi+j + 2aiaj + c, pi+j ← t (mod B), and c ← ⌊t/B⌋.

  6. Set j ← j + 1; if j ≤ n − 1, go to step 5.

  7. Set pi+n ← c.

  8. Set i ← i + 1; if i ≤ n − 1, go to step 3.

  9. Output p = (p2n − 1p2n − 2 … p0)B.

In selecting the necessary data types for the representation of the variables we must note that t can assume the value

Click To expand

(in step 5 of the algorithm). But this means that for representing t to base B more than two digits to base B will be needed, since we also have B2 − 1 < 2B2 − 2B < 2B2 − 1, and so a ULONG will not suffice for representing t (the inequality above is derived from the fact that one additional binary digit is needed). While this poses no problem for an assembler implementation, in which one has access to the carry bit of the CPU, it is difficult in C to handle the additional binary digit. To get around this dilemma, we alter the algorithm in such a way that in step 5 the required multiplication by 2 is carried out in a separate loop. It is then required that step 3 be carried out in its own loop, whereby for a slight extra expenditure of effort in loop management we are spared the additional binary digit. The altered algorithm is as follows.

Algorithm 2 for squaring

  1. Initialization: Set pi ← 0 for i = 0, …, n − 1.

  2. Calculate the product of digits of unequal index: Set i ← 0.

  3. Set j ← i + 1 and c ← 0.

  4. Set t ← pi+j + aiaj + c, pi+j ← t mod B, and c ← ⌊t/B⌋.

  5. Set j ← j + 1; if j ≤ n − 1, go to step 4.

  6. Set pi+n ← c.

  7. Set i ← i + 1; if i ≤ n − 2, go to step 3.

  8. Multiplication of inner products by 2: Set i ← 0 and c ← 0.

  9. Set t ← 2pi + c, pi ← t mod B, and c ← ⌊t/B⌋.

  10. Set i ← i + 1; if i ≤ 2n − 2, go to step 9.

  11. Addition of the inner squares: Set i ← 0 and c ← 0.

  12. Set t ← p2i + [image] + c, p2i ← t mod B, and c ← ⌊t/B⌋.

  13. Set t ← p2i+1 + c, p2i+1 ← t mod B, and c ← ⌊t/B⌋.

  14. Set i ← i + 1; if i ≤ n − 1, go to step 9.

  15. Set p2n−1 c; output p = (p2n − 1p2n − 2 … p0)B.

In the C function for squaring the initialization in step 1 is likewise, in analogy to multiplication, replaced by the calculation and storing of the first partial product a0(an − 1an − 2 … a1)B.

Start Sidebar
Function: | squaring
Syntax: | int sqr_l (CLINT f_l, CLINT pp_l);
Input: | f_l (factor)
Output: | pp_l (square)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
sqr_l (CLINT f_l, CLINT pp_l)
{
  register clint *pptr_l, *bptr_l;
  CLINT a_l;
  CLINTD p_l;
  clint *aptr_l, *csptr_l, *msdptra_l, *msdptrb_l, *msdptrc_l;
  USHORT av;
  ULONG carry;
  int OFL = E_CLINT_OK;

  cpy_l (a_l, f_l);
  if (EQZ_L (a_l))
    {
      SETZERO_L (pp_l);
      return E_CLINT_OK;
    }

  msdptrb_l = MSDPTR_L (a_l);
  msdptra_l = msdptrb_l - 1;
Start Sidebar

The initialization of the result vector addressed by pptr_l is carried out by means of the partial product a0(an − 1an − 2 … a1)B, in analogy with multiplication. The digit p0 is here not assigned; it must be set to zero.

End Sidebar
 *LSDPTR_L (p_l) = 0;
 carry = 0;
 av = *LSDPTR_L (a_l);
 for (bptr_l = LSDPTR_L (a_l) + 1, pptr_l = LSDPTR_L (p_l) + 1;
                 bptr_l <= msdptrb_l; bptr_l++, pptr_l++)
   {
     *pptr_l = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l +
                      (ULONG)(USHORT)(carry >> BITPERDGT));
   }

 *pptr_l = (USHORT)(carry >> BITPERDGT);
Start Sidebar

The loop for summing the inner products aiaj.

End Sidebar
 for (aptr_l = LSDPTR_L (a_l) + 1, csptr_l = LSDPTR_L (p_l) + 3;
                          aptr_l <= msdptra_l; aptr_l++, csptr_l += 2)
   {
     carry = 0;
     av = *aptr_l;
     for (bptr_l = aptr_l + 1, pptr_l = csptr_l; bptr_l <= msdptrb_l;
                 bptr_l++, pptr_l++)
        {
          *pptr_l = (USHORT)(carry = (ULONG)av * (ULONG)*bptr_l +
               (ULONG)*pptr_l + (ULONG)(USHORT)(carry >> BITPERDGT));
        }
     *pptr_l = (USHORT)(carry >> BITPERDGT);
   }
 msdptrc_l = pptr_l;
Start Sidebar

Then comes multiplication of the intermediate result in pptr_l by 2 via shift operations (see also Section 7.1).

End Sidebar
 carry = 0;
 for (pptr_l = LSDPTR_L (p_l); pptr_l <= msdptrc_l; pptr_l++)
   {
     *pptr_l = (USHORT)(carry = (((ULONG)*pptr_l) << 1) +
                          (ULONG)(USHORT)(carry >> BITPERDGT));
   }
 *pptr_l = (USHORT)(carry >> BITPERDGT);
Start Sidebar

Now we compute the "main diagonal."

End Sidebar
   carry = 0;
   for (bptr_l = LSDPTR_L (a_l), pptr_l = LSDPTR_L (p_l);
                 bptr_l <= msdptrb_l; bptr_l++, pptr_l++)
   {
     *pptr_l = (USHORT)(carry = (ULONG)*bptr_l * (ULONG)*bptr_l +
                    (ULONG)*pptr_l + (ULONG)(USHORT)(carry >> BITPERDGT));
     pptr_l++;
     *pptr_l = (USHORT)(carry = (ULONG)*pptr_l + (carry >> BITPERDGT));
   }
Start Sidebar

All the rest follows in analogy to multiplication.

End Sidebar
 SETDIGITS_L (p_l, DIGITS_L (a_l) << 1);
 RMLDZRS_L (p_l);

 if (DIGITS_L (p_l) > (USHORT)CLINTMAXDIGIT)   /* overflow ? */
   {
     ANDMAX_L (p_l);    /* reduce modulo (Nmax + 1) */
     OFL = E_CLINT_OFL;
   }
 cpy_l (pp_l, p_l);

 return OFL;
 }

The run time for squaring is, with O (n2), likewise quadratic in the number of digits of the operators, but with n(n + 1)/2 elementary multiplications it is about twice as fast as multiplication.

4.2.3 Do Things Go Better with Karatsuba?

The antispirit of multiplication and division deconstructed everything and then focused only on a specific part of the whole.

—Sten Nadolny (trans. Breon Mitchell), God of Impertinence

As announced, we shall now consider a method of multiplication named for the Russian mathematician A. Karatsuba, who has published several variants of it (See [Knut], Section 4.3.3). We assume that a and b are natural numbers with n = 2k digits to base B, so that we can write a = [image] and b = [image] with digits a0 and a1, respectively b0 and b0, to base Bk. Were we to multiply a and b in the traditional manner, then we would obtain the expression

Click To expand

with four multiplications to base Bk and thus n2 = 4k2 elementary multiplications to base B. However, if we set

then we have

For calculating ab it now appears that only three more multiplications by numbers to base Bk, or 3k2 multiplications to base B, are necessary, in addition to some additions and shifting operations (multiplication by Bk can be accomplished by left shifting by k digits to base B; see Section 7.1). Let us assume that the number of digits n of our factors a and b is a power of 2, with the result that by recursive application of the procedure on the remaining partial products we can end with having to carry out only elementary multiplications to base B, and this yields a total of [image] elementary multiplications, as opposed to n2 in the classical procedure, in addition to the time for additions and shift operations.

For squaring, this process can be simplified somewhat: With

we have

Furthermore, to our advantage, the factors in the squaring always have the same number of digits, which is not generally the case in multiplication. With all these advantages, we should, however, mention that recursion within a program function always costs something, so that we may hope to experience a savings in time over the classical method, which manages without the added burden of recursion, only when the numbers get large.

To obtain information on the actual time performance of the Karatsuba procedure the functions kmul() and ksqr() are provided. The division of the factors into two halves takes place in situ, and so a copying of the halves is unnecessary. But it is necessary that the functions be passed pointers to the least-significant digits of the factors and that the numbers of digits be passed separately.

The functions presented below in experimental form use the recursive procedure for factors having more than a certain number of digits determined by a macro, while for smaller factors we turn to conventional multiplication or squaring. For the case of nonrecursive multiplication the functions kmul() and ksqr() use the auxiliary functions mult() and sqr(), in which multiplication and squaring are implemented as kernel functions without the support of identical argument addresses (accumulator mode) or reduction in the case of overflow.

Start Sidebar

Function:

Karatsuba multiplication of two numbers a_l and b_l with 2k digits each to base B

Syntax:

void kmul (clint *aptr_l, clint *bptr_l,
               int len_a, int len_b, CLINT p_l);

Input:

aptr_l (pointer to the least-significant digit of the factor a_l)

bptr_l (pointer to the least-significant digit of the factor b_l)

len_a (number of digits of a_l)

len_b (number of digits of b_l)

Output:

p_l (product)
End Sidebar
void
kmul (clint *aptr_l, clint *bptr_l, int len_a, int len_b, CLINT p_l)
{
  CLINT c01_l, c10_l;
  clint c0_l[CLINTMAXSHORT + 2];
  clint c1_l[CLINTMAXSHORT + 2];
  clint c2_l[CLINTMAXSHORT + 2];
  CLINTD tmp_l;
  clint *a1ptr_l, *b1ptr_l;
  int l2;

  if ((len_a == len_b) && (len_a >= MUL_THRESHOLD)
                  && (0 == (len_a & 1)) )
    {
Start Sidebar

If both factors possess the same even number of digits above the value MUL_THRESHOLD, then recursion is entered with the splitting of the factors into two halves. The pointers aptr_l, a1ptr_l, bptr_l, b1ptr_l are passed to the corresponding least-significant digits of one of the halves. By not copying the halves, we save valuable time. The values c0 and c1 are calculated by recursively calling kmul() and then stored in the CLINT variables c0_l and c1_l.

End Sidebar
       l2 = len_a/2;

       a1ptr_l = aptr_l + l2;
       b1ptr_l = bptr_l + l2;

       kmul (aptr_l, bptr_l, l2, l2, c0_l);
       kmul (a1ptr_l, b1ptr_l, l2, l2, c1_l);
Start Sidebar

The value c2:= (a0 + a1) (b0 + b1) − c0 − c1 is computed with two additions, a call to kmul(), and two subtractions. The auxiliary function addkar() takes pointers to the least-significant digits of two equally long summands together with their number of digits, and outputs the sum of the two as a CLINT value.

End Sidebar
       addkar (a1ptr_l, aptr_l, l2, c01_l);
       addkar (b1ptr_l, bptr_l, l2, c10_l);

       kmul (LSDPTR_L (c01_l), LSDPTR_L (c10_l),
                   DIGITS_L (c01_l), DIGITS_L (c10_l), c2_l);

       sub (c2_l, c1_l, tmp_l);
       sub (tmp_l, c0_l, c2_l);
Start Sidebar

The function branch ends with the calculation of Bk (Bkc1 + c2) + c0, which used the auxiliary function shiftadd(), which during the addition left shifts the first of the two CLINT summands by a given number of places to base B.

End Sidebar
        shiftadd (c1_l, c2_l, l2, tmp_l);
        shiftadd (tmp_l, c0_l, l2, p_l);
     }
Start Sidebar

If one of the input conditions is not fulfilled, the recursion is interrupted and the nonrecursive multiplication mult() is called. As a requirement for calling mult() the two factor halves in aptr_l and bptr_l are brought into CLINT format.

End Sidebar
 else
   {
     memcpy (LSDPTR_L (c1_l), aptr_l, len_a * sizeof (clint));
     memcpy (LSDPTR_L (c2_l), bptr_l, len_b * sizeof (clint));

     SETDIGITS_L (c1_l, len_a);
     SETDIGITS_L (c2_l, len_b);

     mult (c1_l, c2_l, p_l);

     RMLDZRS_L (p_l);
   }
}

The Karatsuba squaring process proceeds analogously to this and will not be described in detail. For calling kmul() and ksqr() we make use of the functions kmul_l() and ksqr_l(), which are equipped with the standard interface.

Start Sidebar

Function:

Karatsuba multiplication and squaring

Syntax:

int kmul_l (CLINT a_l, CLINT b_l, CLINT p_l);
int ksqr_l (CLINT a_l, CLINT p_l);

Input:

a_l, b_l (factors)

Output:

p_l (product or square)

Return:

E_CLINT_OK if all is ok

E_CLINT_OFL if overflow
End Sidebar

The implementation of the Karatsuba functions are contained in the source file kmul.c on the included CD-ROM.

Extensive tests with these functions (on a Pentium III processor at 500 MHz under Linux) have given best results when the nonrecursive multiplication routine is called for a digit count under 40 (corresponding to 640 binary digits). The computation time of our implementation appears in Figure 4.3).

Click To expand
Figure 4.3: CPU time for Karatsuba multiplication

We conclude from this overview what we expected, that between standard multiplication and squaring there is a difference in performance of about 40 percent, and that for numbers of over 2000 binary digits a pronounced spread of the measured times becomes noticeable, with the Karatsuba routine in the lead. It is interesting to note that "normal" squaring sqr_l() is noticeably faster than Karatsuba multiplication, and Karatsuba squaring ksqr_l() takes the lead only above 3000 binary digits.

The large drop in performance of the Karatsuba functions for smaller numbers that was remarked on in the first edition of this book has in the meantime been eliminated. Yet there is still potential for improvement. The observable discontinuity in the calculation times of kmul_l() indicates that the recursion breaks off earlier than specified by the threshold value if the factors of a recursion step do not have an even number of digits. In the worst case this occurs right at the beginning of the multiplication, and even for very large numbers we are no better off than we were in the standard case. It would seem worthwhile, then, to extend the Karatsuba functions to be able to process arguments with differing numbers of digits and odd numbers of digits.

At the Max Planck Institute in Saarbrücken, J. Ziegler [Zieg] developed a portable implementation of Karatsuba multiplication and squaring for a 64-bit CPU (Sun Ultra-1) that overtakes the conventional method at 640 binary digits. With squaring an improvement in performance of 10% occurred at 1024 binary digits and 23% at 2048 binary digits.

Once again, the Karatsuba functions have no particular advantage for our cryptographic applications without considerable changes, for which reason we shall prefer to fall back on the functions mul_l() and sqr_l(), which realize the conventional procedures. For applications for which the Karatsuba functions seem suited one could simply substitute those functions for mul_l() and sqr_l().

[2]When we say that the calculation time is asymptotically better, we mean that the larger the numbers in question, the greater the effect. One should not fall victim to premature euphoria, and for our purposes such an improvement may have no significance whatsoever.


4.3 Division with Remainder

And marriage and death and division

Make barren our lives.

—Algernon Charles Swinburne, "Dolores"

We still need to place the last stone in our edifice of the fundamental arithmetic processes on large numbers, namely, division, which is the most complex of them all. Since we are calculating with natural numbers, we have only natural numbers at our disposal to express the results of a division. The principle of division that we are about to expound will be called division with remainder. It is based on the following relation. Given a, b Î [image] , b > 0, there are unique integers q and r that satisfy a = qb + r with 0 ≤ r < b. We call q the quotient and r the remainder of the division of a by b.

Frequently, we are interested only in the remainder and couldn't care less about the quotient. In Chapter 5 we shall see the importance of the operation of calculating remainders, since it is used in many algorithms, always in conjunction with addition, subtraction, multiplication, and exponentiation. Thus it will be worth our while to have at our disposal as efficient a division algorithm as possible.

For natural numbers a and b the simplest way of executing a division with remainder consists in subtracting the divisor b from the dividend a continually until the remaining quantity r is smaller than the divisor. By counting how often we have carried out the subtraction we will have calculated the quotient. The quotient q and the remainder r have the values q = ⌊a/b⌋ and r = a − ⌊a/b⌋b.[3]

This process of division by means of repeated subtraction is, of course, terribly boring. Even the grade school method of long division uses a significantly more efficient algorithm, in which the digits of the quotient are determined one by one and are in turn used as factors by which the divisor is multiplied. The partial products are subtracted in turn from the dividend. As an example consider the division exercise depicted in Figure 4.4.

Already at the determination of the first digit, 8, of the quotient we are required to make an estimate or else discover it by trial and error. If one makes an error, then one discovers either that the product (quotient digit times divisor) is too large (in the example, larger than 3549), or that the remainder after subtraction of the partial product from the digits of the dividend is larger than the divisor. In the first case the chosen quotient digit is too large, while in the second it is too small, and in either case it must be corrected.

Click To expand
Figure 4.4: Calculational schema for division

This heuristic modus operandi must be replaced in an implementation of a division algorithm by a more precise process. In [Knut], Section 4.3.1, Donald Knuth has described how such rough calculations can be made precise. Let us look more closely at our example.

Let a = (am+n − 1am+n − 2 … a0)B and b = (bn − 1bn − 2 … b0)B be two natural numbers, represented to the base B, and for bn − 1, the most-significant digit of b, we have bn − 1 > 0. We are looking for the quotient q and remainder r such that a = qb + r, 0 ≤ r < b.

Following the long division above, for the calculation of q and r a quotient digit qj := ⌊R/b⌋ < B is returned in each step, where in the first step R = (am+n − 1am+n − 2 … ak)B is formed from the most-significant digit of the dividend with the largest k for which 1 ≤ ⌊R/b⌋ holds (in the above example at the start we have m + n − 1 = 3 + 3 − 1 = 5, k = 2, and R = 3549). Then we will set R := R − qjb, where as a control for the correctness of the quotient digit qj the condition 0 ≤ R < b must be satisfied. Then R is replaced by the value RB + (next digit of the dividend), and the next quotient digit is again ⌊R/b⌋. The division is complete when all digits of the dividend have been processed. The remainder of the division is the last calculated value of R.

A necessity for programming this procedure is to determine, for two large numbers R = (rnrn − 1 … r0)B and b = (bn − 1bn − 2 … b0)B with ⌊R/b⌋ < B, the quotient Q := ⌊R/b⌋ (rn= 0 is a possibility). Here we take from Knuth the given approximation [image] of Q, which is computed from the leading digits of R and b.

Let

(4.1)

If bn − 1 ≥ ⌊R/b⌋, then for [image] (see [Knut], Section 4.3.1, Theorems A and B),

Under the favorable assumption that the leading digit of the divisor is sufficiently large in comparison to B, then as an approximation to Q, [image] is at most too large by 2 and is never too small.

By scaling the operands a and b this can always be achieved. We choose d > 0 such that dbn − 1 ≥ ⌊B/2⌋, set â := ad = (âm+nâm+n−1 … â0)B, and set [image] := bd = [image] . The choice of d is then made in such a way that the number of digits of [image] never increases in comparison to that of b. In the above notation it is taken into account that â possibly contains one more digit than a (if this is not the case, then we set âm+n = 0). In any case, it is practical to choose d as a power of 2, since then the scaling of the operands can be carried out with simple shift operations. Since both operands are multiplied by a common factor, the quotient is unchanged; we have ⌊â/ [image] ⌋ = ⌊a/b⌋.

The choice of [image] in (4.1), which we want to apply to the scaled operators â, respectively [image] , and [image] , can be improved with the following test to the extent that [image] = Q or [image] = Q + 1: If from the choice of [image] we have [image] ≥ ( [image] ) B + [image] , then [image] is reduced by 1 and the test is repeated. In this way we have taken care of all cases in which [image] is too large by 2 at the outset, and only in very rare cases is [image] still too large by 1 (see [Knut], Section 4.3.1, Exercises 19, 20). The latter is determined from the subtraction of the partial product "divisor times quotient digit" from what is left of the dividend. Then for the last time [image] must be reduced by 1 and the remainder updated. The algorithm for division with remainder is now essentially the following procedure.

Algorithm for division with remainder of a = (am+n − 1am+n − 2 … a0)B ≥ 0 by b = (bn − 1bn − 2 … b0)B > 0

  1. Determine the scaling factor d as given above.

  2. Set r := (rm+nrn+m−1rm+n−2…r0)B ← (0am+n−1am+n−2 … a0)B.

  3. Set i ← m + n, j ← m.

  4. Set [image] ← min [image] with the digits [image] , [image] − 1, and [image] obtained from scaling by d (see above). If [image] ≥ ( [image] ) B + [image] , set [image] ← [image] − 1 and repeat this test.

  5. If r − b [image] < 0, set [image] ← [image] ← [image] − 1.

  6. Set r := (riri−1 …ri−n)B ← (riri−1 … ri−n)B − b [image] and qj ← [image] .

  7. Set i ← i − 1 and j ← j − 1; if i ≥ n, go to step 4.

  8. Output q = (qmqm−1 …q0)B and r = (rn−1rn−2 …r0)B.

If the divisor has only a single digit b0, then the process can be shortened by initializing r with r ← 0 and dividing the two digits (rai)B by b0 with remainder. Here r is overwritten by the remainder, r ← (rai)B − qib0, and ai runs through all the digits of the dividend. At the end, r contains the remainder and q = (qmqm−1 …q0)B forms the quotient.

Now that we have at hand all the requisite processes for implementing division, we present the C function for the above algorithm.

Start Sidebar

Function:

division with remainder

Syntax:

int div_l (CLINT d1_l, CLINT d2_l, CLINT quot_l,
               CLINT rem_l);

Input:

d1_l (dividend), d2_l (divisor)

Output:

quot_l (quotient), rem_l (remainder)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
div_l (CLINT d1_l, CLINT d2_l, CLINT quot_l, CLINT rem_l)
{
  register clint *rptr_l, *bptr_l;
  CLINT b_l;

  /* Allow double-length remainder plus 1 digit */

  clint r_l[2 + (CLINTMAXDIGIT << 1)];
  clint *qptr_l, *msdptrb_l, *lsdptrr_l, *msdptrr_l;
  USHORT bv, rv, qhat, ri, ri_1, ri_2, bn, bn_1;
  ULONG right, left, rhat, borrow, carry, sbitsminusd;
  unsigned int d = 0;
  int i;
Start Sidebar

The dividend a = (am+n−1am+n−2…a0)B and divisor b(bn−1bn−2…b0)B are copied into the CLINT variables r_l and b_l. Any leading zeros are purged. If the divisor is then equal to zero, the function is terminated with the error code E_CLINT_DBZ.

We allow the dividend to possess up to double the number of digits determined in MAXB. This makes possible the later use of division in the functions of modular arithmetic. The storage allotment for a doubly long quotient must always be available to the calling function.

End Sidebar
   cpy_l (r_l, d1_l);
   cpy_l (b_l, d2_l);
   if (EQZ_L (b_l))
     return E_CLINT_DBZ;
Start Sidebar

A test is made as to whether one of the simple cases is at hand: dividend = 0, dividend < divisor, or dividend = divisor. In these cases we are done.

End Sidebar
   if (EQZ_L (r_l))
     {
        SETZERO_L (quot_l);
        SETZERO_L (rem_l);
        return E_CLINT_OK ;
     }

   i = cmp_l (r_l, b_l);
   if (i == -1)
     {
        cpy_l (rem_l, r_l);
        SETZERO_L (quot_l);
        return E_CLINT_OK ;
     }
   else if (i == 0)
     {
        SETONE_L (quot_l);
        SETZERO_L (rem_l);
        return E_CLINT_OK ;
     }
Start Sidebar

In the next step we check whether the divisor has only one digit. In this case a branch is made to a faster variant of division, which we shall discuss further below.

End Sidebar
 if (DIGITS_L (b_l) == 1)
   goto shortdiv;
Start Sidebar

Now begins the actual division. First the scaling factor d is determined as the exponent of a power of two. As long as bn−1 ≥ BASEDIV2 := ⌊B/2⌋, the most-significant digit bn−1 of the divisor is shifted left by one bit, where d, beginning with d = 0, is incremented by 1. Furthermore, the pointer msdptrb_l is set to the most-significant digit of the divisor. The value BITPERDGT − d will be used frequently in the sequel, and therefore it is saved in the variable sbitsminusd.

End Sidebar
 msdptrb_l = MSDPTR_L (b_l);
 bn = *msdptrb_l;
 while (bn < BASEDIV2)
   {
     d++;
     bn <<= 1;
   }
 sbitsminusd = (int)(BITPERDGT - d);
Start Sidebar

If d > 0, then the two most-significant digits [image] of db are computed and stored in bn and bn_1. In this we must distinguish the two cases that the divisor b has exactly two, or more than two, digits. In the first case, binary zeros are inserted into [image] from the right, while in the second case the least-significant digits of [image] come from bn−3.

End Sidebar
 if (d > 0)
   {
     bn += *(msdptrb_l - 1) >> sbitsminusd;
     if (DIGITS_L (b_l) > 2)
        {
          bn_1 = (USHORT)(*(msdptrb_l - 1) << d) + (*(msdptrb_l - 2) >> sbitsminusd);
        }
     else
        {
          bn_1 = (USHORT)(*(msdptrb_l - 1) << d);
        }
   }
 else
   {
     bn_1 = (USHORT)(*(msdptrb_l - 1));
   }
Start Sidebar

Now the pointers msdptrr_l and lsdptrr_l are set to the most-significant, respectively least-significant, digit of (am+nam+n−1…am+1)B in the CLINT vector r_l, which will represent the remainder of the division. At the digit am+n the variable r_l is initialized to 0. The pointer qptr_l is set to the highest quotient digit.

End Sidebar
 msdptrb_l = MSDPTR_L (b_l);

 msdptrr_l = MSDPTR_L (r_l) + 1;
 lsdptrr_l = MSDPTR_L (r_l) - DIGITS_L (b_l) + 1;
 *msdptrr_l = 0;

 qptr_l = quot_l + DIGITS_L (r_l) - DIGITS_L (b_l) + 1;
Start Sidebar

We now enter the main loop. The pointer lsdptrr_l runs over the digits am, am−2, …, a0 of the dividend in r_l, and the (implicit) index i over the values i = m + n, …, n.

End Sidebar
 while (lsdptrr_l >= LSDPTR_L (r_l))
   {
Start Sidebar

As preparation for determining [image] the three most-significant digits of part (aiai−1 … ai−n)B of the dividend multiplied by the scaling factor d are calculated and stored in the variables ri, ri_1, and ri_2. The case where the part of the dividend under consideration has only three digits is handled as a special case. In the first pass through the loop there are at least three digits present: On the assumption that the divisor b itself has at least two digits, there exist the most-significant digits am+n−1 and am+n−2 of the dividend, and the digit am+n was set to zero during the initialization of r_l.

End Sidebar
     ri = (USHORT)((*msdptrr_l << d) + (*(msdptrr_l - 1) >> sbitsminusd));
     ri_1 = (USHORT)((*(msdptrr_l - 1) << d) + (*(msdptrr_l - 2) >> sbitsminusd));

     if (msdptrr_l - 3 > r_l)    /* there are four dividend digits */
        {
          ri_2 = (USHORT)((*(msdptrr_l - 2) << d) +
                                          (*(msdptrr_l - 3) >> sbitsminusd));
        }
     else /* there are only three dividend digits */
        {
          ri_2 = (USHORT)(*(msdptrr_l - 2) << d);
        }
Start Sidebar

Now comes the determination of [image] , stored in the variable qhat. Here we distinguish the cases ri ≠ bn (frequent) and ri = b (rare).

End Sidebar
     if (ri != bn)    /* almost always */
        {
          qhat = (USHORT)((rhat = ((ULONG)ri << BITPERDGT) + (ULONG)ri_1) / bn);
          right = ((rhat = (rhat - (ULONG)bn * qhat)) << BITPERDGT) + ri_2;
Start Sidebar

If bn_1 * qhat > right, then qhat is too large by at least 1 and by at most 2.

End Sidebar
         if ((left = (ULONG)bn_1 * qhat) > right)
            {
              qhat--;
Start Sidebar

The test is now repeated only if we have rhat = rhat + bn < BASE due to the decrementing of qhat (otherwise, we already have bn_1 * qhat < BASE2 ≤ rhat * BASE).

End Sidebar
            if ((rhat + bn) < BASE)
              {
                if ((left - bn_1) > (right + ((ULONG)bn << BITPERDGT)))
                 {
                   qhat--;
                 }
              }
           }
        }
     else
Start Sidebar

In the second, rare, case, ri = bn, first [image] is set to the value BASE − 1 = 216 − 1 = BASEMINONE. In this case for rhat we have rhat = ri * BASE + ri_1 − qhat * bn = ri_1 + bn. If rhat < BASE, then a test is made as to whether qhat is too large. Otherwise, we have already bn_1 * qhat < BASE2 ≤ rhat * BASE. Under the same condition as above the test of qhat is repeated.

End Sidebar
   {
      qhat = BASEMINONE;
      right = ((ULONG)(rhat = (ULONG)bn + (ULONG)ri_1) << BITPERDGT) + ri_2;
      if (rhat < BASE)
        {
           if ((left = (ULONG)bn_1 * qhat) > right)
             {
               qhat--;
               if ((rhat + bn) < BASE)
                 {
                    if ((left - bn_1) > (right + ((ULONG)bn << BITPERDGT)))
                      {
                        qhat--;
                      }
                  }
              }
          }
    }
Start Sidebar

Then comes the subtraction of the product qhat · b from the part u := (aiai−1 … ai−n)B of the dividend, which is replaced by the difference thus calculated. Multiplication and subtraction take place at one digit per step. There are two things to note: The products qhat · bj can have two digits. Both digits of the product are saved for the time being in the ULONG variable carry. The most-significant digit of carry is dealt with as a carry in the subtraction of the next-higher digit.

For the case where the difference u − qhat · b is negative, since here qhat is still too large by 1, as a precaution the value u′ := Bn+1 + u − qhat · b is calculated and the result considered modulo Bn+1 as the B complement Û of u. After the subtraction the highest digit [image] of u′ is located in the most-significant word of the ULONG variable borrow.

That qhat is here too large by 1 is recognized in that [image] ≠ 0. In this case the result is corrected by the addition u ← u′ + b modulo Bn+1. This possibly needed correction is carried out further below.

End Sidebar
  borrow = BASE;
  carry = 0;
  for (bptr_l = LSDPTR_L (b_l), rptr_l = lsdptrr_l;
                bptr_l <= msdptrb_l; bptr_l++, rptr_l++)
    {
       if (borrow >= BASE)
         {
            *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASE -
                (ULONG)(USHORT)(carry = (ULONG)*bptr_l *
                   qhat + (ULONG)(USHORT)(carry >> BITPERDGT))));
         }
       else
         {
            *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASEMINONEL -
                (ULONG)(USHORT)(carry = (ULONG)*bptr_l * qhat +
                   (ULONG)(USHORT)(carry >> BITPERDGT))));
         }
    }
  if (borrow >= BASE)
         {
            *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASE -
                             (ULONG)(USHORT)(carry >> BITPERDGT)));
         }
       else
         {
            *rptr_l = (USHORT)(borrow = ((ULONG)*rptr_l + BASEMINONEL -
                              (ULONG)(USHORT)(carry >> BITPERDGT)));
         }
Start Sidebar

The quotient digit is stored, subject to a possible necessary correction.

End Sidebar
  *qptr_l = qhat;
Start Sidebar

As promised, now a test is made as to whether the quotient digit is too large by 1. This is extremely seldom the case (further below, special test data will be presented) and is indicated by the high-valued word of the ULONG variable borrow being equal to zero; that is, that borrow < BASE. If this is the case, then u ← u′ + b modulo Bn+1 is calculated (notation as above).

End Sidebar
     if (borrow < BASE)
       {
          carry = 0;
          for (bptr_l = LSDPTR_L (b_l), rptr_l = lsdptrr_l;
              bptr_l <= msdptrb_l; bptr_l++, rptr_l++)
            {
              *rptr_l = (USHORT)(carry = ((ULONG)*rptr_l +
                    (ULONG)*bptr_l + (ULONG)*bptr_l +
                       (ULONG)(USHORT)(carry >> BITPERDGT)));
            }
          *rptr_l += (USHORT)(carry >> BITPERDGT);
          (*qptr_l)--;
       }
Start Sidebar

Now the pointers are set to the remainder and the quotient, and we return to the beginning of the main loop.

End Sidebar
    msdptrr_l--
    lsdptrr_l--;
    qptr_l--;
  }

The length of the remainder and that of the quotient are determined. The number of digits is at most 1 more than the number of digits of the dividend minus the number of digits of the divisor. The remainder possesses at most the number of digits of the divisor. In both cases the exact length is set by the removal of leading zeros.

  SETDIGITS_L (quot_l, DIGITS_L (r_l) - DIGITS_L (b_l) + 1);
  RMLDZRS_L (quot_l);

  SETDIGITS_L (r_l, DIGITS_L (b_l));
  cpy_l (rem_l, r_l);

  return E_CLINT_OK;
Start Sidebar

In the case of "short division" the divisor possesses only the digit b0, by which the two digits (rai)B are to be divided, where ai runs through all digits of the dividend; r is initialized with r ← 0 and assumes the difference r ← (rai)B −qb0. The value r is represented by the USHORT variable rv. The value of (rai)B is stored in the ULONG variable rhat.

End Sidebar
shortdiv:
  rv = 0;
  bv = *LSDPTR_L (b_l);
  for (rptr_l = MSDPTR_L (r_l), qptr_l = quot_l + DIGITS_L (r_l);
                  rptr_l >= LSDPTR_L (r_l); rptr_l--, qptr_l--)
    {
      *qptr_l = (USHORT)((rhat = ((((ULONG)rv) << BITPERDGT) + (ULONG)*rptr_l)) / bv);
      rv = (USHORT)(rhat - (ULONG)bv * (ULONG)*qptr_l);
    }
  SETDIGITS_L (quot_l, DIGITS_L (r_l));
  RMLDZRS_L (quot_l);

  u2clint_l (rem_l, rv);

  return E_CLINT_OK;
}

With t = O(mn), the run time t of the division is analogous to that for multiplication, where m and n are the numbers of digits of the dividend and divisor, respectively, to the base B.

In the sequel we shall describe a number of variants of division with remainder, all of which are based on the general division function. First we have the mixed version of the division of a CLINT type by a USHORT type. For this we return once again to the routine for small divisors of the function div_l(), where it is placed almost without alteration into its own function. We thus present only the interface of the function.

Start Sidebar

Function:

division of a CLINT type by a USHORT type

Syntax:

int udiv_l (CLINT dv_l, USHORT uds, CLINT q_l,
               CLINT r_l);

Input:

dv_l (dividend), uds (divisor)

Output:

q_l (quotient), r_l (remainder)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar

We have already indicated that for a given calculation the quotient of a division is not required, and only the remainder is of interest. This will not result in a great savings of time, but in such cases, at least the passing of a pointer to the storage location of the quotient is burdensome. It is therefore worthwhile to create an independent function for computing remainders, or "residues." The mathematical background of the use of this function is discussed more fully in Chapter 5.

Start Sidebar
Function: | Remainders (reduction modulo n)
Syntax: | int mod_l (CLINT d_l, CLINT n_l, CLINT r_l);
Input: | d_l (dividend), n_l (divisor or modulus)
Output: | r_l (remainder)
Return: | E_CLINT_OK if all is ok E_CLINT_DBZ if division by 0
End Sidebar

Simpler than the general case is the construction of the remainder modulo a power of 2, namely 2k, which is worth implementing in its own function. The remainder of the dividend in a division by 2k results from truncating its binary digits after the kth bit, where counting begins with 0. This truncation corresponds to a bitwise joining of the dividend to 2k − 1 = (111111 … 1)2, the value of k binary ones, by a logical AND (cf. Section 7.2). The operation is concentrated on the digit of the dividend in its representation to base B that contains the kth bit; all higher-valued dividend digits are irrelevant. For specifying the divisor the following function mod_l() is passed only the exponent k.

Start Sidebar
Function: | remainder modulo a power of 2 (reduction modulo 2k)
Syntax: | int mod2_l (CLINT d_l, ULONG k, CLINT r_l);
Input: | d_l (dividend), k (exponent of the divisor or modulus)
Return: | r_l (remainder)
End Sidebar
int
mod2_l (CLINT d_l, ULONG k, CLINT r_l)
{
  int i;
Start Sidebar

Since 2k > 0, there is no test for division by 0. First d_l is copied to r_l and a test is made as to whether k exceeds the maximal binary length of a CLINT number, in which case the function is terminated.

End Sidebar
  cpy_l (r_l, d_l);
  if (k > CLINTMAXBIT)
    return E_CLINT_OK;
Start Sidebar

The digit in r_l in which something changes is determined and is stored as an index in i. If i is greater than the number of digits of r_l, then we are done.

End Sidebar
  i = 1 + (k >> LDBITPERDGT);
  if (i > DIGITS_L (r_l))
    return E_CLINT_OK;
Start Sidebar

Now the determined digit of r_l (counting from 1) is joined by a logical AND to the value 2kmod BITPERDGT − 1 (= 2kmod16 − 1 in this implementation). The new length i of r_l is stored in r_l[0]. After the removal of leading zeros the function is terminated.

End Sidebar
  r_l[i] &= (1U << (k & (BITPERDGT - 1))) - 1U;
  SETDIGITS_L (r_l, i);
  RMLDZRS_L (r_l);

  return E_CLINT_OK;
}

The mixed variant of calculating residues employs a USHORT type as divisor and represents the remainder again as a USHORT type, where here again only the interface is given, and we refer the reader to the FLINT/C source code for the short functions.

Start Sidebar
Function: | remainders, division of a CLINT type by a USHORT type
Syntax: | USHORT umod_l (CLINT dv_l, USHORT uds);
Input: | dv_l (dividend), uds (divisor)
Return: | nonnegative remainder if all is ok 0×FFFF if division by 0
End Sidebar

For testing the division there are—as for all other functions as well—some considerations to be taken into account (see Chapter 12). In particular, it is important that step 5 be tested explicitly, though in randomly selected test cases it will appear with a probability of only about 2/B (= 2−15 in our implementation) (see [Knut], Section 4.3.1, Exercise 21).

In the following the given dividend a and divisor b with associated quotient q and remainder r have the effect that the program sequence associated to step 5 of the division algorithm is run through twice, and can therefore be used as test data for this particular case. Additional values with this property are contained in the test program testdiv.c.

The display of test numbers below shows the digits in hexadecimal, running from right to left in ascending order, without specifying the length:

Test values for step 5 of the division
a = | e3 7d 3a bc 90 4b ab a7 a2 ac 4b 6d 8f 78 2b 2b f8 49 19 d2 91 73 47 690d 9e 93 dc dd 2b 91 ce e9 98 3c 56 4c f1 31 22 06 c9 1e 74 d8 0b a4 79 064c 8f 42 bd 70 aa aa 68 9f 80 d4 35 af c9 97 ce 85 3b 46 57 03 c8 ed ca
b = | 08 0b 09 87 b7 2c 16 67 c3 0c 91 56 a6 67 4c 2e 73 e6 1a 1f d5 27 d4 e7 8b 3f 15 05 60 3c 56 66 5845 9b 83 cc fd 58 7b a9 b5 fc bd c0 ad 09 15 2e 0a c2 65
q = | 1c 48 a1 c7 98 54 1a e0 b9 eb 2c 63 27 b1 ff ff f4 fe 5c 0e 27 23
r = | ca 23 12 fb b3 f4 c2 3a dd 76 55 e9 4c 34 10 b1 5c 60 64 bd 48 a4 e5 fc c3 3d df 55 3e 7c b8 29 bf 66 fb fd 61 b4 66 7f 5e d6 b3 87 ec 47 c5 27 2c f6 fb

[3]Note that for a < 0 with q = − ⌈|a|/b⌉ and r = b − (|a| + qb) if a ∤ b, respectively r = 0 if a | b, division with remainder is reduced to the case a, b Î [image] .


Chapter 5: Modular Arithmetic: Calculating with Residue Classes

Every fine story must leave in the mind of the sensitive reader an intangible residuum of pleasure...

—Willa Cather, Not Under Forty, "Miss Jewett"

WE BEGIN THIS CHAPTER WITH a discussion of the principle of division with remainder. In relation to this we shall explain the significance of these remainders, their possible applications, and how one calculates with them. In order for the functions to be introduced later to be understandable, we begin with a bit of algebra.

We have seen that in division with remainder of an integer a Î [image] by a natural number 0 < m Î [image] one has the unique representation

Here r is called the remainder after division of a by m or the residue of a modulo m, and it holds that m divides a − r without remainder, or in mathematical notation,

This statement about divisibility was given a new notation by Gauss, in analogy to the equal sign:[1]

(say "a is congruent to r modulo m").

Congruence modulo a natural number m is an equivalence relation on the set of natural numbers. This means that the set R := { (a, b) | a ≡ b mod m } of integer pairs satisfying m | (a − b) has the following properties, which result immediately from division with remainder:

  1. R is reflexive: For all integers a it holds that (a, a) is an element of R, that is, we have a ≡ a mod m.

  2. R is symmetric: If (a, b) is in R, then so is (b, a); that is, a ≡ b mod m implies b ≡ a mod m.

  3. R is transitive: If (a, b) and (b, c) are in R, then so is (a, c); that is, a ≡ b mod m and b ≡ c mod m implies a ≡ c mod m.

The equivalence relation R partitions the set of integers into disjoint sets, called equivalence classes: Given a remainder r and a natural number m > 0 the set

or, in other notation, r + m [image] , is called the residue class of r modulo m. This class contains all integers that upon division by m yield the remainder r.

Here is an example: Let m = 7, r = 5; then the set of integers that upon division by 7 yield the remainder 5 is the residue class

Click To expand

Two residue classes modulo a fixed number m are either the same or disjoint.[2] Therefore, a residue class can be uniquely identified by any of its elements. Thus the elements of a residue class are called representatives, and any element can serve as representative of the class. Equality of residue classes is thus equivalent to the congruence of their representatives with respect to the given modulus. Since upon division with remainder the remainder is always smaller than the divisor, for any integer m there can exist only finitely many residue classes modulo m.

Now we come to the reason for this extensive discussion: Residue classes are objects with which one can do arithmetic, and in fact, by employing their representatives. Calculating with residue classes has great significance for algebra and number theory and thus for coding theory and modern cryptography. In what follows we shall attempt to clarify the algebraic aspects of modular arithmetic.

Let a, b, and m be integers, m > 0. For residue classes ā and [image] modulo m we define the relations "+" and "·", which we call addition and multiplication (of residue classes), since they are based on the like-named operations on the integers:

Click To expand

Both relations are well-defined, since in each case the result is a residue class modulo m. The set [image] := { [image] | r is a residue modulo m } of residue classes modulo m together with these relations forms a finite commutative ring ( [image] , +, ·) with unit, which in particular means that the following axioms are satisfied:

  1. Closure with respect to addition:

    The sum of two elements of [image] is again in [image] .

  2. Associativity of addition:

    For every ā, [image] , [image] in [image] one has ā + ( [image] + [image] ) = (ā + [image] ) + [image] .

  3. Existence of an additive identity:

    For every ā in [image] one has ā + [image] = ā.

  4. Existence of an additive inverse:

    For each element ā in [image] there exists a unique element [image] in [image] such that ā + [image] = [image] .

  5. Commutativity of addition:

    For every ā, [image] in [image] one has ā + [image] = [image] + ā.

  6. Closure with respect to multiplication:

    The product of two elements of [image] is again an element of [image] .

  7. Associativity of multiplication:

    For every ā, [image] , [image] in [image] one has ā · ( [image] ) = (ā · [image] ) · [image] .

  8. Existence of a multiplicative identity: For every ā in [image] one has ā · [image] = ā.

  9. Commutativity of multiplication: For each ā, [image] in [image] one has ā · [image] = [image] · ā.

  10. In ( [image] , +, ·) the distributive law holds: ā · ( [image] + [image] ) = ā · [image] + ā · [image] .

On account of properties 1 through 5 we have that ( [image] , +) is an abelian group, where the term abelian refers to the commutativity of addition. From property 4 we can define subtraction in [image] as usual, namely, as addition of the inverse element: If [image] is the additive inverse of [image] , then [image] + [image] = [image] , and so for each ā Î [image] we may define

In ( [image] , ·) the group laws 6, 7, 8, and 9 hold for multiplication, where the multiplicative identity is [image] . However, in [image] it does not necessarily hold that each element possesses a multiplicative inverse, and thus in general, ( [image] , ·) is not a group, but merely a commutative semigroup with unit.[3] However, if we remove from [image] all the elements that have a common divisor with m greater than 1, we then obtain a structure that forms an abelian group with respect to multiplication (see Section 10.2). This structure, which in particular does not contain [image] , is denoted by ( [image] , ·).

The significance of an algebraic structure like ( [image] , ·), in view of the results we have obtained thus far, can be illustrated by looking at some other well-known commutative rings: The set of integers [image] , the set of rational numbers [image] , and the set of real numbers [image] are commutative rings with unit (in fact, the real numbers form a field, indicating additional internal structure), with the difference that these rings are not finite. The rules for computation that we have outlined above for our finite ring are well known to us because we use them every day. We shall return to these laws in Chapter 12. There they will prove to be trusty allies when it comes to testing arithmetic functions. In this chapter we have collected some important prerequisites.

For calculating with residue classes we rely completely on the classes' representatives. For each residue class modulo m we select precisely one representative and thereby form a complete residue system, in terms of which all of our calculations modulo m can be carried out. The smallest nonnegative complete residue system modulo m is the set Rm := {0, 1,..., m − 1}. The set of numbers r satisfying − [image] m < r ≤ [image] m will be called the smallest absolute complete residue system modulo m.

As an example we consider [image] = { [image] }. The smallest nonnegative residue system modulo 26 is R26 = {0, 1,...,25}, and the smallest absolute residue system modulo 26 is the set {−12, −11,...,0, 1,...,13}. The relation between arithmetic with residue classes and modular arithmetic with residue systems can be clarified as follows:

is equivalent to

while

is equivalent to

By identifying the alphabet with the residue class ring [image] or the set of ASCII characters with [image] we can calculate with characters. A simple encoding system that adds a constant from [image] to each letter of a text is ascribed to Julius Caesar, who is said to have preferred the constant [image] . Each letter of the alphabet would thereby be shifted one position to the right, with X moving to A, Y to B, and Z to C.[4]

Calculation in residue class rings can be made clearer by employing composition tables, which we present in Tables 5.1 and 5.2 for the operations "+" and "·" in [image] :

Table 5.1: Composition table for addition modulo 5
+ | 0 | 1 | 2 | 3 | 4
0 | 0 | 1 | 2 | 3 | 4
1 | 1 | 2 | 3 | 4 | 0
2 | 2 | 3 | 4 | 0 | 1
3 | 3 | 4 | 0 | 1 | 2
4 | 4 | 0 | 1 | 2 | 3
Table 5.2: Composition table for multiplication modulo 5
· | 0 | 1 | 2 | 3 | 4
0 | 0 | 0 | 0 | 0 | 0
1 | 0 | 1 | 2 | 3 | 4
2 | 0 | 2 | 4 | 1 | 3
3 | 0 | 3 | 1 | 4 | 2
4 | 0 | 4 | 3 | 2 | 1

The fact that the set of residue classes is finite gives the nice advantage over infinite structures, such as the ring of integers, that the representation of the results of arithmetic expressions within a computer program will not cause overflow if in forming residues a suitable class representative is chosen. This operation, as executed for example by the function mod_l(), is called reduction (modulo m). We can thus calculate to our hearts' content with the bounded representation of numbers and the functions of the FLINT/C package within a complete residue system modulo m, so long as we have m ≤ Nmax. We always choose positive representatives and rely on nonnegative residue systems. Because of these properties of residue classes the FLINT/C package does very well with the CLINT representation of large numbers, except for a few situations, which we shall discuss in some detail.

So much for the theory of the arithmetic of residue classes. Now we shall develop our functions for modular arithmetic. We first recall the functions mod_l() and mod2_l() from Section 4.3, which return the remainder of a reduction modulo m, respectively modulo 2k, and we shall deal in turn with modular addition and subtraction, as well as modular multiplication and squaring. Because of its particular complexity, we devote a separate chapter to modular exponentiation. We shall avoid the notation ā for a residue class by simply omitting the bar and letting the representative a denote the class of a, provided that there is no chance of confusion.

The process by which the functions for modular arithmetic operate consists essentially in carrying out the corresponding nonmodular function on the operands and then using division with remainder to carry out a modular reduction. However, one must note that intermediate results can grow to a size of 2MAXB digits, which due to their size or, in the case of subtraction, on account of a negative sign, cannot be represented in a CLINT object. We have previously called these situations respectively overflow and underflow. The basic arithmetic functions possess mechanisms for dealing with situations of overflow and underflow that reduce these intermediate results modulo (Nmax +1) (see Chapters 3 and 4). These would be effective here if the result of the complete modular operation were representable by a CLINT type. In order to obtain correct results in these cases, we shall extract from the functions that we already have for the basic operations, as announced in Chapter 4, kernel functions

  void add (CLINT, CLINT, CLINT);
  void sub (CLINT, CLINT, CLINT);
  void mult (CLINT, CLINT, CLINT);
  void umul (CLINT, USHORT, CLINT);
  void sqr (CLINT, CLINT);

The kernel functions comprise the arithmetic operations that have been removed from the functions add_l(), sub_l(), mul_l(), and sqr_l(), which we have met earlier. What remains in these functions are simply the processes of removing leading zeros, supporting the accumulator operation, and handling possible overflow or underflow, while for the actual arithmetic operations the kernel functions are invoked. The syntax and semantics of these earlier functions are not altered, and the functions can be used as described.

As an example of multiplication mul_l(), this process leads to the following function (see in this regard the implementation of the function mul_l() on page 35).

Start Sidebar
Function: | multiplication
Syntax: | int mul_l (CLINT f1_l, CLINT f2_l, CLINT pp_l);
Input: | f1_l, f2_l (factors)
Output: | pp_l (product)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
mul_l (CLINT f1_l, CLINT f2_l, CLINT pp_l)
{
  CLINT aa_l, bb_l;
  CLINTD p_l;
  int OFL = E_CLINT_OK;
Start Sidebar

Purging of leading zeros and support of the accumulator operation.

End Sidebar
  cpy_l (aa_l, f1_l);
  cpy_l (bb_l, f2_l);
Start Sidebar

Call the kernel function for multiplication.

End Sidebar
  mult (a_l, b_l, p_l);
Start Sidebar

Check for and deal with overflow.

End Sidebar
  if (DIGITS_L (p_l) > (USHORT)CLINTMAXDIGIT)    /* overflow ? */
    {
      ANDMAX_L (p_l);    /* reduce modulo (Nmax + 1) */
      OFL = E_CLINT_OFL;
    }
  cpy_l (pp_l, p_l);
  return OFL;
}

For the remaining functions add_l(), sub_l(), and sqr_l() the changes are similar. The arithmetic kernel functions themselves contain no new components and therefore do not need to be given here. For details look at the implementation in flint.c.

The kernel functions do not allow overflow, and they execute no reduction modulo (Nmax + 1). They are intended for internal use by the FLINT/C functions and therefore are declared as static. In using them, however, one must note that they are not equipped for dealing with leading zeros and that they cannot be used in accumulator mode (see Chapter 3).

The use of sub() presupposes that the difference is positive. Otherwise, the result is undefined; there is no control in sub() in this regard. Finally, the calling functions must make available enough space for the result of oversized intermediate results. In particular, sub() requires that the result variable have available at least enough storage space as for the representation of the minuend. We are now equipped to develop the functions madd_l(), msub_l(), mmul_l(), and msqr_l() for modular arithmetic.

Start Sidebar

Function:

modular addition

Syntax:

int madd_l (CLINT aa_l, CLINT bb_l, CLINT c_l,
               CLINT m_l);

Input:

aa_l, bb_l (summands), m_l (modulus)

Output:

c_l (remainder)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
madd_l (CLINT aa_l, CLINT bb_l, CLINT c_l, CLINT m_l)
{
  CLINT a_l, b_l;
  clint tmp_l[CLINTMAXSHORT + 1];

  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;
    }

  cpy_l (a_l, aa_l);
  cpy_l (b_l, bb_l);

  if (GE_L (a_l, m_l) || GE_L (b_l, m_l))
    {
      add (a_l, b_l, tmp_l);
      mod_l (tmp_l, m_l, c_l);
    }
  else
Start Sidebar

If a_l and b_l both lie below m_l, then we are spared a division.

End Sidebar
    {
      add (a_l, b_l, tmp_l);

      if (GE_L (tmp_l, m_l))
         {
           sub_l (tmp_l, m_l, tmp_l);    /* underflow excluded */
         }
Start Sidebar

In the preceding call by sub_l() some care was taken: We supply sub_l() with the argument tmp_l, which here as the sum of a_l and b_l is possibly one digit larger than allowed by the constant MAXB. Within the function sub_l() nothing can go awry as long as we provide storage space for an additional digit in the result. Therefore, we let the result be stored in tmp_l and not immediately in c_l, as one might suppose. Because of these conditions, at the end of sub_l() we have that tmp_l has at most MAXB digits.

End Sidebar
      cpy_l (c_l, tmp_l);
    }

  return E_CLINT_OK;
}

The function for modular subtraction msub_l() uses only the positive intermediate results of the functions add_l(), sub_l(), and mod_l(), in order to remain within a positive residue system.

Start Sidebar

Function:

modular subtraction

Syntax:

int msub_l (CLINT aa_l, CLINT bb_l, CLINT c_l,
                CLINT m_l);

Input:

aa_l (minuend), bb_l (subtrahend), m_l (modulus)

Output:

c_l (remainder)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
msub_l (CLINT aa_l, CLINT bb_l, CLINT c_l, CLINT m_l)
{
  CLINT a_l, b_l, tmp_l;

  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;
    }

  cpy_l (a_l, aa_l);
  cpy_l (b_l, bb_l);
Start Sidebar

We distinguish the cases a_l ≥ b_l and a_l < b_l. The first case is a standard situation; in the second case we compute (b_l − a_l), reduce modulo m_l, and subtract a positive remainder from m_l.

End Sidebar
  if (GE_L (a_l, b_l))    /* a_l − b_l ≥ 0 */
    {
      sub (a_l, b_l, tmp_l);
      mod_l (tmp_l, m_l, c_l);
    }
  else    /* a_l − b_l < 0 */
    {
      sub (b_l, a_l, tmp_l);
      mod_l (tmp_l, m_l, tmp_l);
      if (GTZ_L (tmp_l))
         {
           sub (m_l, tmp_l, c_l);
         }
      else
         {
           SETZERO_L (c_l);
         }
    }
  return E_CLINT_OK;
}

Now come the functions mmul_l() and msqr_l() for modular multiplication and squaring, of which we show only that for multiplication.

Start Sidebar

Function:

modular multiplication

Syntax:

int mul_l (CLINT aa_l, CLINT bb_l, CLINT c_l,
              CLINT m_l);

Input:

aa_l, bb_l (factors), m_l (modulus)

Output:

c_l (remainder)

Return:

E_CLINT_OK if all ok

E_CLINT_DBZ if division by 0
End Sidebar
int
mmul_l (CLINT aa_l, CLINT bb_l, CLINT c_l, CLINT m_l)
{
  CLINT a_l, b_l;
  CLINTD tmp_l;

  if (EQZ_L (m_l))
    {
       return E_CLINT_DBZ;
    }
  cpy_l (a_l, aa_l);
  cpy_l (b_l, bb_l);
  mult (a_l, b_l, tmp_l);
  mod_l (tmp_l, m_l, c_l);
  return E_CLINT_OK;
}

The functions for modular multiplication and squaring are so similar that for modular multiplication we give only the interface of the function.

Start Sidebar
Function: | modular squaring
Syntax: | int msqr_l (CLINT aa_l, CLINT c_l, CLINT m_l);
Input: | aa_l (factor), m_l (modulus)
Output: | c_l (remainder)
Return: | E_CLINT_OK if all is ok E_CLINT_DBZ if division by 0
End Sidebar

To each of these functions (of course, with the exception of squaring) there is a corresponding mixed function, which as its second argument takes a USHORT argument. As an example, we demonstrate the function umadd_l(). The functions umsub_l() and ummul_l() follow exactly the same pattern, and so we shall not go into them in detail.

Start Sidebar

Function:

modular addition of a CLINT type and a USHORT type

Syntax:

int umadd_l (CLINT a_l, USHORT b, CLINT c_l,
               CLINT m_l);

Input:

a_l, b (summands), m_l (modulus)

Output:

c_l (remainder)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
umadd_l (CLINT a_l, USHORT b, CLINT c_l, CLINT m_l)
{
  int err;
  CLINT tmp_l;
  u2clint_l (tmp_l, b);
  err = madd_l (a_l, tmp_l, c_l, m_l);
  return err;
}

Our collection of mixed functions with a USHORT argument will be extended in the following chapter to include two further functions. To end this chapter we would like, with the help of modular subtraction, to construct an additional useful auxiliary function that determines whether two CLINT values are equal as representatives of a residue class modulo m. The following function mequ_l() accomplishes this by using the definition of the congruence relationship a ≡ b mod m ⇔ m | (a − b).

To determine whether two CLINT objects a_l and b_l are equivalent modulo m_l, we need do nothing further than apply msub_l(a_l, b_l, r_l, m_l) and check whether the remainder r_l of this operation is equal to zero.

Start Sidebar
Function: | test for equivalence modulo m
Syntax: | int mequ_l (CLINT a_l, CLINT b_l, CLINT m_l);
Input: | a_l, b_l (operands), m_l (modulus)
Return: | 1 if (a_l == b_l) modulo m_l 0 otherwise
End Sidebar
int
mequ_l (CLINT a_l, CLINT b_l, CLINT m_l)
{
  CLINT r_l;

  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;
    }

  msub_l (a_l, b_l, r_l, m_l);
  return ((0 == DIGITS_L (r_l))?1:0);
}

[1]Carl Friedrich Gauss, 1777–1855, is to be counted among the greatest mathematicians of all time. He made many significant discoveries in mathematics as well as in the natural sciences, and in particular, at the age of 24 he published his famous Disquisitiones Arithmeticae, which is the foundation upon which modern number theory has been built.

[2]Two sets are said to be disjoint if they have no elements in common, or put another way, if their intersection is the empty set.

[3]A semigroup (H, *) exists merely by virtue of there existing on the set H an associative relation *.

[4]See Aulus Gellius, XII, 9 and Suetonius, Caes. LVI.


Chapter 6: Where All Roads Meet: Modular Exponentiation

Overview

Two roads diverged in a yellow wood,

And sorry I could not travel both

And be one traveler, long I stood ...

—Robert Frost, "The Road Not Taken"

IN ADDITION TO THE CALCULATIONAL rules for addition, subtraction, and multiplication in residue classes we can also define an operation of exponentiation, where the exponent specifies how many times the base is to be multiplied by itself. Exponentiation is carried out, as usual, by means of recursive calls to multiplication: For a in [image] we have a0 := [image] and ae+1 := a · ae.

It is easy to see that for exponentiation in [image] the usual rules apply (cf. Chapter 1):

Click To expand


6.1 First Approaches

The simplest approach to exponentiation consists in following the recursive rule defined above and multiplying the base a by itself e times. This requires e − 1 modular multiplications, and for our purposes that is simply too many.

A more efficient way of proceeding is demonstrated in the following examples, in which we consider the binary representation of the exponent:

Click To expand

Here raising the base to the fifteenth power requires only six multiplications, as opposed to fourteen in the first method. Half of these are squarings, which, as we know, require only about half as much CPU time as regular multiplications. Exponentiation to the sixteenth power is accomplished with only four squarings.

Algorithms for exponentiation of ae modulo m that calculate with the binary representation of the exponent are in general much more favorable than the first approach, as we are about to see. But first we must observe that the intermediate results of many integer multiplications one after the other quickly occupy more storage space than can be supplied by all the computer memory in the world, for from p = ab follows log p = b log a, and thus the number of digits of the exponentiated ab is the product of the exponent and the number of digits of the base. However, if we carry out the calculation of ae in a residue class ring [image] , that is, by means of modular multiplication, then we avoid this problem. In fact, most applications require exponentiation modulo m, so we may as well focus our attention on this case.

Let e = (en−1en−2...e0)2 with en−1 > 0 be the binary representation of the exponent e. Then the following binary algorithm requires ⌊log2 e⌋ = n modular squarings and δ(e) − 1 modular multiplications, where

is the number of ones in the binary representation of e. If we assume that each digit takes on the value 0 or 1 with equal probability, then we may conclude that δ(e) has the expected value δ(e) = n/2, and altogether we have [image] ⌊log2 e⌋ multiplications for the algorithm.

Binary algorithm for exponentiation of ae modulo m

  1. Set p ← [image] and i ← n − 2.

  2. Set p ← p2 mod m.

  3. If ei = 1, set p ← p · a mod m.

  4. Set i ← i − 1; if i ≥ 0, go to step 2.

  5. Output p.

The following implementation of this algorithm gives good results already for small exponents, those that can be represented by the USHORT type.

Start Sidebar

Function:

mixed modular exponentiation with USHORT exponent

Syntax:

int umexp_l (CLINT bas_l, USHORT e, CLINT p_l,
               CLINT m_l);

Input:

bas_l (base)

e (exponent)

m_l (modulus)

Output:

p_l (power residue)

Return:

E_CLINT_OK if all ok

E_CLINT_DBZ if division by 0
End Sidebar
int
umexp_l (CLINT bas_l, USHORT e, CLINT p_l, CLINT m_l)
{
  CLINT tmp_l, tmpbas_l;
  USHORT k = BASEDIV2;
  int err = E_CLINT_OK;

  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;    /* division by zero */
    }
  if (EQONE_L (m_l))
    {
      SETZERO_L (p_l);    /* modulus = 1 ==> remainder = 0 */
      return E_CLINT_OK;
    }
  if (e == 0)    /* exponent = 0 ==> remainder = 1 */
    {
      SETONE_L (p_l);
      return E_CLINT_OK;
    }
  if (EQZ_L (bas_l))
    {
      SETZERO_L (p_l);
      return E_CLINT_OK;
    }
  mod_l (bas_l, m_l, tmp_l);
  cpy_l (tmpbas_l, tmp_l);
Start Sidebar

After various checks the position of the leading 1 of the exponent e is determined. Here the variable k is used to mask the individual binary digits of e. Then k is shifted one more place to the right, corresponding to setting i ← n − 2 in step 1 of the algorithm.

End Sidebar
  while ((e & k) == 0)
    {
      k >>= 1;
    }
  k >>= 1;
Start Sidebar

For the remaining digits of e we run through steps 2 and 3. The mask k serves as loop counter, which we shift to the right one digit each time. We then multiply by the base reduced modulo m_l.

End Sidebar
  while (k != 0)
    {
      msqr_l (tmp_l, tmp_l, m_l);
      if (e & k)
         {
           mmul_l (tmp_l, tmpbas_l, tmp_l, m_l);
         }
      k >>= 1;
    }
  cpy_l (p_l, tmp_l);
  return err;
}

The binary algorithm for exponentiation offers particular advantages when it is used with small bases. If the base is of type USHORT, then all of the multiplications p ← pa mod m in step 3 of the binary algorithm are of the type CLINT * USHORT modulo CLINT, which makes possible a substantial increase in speed in comparison to other algorithms that in this case would also require the multiplication of two CLINT types. The squarings, to be sure, use CLINT objects, but here we are able to use the advantageous squaring function.

Thus in the following we shall implement the exponentiation function wmexp_l(), the dual to umexp_l(), which accepts a base of type USHORT. The masking out of bits of the exponent is a good preparatory exercise in view of the following "large" functions for exponentiation. The way of proceeding consists essentially in testing one after the other each digit ei of the exponent against a variable b initialized to 1 in the highest-valued bit, and then shifting to the right and repeating the test until b is equal to 0.

The following function wmexp_l() offers for small bases and exponents up to 1000 bits, for example, a speed advantage of about ten percent over the universal procedures that we shall tackle later.

Start Sidebar

Function:

modular exponentiation of a USHORT base

Syntax:

int wmexp_l (USHORT bas, CLINT e_l, CLINT rest_l,
               CLINT m_l);

Input:

bas (base)

e_l (exponent)

m_l (modulus)

Output:

rest_l (remainder of base_l mod m_l)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
wmexp_l (USHORT bas, CLINT e_l, CLINT rest_l, CLINT m_l)
{
  CLINT p_l, z_l;
  USHORT k, b, w;
  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;    /* division by 0 */
    }

  if (EQONE_L (m_l))
    {
      SETZERO_L (rest_l);    /* modulus = 1 ==> remainder = 0 */
      return E_CLINT_OK;
    }
  if (EQZ_L (e_l))
    {
      SETONE_L (rest_l);
      return E_CLINT_OK;
    }
  if (0 == bas)
    {
      SETZERO_L (rest_l);
      return E_CLINT_OK;
    }
  SETONE_L (p_l);
  cpy_l (z_l, e_l);
Start Sidebar

Beginning with the highest-valued nonzero bit in the highest-valued word of the exponent z_l the bits of the exponent are processed, where always we have first a squaring and then, if applicable, a multiplication. The bits of the exponent are tested in the expression if ((w & b) > 0) by masking their value with a bitwise AND.

End Sidebar
  b = 1 << ((ld_l (z_l) - 1) & (BITPERDGT - 1UL));
  w = z_l[DIGITS_L (z_l)];
  for (; b > 0; b >>= 1)
    {
      msqr_l (p_l, p_l, m_l);
      if ((w & b) > 0)
         {
           ummul_l (p_l, bas, p_l, m_l);
         }
    }
Start Sidebar

Then follows the processing of the remaining digits of the exponent.

End Sidebar
  for (k = DIGITS_L (z_l) - 1; k > 0; k--)
    {
      w = z_l[k];
      for (b = BASEDIV2; b > 0; b >>= 1)
         {
           msqr_l (p_l, p_l, m_l);
           if ((w & b) > 0)
              {
                ummul_l (p_l, bas, p_l, m_l);
              }
         }
    }
  cpy_l (rest_l, p_l);

  return E_CLINT_OK;
}


6.2 M-ary Exponentiation

Through a generalization of the binary algorithm on page 80 the number of modular multiplications for exponentiation can be reduced even further. The idea is to represent the exponent in a base greater than 2 and to replace multiplication by a in step 3 by multiplication by powers of a. Thus let the exponent e be given by e = (en−1 en−2...e0)M, to a base M yet to be determined. The following algorithm calculates the powers ae mod m.

M-ary algorithm for exponentiation ae mod m

  1. Calculate and store a2 mod m, a3 mod m,..., aM−1 mod m as a table.

  2. Set p ← [image] mod m and i ← n − 2.

  3. Set p ← pM mod m.

  4. If ei ≠ 0, set p ← [image] mod m.

  5. Set i ← i − 1; if i ≥ 0, go to step 3.

  6. Output p.

The number of necessary multiplications evidently depends on the number of digits of the exponent e and thus on the choice of M. Therefore, we would like to determine M such that the exponentiation in step 3 can be computed to the greatest extent possible by means of squaring, as in the example above for 216, and such that the number of multiplications by the precomputed powers of a be minimized to a justifiable cost of storage space for the table.

The first condition suggests that we choose M as a power of 2: M = 2k. In view of the second condition we consider the number of modular multiplications as a function of M:

We require

(6.1)

squares in step 3 and on average

(6.2) Click To expand

modular multiplications in step 4, where

is the probability that a digit ei of e is nonzero. If we include the M − 2 multiplications for the computation of the table, then the M-ary algorithm requires on average

(6.3) Click To expand
(6.4)

modular squarings and multiplications.

For exponents e and moduli m of, say, 512 binary places and M = 2k we obtain the numbers of modular multiplications for the calculation of ae mod m as shown in Table 6.1. The table shows as well the memory requirement for the precomputed powers of a mod m, which result from the product (2k − 2)CLINTMAXSHORT · sizeof(USHORT).

Table 6.1: Requirements for exponentiation
k | Multiplications | Memory (in bytes)
1 | 766 | 0
2 | 704 | 1028
3 | 666 | 3084
4 | 644 | 7196
5 | 640 | 15420
6 | 656 | 31868

We see from the table that the average number of multiplications reaches a minimum of 640 at k = 5, where the required memory for each larger k grows by approximately a factor of 2. But what are the time requirements for other orders of magnitude of the exponents?

Table 6.2 gives information about this. It displays the requirements for modular multiplication for exponentiation with exponents with various numbers of binary digits and for various values of M = 2k. The exponent length of 768 digits was included because it is a frequently used key length for the RSA cryptosystem (see Chapter 16). The favorable numbers of multiplications appear in boldface type.

Table 6.2: Numbers of multiplications for typical sizes of exponents and various bases 2k
Number of binary digits in the exponent
k | 32 | 64 | 128 | 512 | 768 | 1024 | 2048 | 4096
1 | 45 | 93 | 190 | 766 | 1150 | 1534 | 3070 | 6142
2 | 44 | 88 | 176 | 704 | 1056 | 1408 | 2816 | 5632
3 | 46 | 87 | 170 | 666 | 996 | 1327 | 2650 | 5295
4 | 52 | 91 | 170 | 644 | 960 | 1276 | 2540 | 5068
5 | 67 | 105 | 181 | 640 | 945 | 1251 | 2473 | 4918
6 | 98 | 135 | 209 | 656 | 954 | 1252 | 2444 | 4828
7 | 161 | 197 | 271 | 709 | 1001 | 1294 | 2463 | 4801
8 | 288 | 324 | 396 | 828 | 1116 | 1404 | 2555 | 4858

In consideration of the ranges of numbers for which the FLINT/C package was developed, it appears that with k = 5 we have found the universal base M = 2k, with which, however, there is a rather high memory requirement of 15 kilobytes for the powers a2, a3,..., a31 to base a that are to be precomputed. The M-ary algorithm can be improved, however, according to [Cohe], Section 1.2, to the extent that we can employ not M − 2, but only M/2, premultiplications and thus require only half the memory. The task now additionally consists in the calculation of the power ae mod m, where e = (en−1en−2...e0)M is the representation of the exponent to the base M = 2k.

M-ary Algorithm for exponentiation with reduced number of premultiplications

  1. Compute and store a3 mod m, a5 mod m, a7 mod m,..., [image] mod m.

  2. If en−1 = 0, set p ← 1.

    If en−1 ≠ 0, factor en−1 = 2tu with odd u. Set p ← au mod m.

    In each case set i ← n − 2.

  3. If ei = 0, set p ← [image] mod m by calculating [image] mod m (k-fold squaring modulo m).

    If ei ≠ 0, factor ei = 2tu with odd u; set p ← [image] mod m and then p ← pau mod m; now set p ← [image] mod m.

  4. Set i ← i − 1; if i ≥ 0, go to step 3.

  5. Output p.

The trick of this algorithm consists in dividing up the squarings required in step 3 in a clever way, such that the exponentiation of a is taken care of together with the even part 2t of ei. Within the squaring process the exponentiation of a by the odd part u of ei remains. The balance between multiplication and squaring is shifted to the more favorable squaring, and only the powers of a with odd exponent need to be precomputed and stored.

For this splitting one requires the uniquely determined representation ei = 2tu, u odd, of the exponent digit ei. For rapid access to t and u a table is used, which, for example, for k = 5 is displayed in Table 6.3.

Table 6.3: Values for the factorization of the exponent digits into products of a power of 2 and an odd factor
ei | t | u | ei | t | u | ei | t | u
0 | 0 | 0 | 11 | 0 | 11 | 22 | 1 | 11
1 | 0 | 1 | 12 | 2 | 3 | 23 | 0 | 23
2 | 1 | 1 | 13 | 0 | 13 | 24 | 3 | 3
3 | 0 | 3 | 14 | 1 | 7 | 25 | 0 | 25
4 | 2 | 1 | 15 | 0 | 15 | 26 | 1 | 13
5 | 0 | 5 | 16 | 4 | 1 | 27 | 0 | 27
6 | 1 | 3 | 17 | 0 | 17 | 28 | 2 | 7
7 | 0 | 7 | 18 | 1 | 9 | 29 | 0 | 29
8 | 3 | 1 | 19 | 0 | 19 | 30 | 1 | 15
9 | 0 | 9 | 20 | 2 | 5 | 31 | 0 | 31
10 | 1 | 5 | 21 | 0 | 21 |  |  |

To calculate these values we can use the auxiliary function twofact_l(), which will be introduced in Section 10.4.1. Before we can program the improved M-ary algorithm there remains one problem to be solved: How, beginning with the binary representation of the exponent or the representation to base B = 216, do we efficiently obtain its representation to base M = 2k for a variable k > 0? It will be of use here to do a bit of juggling with the various indices, and we can "mask out" the required digits ei to base M from the representation of e to base B. For this we set the following: Let (εr−1εr−2...ε0)2 be the representation of the exponent e to base 2 (we need this on account of the number r of binary digits). Let (eu−1eu−2...e0)B be the representation of e as a CLINT type to base B = 216, and let [image] be the representation of e to the base M = 2k, k ≤ 16 (M should not be greater than our base B). The representation of e in memory as a CLINT object e_l corresponds to the sequence [u + 1],[e0],[e1],...,[eu−1],[0] of USHORT values e_l[i] for i = 0,..., u + 1; one should note that we have added a leading zero.

Let f := [image] , and for i = 0,..., f let si := [image] and di := ki mod 16. With these settings the following statements hold:

  1. There are f + 1 digits in [image] ; that is, n − 1 = f.

  2. contains the least-significant bit of the digit [image] .

  3. di specifies the position of the least-significant bit of [image] in [image] (counting of positions begins with 0). If i < f and di > 16 − k, then not all the binary digits of [image] are in [image] ; the remaining (higher-valued) bits of [image] are in [image] +1. The desired digit [image] thus corresponds to the k least-significant binary digits of

As a result we have for i Î {0,..., f} the following expression for determining [image] :

(6.5) Click To expand

Since for the sake of simplicity we set e_l [sf + 2] ← 0, this expression holds as well for i = f.

We have thus found an efficient method for accessing the digits of the exponent in its CLINT representation, which arise from its representation in a power-of-two base 2k with k ≤ 16, whereby we are saved an explicit transformation of the exponent. The number of necessary multiplications and squarings for the exponentiation is now

(6.6) Click To expand

where in comparison to μ1(k) (see page 86) the expenditure for the precomputations has been reduced by half. The table for determining the favorable values of k (Table 6.4) now has a somewhat different appearance.

Table 6.4: Numbers of multiplications for typical sizes of exponents and various bases 2k
Number of binary digits in the exponent
k | 32 | 64 | 128 | 512 | 768 | 1024 | 2048 | 4096
1 | 47 | 95 | 191 | 767 | 1151 | 1535 | 3071 | 6143
2 | 44 | 88 | 176 | 704 | 1056 | 1408 | 2816 | 5632
3 | 44 | 85 | 168 | 664 | 994 | 1325 | 2648 | 5293
4 | 46 | 85 | 164 | 638 | 954 | 1270 | 2534 | 5066
5 | 53 | 91 | 167 | 626 | 931 | 1237 | 2459 | 4904
6 | 68 | 105 | 179 | 626 | 924 | 1222 | 2414 | 4798
7 | 99 | 135 | 209 | 647 | 939 | 1232 | 2401 | 4739
8 | 162 | 198 | 270 | 702 | 990 | 1278 | 2429 | 4732

Starting with 768 binary digits of the exponent, the favorable values of k are larger by 1 than those given in the table for the previous version of the exponentiation algorithm, while the number of required modular multiplications has easily been reduced. It is to be expected that this procedure is on the whole more favorable than the variant considered previously. Nothing now stands in the way of an implementation of the algorithm.

To demonstrate the implementation of these principles we select an adaptive procedure that uses the appropriate optimal value for k. To accomplish this we rely again on [Cohe] and look for, as is specified there, the smallest integer value k that satisfies the inequality

(6.7)

which comes from the formula μ2(k) given previously for the number of necessary multiplications based on the condition μ2(k + 1) − μ2(k) ≥ 0. The constant number of modular squarings ⌊log2 e⌋ for all algorithms for exponentiation introduced thus far is eliminated; here only the "real" modular multiplications, that is, those that are not squarings, are considered.

The implementation of exponentiation with variable k requires a large amount of main memory for storing the precomputed powers of a; for k = 8 we require about 64 Kbyte for 127 CLINT variables (this is arrived at via (27 − 1) * sizeof(USHORT) * CLINTMAXSHORT), where two additional automatic CLINT fields were not counted. For applications with processors or memory models with segmented 16-bit architecture this already has reached the limit of what is possible (see in this regard, for example, [Dunc], Chapter 12, or [Petz], Chapter 7).

Depending on the system platform there are thus various strategies appropriate for making memory available. While the necessary memory for the function mexp5_l() is taken from the stack (as automatic CLINT variables), with each call of the following function mexpk_l() memory is allocated from the heap. To save the expenditure associated with this, one may imagine a variant in which the maximum needed memory is reserved during a one-time initialization and is released only at the end of the entire program. In each case it is possible to fit memory management to the concrete requirements and to orient oneself to this in the commentaries on the following code.

One further note for applications: It is recommended always to check whether it suffices to employ the algorithm with the base M = 25. The savings in time that comes with larger values of k is relatively not so large in comparison to the total calculation time so as to justify in all cases the greater demand on memory and the thereby requisite memory management. Typical calculation times for various exponentiation algorithms, on the basis of which one can decide whether to use them, are given in Appendix D.

The algorithm, implemented with M = 25, is contained in the FLINT/C package as the function mexp5_l(). With the macro EXP_L() contained in flint.h one can set the exponentiation function to be used: mexp5_l() or the following function mexpk_l() with variable k.

Start Sidebar

Function:

modular exponentiation

Syntax:

int mexpk_l (CLINT bas_l, CLINT exp_l,
               CLINT p_l, CLINT m_l);

Input:

bas_l (base)

exp_l (exponent)

m_l (modulus)

Output:

p_l (power residue)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0

E_CLINT_MAL if malloc() error
End Sidebar
Start Sidebar

We begin with a segment of the table for representing ei = 2tu, u odd, 0 ≤ ei < 28. The table is represented in the form of two vectors. The first, twotab[], contains the exponents t of the two-factor 2t, while the second, oddtab[], holds the odd part u of a digit 0 ≤ ei < 25. The complete table is contained, of course, in the FLINT/C source code.

End Sidebar
static int twotab[] =
{0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,5, ...};
static USHORT oddtab[]=
{0,1,1,3,1,5,3,7,1,9,5,11,3,13,7,15,1,17,9,19,5,21,11,23,3,25,13, ...};

int
mexpk_l (CLINT bas_l, CLINT exp_l, CLINT p_l, CLINT m_l)
{
Start Sidebar

The definitions reserve memory for the exponents plus the leading zero, as well as a pointer clint **aptr_l to the memory still to be allocated, which will take pointers to the powers of bas_l to be precomputed. In acc_l the intermediate results of the exponentiation will be stored.

End Sidebar
  CLINT a_l, a2_l;
  clint e_l[CLINTMAXSHORT + 1];
  CLINTD acc_l;
  clint **aptr_l, *ptr_l;
  int noofdigits, s, t, i;
  unsigned int k, lge, bit, digit, fk, word, pow2k, k_mask;
Start Sidebar

Then comes the usual checking for division by 0 and reduction by 1.

End Sidebar
  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;
    }

  if (EQONE_L (m_l))
    {
      SETZERO_L (p_l);    /* modulus = 1 ==> residue = 0 */
      return E_CLINT_OK;
    }
Start Sidebar

Base and exponent are copied to the working variables a_l and e_l, and any leading zeros are purged.

End Sidebar
  cpy_l (a_l, bas_l);
  cpy_l (e_l, exp_l);
Start Sidebar

Now we process the simple cases a0 = 1 and 0e = 0 (e > 0).

End Sidebar
  if (EQZ_L (e_l))
    {
      SETONE_L (p_l);
      return E_CLINT_OK;
    }

  if (EQZ_L (a_l))
    {
      SETZERO_L (p_l);
      return E_CLINT_OK;
    }
Start Sidebar

Next, the optimal value for k is determined; the value 2k is stored in pow2k, and in k_mask the value 2k−1. For this the function ld_l() is used, which returns the number of binary digits of its argument.

End Sidebar
  lge = ld_l (e_l);
  k = 8;
  while (k > 1 && ((k - 1) * (k << ((k - 1) << 1))/((1 << k ) - k - 1)) >= lge - 1)
    {
      --k;
    }
  pow2k = 1U << k;
  k_mask = pow2k - 1U;
Start Sidebar

Memory is allocated for the pointers to the powers of a_l to be computed. The base a_l is reduced modulo m_l.

End Sidebar
  if ((aptr_l = (clint **) malloc (sizeof(clint *) * pow2k)) == NULL)
    {
      return E_CLINT_MAL;
    }
  mod_l (a_l, m_l, a_l);
  aptr_l[1] = a_l;
Start Sidebar

If k > 1, then memory is allocated for the powers to be computed. This is not necessary for k = 1, since then no powers have to be precomputed. In the following setting of the pointer aptr_l[i] one should note that in the addition of an offset to a pointer p a scaling of the offset by the compiler takes place, so that it counts objects of the pointer type of p.

We have already mentioned that the allocation of working memory can be carried out alternatively in a one-time initialization. The pointers to the CLINT objects would in this case be contained in global variables outside of the function or in static variables within mexpk_l().

End Sidebar
    if (k > 1)
      {
         if ((ptr_l = (clint *) malloc (sizeof(CLINT) * ((pow2k >> 1) - 1))) == NULL)
           {
              return E_CLINT_MAL;
           }

         aptr_l[2] = a2_l;
         for (aptr_l[3] = ptr_l, i = 5; i < (int)pow2k; i+=2)
           {
              aptr_l[i] = aptr_l[i - 2] + CLINTMAXSHORT;
           }
Start Sidebar

Now comes the precomputation of the powers of the value a stored in a_l. The values a3, a5, a7,...,ak − 1 are computed (a2 is needed only in an auxiliary role).

End Sidebar
    msqr_l (a_l, aptr_l[2], m_l);
    for (i = 3; i < (int)pow2k; i += 2)
       {
         mmul_l (aptr_l[2], aptr_l[i - 2], aptr_l[i], m_l);
       }
   }
Start Sidebar

This ends the case distinction for k > 1. The exponent is lengthened by the leading zero.

End Sidebar
  *(MSDPTR_L (e_l) + 1) = 0;
Start Sidebar

The determination of the value f (represented by the variable noofdigits).

End Sidebar
  noofdigits = (lge - 1)/k;
  fk = noofdigits * k;
Start Sidebar

Word position si and bit position di of the digit ei in the variables word and bit.

End Sidebar
  word = fk >> LDBITPERDGT;    /* fk div 16 */
  bit = fk & (BITPERDGT-1U);    /* fk mod 16 */
Start Sidebar

Calculation of the digit en−1 with the above-derived formula; en−1 is represented by the variable digit.

End Sidebar
  switch (k)
    {
      case 1:
      case 2:
      case 4:
      case 8:
         digit = ((ULONG)(e_l[word + 1] ) >> bit) & k_mask;
         break;
      default:
         digit = ((ULONG)(e_l[word + 1] | ((ULONG)e_l[word + 2]
                                               << BITPERDGT)) >> bit) & k_mask;
    }
Start Sidebar

First run through step 3 of the algorithm, the case digit = en−1 ≠ 0.

End Sidebar
    if (digit != 0)    /* k-digit > 0 */
       {
         cpy_l (acc_l, aptr_l[oddtab[digit]]);
Start Sidebar

Calculation of [image] ; t is set to the two-part of en−1 via twotab[en−1]; p is represented by acc_l.

End Sidebar
         t = twotab[digit];
         for (; t > 0; t--)
            {
              msqr_l (acc_l, acc_l, m_l);
            }
            }
         else    /* k-digit == 0 */
            {
              SETONE_L (acc_l);
            }
Start Sidebar

Loop over noofdigits beginning with f - 1.

End Sidebar
  for (--noofdigits, fk -= k; noofdigits >= 0; noofdigits--, fk -= k)
    {
Start Sidebar

Word position si and bit position di of the digit ei in the variables word and bit.

End Sidebar
     word = fk >> LDBITPERDGT;     /* fk div 16 */
     bit = fk & (BITPERDGT - 1U);     /* fk mod 16 */
Start Sidebar

Computation of the digit ei with the formula derived above; ei is represented by the variable digit.

End Sidebar
     switch (k)
        {
          case 1:
          case 2:
          case 4:
          case 8:
             digit = ((ULONG)(e_l[word + 1] ) >> bit) & k_mask;
             break;
          default:
             digit = ((ULONG)(e_l[word + 1] | ((ULONG)e_l[word + 2]
                                                   << BITPERDGT)) >> bit) & k_mask;
        }
Start Sidebar

Step 3 of the algorithm, the case digit = ei ≠ 0; t is set via the table twotab[ei] to the two-part of ei.

End Sidebar
     if (digit != 0)    /* k-digit > 0 */
        {
          t = twotab[digit];
Start Sidebar

Calculation of [image] au in acc_l. Access to au with the odd part u of ei is via aptr_l [oddtab [ei]].

End Sidebar
         for (s = k - t; s > 0; s--)
            {
              msqr_l (acc_l, acc_l, m_l);
            }

         mmul_l (acc_l, aptr_l[oddtab[digit]], acc_l, m_l);
Start Sidebar

Calculation of [image] ; p is still represented by acc_l.

End Sidebar
         for (; t > 0; t--)
            {
              msqr_l (acc_l, acc_l, m_l);
            }
       }
    else    /* k-digit == 0 */
       {
Start Sidebar

Step 3 of the algorithm, case ei = 0: Calculate [image] .

End Sidebar
         for (s = k; s > 0; s--)
            {
              msqr_l (acc_l, acc_l, m_l);
            }
        }
    }
Start Sidebar

End of the loop; output of acc_l as power residue modulo m_l.

End Sidebar
  cpy_l (p_l, acc_l);
Start Sidebar

At the end, allocated memory is released.

End Sidebar
  free (aptr_l);
  if (ptr_l != NULL)
    free (ptr_l);
  return E_CLINT_OK;
}

The various processes of M-ary exponentiation can be clarified with the help of a numerical example. To this end let us examine the calculation of the power 1234667 mod 18577, which will be carried out by the function mexpk_l() in the following steps:

  1. Precomputations

    The representation of the exponent e = 667 can be expressed to the base 2k with k = 2 (cf. the algorithm for M-ary exponentiation on page 86), whereby the exponent e has the representation e = [image] .

    The power a3 mod 18577 has the value 17354. Further powers of a do not arise in the precomputation because of the small size of the exponent.

  2. Exponentiation loop

    exponent digit ei = 2tu

    21 · 1

    21 · 1

    20 · 1

    21 · 1

    20 · 3

    p ← p2 mod n

    -

    14132

    13261

    17616

    13599

    p ← [image] mod n

    -

    -

    4239

    -

    17343

    p ← pau mod n

    1234

    13662

    10789

    3054

    4445

    p ← p2 mod n

    18019

    7125

    -

    1262

    -

  3. Result

    p = 1234667 mod 18577 = 4445.

As an extension to the general case we shall introduce a special version of exponentiation with a power of two 2k as exponent. From the above considerations we know that this function can be implemented very easily by means of k-fold exponentiation. The exponent 2k will be specified by k.

Start Sidebar

Function:

modular exponentiation with exponent a power of 2

Syntax:

int mexp2_l (CLINT a_l, USHORT k, CLINT p_l,
               CLINT m_l);

Input:

a_l (base)

k (exponent of 2)

m_l (modulus)

Output:

p_l (residue of [image] mod m_l)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0
End Sidebar
int
mexp2_l (CLINT a_l, USHORT k, CLINT p_l, CLINT m_l)
{
  CLINT tmp_l;

  if (EQZ_L (m_l))
    {
      return E_CLINT_DBZ;
    }
Start Sidebar

If k > 0, then a_l is squared k times modulo m_l.

End Sidebar
  if (k > 0)
    {
      cpy_l (tmp_l, a_l);
      while (k-- > 0)
         {
           msqr_l (tmp_l, tmp_l, m_l);
         }
      cpy_l (p_l, tmp_l);
    }
  else
Start Sidebar

Otherwise, if k = 0, we need only to reduce modulo m_l.

End Sidebar
    {
      mod_l (a_l, m_l, p_l);
    }
  return E_CLINT_OK;
}


6.3 Addition Chains and Windows

A number of algorithms for exponentiation have been published, some of which are conceived for special cases and others for general operands. The goal is always to find procedures that employ as few multiplications and divisions as possible, such as in the reduction in the number of operations achieved through passage from binary to M-ary exponentiation.

Binary and M-ary exponentiation are themselves special cases of the construction of addition chains (cf. [Knut], Section 4.6.3). We have already taken advantage of the fact that the laws of exponentiation allow the additive decomposition of the exponent of a power: e = k + l ⇒ ae = ak+l = ak al. Binary exponentiation decomposes the exponent into a sum

Click To expand

from which follows the exponentiation in the form of alternating squarings and multiplications (cf. page 80):

Click To expand

The associated addition chain is obtained by considering the exponents to powers of a that arise as intermediate results in this process:

Click To expand

Here terms of the sequence are omitted if ej = 0 for a particular j. For the number 123, for example, based on the binary method the result is the following addition chain with 12 elements: 1, 2, 3, 6, 7, 14, 15, 30, 60, 61, 122, 123.

In general, a sequence of numbers 1 = a0, a1, a2,..., ar = e for which for every i = 1,...,r there exists a pair (j, k) with j ≤ k < i such that ai = aj + ak holds is called an addition chain for e of length r.

The M-ary method generalizes this principle for the representation of the exponent to other bases. Both methods have the goal of producing addition chains that are as short as possible, in order to minimize the calculational expense for the exponentiation. The addition chain for 123 produced by the 23-ary method is 1, 2, 3, 4, 7, 8, 15, 30, 60, 120, 123; with the 24-ary method the addition chain 1, 2, 3, 4, 7, 11, 14, 28, 56, 112, 123 is created. These last chains are, as expected, considerably shorter than those obtained by the binary method, which for larger numbers will have a greater effect than in this example. In view of the real savings in time one must, however, note that in the course of initialization for the calculation of ae mod n the M-ary methods construct the powers a2, a3, a5, aM−1 also for those exponents that are not needed in the representation of e to the base M or for the construction of the addition chain.

Binary exponentiation represents the worst case of an addition chain: By considering it we obtain a bound on the greatest possible length of an addition chain of log2 e + H(e) − 1, where H(e) denotes the Hamming weight of e.[1] The length of an addition chain is bounded below by log2 e + log2 H(e) − 2.13, and so a shorter addition chain for e is not to be found (cf. [Scho] or [Knut], Section 4.6.3, Exercises 28, 29). For our example this means that the shortest addition chain for e = 123 has length at least 8, and so the results of the M-ary methods cited earlier seem not to be the best possible.

The search for shortest addition chains is a problem for which there is as yet no known polynomial-time procedure. It lies in the complexity class NP of those problems that can be solved in polynomial time by nondeterministic methods, that is, those that can be solved by "guessing," in contrast to the class P of problems that can be solved deterministically in polynomial time. It is not surprising that P is a subset of NP, since all polynomial-time deterministic problems can also be solved nondeterministically.

The determination of the shortest addition chain is an NP-complete problem, that is, a problem that is at least as difficult to solve as all other problems in the set NP (cf. [HKW], page 302). The NP-complete problems are therefore of particular interest, since if for even one of them a deterministic polynomial-time procedure could be found, then all other problems in NP could be solved in polynomial time as well. In this case the classes P and NP would collapse into a single set of problems. The unsolved problem of whether P = NP is a central problem of complexity theory. However, as of the present, the betting is in favor of P ≠ NP.

With this it is clear that all practical procedures for generating addition chains must rest on heuristics, that is to say, mathematical rules of thumb such as that for the determination of the exponent k in 2k-ary exponentiation, of which one knows that it has better time behavior than other methods.

For example, in 1990 Y. Yacobi [Yaco] described a connection between the construction of addition chains and the compression of data according to the Lempel-Ziv procedure; there an exponentiation algorithm based on this compression procedure as well as on the M-ary method is also given.

In the search for the shortest possible addition chains the M-ary exponentiation can be further generalized, which we shall pursue below in greater detail. The window methods represent the exponent not as in the M-ary method by digits to a fixed base M, but by digits of varying binary lengths. Thus, for example, long sequences of binary zeros, called zero windows, can appear as digits of the exponent. If we recall the M-ary algorithm from page 86, it is clear that for a zero window of length l only the l-fold repetition of squaring is required, and the corresponding step is then

Click To expand

Digits different from zero will be treated, depending on the process, either as windows of fixed length or as variable windows with a maximal length. As with the M-ary process, for every nonzero window (in the following called not quite aptly a "1-window") of length t, in addition to the repeated squaring an additional multiplication by a precalculated factor is required, in analogy to the corresponding step of the 2k-ary procedure:

Click To expand

The number of factors to be precomputed depends on the permitted maximal length of the 1-window. One should note that the 1-windows in the least-significant position always have a 1 and thus are always odd. The factorization of the exponent digit on page 86 into an even and odd factor will thus at first not be needed. On the other hand, in the course of exponentiation the exponent is processed from left to right, which means for the implementation that first the complete decomposition of the exponent must be carried out and stored before the actual exponentiation can take place.

Yet, if we begin the factorization of the exponent at the most-significant digit and travel from left to right, then every 0- or 1-window can be processed immediately, as soon as it is complete. This means, of course, that we will also obtain 1-windows with an even value, but the exponentiation algorithm is prepared for that.

Both directions of decomposition of the exponent into 1-windows with fixed length l follow essentially the same algorithm, which we formulate below for decomposition from right to left.

Decomposition of an integer e into 0-windows and 1-windows having fixed length l

  1. If the least-significant binary digit is equal to 0, then begin a 0-window and go to step 2; otherwise, begin a 1-window and go to step 3.

  2. Add the next-higher binary digits in a 0-window as long as no 1 appears. If a 1 appears, then close the 0-window, begin a 1-window, and go to step 3.

  3. Collect a further l − 1 binary digits into a 1-window. If the next-higher digit is a 0, begin a 0-window and go to step 3; otherwise, begin a 1-window and go to step 3. If in the process all digits of e have been processed, then terminate the algorithm.

The decomposition from left to right begins with the most-significant binary digit and otherwise proceeds analogously. If we suppose that e has no leading binary zeros, then the algorithm cannot reach the end of the representation of e within step 2, and the procedure terminates in step 3 under the same condition given there. The following examples illustrate this process:

  • Let e = 1896837 = (111001111000110000101)2, and let l = 3. Beginning with the least-significant binary digit, e is decomposed as follows:

    The choice l = 4 leads to the following decomposition of e:

    The 2k-ary exponentiation considered above yields, for example for k = 2, the following decomposition:

    The window decomposition of e for l = 3 contains five 1-windows, while that for l = 4 has only four, and for each the same number of additional multiplications is required. On the other hand, the 22-ary decomposition of e contains eight 1-windows, requires double the number of additional multiplications compared to the case l = 4, and is thus significantly less favorable.

  • The same procedure, but beginning with the most-significant binary digit, yields for l = 4 and e = 123 the decomposition

    likewise with four 1-windows, which, as already established above, are not all odd.

Finally, then, exponentiation with a window decomposition of the exponent can be formalized by the following algorithm. Both directions of window decomposition are taken into account.

Algorithm for exponentiation ae mod m with the representation of e in windows of (maximal) length l for odd 1-windows

  1. Decompose the exponent e into 0- and 1-windows (wk−1...w0) of respective lengths lk−1,...,l0.

  2. Calculate and store a3 mod m, a5 mod m, a7 mod m,...,a2−1 mod m.

  3. Set p ← [image] mod m and i ← k − 2.

  4. Set p ← [image] mod m.

  5. If wi ≠ 0, set p ← [image] mod m.

  6. Set i ← i − 1; if i ≥ 0, go to step 4.

  7. Output p.

If not all 1-windows are odd, then steps 3 through 6 are replaced by the following, and there is no step 7:

3′. | If wk−1 = 0, set p ← [image] mod m = [image] (lk−1 times) mod m. If wk−1 ≠ 0, factor wk−1 = 2tu with odd u; set p ← au mod m, and then p ← [image] mod m. In each case set i ← k − 2.
4′. | If wi = 0, set p ← [image] mod m = [image] (li times) mod m. If wi ≠ 0, factor wi = 2tu with odd u; set p ← [image] mod m, and then p ← pau mod m; now set p ← [image] mod m.
5′. | Set i ← i − 1; if i ≥ 0, go to step 4′.
6′. | Output p.

[1]If n possesses a representation n = (nk−1 nk−2 ... n0)2, then H(n) is defined as Σi ni. (See [HeQu], Chapter 8.)


6.4 Montgomery Reduction and Exponentiation

Now we are going to abandon addition chains and turn our attention to another idea, one that is interesting above all from the algebraic point of view. It makes it possible to replace multiplications modulo an odd number n by multiplications modulo a power of 2, that is, 2k, which requires no explicit division and is therefore more efficient than a reduction modulo an arbitrary number n. This useful method for modular reduction was published in 1985 by P. Montgomery [Mont] and since then has found wide practical application. It is based on the following observation.

Let n and r be relatively prime integers, and let r−1 be the multiplicative inverse of r modulo n; and likewise let n−1 be the multiplicative inverse of n modulo r; and furthermore, define n′ := −n−1 mod r and m := tn′ mod r. For integers t we then have

(6.8)

Note that on the left side of the congruence we have taken congruences modulo r and a division by r (note that t + mn ≡ 0 mod r, so the division has no remainder), but we have not taken congruences modulo n. By choosing r as a power of 2 in the form 2s we can reduce a number x modulo r simply by slicing off x at the sth bit (counting from the least-significant bit), and we can carry out the division of x by r by shifting x to the right by s bit positions. The left side of (6.8) thus requires significantly less computational expense than the right side, which is what gives the equation its charm. For the two required operations we can invoke the functions mod2_l() (cf. Section 4.3) and shift_l() (cf. Section 7.1).

This principle of carrying out reduction modulo n is called Montgomery reduction. Below, we shall institute Montgomery reduction for the express purpose of speeding up modular exponentiation significantly in comparison to our previous results. Since the procedure requires that n and r be relatively prime, we must take n to be odd. First we have to deal with a couple of considerations.

We can clarify the correctness of the previous congruence with the help of some simple checking. Let us replace m on the left-hand side of (6.8) by the expression tn′ mod r, which is (6.9), and further, replace tn′ mod r by tn′ − r ⌊tn′/r⌋ Î [image] to get (6.10), and then in (6.10) for n′ the integer expression (r′r − 1)/n for a certain r′ Î [image] and obtain (6.11). After reduction modulo n we obtain the result (6.12):

(6.9)
(6.10)
(6.11)
(6.12)

To summarize equation (6.8) we record the following: Let n, t, r Î [image] with gcd(n, r) = 1, n′ := −n−1 mod r. For

(6.13)

we have

(6.14)
(6.15)

We shall return to this result later.

To apply Montgomery reduction we shift our calculations modulo n into a complete residue system (cf. Chapter 5)

Click To expand

with a suitable r := 2s > 0 such that 2s−1 ≤ n < 2s. Then we define the Montgomery product "×" of two numbers a and b in R:

with r−1 representing the multiplicative inverse of r modulo n. We have

Click To expand

and thus the result of applying × to members of R is again in R. The Montgomery product is formed by applying Montgomery reduction, where again n′ := −n−1 mod r. From n′ we derive the representation 1 = gcd(n, r) = r′r − n′n, which we calculate in anticipation of Section 10.2 with the help of the extended Euclidean algorithm. From this representation of 1 we immediately obtain

and

so that r′ = r−1 mod n is the multiplicative inverse of r modulo n, and n′ = −n−1 mod r the negative of the inverse of n modulo r (we are anticipating somewhat; cf. Section 10.2). The calculation of the Montgomery product now takes place according to the following algorithm.

Calculation of the Montgomery product a × b in R(r, n)

  1. Set t ← ab.

  2. Set m ← tn′ mod r.

  3. Set u ← (t + mn)/r (the quotient is an integer; see above).

  4. If u ≥ n, output u − n, and otherwise u. Based on the above selection of the parameter we have a, b < n as well as m, n < r and finally u < 2n; cf. (6.21).

The Montgomery product requires three long-integer multiplications, one in step 1 and two for the reduction in steps 2 and 3. An example with small numbers will clarify the situation: Let a = 386, b = 257, and n = 533. Further, let r = 210. Then n′ = −n−1 mod r = 707, m = 6, t + mn = 102400, and u = 100.

A modular multiplication ab mod n with odd n can now be carried out by first transforming a′ ← ar mod n and b′ ← br mod n to R, there forming the Montgomery product p′ ← a′ × b′ = a′b′r−1 mod n and then with p ← p′ × 1 = p′r−1 = ab mod n obtaining the desired result. However, we can spare ourselves the reverse transformation effected in the last step by setting p ← a′ × b at once and thus avoid the transformation of b, so that in the end we have the following algorithm.

Calculation of p = ab mod n (n odd) with the Montgomery product

  1. Determine r := 2s with 2s−1 ≤ n < 2s. Calculate 1 = r′r − n′n by means of the extended Euclidean algorithm.

  2. Set a′ ← ar mod n.

  3. Set p ← a′ × b and output p.

Again we present an example with small numbers for clarification: Let a = 123, b = 456, n = 789, r = 210. Then n′ = −n−1 mod r = 963, a′ = 501, and p = a′ × b = 69 = ab mod n.

Since the precalculation of r′ and n′ in steps 1 and 2 is very time-consuming and Montgomery reduction in this version also has two long-number multiplications on its balance sheet, there is actually an increased computational expenditure compared with "normal" modular multiplication, so that the computation of individual products with Montgomery reduction is not worthwhile.

However, in cases where many modular multiplications with a constant modulus are required, for which therefore the time-consuming precalculations occur only once, we may expect more favorable results. Particularly suited for the Montgomery product is modular exponentiation, for which we shall suitably modify the M-ary algorithm. To this end let once again e = (em−1 em−2...e0)B and n = (nl−1 nl−2...n0)B be the representations of the exponent e and the modulus n to the base B = 2k. The following algorithm calculates powers ae mod n in [image] with odd n using Montgomery multiplication. The squarings that occur in the exponentiation become Montgomery products a × a, in the computation of which we can use the advantages of squaring.

Exponentiation modulo n (n odd) with the Montgomery product

  1. Set r ← Bl = 2kl. Calculate 1 = rr′ − nn′ with the Euclidean algorithm.

  2. Set ā ← ar mod n. Calculate and store the powers ā3, ā5,..., [image] using the Montgomery product × in R(r, n).

  3. If em−1 ≠ 0, factor em−1 = 2tu with odd u. Set [image] ← [image] .

    If em−1 = 0, set [image] ← r mod n.

    In each case set i ← m − 2.

  4. If ei = 0, set [image] ← [image] = [image] (k-fold squaring [image] = [image] × [image] ).

    If ei ≠ 0, factor ei = 2tu with odd u. Set [image] ← [image] .

  5. If i ≥ 0, set i ← i − 1 and go to step 4.

  6. Output the Montgomery product [image] × 1.

Further possibilities for improving the algorithm lie less in the exponentiation algorithm than in the implementation of the Montgomery product itself, as demonstrated by S. R. Dussé and B. S. Kaliski in [DuKa]: In calculating the Montgomery product on page 105, in step 2 we can avoid the assignment m ← tn′ mod r in the reduction modulo r. Furthermore, we can calculate with [image] := n′ mod B instead of with n′ in executing the Montgomery reduction. We can create a digit mi ← ti [image] modulo B, multiply it by n, scale by the factor Bi, and add to t. To calculate ab mod n with a, b < n the modulus n has the representation n = (nl−1nl−2...n0)B as above, and we let r := Bl as well as rr′ − nn′ = 1 and [image] := n′ mod B.

Calculation of the Montgomery product a × b à la Dussé and Kaliski

  1. Set t ← ab, [image] ← n′ mod B, i ← 0.

  2. Set mi ← ti [image] mod B (mi is a one-digit integer).

  3. Set t ← t + minBi.

  4. Set i ← i + 1; if i ≤ l − 1, go to step 2.

  5. Set t ← t/r.

  6. If t ≥ n, output t − n and otherwise t.

Dussé and Kaliski state that the basis for their clever simplification is the method of Montgomery reduction to develop t as a multiple of r, but they offer no proof. Before we use this procedure we wish to make more precise why it suffices to calculate a × b. The following is based on a proof of Christoph Burnikel [Zieg]:

In steps 2 and 3 the algorithm calculates a sequence [image] by means of the recursion

(6.16)
(6.17) Click To expand

where

Click To expand

is the already familiar function that is induced by the Montgomery equation (cf. (6.13), and there set r ← B in f(t)). The members of the sequence t(i) have the properties

(6.18)
(6.19)
(6.20)
(6.21)

Properties (6.18) and (6.19) are derived inductively from (6.14), (6.15), (6.16), and (6.17); from (6.18) we obtain Bl | t(l) ⇔ r | t(l). From this and from t(l) = ab mod n follows (6.20), and lastly we have (6.21) on account of

(note here that t(0) = ab < n2 < nBl).

The expenditure for the reduction is now determined essentially by multiplication of numbers of order of magnitude the size of the modulus. This variant of Montgomery multiplication can be elegantly implemented in code that forms the core of the multiplication routine mul_l() (cf. page 35).

Start Sidebar

Function:

Montgomery product

Syntax:

void mulmon_l (CLINT a_l, CLINT b_l, CLINT n_l,
               USHORT nprime, USHORT logB_r,
               CLINT p_l);

Input:

a_l, b_l (factors a and b)

n_l (modulus n > a, b)

nprime (n′ mod B)

logB_r (logarithm of r to base B = 216;

it must hold that BlogB_r−1 ≤ n < BlogB_r)

Output:

p_l (Montgomery product a × b = a · b · r−1 mod n)
End Sidebar
void
mulmon_l (CLINT a_l, CLINT b_l, CLINT n_l, USHORT nprime,
         USHORT logB_r, CLINT p_l)
{
  CLINTD t_l;
  clint *tptr_l, *nptr_l, *tiptr_l, *lasttnptr, *lastnptr;
  ULONG carry;
  USHORT mi;
  int i;

  mult (a_l, b_l, t_l);
  lasttnptr = t_l + DIGITS_L (n_l);
  lastnptr = MSDPTR_L (n_l);
Start Sidebar

The earlier use of the function mult() makes possible the multiplication of a_l and b_l without the possibility of overflow (see page 70); for the Montgomery squaring we simply insert sqr(). The result has sufficient space in t_l. Then t_l is given leading zeros to bring it to double the number of digits of n_l if t_l is smaller than this.

End Sidebar
  for (i = DIGITS_L (t_l) + 1; i <= (DIGITS_L (n_l) << 1); i++)
    {
      t_l[i] = 0;
    }

  SETDIGITS_L (t_l, MAX (DIGITS_L (t_l), DIGITS_L (n_l) << 1));
Start Sidebar

Within the following double loop the partial products minBi with mi := ti [image] are calculated one after the other and added to t_l. Here again the code is essentially that of our multiplication function.

End Sidebar
  for (tptr_l = LSDPTR_L (t_l); tptr_l <= lasttnptr; tptr_l++)
    {
      carry = 0;
      mi = (USHORT)((ULONG)nprime * (ULONG)*tptr_l);

      for (nptr_l = LSDPTR_L (n_l), tiptr_l = tptr_l;
           nptr_l <= lastnptr; nptr_l++, tiptr_l++)
         {
           *tiptr_l = (USHORT)(carry = (ULONG)mi * (ULONG)*nptr_l +
                (ULONG)*tiptr_l + (ULONG)(USHORT)(carry >> BITPERDGT));
         }
Start Sidebar

In the following inner loop a possible overflow is transported to the most-significant digit of t_l, and t_l contains an additional digit in case it is needed. This step is essential, since at the start of the main loop t_l was given a value and not initialized via multiplication by 0 as was the variable p_l.

End Sidebar
    for ( ;
         ((carry >> BITPERDGT) > 0) && tiptr_l <= MSDPTR_L (t_l);
         tiptr_l++)
       {
        *tiptr_l = (USHORT)(carry = (ULONG)*tiptr_l +
                       (ULONG)(USHORT)(carry >> BITPERDGT));
       }
     if (((carry >> BITPERDGT) > 0))
        {
          *tiptr_l = (USHORT)(carry >> BITPERDGT);
          INCDIGITS_L (t_l);
        }
    }
Start Sidebar

Now follows division by Bl, and we shift t_l by logB_r digits to the right, or ignore the logB_r least-significant digits of t_l. Then if applicable the modulus n_l is subtracted from t_l before t_l is returned as result into p_l.

End Sidebar
  tptr_l = t_l + (logB_r);
  SETDIGIT_L (tptr_l, DIGITS_L (t_l) - (logB_r));
  if (GE_L (tptr_l, n_l))
    {
      sub_l (tptr_l, n_l, p_l);
    }
  else
    {
      cpy_l (p_l, tptr_l);
    }
}

The Montgomery squaring sqrmon_l() differs from this function only marginally: There is no parameter b_l in the function call, and instead of multiplication with mult(a_l, b_l, t_l) we employ the squaring function sqr(a_l, t_l), which likewise ignores a possible overflow. However, in modular squaring in the Montgomery method one must note that after the calculation of p′ ← a′ × a′ the reverse transformation p ← p′ × 1 = p′r−1 = a2 mod n must be calculated explicitly (cf. page 105).

Start Sidebar

Function:

Montgomery square

Syntax:

void sqrmon_l (CLINT a_l, CLINT n_l, USHORT nprime,
               USHORT logB_r, CLINT p_l);

Input:

a_l (factor a), n_l (modulus n > a)

nprime (n′ mod B)

logB_r (logarithm of r to base B = 216);

it must hold that BlogB_r−1 ≤ n < BlogB_r

Output:

p_l (Montgomery square a2r−1 mod n)
End Sidebar

In their article Dussé and Kaliski also present the following variant of the extended Euclidean algorithm, to be dealt with in detail in Section 10.2, for calculating [image] = n′ mod B, with which the expenditure for the precalculations can be reduced. The algorithm calculates −n−1 mod 2s for an s > 0 and for this requires long-number arithmetic.

Algorithm for calculating the inverse −n−1 mod 2s for s > 0, n odd

  1. Set x ← 2, y ← 1, and i ← 2.

  2. If x < ny mod x, set y ← y + x.

  3. Set x ← 2x and i ← i + 1; if i ≤ s, go to step 2.

  4. Output x − y.

With complete induction it can be shown that in step 2 of this algorithm yn ≡ 1 mod x always holds, and thus y ≡ n−1 mod x. After x has taken on the value 2s in step 3, we obtain with 2s − y ≡ −n−1 mod 2s the desired result if we choose s such that 2s = B. The short function for this can be obtained under the name invmon_l() in the FLINT/C source. It takes only the modulus n as argument and outputs the value −n−1 mod B.

These considerations are borne out in the creation of the functions mexp5m_l() and mexpkm_l(), for which we give here only the interface, together with a computational example.

Start Sidebar

Function:

modular exponentiation with odd modulus (25-ary or 2k-ary method with Montgomery product)

Syntax:

int mexp5m_l (CLINT bas_l, CLINT exp_l,
               CLINT p_l, CLINT m_l);
int mexpkm_l (CLINT bas_l, CLINT exp_l,
               CLINT p_l, CLINT m_l);

Input:

bas_l (base)

exp_l (exponent)

m_l (modulus)

Output:

p_l (power residue)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0

E_CLINT_MAL if malloc() error

E_CLINT_MOD if even modulus
End Sidebar

These functions employ the routines invmon_l(), mulmon_l(), and sqrmon_l() to compute the Montgomery products. Their implementation is based on the functions mexp5_l() and mexpk_l() modified according to the exponentiation algorithm described above.

We would like to reconstruct the processes of Montgomery exponentiation in mexpkm_l() with the same numerical example that we looked at for M-ary exponentiation (cf. page 97). In the following steps we shall calculate the power 1234667 mod 18577:

  1. Precomputations

    The exponent e = 667 is represented to the base 2k with k = 2 (cf. the algorithm for Montgomery exponentiation on page 111). The exponent e thereby has the representation

    The value r for Montgomery reduction is r = 216 = B = 65536.

    The value [image] (cf. page 107) is now calculated as [image] = 34703.

    The transformation of the base a into the residue system R(r, n) (cf. page 104) follows from

    Click To expand

    The power ā3 in R(r, n) has the value ā3 = 9227. Because of the small exponent, further powers of ā do not arise in the precomputation.

  2. Exponentiation loop

    exponent digit ei = 2tu

    21 · 1

    21 · 1

    20 · 1

    21 · 1

    20 · 3

    Click To expand

    -

    16994

    3682

    14511

    11066

    Click To expand

    -

    -

    6646

    -

    12834

    Click To expand

    5743

    15740

    8707

    16923

    1583

    Click To expand

    9025

    11105

    -

    1628

    -

  3. Result

    The value of the power p after normalization:

    Click To expand

Those interested in reconstructing the coding details of the functions mexp5m_l() and mexpkm_l() and the calculational steps of the example related to the function mexpkm_l() are referred to the FLINT/C source code.

At the start of this chapter we developed the function wmexp_l(), which has the advantage for small bases that only multiplications p ← pa mod m of the type CLINT * USHORT mod CLINT occur. In order to profit from the Montgomery procedure in this function, too, we adjust the modular squaring to Montgomery squaring, as in mexpkm_l(), with the use of the fast inverse function invmon_l(), though we leave the multiplication unchanged. We can do this because with the calculational steps for Montgomery squaring and for conventional multiplication modulo n,

we do not abandon the residue system R(r, n) = {ir mod n | 0 ≤ i < n} introduced above. This process yields us both the function wmexpm_l() and the dual function umexpm_l() for USHORT exponents, respectively for odd moduli, which in comparison to the two conventional functions wmexp_l() and umexp_l() again yields a significant speed advantage. For these functions, too, we present here only the interface and a numerical example. The reader is again referred to the FLINT/C source for details.

Start Sidebar

Function:

modular exponentiation with Montgomery reduction for USHORT-base, respectively USHORT exponents and odd modulus

Syntax:

int wmexpm_l (USHORT bas, CLINT e_l,
               CLINT p_l, CLINT m_l);
int umexpm_l (CLINT bas_l, USHORT e,
               CLINT p_l, CLINT m_l);

Input:

bas, bas_l (base)

e, e_l (exponent)

m_l (modulus)

Output:

p_l (residue of base_l mod m_l, respectively of bas_le mod m_l)

Return:

E_CLINT_OK if all is ok

E_CLINT_DBZ if division by 0

E_CLINT_MOD if even modulus
End Sidebar

The function wmexpm_l() is tailor-made for our primality test in Section 10.5, where we shall profit from our present efforts. The function will be documented with the example used previously of the calculation of 1234667 mod 18577.

  1. Precalculations

    The binary representation of the exponent is e = (1010011011)2.

    The value r for the Montgomery reduction is r = 216 = B = 65536.

    The value [image] (cf. page 107) is calculated as above, yielding [image] = 34703.

    The initial value of [image] is set as [image] ← pr mod 18577.

  2. Exponentiation loop

    Exponent bit

    1

    0

    1

    0

    0

    1

    1

    0

    1

    1

    ← [image] × [image] in R(r, n)

    9805

    9025

    16994

    11105

    3682

    6646

    14511

    1628

    11066

    9350

    ← [image] a mod n

    5743

    -

    15740

    -

    -

    8707

    16923

    -

    1349

    1583

  3. Result

    The value of the exponent p after normalization:

    Click To expand

A detailed analysis of the time behavior of Montgomery reduction with the various optimizations taken into account can be found in [Boss]. There we are promised a ten to twenty percent saving in time over modular exponentiation by using Montgomery multiplication. As can be seen in the overviews in Appendix D of typical calculation times for FLINT/C functions, our implementations bear out this claim fully. To be sure, we have the restriction that the exponentiation functions that use Montgomery reduction can be used only for odd moduli. Nonetheless, for many applications, for example for encryption and decryption, as well as for computing digital signatures according to the RSA procedure (see Chapter 16), the functions mexp5m_l() and mexpkm_l() are the functions of choice.

Altogether, we have at our disposal a number of capable functions for modular exponentiation. To obtain an overview, in Table 6.5 we collect these functions together with their particular properties and domains of application.

Table 6.5: Exponentiation functions in FLINT/C
Function | Domain of application
mexp5_l() | General 25-ary exponentiation, without memory allocation, greater stack requirements.
mexpk_l() | General 2k-ary exponentiation with optimal k for CLINT numbers, with memory allocation, lower stack requirements.
mexp5m_l() | 25-ary Montgomery exponentiation for odd moduli, without memory allocation, greater stack requirements.
mexpkm_l() | 2k-ary Montgomery exponentiation for odd moduli, with optimal k for CLINT numbers up to 4096 binary digits, with memory allocation, lower stack requirements.
umexp_l() | Mixed binary exponentiation of a CLINT base with USHORT exponent, lower stack requirements.
umexpm_l() | Mixed binary exponentiation of a CLINT base with USHORT exponent and Montgomery reduction, thus only for odd moduli, lower stack requirements.
wmexp_l() | Mixed binary exponentiation of a USHORT base with CLINT exponent, lower stack requirements.
wmexpm_l() | Mixed binary exponentiation with Montgomery squaring of a USHORT base with CLINT exponent, odd moduli, lower stack requirements.
mexp2_l() | Mixed exponentiation with a power-of-2 exponent, lower stack requirements.


6.5 Cryptographic Application of Exponentiation

We have worked hard in this chapter in our calculation of powers, and it is reasonable to ask at this point what modular exponentiation might have to offer to cryptographic applications. The first example to come to mind is, of course, the RSA procedure, which requires a modular exponentiation for encryption and decryption—assuming suitable keys. However, the author would like to ask his readers for a bit (or perhaps even a byte) of patience, since for the RSA procedure we still must collect a few more items, which we do in the next chapter. We shall return to this extensively in Chapter 16.

For those incapable of waiting, we offer as examples of the application of exponentiation two important algorithms, namely, the procedure suggested in 1976 by Martin E. Hellman and Whitfield Diffie [Diff] for the exchange of cryptographic keys and the encryption procedure of Taher ElGamal as an extension of the Diffie-Hellman procedure.

The Diffie-Hellman procedure represents a cryptographic breakthrough, namely, the first public key, or asymmetric, cryptosystem (see Chapter 16). Two years after its publication, Rivest, Shamir, and Adleman published the RSA procedure (see [Rive]). Variants of the Diffie-Hellman procedure are used today for key distribution in the Internet communications and security protocols IPSec, IPv6, and SSL, which were developed to provide security in the transfer of data packets in the IP protocol layer and the transfer of data at the application level, for example from the realms of electronic commerce. This principle of key distribution thus has a practical significance that would be difficult to overestimate.[2]

With the aid of the Diffie-Hellman protocol two communicators, Ms. A and Mr. B, say, can negotiate in a simple way a secret key that then can be used for the encryption of communications between the two. After A and B have agreed on a large prime number p and a primitive root a modulo p (we shall return to this below), the Diffie-Hellman protocol runs as follows.

Protocol for key exchange à la Diffie-Hellman

  1. A chooses an arbitrary value xA ≤ p − 1 and sends yA := [image] mod p as her public key to B.

  2. B chooses an arbitrary value xB ≤ p − 1 and sends yB:= [image] mod p as his public key to A.

  3. A computes the secret key sA := [image] mod p.

  4. B computes the secret key sB := [image] mod p.

Since

after step 4, A and B are dealing with a common key. The values p and a do not have to be kept secret, nor the values yA and yB exchanged in steps 1 and 2. The security of the procedure depends on the difficulty in calculating discrete logarithms in finite fields, and the difficulty of breaking the system is equivalent to that of calculating values xA or xB from values yA or yB in [image] .[3] That the calculation of axy from ax and ay in a finite cyclic group (the Diffie-Hellman problem) is just as difficult as the calculation of discrete logarithms and thus equivalent to this problem is, in fact, conjectured but has not been proved.

To ensure the security of the procedure under these conditions the modulus p must be chosen sufficiently large (at least 1024 bits, better 2048 or more; see Table 16.1), and one should ensure that p − 1 contains a large prime divisor close to (p − 1)/2 to exclude particular calculational procedures for discrete logarithms (a constructive procedure for such prime numbers will be presented in Chapter 16 in connection with the generation of strong primes, for example for the RSA procedure).

The procedure has the advantage that secret keys can be generated as needed on an ad hoc basis, without the need for secret information to be held for a long time. Furthermore, for the procedure to be used there are no further infrastructure elements necessary for agreeing on the parameters a and b. Nonetheless, this protocol possesses some negative characteristics, the gravest of which is the lack of authentication proofs for the exchanged parameters yA and yB. This makes the procedure susceptible to man-in-the-middle attacks, whereby attacker X intercepts the messages of A and B with their public keys yA and yB and replaces them with falsified messages to A and B containing his own public key yX.

Then A and B calculate "secret" keys [image] := [image] mod p and [image] := [image] mod p, while X on his or her part calculates [image] from [image] mod p and [image] analogously. The Diffie-Hellman protocol has now been executed not between A and B, but between X and A as well as between X and B. Now X is in a position to decode messages from A or B and to replace them by falsified messages to A or B. What is fatal is that from a cryptographic point of view the participants A and B are clueless as to what has happened.

To compensate for these defects without giving up the advantages, several variants and extensions have been developed for use in the Internet. They all take into account the necessity that key information be exchanged in such a way that its authenticity can be verified. This can be achieved, for example, by the public keys being digitally signed by the participants and the associated certificate of a certification authority being sent with them (see in this regard page 339, Section 16.3), which is implemented, for example, in the SSL protocol. IPSec and IPv6 use a complexly constructed procedure with the name ISAKMP/Oakley,[4] which overcomes all the drawbacks of the Diffie-Hellman protocol (for details see [Stal], pages 422–423).

To determine a primitive root modulo p, that is, a value a whose powers ai mod p with i = 0, 1,....,p − 2 constitute the entire set of elements of the multiplicative group [image] = { 1,...,p − 1 } (see in this regard Section10.2), the following algorithm can be used (see [Knut], Section 3.2.1.2, Theorem C). It is assumed that the prime factorization p − 1 = [image] of the order of [image] is known.

Finding a primitive root modulo p

  1. Choose a random integer a Î [0, p − 1] and set i ← 1.

  2. Compute t ← [image] mod p.

  3. If t = 1, go to step 1. Otherwise, set i ← i + 1. If i ≤ k, go to step 2. If i > k, output a and terminate the algorithm.

The algorithm is implemented in the following function.

Start Sidebar

Function:

ad hoc generation of a primitive root modulo p (2 < p prime)

Syntax:

int primroot_l (CLINT a_l, unsigned noofprimes,
               clint **primes_l);

Input:

noofprimes (number of distinct prime factors in p − 1, the order of the group)

primes_l (vector of pointers to CLINT objects, beginning with p − 1, then follow the prime divisors p1,...,pk of the group order p − 1 = [image] , k = noofprimes)

Output:

a_l (primitive root modulo p_l)

Return:

E_CLINT_OK if all is ok

−1 if p − 1 odd and thus p is not prime
End Sidebar
int
primroot_l (CLINT a_l, unsigned int noofprimes, clint *primes_l[])
{
  CLINT p_l, t_l, junk_l;
  ULONG i;

  if (ISODD_L (primes_l[0]))
    {
      return -1;
    }
Start Sidebar

primes_l[0] contains p − 1, from which we obtain the modulus in p_l.

End Sidebar
  cpy_l (p_l, primes_l[0]);
  inc_l (p_l);
  SETONE_L (a_l);

  do
    {
      inc_l (a_l);
Start Sidebar

As candidates a for the sought-after primitive root only natural numbers greater than or equal to 2 are tested. If a is a square, then a cannot be a primitive root modulo p, since then already a(p−1)/2 ≡ 1 mod p, and the order of a must be less than φ(p) = p − 1. In this case a_l is incremented. We test whether a_l is a square with the function issqr_l() (cf. Section 10.3).

End Sidebar
    if (issqr_l (a_l, t_l))
      {
         inc_l (a_l);
      }

    i = 1;
Start Sidebar

The calculation of t ← [image] mod p takes place in two steps. All prime factors pi are tested in turn; we use Montgomery exponentiation. If a primitive root is found, it is output in a_l.

End Sidebar
      do
         {
           div_l (primes_l[0], primes_l[i++], t_l, junk_l);
           mexpkm_l (a_l, t_l, t_l, p_l);
         }
      while ((i <= noofprimes) && !EQONE_L (t_l));
    }
  while (EQONE_L (t_l));

  return E_CLINT_OK;
}

As a second example for the application of exponentiation we consider the encryption procedure of ElGamal, which as an extension of the Diffie-Hellman procedure also provides security in the matter of the difficulty of computing discrete logarithms, since breaking the procedure is equivalent to solving the Diffie-Hellman problem (cf. page 116). Pretty good privacy (PGP), the workhorse known throughout the world for encrypting and signing e-mail and documents whose development goes back essentially to the work of Phil Zimmermann, uses the ElGamal procedure for key management (see [Stal], Section 12.1).

A participant A selects a public and associated private key as follows.

ElGamal key generation

  1. A chooses a large prime number p such that p − 1 has a large prime divisor close to (p − 1)/2 (cf. page 328) and a primitive root a of the multiplicative group [image] as above (cf. page 117).

  2. A chooses a random number x with 1 ≤ x < p − 1 and computes b := ax mod p with the aid of Montgomery exponentiation.

  3. As public key A uses the triple <p, a, b>A, and the associated secret key is <p, a, x>A.

Using the public key triple <p, a, b>A a participant B can now encrypt a message M Î {1,...,p − 1 } and send it to A. The procedure is as follows.

Protocol for encryption à la ElGamal

  1. B chooses a random number y with 1 ≤ y < p − 1.

  2. B calculates α := ay mod p and β := Mby mod p = M (ax)y mod p.

  3. B sends the cryptogram C := (α, β) to A.

  4. A computes from C the plain text using M = β/αx modulo p.

Since

the procedure works. The calculation of β/αx is carried out by means of a multiplication βαp−1−x modulo p.

The size of p should be, depending on the application, 1024 bits or longer (see Table 16.1), and for the encryption of different messages M1 and M2 unequal random values y1 ≠ y2 should be chosen, since otherwise, from

it would follow that knowledge of M1 was equivalent to knowledge of M2. In view of the practicability of the procedure one should note that the cryptogram C is twice the size of the plain text M, which means that this procedure has a higher transmission cost than others.

The procedure of ElGamal in the form we have presented has an interesting weak point, which is that an attacker can obtain knowledge of the plain text with a small amount of information. We observe that the cyclic group [image] contains the subgroup U := { ax | x even} of order (p − 1)/2 (cf. [Fisc], Chapter 1). If now b = ax or α = ay lies in U, then this holds, of course, for bα = axy. If this is the case and the encrypted text β is also in U, then M = βa−xy is in U as well. The same holds if axy and β are both not contained in U. In the other two cases, in which precisely one of axy and β does not lie in U, thenM is also not in U. The following criteria provide information about this situation:

  1. axy Î U ⇔ (ax Î U or ay Î U). This, and whether also β Î U, is tested with

  2. For all u Î [image] , u Î U ⇔ u(p−1)/2 = 1.

One may ask how bad it might be if an attacker could gain such information about M. From the point of view of cryptography it is a situation difficult to accept, since the message space to be searched is reduced by half with little effort. Whether in practice this is acceptable certainly depends on the application. Surely, it is a valid reason to be generous in choosing the length of a key.

Furthermore, one can take some action against the weakness of the procedure, without, one hopes, introducing new, unknown, weaknesses: The multiplication Mby mod p in step 2 of the algorithm can be replaced with an encryption operation V (H (axy), M) using a suitable symmetric encryption procedure V (such as Triple-DES, IDEA, or Rijndael, which has become the new advanced encryption standard; cf. Chapter 19) and a hash function H (cf. page 337) that so condenses the value axy that it can be used as a key for V.

So much for our examples of the application of modular exponentiation. In number theory, and therefore in cryptography as well, modular exponentiation is a standard operation, and we shall meet it repeatedly later on, in particular in Chapters 10 and 16. Furthermore, refer to the descriptions and numerous applications in [Schr] as well as in the encyclopedic works [Schn] and [MOV].

[2]IP Security (IPSec), developed by the Internet Engineering Task Force (IETF), is, as an extensive security protocol, a part of the future Internet protocol IPv6. It was created so that it could also be used in the framework of the then current Internet protocol (IPv4). Secure Socket Layer (SSL) is a security protocol developed by Netscape that lies above the TCP protocol, which offers end-to-end security for applications such as HTTP, FTP, and SMTP (for all of this see [Stal], Chapter 13 and14).

[3]For the problem of calculating discrete logarithms see [Schn], Section 11.6, as well as [Odly].

[4]ISAKMP: Internet Security Association and Key Management Protocol.


Chapter 7: Bitwise and Logical Functions

Overview

And sprinkled just a bit

Over each banana split.

—Tom Lehrer, "In My Home Town"

"Contrariwise," continued Tweedledee, "if it was so, it might be; and if it were so, it would be: but as it isn't, it ain't. That's logic."

—Lewis Carroll, Through the Looking-Glass

IN THIS CHAPTER WE SHALL present functions that carry out bitwise operations on CLINT objects, and we shall also introduce functions for determining the equality and size of CLINT objects, which we have already used quite a bit.

Among the bitwise functions are to be found the shift operations, which shift a CLINT argument in its binary representation by individual bit positions, and certain other functions taking two CLINT arguments that enable the direct manipulation of the binary representation of CLINT objects. How such operations can be applied to arithmetic purposes is most clearly seen in the shift operations described below, but we have also seen, in Section 4.3, how the bitwise AND operation can be used in reduction modulo a power of two.


7.1 Shift Operations

Necessity devises all manner of shifts.

—Rabelais

The simplest way to multiply a number a with the representation a = (an−1 an−2 ... a0)B to the base B by a power Be is to "shift a to the left by e digits." This works with the binary representation exactly as it does in our familiar decimal system:

Click To expand

where

Click To expand

For B = 2 this corresponds to multiplication of a number in binary representation by 2e, while for B = 10 it corresponds to multiplication by a power of ten in the decimal system.

In the analogous procedure for whole-number division by powers of B the digits of a number are "shifted to the right":

Click To expand

where

Click To expand

For B = 2 this corresponds to integer division of a number in binary representation by 2e, and the analogous result holds for other bases.

Since the digits of CLINT objects are represented in memory in binary form, CLINT objects can easily be multiplied by powers of two by shifting left, where the next digit to the right is shifted into each place where a digit has been shifted left, and the binary digits left over on the right are filled with zeros.

In an analogous way CLINT objects can be divided by powers of two by shifting each binary digit to the right into the next lower-valued digit. Digits left free at the end are either filled with zeros or ignored as leading zeros, and at each stage in the process (shifting by one digit) the lowest-valued digit is lost.

The advantage of this process is clear: Multiplication and division of a CLINT object a by a power of two 2e are simple, and they require at most e ⌈logB a⌉ shift operations to shift each USHORT value by one binary digit. Multiplication and division of a by a power Be uses only ⌈logB a⌉ operations for storing USHORT values.

In the following we shall present three functions. The function shl_l() executes a rapid multiplication of a CLINT number by 2, while the function shr_l() divides a CLINT number by 2 and returns the integer quotient.

Lastly, the function shift_l() multiplies or divides a CLINT type a by a power of two 2e. Which operation is executed is determined by the sign of the exponent e of the power of two that is passed as argument. If the exponent is positive, then the operation is multiplication, while if it negative, then division is carried out. If e has the representation e = Bk + l, l < B, then shift_l() carries out the multiplication or division in (l + 1) ⌈logB a⌉ operations on USHORT values.

All three functions operate modulo (Nmax + 1) on objects of CLINT type. They are implemented as accumulator functions, and thus they change their CLINT operands in that they overwrite the operand with the result of the operation. The functions test for overflow, respectively underflow. However, in shifting, underflow cannot really arise, since in those cases where more positions are to be shifted than there are digits the result is simply zero, almost as it is in real life. The status value E_CLINT_UFL for underflow then merely indicates that there was less to shift than was required, or, in other words, that the power of two by which division was to be carried out was larger than the dividend, and so the quotient is zero. The three functions are implemented in the following manner.

Start Sidebar
Function: | shift left (multiplication by 2)
Syntax: | int shl_l (CLINT a_l);
Input: | a_l (multiplicand)
Output: | a_l (product)
Return: | E_CLINT_OK if all is ok E_CLINT_OFL if overflow
End Sidebar
int
shl_l (CLINT a_l)
{
  clint *ap_l, *msdptra_l;
  ULONG carry = 0L;
  int error = E_CLINT_OK;

  RMLDZRS_L (a_l);
  if (ld_l (a_l) >= (USHORT)CLINTMAXBIT)
    {
      SETDIGITS_L (a_l, CLINTMAXDIGIT);
      error = E_CLINT_OFL;
    }
  msdptra_l = MSDPTR_L (a_l);
  for (ap_l = LSDPTR_L (a_l); ap_l <= msdptra_l; ap_l++)
    {
      *ap_l = (USHORT)(carry = ((ULONG)*ap_l << 1) | (carry >> BITPERDGT));
    }
  if (carry >> BITPERDGT)
    {
      if (DIGITS_L (a_l) < CLINTMAXDIGIT)
         {
           *ap_l = 1;
           SETDIGITS_L (a_l, DIGITS_L (a_l) + 1);
           error = E_CLINT_OK;
         }
      else
         {
           error = E_CLINT_OFL;
         }
    }
  RMLDZRS_L (a_l);
  return error;
}
Start Sidebar
Function: | shift right (integer division by 2)
Syntax: | int shr_l (CLINT a_l);
Input: | a_l (dividend)
Output: | a_l (quotient)
Return: | E_CLINT_OK if all is ok E_CLINT_UFL if "underflow"
End Sidebar
int
shr_l (CLINT a_l)
{
  clint *ap_l;
  USHORT help, carry = 0;

  if (EQZ_L (a_l))
    return E_CLINT_UFL;

  for (ap_l = MSDPTR_L (a_l); ap_l > a_l; ap_l--)
    {
      help = (USHORT)((USHORT)(*ap_l >> 1) | (USHORT)(carry <<
                                          (BITPERDGT - 1)));
      carry = (USHORT)(*ap_l & 1U);
      *ap_l = help;
    }
  RMLDZRS_L (a_l);
  return E_CLINT_OK;
}
Start Sidebar
Function: | left/right shift (multiplication and division by powers of two)
Syntax: | int shift_l (CLINT n_l, long int noofbits);
Input: | n_l (operand) noofbits (exponent of the power of two)
Output: | n_l (product or quotient, depending on sign of noofbits)
Return: | E_CLINT_OK if all ok E_CLINT_UFL if "underflow" E_CLINT_OFL if overflow
End Sidebar
int
shift_l (CLINT n_l, long int noofbits)
{
  USHORT shorts = (USHORT)((ULONG)(noofbits < 0 ? -notofbits : noofbits) / BITPERDGT);
  USHORT bits = (USHORT)((ULONG)(noofbits < 0 ? -noofbits : noofbits) % BITPERDGT);
  long int resl;
  USHORT i;
  int error = E_CLINT_OK;

  clint *nptr_l;
  clint *msdptrn_l;

  RMLDZRS_L (n_l);
  resl = (int) ld_l (n_l) + noofbits;
Start Sidebar

If n_l == 0, we need only set the error code correctly, and we are done. The same holds if noofbits == 0.

End Sidebar
 if (*n_l == 0)
   {
     return ((resl < 0) ? E_CLINT_UFL : E_CLINT_OK);
   }

 if (noofbits == 0)
   {
     return E_CLINT_OK;
   }
Start Sidebar

Next it is checked whether there is an overflow or underflow to announce. Then a branch is taken depending on the sign of noofbits to shift either to the left or to the right.

End Sidebar
  if ((resl < 0) || (resl > (long) CLINTMAXBIT))
      {
         error = ((resl < 0) ? E_CLINT_UFL : E_CLINT_OFL); /*underflow or overflow*/
      }

    msdptrn_l = MSDPTR_L (n_l);

    if (noofbits < 0)
      {
Start Sidebar

If noofbits < 0, then n_l is divided by 2noofbits. The number of digits of n_l to shift is bounded by DIGITS_L (n_l). First the whole digits are shifted, and then the remaining bits with shr_l().

End Sidebar
       shorts = MIN (DIGITS_L (n_l), shorts);
       msdptrn_l = MSDPTR_L (n_l) - shorts;
       for (nptr_l = LSDPTR_L (n_l); nptr_l <= msdptrn_l; nptr_l++)
         {
            *nptr_l = *(nptr_l + shorts);
         }
       SETDIGITS_L (n_l, DIGITS_L (n_l) - (USHORT)shorts);
       for (i = 0; i < bits; i++)
         {
            shr_l (n_l);
         }
    }
  else
    {
Start Sidebar

If noofbits > 0, then n_l is multiplied by 2noofbits. If the number shorts of digits to be shifted is greater than MAXB, then the result is zero. Otherwise, first the number of digits of the new value is determined and stored, and then the whole digits are shifted, and the freed-up digits filled with zeros. To avoid an overflow the start position is limited by n_l + MAXB and stored in nptr_l. As before, the last bits are shifted individually, here with shl_l().

End Sidebar
        if (shorts < CLINTMAXDIGIT)
         {
            SETDIGITS_L (n_l, MIN (DIGITS_L (n_l) + shorts, CLINTMAXDIGIT));
            nptr_l = n_l + DIGITS_L (n_l);
            msdptrn_l = n_l + shorts;

            while (nptr_l > msdptrn_l)
              {
                *nptr_l = *(nptr_l - shorts);
                --nptr_l;
              }

            while (nptr_l > n_l)
              {
                *nptr_l-- = 0;
              }

            RMLDZRS_L (n_l);

            for (i = 0; i < bits; i++)
              {
                shl_l (n_l);
              }
         }
       else
         {
            SETZERO_L (n_l);
         }
    }
  return error;
}


7.2 All or Nothing: Bitwise Relations

The FLINT/C package contains functions that allow the built-in bitwise C operators &, |, and ^ to be used for the type CLINT as well. However, before we program these functions we would like to understand what their implementation will net us.

From a mathematical viewpoint we are looking at relations of the generalized Boolean functions f : { 0, 1 }k → { 0, 1 } that map a k-tuple (x1,..., xk) Î { 0, 1 }k to the value 0 or 1. The effect of a Boolean function is usually presented in the form of a table of values such as that shown in Table 7.1.

Table 7.1: Values of a Boolean function
x1 | x2 | ... | xk | f (x1, ..., xk)
0 | 0 | ... | 0 | 0
1 | 0 | ... | 0 | 1
0 | 1 | ... | 0 | 0
⋮ | ⋮ | ⋮ | ⋮ | ⋮
1 | 1 | ... | 1 | 1

For the bitwise relations between CLINT types we first regard the variables as bit vectors (x1, ..., xn), and furthermore, the function values of the Boolean functions will be formed into a sequence. Thus we have functions

that map n-bit variables [image] := ( [image] ) and [image] := ( [image] ) by

Click To expand

with fi ( [image] ) := f ( [image] ), again to an n-bit variable (x1, ..., xn), which is then interpreted as a number of type CLINT.

Decisive for the operation of the function [image] is the definition of the partial functions fi, each of which is defined in terms of a Boolean function f. For the CLINT functions and_l(), or_l(), and xor_l() the Boolean functions that are implemented are defined as in Tables 7.2–7.4.

Table 7.2: Values of the CLINT function and_l()
x1 | x2 | f (x1, x2)
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1
Table 7.3: Values of the CLINT function or_l()
x1 | x2 | f (x1, x2)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1
Table 7.4: Values of the CLINT function xor_l()
x1 | x2 | f (x1, x2)
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

The implementations of these Boolean functions in the three C functions and_l(), or_l(), and xor_l() do not actually proceed bitwise, but process the digits of CLINT variables by means of the standard C operators &, |, and ^. Each of these functions accepts three arguments of CLINT type, where the first two are the operands and the last the result variable.

Start Sidebar
Function: | operating by bitwise AND
Syntax: | void and_l (CLINT a_l, CLINT b_l, CLINT c_l);
Input: | a_l, b_l (arguments to be operated on)
Output: | c_l (value of the AND operation)
End Sidebar
void
and_l (CLINT a_l, CLINT b_l, CLINT c_l)
{
  CLINT d_l;
  clint *r_l, *s_l, *t_l;
  clint *lastptr_l;
Start Sidebar

First pointers r_l and s_l are set to the respective digits of the arguments. If the arguments have different numbers of digits, then s_l points to the shorter of the two. The pointer msdptra_l points to the last digit of a_l.

End Sidebar
  if (DIGITS_L (a_l) < DIGITS_L (b_l))
    {
      r_l = LSDPTR_L (b_l);
      s_l = LSDPTR_L (a_l);
      lastptr_l = MSDPTR_L (a_l);
    }
  else
    {
      r_l = LSDPTR_L (a_l);
      s_l = LSDPTR_L (b_l);
      lastptr_l = MSDPTR_L (b_l);
    }
Start Sidebar

Now the pointer t_l is set to point to the first digit of the result, and the maximal length of the result is stored in d_l[0].

End Sidebar
  t_l = LSDPTR_L (d_l);
  SETDIGITS_L (d_l, DIGITS_L (s_l - 1));
Start Sidebar

The actual operation runs in the following loop over the digits of the shorter argument. The result cannot have a larger number of digits.

End Sidebar
  while (s_l <= lastptr_l)
    {
      *t_l++ = *r_l++ & *s_l++;
    }
Start Sidebar

After the result is copied to c_l, where any leading zeros are expunged, the function is ended.

End Sidebar
  cpy_l (c_l, d_l);
}
Start Sidebar
Function: | operating by bitwise OR
Syntax: | void or_l (CLINT a_l, CLINT b_l, CLINT c_l);
Input: | a_l, b_l (arguments to be operated on)
Output: | c_l (value of the OR operation)
End Sidebar
void
or_l (CLINT a_l, CLINT b_l, CLINT c_l)
{
  CLINT d_l;
  clint *r_l, *s_l, *t_l;
  clint *msdptrr_l;
  clint *msdptrs_l;
Start Sidebar

The pointers r_l and s_l are set as above.

End Sidebar
  if (DIGITS_L (a_l) < DIGITS_L (b_l))
    {
      r_l = LSDPTR_L (b_l);
      s_l = LSDPTR_L (a_l);
      msdptrr_l = MSDPTR_L (b_l);
      msdptrs_l = MSDPTR_L (a_l);
    }
  else
    {
      r_l = LSDPTR_L (a_l);
      s_l = LSDPTR_L (b_l);
      msdptrr_l = MSDPTR_L (a_l);
      msdptrs_l = MSDPTR_L (b_l);
    }

  t_l = LSDPTR_L (d_l);
  SETDIGITS_L (d_l, DIGITS_L (r_l - 1));
Start Sidebar

The actual operation takes place within a loop over the digits of the shorter of the two arguments.

End Sidebar
  while (s_l <= msdptrs_l)
    {
      *t_l++ = *r_l++ | *s_l++;
    }
Start Sidebar

The remaining digits of the longer argument are taken into the result. After the result is copied to c_l, where any leading zeros are eliminated, the function is terminated.

End Sidebar
  while (r_l <= msdptrr_l)
    {
      *t_l++ = *r_l++;
    }
  cpy_l (c_l, d_l);
}
Start Sidebar
Function: | operation by bitwise exclusive OR (XOR)
Syntax: | void xor_l (CLINT a_l, CLINT b_l, CLINT c_l);
Input: | a_l, b_l (arguments to be operated on)
Output: | c_l (value of the XOR operation)
End Sidebar
void
xor_l (CLINT a_l, CLINT b_l, CLINT c_l)
{
  CLINT d_l;
  clint *r_l, *s_l, *t_l;
  clint *msdptrr_l;
  clint *msdptrs_l;

  if (DIGITS_L (a_l) < DIGITS_L (b_l))
    {
      r_l = LSDPTR_L (b_l);
      s_l = LSDPTR_L (a_l);
      msdptrr_l = MSDPTR_L (b_l);
      msdptrs_l = MSDPTR_L (a_l);
    }
  else
    {
      r_l = LSDPTR_L (a_l);
      s_l = LSDPTR_L (b_l);
      msdptrr_l = MSDPTR_L (a_l);
      msdptrs_l = MSDPTR_L (b_l);
    }

  t_l = LSDPTR_L (d_l);
  SETDIGITS_L (d_l, DIGITS_L (r_l - 1));
Start Sidebar

Now the actual operation takes place. The loop runs over the digits of the shorter of the two arguments.

End Sidebar
  while (s_l <= msdptrs_l)
    {
      *t_l++ = *r_l++ ^ *s_l++;
    }
Start Sidebar

The remaining digits of the other argument are copied as above.

End Sidebar
  while (r_l <= msdptrr_l)
    {
      *t_l++ = *r_l++;
    }
  cpy_l (c_l, d_l);
}

The function and_l() can be used to reduce a number a modulo a power of two 2k by setting a CLINT variable a_l to the value a, a CLINT variable b_l to the value 2k − 1, and executing and_l(a_l, b_l, c_l). However, this operation executes faster with the function mod2_l() created for this purpose, which takes into account that the binary representation of 2k − 1 consists exclusively of ones (see Section 4.3).


7.3 Direct Access to Individual Binary Digits

Occasionally, it is useful to be able to access individual binary digits of a number in order to read or change them. As an example of this we might mention the initialization of a CLINT object as a power of 2, which can be accomplished easily by setting a single bit.

In the following we shall develop three functions, setbit_l(), testbit_l(), and clearbit_l(), which set an individual bit, test a particular bit, and delete a single bit. The functions setbit_l() and clearbit_l() each return the state of the specified bit before the operation. The bit positions are counted from 0, and thus the specified positions can be understood as logarithms of powers of two: If n_l is equal to 0, then setbit_l(n_l, 0) returns the value 0, and afterwards, n_l has the value 20 = 1; after a call to setbit_l(n_l, 512), n_l has the value 2512 + 1.

Start Sidebar
Function: | test and set a bit in a CLINT object
Syntax: | int setbit_l (CLINT a_l, unsigned int pos);
Input: | a_l (CLINT argument) pos (bit position counted from 0)
Output: | a_l (result)
Return: | 1 if the bit at position pos was already set 0 if the bit at position pos was not set E_CLINT_OFL if overflow
End Sidebar
  int
  setbit_l (CLINT a_l, unsigned int pos)
  {
    int res = 0;
    unsigned int i;
    USHORT shorts = (USHORT)(pos >> LDBITPERDGT);
    USHORT bitpos = (USHORT)(pos & (BITPERDGT - 1));
    USHORT m = 1U << bitpos;

    if (pos > CLINTMAXBIT)
      {
        return E_CLINT_OFL;
      }

    if (shorts >= DIGITS_L (a_l))
      {
Start Sidebar

If necessary, a_l is zero filled word by word, and the new length is stored in a_l[0].

End Sidebar
      for (i = DIGITS_L (a_l) + 1; i <= shorts + 1; i++)
         {
           a_l[i] = 0;
         }
      SETDIGITS_L (a_l, shorts + 1);
    }
Start Sidebar

The digit of a_l that contains the specified bit position is tested by means of the mask prepared in m, and then the bit position is set to 1 via an OR of the relevant digit with m. The function ends by returning the previous status.

End Sidebar
  if (a_l[shorts + 1] & m)
    {
      res = 1;
    }

  a_l[shorts + 1] |= m;
  return res;
}
Start Sidebar
Function: | test a binary digit of a CLINT object
Syntax: | int testbit_l (CLINT a_l, unsigned int pos);
Input: | a_l (CLINT argument) pos (bit position counted from 0)
Return: | 1 if bit at position pos is set 0 otherwise
End Sidebar
int
testbit_l (CLINT a_l, unsigned int pos)
{
  int res = 0;
  USHORT shorts = (USHORT)(pos >> LDBITPERDGT);
  USHORT bitpos = (USHORT)(pos & (BITPERDGT - 1));

  if (shorts < DIGITS_L (a_l))
    {
      if (a_l[shorts + 1] & (USHORT)(1U << bitpos))
         res = 1;
    }
  return res;
}
Start Sidebar
Function: | test and delete a bit in a CLINT object
Syntax: | int clearbit_l (CLINT a_l, unsigned int pos);
Input: | a_l (CLINT argument) pos (bit position counted from 0)
Output: | a_l (result)
Return: | 1 if bit at position pos was set before deletion 0 otherwise
End Sidebar
int
clearbit_l (CLINT a_l, unsigned int pos)
{
  int res = 0;
  USHORT shorts = (USHORT)(pos >> LDBITPERDGT);
  USHORT bitpos = (USHORT)(pos & (BITPERDGT - 1));
  USHORT m = 1U << bitpos;

  if (shorts < DIGITS_L (a_l))
    {
Start Sidebar

If a_l has enough digits, then the digit of a_l that contains the specified bit position is tested by means of the mask prepared in m, and then the bit position is set to 0 by an AND of the corresponding digit with the complement of m. The previous status of the bit position is returned at the termination of the function.

End Sidebar
      if (a_l[shorts + 1] & m)
         {
           res = 1;
         }
      a_l[shorts + 1] &= (USHORT)(~m);
      RMLDZRS_L (a_l);
    }
  return res;
}


7.4 Comparison Operators

Every program requires the ability to make assertions about the equality or inequality or the size relationship of arithmetic variables, and this holds as well for our dealings with CLINT objects. Here, too, the principle is obeyed that the programmer does not need knowledge of the internal structure of the CLINT type, and the determination of how two CLINT objects are related to each other is left to functions designed for such purposes.

The primary function that accomplishes these tasks is the function cmp_l(). It determines which of the relations a_l < b_l, a_l == b_l, or a_l > b_l holds for two CLINT values a_l and b_l. To this end, first the numbers of digits of the CLINT objects, which have been liberated from any leading zeros, are compared. If the number of digits of the operands is the same, then the process begins with a comparison of the most-significant digits; as soon as a difference is detected, the comparison is terminated.

Start Sidebar
Function: | comparison of two CLINT objects
Syntax: | int cmp_l (CLINT a_l, CLINT b_l);
Input: | a_l, b_l (arguments)
Return: | −1 if (value of a_l) < (value of b_l) 0 if (value of a_l) = (value of b_l) 1 if (value of a_l) > (value of b_l)
End Sidebar
int
cmp_l (CLINT a_l, CLINT b_l)
{
  clint *msdptra_l, *msdptrb_l;
  int la = DIGITS_L (a_l);
  int lb = DIGITS_L (b_l);
Start Sidebar

The first test checks whether both arguments have length, and hence value, 0. Then any leading zeros are eliminated, and a decision is attempted on the basis of the number of digits.

End Sidebar
  if (la == 0 && lb == 0)
    {
      return 0;
    }

  while (a_l[la] == 0 && la > 0)
    {
      --la;
    }
  while (b_l[lb] == 0 && lb > 0)
    {
     --lb;
    }

  if (la == 0 && lb == 0)
    {
      return 0;
    }
  if (la > lb)
    {
      return 1;
    }
  if (la < lb)
    {
      return -1;
    }
Start Sidebar

If the operands have the same number of digits, then the actual values must be compared. For this we begin with a comparison of the most-significant digits and proceed digit by digit until two digits are found that are unequal or until the least-significant digits are reached.

End Sidebar
  msdptra_l = a_l + la;
  msdptrb_l = b_l + lb;

  while ((*msdptra_l == *msdptrb_l) && (msdptra_l > a_l))
    {
      msdptra_l--;
      msdptrb_l--;
    }
Start Sidebar

Now we compare the two digits and make our determination, and the corresponding function value is returned.

End Sidebar
 if (msdptra_l == a_l)
   {
     return 0;
   }
 if (*msdptra_l > *msdptrb_l)
   {
     return 1;
   }
 else
   {
     return -1;
   }
}

If we are interested in the equality of two CLINT values, then the application of the function cmp_l() is a bit more than is necessary. In this case there is a simpler variant, which avoids the size comparison.

Start Sidebar
Function: | comparison of two CLINT objects
Syntax: | int equ_l (CLINT a_l, CLINT b_l);
Input: | a_l, b_l (arguments)
Return: | 0 if (value of a_l) ≠ (value of b_l) 1 if (value of a_l) = (value of b_l)
End Sidebar
int
equ_l (CLINT a_l, CLINT b_l)
{
  clint *msdptra_l, *msdptrb_l;
  int la = DIGITS_L (a_l);
  int lb = DIGITS_L (b_l);
  if (la == 0 && lb == 0)
    {
      return 1;
    }
  while (a_l[la] == 0 && la > 0)
    {
      --la;
    }

  while (b_l[lb] == 0 && lb > 0)
    {
      --lb;
    }

  if (la == 0 && lb == 0)
    {
      return 1;
    }

  if (la != lb)
    {
      return 0;
    }

  msdptra_l = a_l + la;
  msdptrb_l = b_l + lb;
  while ((*msdptra_l == *msdptrb_l) && (msdptra_l > a_l))
    {
      msdptra_l--;
      msdptrb_l--;
    }

  return (msdptra_l > a_l ? 0 : 1);
}

These two functions in their raw form can easily lead the user into the thickets of error. In particular, the meaning of the function values of cmp_l() must be kept constantly in mind or looked up repeatedly. As a measure against errors a number of macros have been created by means of which comparisons can be formulated in a more mnemonically satisfactory way (see in this regard Appendix C, "Macros with Parameters"). For example, we have the following macros, where we equate the objects a_l and b_l with the values they represent:

GE_L (a_l, b_l)   returns 1 if a_l ≥ b_l, and 0 otherwise;
EQZ_L (a_l)       returns 1 if a_l == 0, and 0 if a_l > 0.


Chapter 8: Input, Output, Assignment, Conversion

The numerals were now being converted automatically from base 2 to base 10 ... 881, 883, 887, 907 ... each one confirmed as a prime number.

—Carl Sagan, Contact

WE BEGIN THIS CHAPTER WITH assignment, the simplest and also the most important function. To be able to assign to a CLINT object a_l the value of another CLINT object b_l, we require a function that copies the digits of b_l to the reserved storage space for a_l, an event that we shall call elementwise assignment. It will not suffice merely to copy the address of the object b_l into the variable a_l, since then both objects would refer to the same location in memory, namely that of b_l, and any change in a_l would be reflected in a change in the object b_l, and conversely. Furthermore, access to the area of memory addressed by a_l could become lost.

We shall return to the problems of elementwise assignment in the second part of this book when we concern ourselves with the implementation of the assignment operator "=" in C++ (see Section 13.3).

The assignment of the value of a CLINT object to another CLINT is effected with the function cpy_l().

Start Sidebar
Function: | copy a CLINT object as an assignment
Syntax: | void cpy_l (CLINT dest_l, CLINT src_l);
Input: | src_l (assigned value)
Output: | dest_l (destination object)
End Sidebar
void
cpy_l (CLINT dest_l, CLINT src_l)
{
  clint *lastsrc_l = MSDPTR_L (src_l);
  *dest_l = *src_l;
Start Sidebar

In the next step leading zeros are found and then ignored. At the same time, the number of digits of the target object is adjusted.

End Sidebar
  while ((*lastsrc_l == 0) && (*dest_l > 0))
    {
      --lastsrc_l;
      --*dest_l;
    }
Start Sidebar

Now the relevant digits of the source object are copied into the goal object. Then the function is terminated.

End Sidebar
  while (src_l < lastsrc_l)
    {
      *++dest_l = *++src_l;
    }
}

The exchange of the values of two CLINT objects can be accomplished with the help of the macro SWAP_L, the FLINT/C variant of the macro SWAP, which manages in an interesting way to accomplish the exchange of two variables using XOR operations without the requirement of intermediate storage in a temporary variable:

#define SWAP(a, b) ((a)^=(b), (b)^=(a), (a)^=(b))
#define SWAP_L(a_l, b_l) \
    (xor_l((a_l), (b_l), (a_l)), \
      xor_l((b_l), (a_l), (b_l)), \
      xor_l((a_l), (b_l), (a_l)))
Start Sidebar
Function: | swap the values of two CLINT objects
Syntax: | void fswap_l (CLINT a_l, CLINT b_l);
Input: | a_l, b_l (values to be exchanged)
Output: | a_l, b_l
End Sidebar

The functions in the FLINT/C library for the input and output of numbers in a form readable by human beings are not among the most exciting functions in this library, yet for many applications they are unavoidable. For practical reasons a form was selected to allow for the input and output by means of character strings, as vectors of type char. For this the two essentially complementary functions str2clint_l() and xclint2str_l() were developed: The first transforms a character string with digits into a CLINT object, and the second, conversely, transforms a CLINT object into a character string. The base of the character string's representation is specified, with representations to bases in the range from 2 to 16 allowed.

The conversion to be carried out by the function str2clint_l() of a representation of type CLINT into a representation in the base specified is accomplished by means of a sequence of multiplications and additions to base B (cf. [Knut], Section 4.4). The function registers any overflow that occurs, the use of invalid bases, and the passing of the null pointer and returns the corresponding error code. Any prefixes indicating the number's representation, "0X," "0x," "0B," or "0b," are ignored.

Start Sidebar
Function: | conversion of a character string into a CLINT object
Syntax: | int str2clint_l (CLINT n_l, char *str, USHORT b);
Input: | str (pointer to a sequence of char) base (base of the numerical representation of the character string, 2 ≤ base ≤ 16)
Output: | n_1 (target CLINT object)
Return: | E_CLINT_OK if all is ok E_CLINT_BOR if base < 2 or base > 16, or if the number of digits in str is larger than base E_CLINT_OFL if overflow E_CLINT_NPT if in str the null pointer was passed
End Sidebar
int
str2clint_l (CLINT n_l, char *str, USHORT base)
{
  clint base_l[10];
  USHORT n;
  int error = E_CLINT_OK;
  if (str == NULL)
    {
      return E_CLINT_NPT;
    }
  if (2 > base || base > 16)
    {
      return E_CLINT_BOR;    /* error: invalid base */
    }

  u2clint_l (base_l, base);

  SETZERO_L (n_l);

  if (*str == '0')
    {
      if ((tolower_l(*(str+1)) == 'x') ||
           (tolower_l(*(str+1)) == 'b'))    /* ignore any prefix */
         {
           ++str;
           ++str;
         }
    }

  while (isxdigit ((int)*str) || isspace ((int)*str))
    {
      if (!isspace ((int)*str))
         {
           n = (USHORT)tolower_l (*str);
Start Sidebar

Many implementations of tolower() from non-ANSI-conforming C libraries return undefined results if a character is not uppercase. The FLINT/C function tolower_l() calls tolower() only for uppercase A–Z and otherwise returns the character unchanged.

End Sidebar
           switch (n)
              {
                case 'a':
                case 'b':
                case 'c':
                case 'd':
                case 'e':
                case 'f':
                  n -= (USHORT)('a' -- 10);
                  break;
                default:
                  n -= (USHORT)'0';
              }

           if (n >= base)
              {
                error = E_CLINT_BOR;
                break;
              }

           if ((error = mul_l (n_l, base_l, n_l)) != E_CLINT_OK)
              {
                break;
              }
           if ((error = uadd_l (n_l, n, n_l)) != E_CLINT_OK)
              {
                break;
              }
         }
      ++str;
    }

  return error;
}

The function xclint2str_l(), complementary to str2clint_l(), returns a pointer to an internal buffer of storage class static (cf. [Harb], Section 4.3), which contains the calculated numerical representation and its value until xclint2str_l() is called again or the program is ended.

The function xclint2str_l() carries the required conversion of the CLINT representation into the representation to the specified base by means of a sequence of divisions with remainder to the base B.

Start Sidebar

Function:

Conversion of a CLINT object into a character string

Syntax:

char * xclint2str_l (CLINT n_l, USHORT base,
               int showbase);

Input:

n_l (CLINT object to be converted)

base (base of the numerical representation of the character string to be specified);

showbase (value ≠ 0: The numerical representation has a "0x" in the case base = 16 or "0b" if base = 2. Value = 0: there is no prefix.)

Return:

pointer to the calculated character string if all ok NULL if 2 < b or b > 16
End Sidebar
static char ntable[16] =
{'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};

char *
xclint2str_l (CLINT n_l, USHORT base, int showbase)
{
  CLINTD u_l, r_l;
  clint base_l[10];
  int i = 0;
  static char N[CLINTMAXBIT + 3];

  if (2U > base || base > 16U)
    {
      return (char *)NULL;    /* error: invalid base */
    }

  u2clint_l (base_l, base);
  cpy_l (u_l, n_l);

  do
    {
      (void) div_l (u_l, base_l, u_l, r_l);
      if (GTZ_L (r_l))
         {
           N[i++] = (char) ntable[*LSDPTR_L (r_l) & 0xff];
         }
      else
         {
           N[i++] = '0';
         }
    }
  while (GTZ_L (u_l));

  if (showbase)
    {
      switch (base)
         {
           case 2:
              N[i++] = 'b';
              N[i++] = '0';
              break;
         case 8:
              N[i++] = '0';
              break;
         case 16:
              N[i++] = 'x';
              N[i++] = '0';
              break;
      }
    }

  N[i] = '0';

  return strrev_l (N);
}

For reasons of compatibility with the function clint2str_l() in the first edition of this book, clint2str_l(n_l, base) was defined as a macro that calls the function xclint2str(n_l, base, 0).

Furthermore, macros HEXSTR_L(), DECSTR_L(), OCTSTR_L(), and BINSTR_L() were created, which create, from a passed CLINT object as argument, a character string without prefix with the numerical representation specified by the macro name and thus eliminate the base of the representation as an argument (see Appendix C).

As standard form for the output of CLINT values we have available the macro DISP_L(), which processes a pointer to a character string and a CLINT object as arguments. The character string contains, according to the purpose to which it will be put, information about the following CLINT value to be output, such as "The product of a_l and b_l has the value ... ." The output of the CLINT value is in hexadecimal, that is, to base 16. Additionally, DISP_L() outputs in a new line the number of significant binary digits (that is, without leading zeros) of the indicated CLINT object (cf. Appendix C).

If there are to be conversions between byte vectors and CLINT objects, then the pair of functions byte2clint_l() and clint2byte_l() can be employed (cf. [IEEE], 5.5.1).

It is assumed that the byte vectors embody a numerical representation to base 256 with values increasing from right to left. For the implementation of these functions the reader is referred to the file flint.c. We give here only the function headers.

Start Sidebar

Function:

conversion of a byte vector into a CLINT object

Syntax:

int byte2clint_l (CLINT n_l, UCHAR *bytestr,
int len);

Input:

bytestr (pointer to a sequence of UCHAR)

len (length of the byte vector)

Output:

n_l (target CLINT object)

Return:

E_CLINT_OK if all ok

E_CLINT_OFL if overflow

E_CLINT_NPT if in bytestr the null pointer was passed
End Sidebar
Start Sidebar
Function: | conversion of a CLINT object into a byte vector
Syntax: | UCHAR * clint2byte_l (CLINT n_l, int *len);
Input: | n_l (CLINT object to be converted)
Output: | len (length of the generated byte vector)
Return: | pointer to the calculated byte vector NULL, if in len the null pointer was passed
End Sidebar

Finally, for the transformation of unsigned values into the CLINT numerical format the two functions u2clint_l() and ul2clint_l() can be used. The function u2clint_l() converts USHORT arguments, and the function ul2clint_l() converts ULONG arguments, into the CLINT numerical format. The function ul2clint_l() will be described in the following as an example.

Start Sidebar
Function: | conversion of a value of type ULONG into a CLINT object
Syntax: | void ul2clint_l (CLINT num_l, ULONG ul);
Input: | ul (value to be converted)
Output: | num_l (target CLINT object)
End Sidebar
void
ul2clint_l (CLINT num_l, ULONG ul)
{
  *LSDPTR_L (num_l) = (USHORT)(ul & 0xffff);
  *(LSDPTR_L (num_l) + 1) = (USHORT)((ul >> 16) & 0xffff);
  SETDIGITS_L (num_l, 2);
  RMLDZRS_L (num_l);
}

To end this chapter we shall discuss a function that carries out a validity check of a memory object for the CLINT number format. Control functions of this type are called as needed whenever "foreign" values are imported into a system for further processing into a subsystem. Such a subsystem can be, for example, a cryptographic module that before every processing of input data must check whether it is dealing with valid values or arguments. Checking at run time whether the assumptions about the input values of a function have been met is good programming practice, one that helps to avoid undefined situations and that can contribute decisively to the stability of an application. For testing and debugging this usually takes place with assertions, with the help of which run-time conditions can be tested. Assertions are inserted as macros and can be decommissioned for the actual running of the program, usually during compilation via #define NDEBUG. In addition to the assert macro of the C standard library (see [Pla1], Chapter 1) there are a number of further implementations of similar mechanisms that take various actions when the test conditions are violated, such as listing recognized exceptional conditions in a log file, with or without program termination in the event of an error. For extensive information in this area the reader is referred to [Magu], Chapters 2 and 3, as well as [Murp], Chapter 4.

The protection of the functions of a program library like the FLINT/C package against being passed values that lie outside of the domain of definition of the respective parameters can occur within the invoked functions themselves or within the calling functions, where in the latter case the responsibility lies with the programmer who employs the library. For performance considerations, in the development of the FLINT/C functions we did not test every passed CLINT argument for a valid address and possible overflow. The thought of carrying out multiply redundant checks of the numerical format in thousands of modular multiplications of an exponentiation moved the author to offload this control task to the programs that use the FLINT/C functions. An exception is the passing of divisors with the value zero, which is checked as a matter of principle and if it occurs is acknowledged with a suitable error notification, even in all the functions for residue class arithmetic. The code of all the functions was particularly carefully tested to make sure that the FLINT/C library generates only valid formats (cf. Chapter 12).

The function vcheck_l() was created for the analysis of CLINT arguments with regard to the validity of their format. It should help to protect the FLINT/C functions from being passed invalid parameters as CLINT values.

Start Sidebar
Function: | test for a valid CLINT numerical format
Syntax: | int vcheck_l (CLINT n_l);
Input: | n_l (object to be tested)
Return: | E_VCHECK_OK if format ok errors and warnings according to Table 8.1
End Sidebar
Table 8.1: Diagnostic values of the function vcheck_l()
Return value | Diagnosis | Explanation
E_VCHECK_OK | Format is ok | Info: The number has a valid representation and a value with the range of definition of a CLINT type.
E_VCHECK_LDZ | leading zeros | Warning: The number has leading zeros, but otherwise a valid definition within the range of definition.
E_VCHECK_MEM | memory error | Error: NULL Pointer was passed.
E_VCHECK_OFL | genuine overflow | Error: The passed number is too large; it cannot be represented as a CLINT object.
int
vcheck_l (CLINT n_l)
{
  unsigned int error = E_VCHECK_OK;
Start Sidebar

Check for the null pointer: the ugliest error of them all.

End Sidebar
  if (n_l == NULL)
    {
      error = E_VCHECK_MEM;
    }
  else
    {
Start Sidebar

Check for overflow: Does the number have too many digits?

End Sidebar
    if (((unsigned int) DIGITS_L (n_l)) > CLINTMAXDIGIT)
       {
         error = E_VCHECK_OFL;
       }
    else
       {
Start Sidebar

Check for leading zeros: These we can live with ;-)

End Sidebar
           if ((DIGITS_L (n_l) > 0) && (n_l[DIGITS_L (n_l)] == 0))
              {
                error = E_VCHECK_LDZ;
              }
         }
    }

  return error;
}

The return values of the function are defined as macros in the file flint.h. An explanation of these values is provided in Table 8.1.

The numeric values of the error codes are smaller than zero, so that a simple comparison with zero suffices to distinguish between errors on the one hand and warnings or the valid case on the other.


Chapter 9: Dynamic Registers

"What a depressingly stupid machine," said Marvin and trudged away.

—Douglas Adams, The Restaurant at the End of the Universe

IN ADDITION TO THE AUTOMATIC, or in exceptional cases global, CLINT objects used up to now, it is sometimes practical to be able to create and purge CLINT variables automatically. To this end we shall create several functions that will enable us to generate, use, clear, and remove a set of CLINT objects, the so-called register bank, as a dynamically allocated data structure, where we take up the sketch presented in [Skal] and work out the details for its use with CLINT objects.

We shall divide the functions into private management functions and public functions; the latter of these will be made available to other external functions for manipulating the registers. However, the FLINT/C functions do not use the registers themselves, so that complete control over the use of the registers can be guaranteed to the user's functions.

The number of registers available should be configurable while the program is running, for which we need a static variable NoofRegs that takes the number of registers, which is predefined in the constant NOOFREGS.

static USHORT NoofRegs = NOOFREGS;

Now we define the central data structure for managing the register bank:

struct clint_registers
{
  int noofregs;
  int created;
  clint **reg_l;    /* pointer to vector of CLINT addresses */
};

The structure clint_registers contains the variable noofregs, which specifies the number of registers contained in our register bank, and the variable created, which will indicate whether the set of registers is allocated, as well as the pointer reg_l to a vector that takes the start address of the individual registers:

static struct clint_registers registers = {0, 0, 0};

Now come the private management functions allocate_reg_l() to set up the register bank and destroy_reg_l() to clear it. After space for the storage of the addresses of the registers to be allocated has been created and a pointer is then set to the variable registers.reg_l, there follows the allocation of memory for each individual register by a call to malloc() from the C standard library. The fact that CLINT registers are memory units allocated by means of malloc() plays an important role in testing the FLINT/C functions. We shall see in Section 12.2 how this makes possible the examination of any memory errors that may occur.

static int
allocate_reg_l (void)
{
  USHORT i, j;
Start Sidebar

First, memory is allocated for the vector of register addresses.

End Sidebar
  if ((registers.reg_l = (clint **) malloc (sizeof(clint *) * NoofRegs)) == NULL)
    {
      return E_CLINT_MAL;
    }
Start Sidebar

Now comes the allocation of individual registers. If in the process a call to malloc() ends in an error, all previously allocated registers are cleared and the error code E_CLINT_MAL is returned.

End Sidebar
  for (i = 0; i < NoofRegs; i++)
    {
      if ((registers.reg_l[i] = (clint *) malloc (CLINTMAXBYTE)) == NULL)
         {
           for (j = 0; j < i; j++)
              {
                free (registers.reg_l[j]);
              }
           return E_CLINT_MAL;    /* error: malloc */
         }
    }

  return E_CLINT_OK;
}

The function destroy_reg_l() is essentially the inverse of the function create_reg_l(): First, the content of the registers is cleared by overwriting them with zeros. Then each individual register is returned by means of free(). Finally, memory pointed to by registers.reg_l is released.

 static void
 destroy_reg_l (void)
 {
   USHORT i;

   for (i = 0; i < registers.noofregs; i++)
     {
        memset (registers.reg_l[i], 0, CLINTMAXBYTE);
        free (registers.reg_l[i]);
     }

   free (registers.reg_l);
 }

Now come the public functions for register management. With the function create_reg_l() we create a set of registers consisting of the number of individual registers determined in NoofRegs. This takes place via a call to the private function allocate_reg_l().

Start Sidebar
Function: | Allocation of a set of registers of type CLINT
Syntax: | int create_reg_l (void);
Return: | E_CLINT_OK if allocation is ok E_CLINT_MAL if error with malloc()
End Sidebar
int
create_reg_l (void)
{
  int error = E_CLINT_OK;

  if (registers.created == 0)
    {
      error = allocate_reg_l ();
      registers.noofregs = NoofRegs;
    }

  if (!error)
    {
      ++registers.created;
    }

  return error;
}

The structure registers involves the variable registers.created, which is used for counting the number of requested registers to be created. A call to the function free_reg_l() described below results in the set of registers being released only if registers.created has the value 1. Otherwise, registers.created is simply reduced by 1. With the use of this mechanism, called a semaphore, we manage to prevent a set of registers allocated by one function being inadvertently released by another function. On the other hand, every function that requests the set of registers by calling create_reg_l() is responsible for releasing it again with free_reg_l(). Moreover, in general, one cannot assume that the registers contain specific values after a function has been called.

The variable NoofRegs, which determines the number of registers created by create_reg_l(), can be changed by the function set_noofregs_l(). This change, however, remains in effect only until the currently allocated set of registers is released and a new set is created with create_reg_l().

Start Sidebar
Function: | set number of registers
Syntax: | void set_noofregs_l (unsigned int nregs);
Input: | nregs (number of registers in the register bank)
End Sidebar
void
set_noofregs_l (unsigned int nregs)
{
  NoofRegs = (USHORT)nregs;
}

Now that a set of registers can be allocated, one may ask how individual registers can be accessed. For this it is necessary to select the address field reg_l, dynamically allocated by create_reg_l(), of the above-defined structure clint_reg. This will be accomplished with the help of the function get_reg_l(), introduced below, which returns a pointer to an individual register of the set of registers, provided that the specified ordinal number denotes an allocated register.

Start Sidebar
Function: | output a pointer to a register
Syntax: | clint * get_reg_l (unsigned int reg);
Input: | reg (register number)
Return: | pointer to the desired register reg, if it is allocated NULL if the register is unallocated
End Sidebar
clint *
get_reg_l (unsigned int reg)
{
  if (!registers.created || (reg >= registers.noofregs))
    {
      return (clint *) NULL;
    }

  return registers.reg_l[reg];
}

Since the set of registers can be changed dynamically with respect to its size and location in memory, it is not recommended that addresses of registers once read be stored for further use. It is much to be preferred that one obtain the register addresses afresh for each use. In the file flint.h are to be found several predefined macros of the form

   #define r0_l get_reg_l(0);

with the help of which the registers can be invoked, without additional syntactic effort, by their actual current addresses. With the function purge_reg_l(), introduced below, an individual register of the set can be cleared by overwriting it.

Start Sidebar
Function: | Clear a CLINT register of the register bank by completely overwriting it with zeros
Syntax: | int purge_reg_l (unsigned int reg);
Input: | reg (register number)
Return: | E_CLINT_OK if deletion is ok E_CLINT_NOR if register is unallocated
End Sidebar
int
purge_reg_l (unsigned int reg)
{
  if (!registers.created || (reg >= registers.noofregs))
    {
      return E_CLINT_NOR;
    }

  memset (registers.reg_l[reg], 0, CLINTMAXBYTE);
  return E_CLINT_OK;
}

Just as an individual register can be cleared with the function purge_reg_l(), with the function purgeall_reg_l() the complete set of registers can be cleared by overwriting.

Start Sidebar
Function: | clear all CLINT registers by overwriting with zeros
Syntax: | int purgeall_reg_l (void);
Return: | E_CLINT_OK if deletion is ok E_CLINT_NOR if registers are not allocated
End Sidebar
int
purgeall_reg_l (void)
{
  USHORT i;
  if (registers.created)
    {
      for (i = 0; i < registers.noofregs; i++)
         {
           memset (registers.reg_l[i], 0, CLINTMAXBYTE);
         }

      return E_CLINT_OK;
    }

  return E_CLINT_NOR;
}

It is good programming style and etiquette to release allocated memory when it is no longer needed. An existing set of registers can be released with the function free_reg_l(). However, as we have explained above, the semaphore registers.created in the structure registers must have been set to 1 before the allocated memory is actually released:

  void
  free_reg_l (void)
  {
    if (registers.created == 1)
      {
         destroy_reg_l ();
      }

    if (registers.created)
      {
         −−registers.created;
      }
  }

We now present three functions that create, clear, and again free individual CLINT registers, in analogy to the management of the complete set of registers.

Start Sidebar
Function: | allocation of a register of type CLINT
Syntax: | clint * create_l (void);
Return: | pointer to allocated registers, if allocation ok NULL if error with malloc()
End Sidebar
clint *
create_l (void)
{
  return (clint *) malloc (CLINTMAXBYTE);
}

It is important to treat the pointer returned by create_l() in such a way that it does not "become lost," since otherwise, it is impossible to access the created registers. The sequence

  clint * do_not_overwrite_l;
  clint * lost_l;
  /* ... */
  do_not_overwrite _l = create_l();
  /* ... */
  do_not_overwrite _l = lost_l;

allocates a register and stores its address in a variable with the suggestive name do_not_overwrite_l. If this variable contains the only reference to the register, then after the last instruction,

  do_not_overwrite _l = lost_l;

the register is lost, which is a typical error in the jungle of pointer management.

A register can, like any other CLINT variable, be cleared with the function purge_l() that follows, whereby the memory reserved for the specified register is overwritten with zeros and thereby cleared.

Start Sidebar
Function: | clear a CLINT object by completely overwriting with zeros
Syntax: | void purge_l (CLINT n_l);
Input: | n_l (CLINT object)
End Sidebar
 void
 purge_l (CLINT n_l)
 {
   if (NULL != n_l)
     {
        memset (n_l, 0, CLINTMAXBYTE);
     }
 }

The following function additionally releases the memory allocated for the specified register after it has been cleared. Afterwards, the register can no longer be accessed.

Start Sidebar
Function: | clear and release a CLINT register
Syntax: | void free_l (CLINT reg_l);
Input: | reg_l (pointer to a CLINT register)
End Sidebar
 void
 free_l (CLINT reg_l)
 {
   if (NULL != reg_l)
     {
       memset (reg_l, 0, CLINTMAXBYTE);
       free (n_l);
     }
 }


Chapter 10: Basic Number-Theoretic Functions

Overview

I am dying to hear about it, since I always thought number theory was the Queen of Mathematics—the purest branch of mathematics—the one branch of mathematics which has NO applications!

—D. R. Hofstadter, Gödel, Escher, Bach

NOW THAT WE ARE FITTED out with a sturdy tool box of arithmetic functions that we developed in the previous chapters, we turn our attention to the implementation of several fundamental algorithms from the realm of number theory. The number-theoretic functions discussed in the following chapters form a collection that on the one hand exemplifies the application of the arithmetic of large numbers and on the other forms a useful foundation for more complex number-theoretic calculations and cryptographic applications. The resources provided here can be extended in a number of directions, so that for almost every type of application the necessary tools can be assembled with the demonstrated methods.

The algorithms on which the following implementations are based are drawn primarily from the publications [Cohe], [HKW], [Knut], [Kran], and [Rose], where as previously, we have placed particular value on efficiency and on as broad a range of application as possible.

The following sections contain the minimum of mathematical theory required to explicate the functions that we present and their possibilities for application. We would like, after all, to have some benefit from all the effort that will be required in dealing with this material. Those readers who are interested in a more thoroughgoing introduction to number theory are referred to the books [Bund] and [Rose]. In [Cohe] in particular the algorithmic aspects of number theory are considered and are treated clearly and concisely. An informative overview of applications of number theory is offered by [Schr], while cryptographic aspects of number theory are treated in [Kobl].

In this chapter we shall be concerned with, among other things, the calculation of the greatest common divisor and the least common multiple of large numbers, the multiplicative properties of residue class rings, the identification of quadratic residues and the calculation of square roots in residue class rings, the Chinese remainder theorem for solving systems of linear congruences, and the identification of prime numbers. We shall supplement the theoretical foundations of these topics with practical tips and explanations, and we shall develop several functions that embody a realization of the algorithms that we describe and make them usable in many practical applications.


10.1 Greatest Common Divisor

That schoolchildren are taught to use the method of prime factorization rather than the more natural method of the Euclidean algorithm to compute the greatest common divisor of two integers is a disgrace to our system of education.

—W. Heise, P. Quattrocci, Information and Coding Theory

Stated in words, the greatest common divisor (gcd) of integers a and b is the positive divisor of a and b that is divisible by all common divisors of a and b. The greatest common divisor is thereby uniquely determined. In mathematical notation the greatest common divisor d of two integers a and b, not both zero, is defined as follows: d = gcd(a, b) if d > 0, d | a, d | b, and if for some integer d′ we have d′ | a and d′ | b, then we also have d′ | d.

It is convenient to extend the definition to include

The greatest common divisor is thus defined for all pairs of integers, and in particular for the range of integers that can be represented by CLINT objects. The following rules hold:

(10.1) Click To expand

of which, however, only (i)–(iii) are relevant for CLINT objects.

It is obligatory first to consider the classical procedure for calculating the greatest common divisor according to the Greek mathematician Euclid (third century B.C.E.), which Knuth respectfully calls the grandfather of all algorithms (definitely see [Knut], pages 316 ff.). The Euclidean algorithm consists in a sequence of divisions with remainder, beginning with the reduction of a mod b, then b mod (a mod b), and so on until the remainder vanishes.

Euclidean algorithm for calculating gcd(a, b) for a, b ≥ 0

  1. If b = 0, output a and terminate the algorithm.

  2. Set r ← a mod b, a ← b, b ← r, and go to step 1.

For natural numbers a1, a2 the calculation of the greatest common divisor according to the Euclidean algorithm goes as follows:

Click To expand

Result:

We compute as an example gcd(723, 288):

Result:

This procedure works very well for calculating the greatest common divisor or for letting a computer program do the work. The corresponding program is short, quick, and, due to its brevity, provides few opportunities for error.

A consideration of the following properties of integers and of the greatest common divisor indicates—at least theoretically—possibilities for improvement for programming this procedure:

(10.2) Click To expand

The advantage of the following algorithm based on these properties is that it uses only size comparisons, subtractions, and shifts of CLINT objects, operations that do not require a great deal of computational time and for which we use efficient functions; above all, we need no divisions. The binary Euclidean algorithm for calculating the greatest common divisor can be found in almost the identical form in [Knut], Section 4.5.2, Algorithm B, and in [Cohe], Section 1.3, Algorithm 1.3.5.

Binary Euclidean algorithm for calculating gcd(a, b) for a, b ≥ 0

  1. If a < b, exchange the values of a and b. If b = 0, output a and terminate the algorithm. Otherwise, set k ← 0, and as long as a and b are both even, set k ← k + 1, a ← a/2, b ← b/2. (We have exhausted property (i); a and b are now no longer both even.)

  2. As long as a is even, set repeatedly a ← a/2 until a is odd. Or else, if b is even, set repeatedly b ← b/2 until b is odd. (We have exhausted property (ii); a and b are now both odd.)

  3. Set t ← (a − b)/2. If t = 0, output 2k a and terminate the algorithm. (We have used up properties (ii), (iii), and (iv).)

  4. As long as t is even, set repeatedly t ← t/2, until t is odd. If t > 0, set a ← t; otherwise, set b ← −t; and go to step 3.

This algorithm can be translated step for step into a programmed function, where we take the suggestion from [Cohe] to execute in step 1 an additional division with remainder and set r ← a mod b, a ← b, and b ← r. We thereby equalize any size differences between the operands a and b that could have an adverse effect on the running time.

Start Sidebar
Function: | greatest common divisor
Syntax: | void gcd_l (CLINT aa_l, CLINT bb_l, CLINT cc_l);
Input: | aa_l, bb_l (operands)
Output: | cc_l (greatest common divisor)
End Sidebar
void
gcd_l (CLINT aa_l, CLINT bb_l, CLINT cc_l)
{
  CLINT a_l, b_l, r_l, t_l;
  unsigned int k = 0;
  int sign_of_t;
Start Sidebar

Step 1: If the arguments are unequal, the smaller argument is copied to b_l. If b_l is equal to 0, then a_l is output as the greatest common divisor.

End Sidebar
  if (LT_L (aa_l, bb_l))
    {
      cpy_l (a_l, bb_l);
      cpy_l (b_l, aa_l);
    }
  else
    {
      cpy_l (a_l, aa_l);
      cpy_l (b_l, bb_l);
    }

  if (EQZ_L (b_l))
    {
      cpy_l (cc_l, a_l);
      return;
    }
Start Sidebar

The following division with remainder serves to scale the larger operand a_l. Then the powers of two are removed from a_1 and b_1.

End Sidebar
  (void) div_l (a_l, b_l, t_l, r_l);
  cpy_l (a_l, b_l);
  cpy_l (b_l, r_l);

  if (EQZ_L (b_l))
    {
      cpy_l (cc_l, a_l);
      return;
    }

  while (ISEVEN_L (a_l) && ISEVEN_L (b_l))
    {
      ++k;
      shr_l (a_l);
      shr_l (b_l);
    }
Start Sidebar

Step 2.

End Sidebar
  while (ISEVEN_L (a_l))
    {
      shr_l (a_l);
    }

  while (ISEVEN_L (b_l))
    {
      shr_l (b_l);
    }
Start Sidebar

Step 3: Here we have the case that the difference of a_l and b_l can be negative. This situation is caught by a comparison between a_l and b_l. The absolute value of the difference is stored in t_l, and the sign of the difference is stored in the integer variable sign_of_t. If t_l == 0. Then the algorithm is terminated.

End Sidebar
  do
    {
      if (GE_L (a_l, b_l))
         {
           sub_l (a_l, b_l, t_l);
           sign_of_t = 1;
         }
      else
         {
           sub_l (b_l, a_l, t_l);
           sign_of_t = −1;
         }

      if (EQZ_L (t_l))
         {
           cpy_l (cc_l, a_l);     /* cc_l <- a */
           shift_l (cc_l, (long int) k);    /* cc_l <- cc_l*2**k */
           return;
         }
Start Sidebar

Step 4: Depending on the sign of t_l, we have t_l allocated to a_l or b_l.

End Sidebar
      while (ISEVEN_L (t_l))
         {
           shr_l (t_l);
         }
      if (−1 == sign_of_t)
         {
           cpy_l (b_l, t_l);
         }
      else
         {
           cpy_l (a_l, t_l);
         }
    }
  while (1);
}

Although the operations used are all linear in the number of digits of the operands, tests show that the simple two-line greatest common divisor on page 166 is hardly slower as a FLINT/C function than this variant. Somewhat surprised at this, we ascribe this situation, for lack of better explanation, to the efficiency of our division routine, as well as to the fact that the latter version of the algorithm requires a somewhat more complex structure.

The calculation of the greatest common divisor for more than two arguments can be carried out with multiple applications of the function gcd_l(), since as we showed above in (10.1)(iii) the general case can be reduced recursively to the case with two arguments:

(10.3) Click To expand

With the help of the greatest common divisor it is easy to determine the least common multiple (lcm) of two CLINT objects a_l and b_l. The least common multiple of integers n1,...,nr, all nonzero, is defined as the smallest element of the set { m Î [image] | ni divides m, i = 1,...,r}. Since it contains at least the product [image] |ni|, this set is nonempty. For two arguments a, b Î [image] the least common multiple can be computed as the absolute value of their product divided by the greatest common divisor:

(10.4)

We shall use this relation for the calculation of the least common multiple of a_l and b_l.

Start Sidebar
Function: | least common multiple (lcm)
Syntax: | int lcm_l (CLINT a_l, CLINT b_l, CLINT c_l);
Input: | a_l, b_l (operands)
Output: | c_l (lcm)
Return: | E_CLINT_OK if all ok E_CLINT_OFL if overflow
End Sidebar
int
lcm_l (CLINT a_l, CLINT b_l, CLINT c_l)
{
  CLINT g_l, junk_l;

  if (EQZ_L (a_l) || EQZ_L (b_l))
    {
      SETZERO_L (c_l);
      return E_CLINT_OK;
    }

  gcd_l (a_l, b_l, g_l);
  div_l (a_l, g_l, g_l, junk_l);
  return (mul_l (g_l, b_l, c_l));
}

It holds for the least common multiple as well that its calculation for more than two arguments can be recursively reduced to the case of two arguments:

(10.5) Click To expand

Formula (10.4) above does not, however, hold for more than two numbers: The simple fact that lcm(2, 2, 2) · gcd(2, 2, 2) = 4 ≠ 23 can serve as a counterexample. There does exist, however, a generalization of this relation between the greatest common divisor and the least common multiple for more than two arguments. Namely, we have

(10.6)

and also

(10.7)

The special relationship between the greatest common divisor and the least common multiple is expressed in additional interesting formulas, demonstrating an underlying duality, in the sense that exchanging the roles of greatest common divisor and least common multiple does not affect the validity of the formula, just as in (10.6) and (10.7). We have the distributive law for gcd and lcm, namely,

(10.8) Click To expand
(10.9) Click To expand

and to top it all off we have (see [Schr], Section 2.4)

(10.10) Click To expand

Aside from the obvious beauty of these formulas on account of their fearful symmetry, they also serve to provide excellent tests for functions that deal with greatest common divisor and least common multiple, where the arithmetic functions used are implicitly tested as well (on the subject of testing, see Chapter 12).

Don't blame testers for finding your bugs.

—Steve Maguire


10.2 Multiplicative Inverse in Residue Class Rings

In contrast to the arithmetic of whole numbers, in residue class rings it is possible, under certain assumptions, to calculate with multiplicative inverses. Namely, many elements ā Î [image] , not necessarily all, possess a suitable [image] Î [image] such that ā · [image] = [image] . This is equivalent to the assertion that the congruence a · x ≡ 1 mod n and the statement a · x mod n = 1 hold. For example, in [image] , [image] and [image] are multiplicative inverses of each other, since 15 mod 14 = 1.

The existence of multiplicative inverses in [image] is not obvious. In Chapter 5, on page 68, it was determined only that ( [image] , ·) is a finite commutative semigroup with unit [image] . A sufficient condition for an element ā Î [image] to possess a multiplicative inverse can be obtained with the help of the Euclidean algorithm: The second-to-last equation in the Euclidean algorithm procedure on page 167,

Click To expand

can be transformed into

(1)

If we continue in this fashion, then we obtain in succession

(2)
(3)

If in (1) we replace am−1 by the right side of equation (2), then we obtain

Click To expand

or

Click To expand

Proceeding thus one obtains in equation (m − 2) an expression for am as a linear combination of the starting values a1 and a2 with factors composed of the quotients qi of the Euclidean algorithm.

In this way we obtain a representation of gcd(a, b) = u · a + v · b =: g as a linear combination of a and b with integer factors u and v, where u modulo a/g and v modulo b/g are uniquely determined. If for an element ā Î [image] we now have gcd(a, n) = 1 = u · a + v · n, then it follows immediately that 1 ≡ u · a mod n, or, equivalently, ā · ū = [image] . In this case u modulo n is uniquely determined, and ū is consequently the inverse of ā in [image] . We have thus found a condition for the existence of a multiplicative inverse of an element in the residue class ring [image] , and we have simultaneously obtained a procedure for constructing such an inverse, which shall demonstrate with the following example. From the calculation above of gcd(723, 288) we obtain by rearrangement

From this we obtain our representation of the greatest common divisor:

Click To expand

A fast procedure for calculating this representation of the greatest common divisor would consist in storing the quotients qi (as is done here on the page) so that they would be available for the backward calculation of the desired factors. Because of the high memory requirement, such a procedure would not be practicable. It is necessary to find a compromise between memory requirements and computational time, which is a typical tradeoff in the design and implementation of algorithms. To obtain a realistic procedure we shall further alter the Euclidean algorithm in such a way that the representation of the greatest common divisor as a linear combination can be calculated along with the greatest common divisor itself. For ā in [image] there exists an inverse [image] Î [image] if gcd(a, n) = 1. The converse of this statement can also be demonstrated: If ā in [image] has a multiplicative inverse, then gcd(a, n) = 1 (one may find a mathematical proof of this statement in [Nive], the proof to Theorem 2.13). We see, then, that here the issue of having no common factors (that is, being relatively prime) is of great significance: If we consider the subset [image] := {ā Î [image] | gcd(a, n) = 1} of those elements ā Î [image] for which a has no common factor with n other than 1, then with the operation of multiplication one has an abelian group, which we have denoted by ( [image] , ·) already in Chapter 5. The properties of ( [image] , ·) as an abelian semigroup with unit,

  • associativity of ( [image] , ·),

  • commutativity of ( [image] , ·),

  • existence of a unit: For all ā Î [image] one has ā · [image] = ā,

carry over directly to ( [image] , ·). The existence of multiplicative inverses holds because we have selected precisely those elements that have such inverses, so that we have now only to demonstrate closure, namely, that for two elements ā and [image] in [image] the product ā · [image] is again an element of [image] . Closure is easily proved: If a and b are relatively prime to n, then the product of a and b cannot have a nontrivial factor in common with n, so that ā · [image] must belong to the set [image] . The group ( [image] , ·) is called the group of residue classes relatively prime to n.

The number of elements in [image] , or, equivalently, the number of integers relatively prime to n in the set {1, 2,...,n − 1 }, is given by the Euler phi function φ(n). For n = [image] written as a product of distinct primes p1,...,pt to positive powers ei, we have

(see, for example, [Nive], Sections 2.1 and 2.4). This means, for example, that [image] has p − 1 elements if p is a prime number.[1]

If gcd(a, n) = 1, then according to a theorem of Euler, a generalization of the little theorem of Fermat, aφ(n) ≡ 1 mod n, so that the calculation of aφ(n)−1 mod n also represents a way to determine the multiplicative inverse of ā.[2] For example, if n = p · q with prime numbers p ≠ q and a Î [image] , then a(p−1)(q−1) ≡ 1 mod n, and therefore a(p−1)(q−1)−1 mod n is the inverse of a modulo n. However, this calculation requires even in the best case that φ(n) be known, a modular exponentiation whose computational cost is O (log3 n).

We do significantly better, namely with a computational cost of O (log2 n) and without knowing the value of the Euler phi function, by integrating the above constructive procedure into the Euclidean algorithm. For this we introduce variables u and v, with the help of which the invariants

are maintained in the individual steps of the procedure presented on page 167, in which we have

and these invariants provide us at the end of the algorithm the desired representation of the greatest common divisor as a linear combination of a and b. Such a procedure is called an extended Euclidean algorithm.

The following extension of the Euclidean algorithm is taken from [Cohe], Section 1.3, Algorithm 1.3.6. The variable v in the above invariant condition is employed only implicitly, and only at the end is it calculated as v := (d − u · a)/b.

Extended Euclidean algorithm for calculating gcd(a, b) and factors u and v such that gcd(a, b) = u · a + v · b, 0 ≤ a, b

  1. Set u ← 1, d ← a. If b = 0, set v ← 0 and terminate the algorithm; otherwise, set v1 ← 0 and v3 ← b.

  2. Calculate q and t3 with d = q · v3 + t3 and t3 < v3 by a division with remainder, and set t1 ← u − q · v1, u ← v1, d ← v3, v1 ← t1, and v3 ← t3.

  3. If v3 = 0, set v ← (d − u · a)/b and terminate the algorithm; otherwise, go to step 2.

The following function xgcd_l() uses the auxiliary functions sadd() and ssub() for the (exceptional) calculation of a signed addition and subtraction. Each of these functions contains a prelude that deals with the sign as an argument to be passed, and then calls the kernel functions add() and sub() (cf. Chapter 5), which execute addition and subtraction, respectively, without consideration of overflow or underflow. Based on the division function div_l() for natural numbers there exists the auxiliary function smod(), which forms the residue a mod b with a, b Î [image] , b > 0. These auxiliary functions will be needed again later, in connection with the application of the Chinese remainder theorem in the function chinrem_l() (see Section 10.4.3). In a possible extension of the FLINT/C library for processing integers they could be used as models for handling signs.

A hint for using the following function is in order: If the arguments satisfy a, b ≥ Nmax/2, an overflow in the factors u and v, which are returned as the result of xgcd_l(), can occur. In such cases enough space must be reserved for holding u and v, which are then declared by the calling program as type CLINTD or CLINTQ as required (see Chapter 2).

Start Sidebar

Function:

extended Euclidean algorithm for calculating the representation gcd(a, b) = u · a + v · b for natural numbers a, b

Syntax:

void xgcd_l (CLINT a_l, CLINT b_l, CLINT g_l,
                CLINT u_l, int *sign_u,
                CLINT v_l, int *sign_v);

Input:

a_l, b_l (operands)

Output:

g_l (gcd of a_l and b_l)

u_l, v_l (factors of a_l and b_l in the representation of g_l)

*sign_u (sign of u_l)

*sign_v (sign of v_l).
End Sidebar
void
xgcd_l (CLINT a_l, CLINT b_l, CLINT d_l, CLINT u_l, int *sign_u, CLINT v_l,
                                                           int *sign_v)
{
  CLINT v1_l, v3_l, t1_l, t3_l, q_l;
  CLINTD tmp_l, tmpu_l, tmpv_l;
  int sign_v1, sign_t1;
Start Sidebar

Step 1: Initialization.

End Sidebar
  cpy_l (d_l, a_l);
  cpy_l (v3_l, b_l);

  if (EQZ_L (v3_l))
    {
      SETONE_L (u_l);
      SETZERO_L (v_l);
      *sign_u = 1;
      *sign_v = 1;
      return;
    }

  SETONE_L (tmpu_l);
  *sign_u = 1;
  SETZERO_L (v1_l);
  sign_v1 = 1;
Start Sidebar

Step 2: Main loop; calculation of the greatest common divisor and of u.

End Sidebar
  while (GTZ_L (v3_l))
    {
      div_l (d_l, v3_l, q_l, t3_l);
      mul_l (v1_l, q_l, q_l);
      sign_t1 = ssub (tmpu_l, *sign_u, q_l, sign_v1, t1_l);

      cpy_l (tmpu_l, v1_l);
      *sign_u = sign_v1;
      cpy_l (d_l, v3_l);
      cpy_l (v1_l, t1_l);
      sign_v1 = sign_t1;
      cpy_l (v3_l, t3_l);
    }
Start Sidebar

Step 3: Calculation of v and the end of the procedure.

End Sidebar
  mult (a_l, tmpu_l, tmp_l);
  *sign_v = ssub (d_l, 1, tmp_l, *sign_u, tmp_l);
  div_l (tmp_l, b_l, tmpv_l, tmp_l);

  cpy_l (u_l, tmpu_l);
  cpy_l (v_l, tmpv_l);

  return;
}

Since dealing with negative numbers within the FLINT/C package requires additional cost, we arrive at the observation that for calculating the inverse of a residue class ā Î [image] only the one factor u of the representation 1 = u · a + v · n of the greatest common divisor is necessary. A positive representative for u can always be found, and we can thereby spare ourselves the need to deal with negative numbers. The following algorithm is a variant of the previous one that makes use of this observation and eliminates entirely the calculation of v.

Extended Euclidean algorithm for calculating gcd(a, n) and the multiplicative inverse of a mod n, 0 ≤ a, 0 < n

  1. Set u ← 1, g ← a, v1 ← 0, and v3 ← n.

  2. Calculate q and t3 with g = q · v3 + t3 and t3 < v3 by a division with remainder and set t1 ← u − q · v1 mod n, u ← v1, g ← v3, v1 ← t1, and v3 ← t3.

  3. If v3 = 0, output g as gcd(a, n) and u as the inverse of a mod n and terminate the algorithm; otherwise, return to step 2.

The modular step t1 ← u − q · v1 mod n ensures that t1, v1, and u do not become negative. At the end we have u Î {1,...,n − 1}. The coding of the algorithm leads us to the following function.

Start Sidebar
Function: | calculation of the multiplicative inverse in
Syntax: | void inv_l (CLINT a_l, CLINT n_l, CLINT g_l, CLINT i_l);
Input: | a_l, n_l (operands)
Output: | g_l (gcd of a_l and n_l) i_l (inverse of a_l mod n_l, if defined)
End Sidebar
void
inv_l (CLINT a_l, CLINT n_l, CLINT g_l, CLINT i_l)
{
  CLINT v1_l, v3_l, t1_l, t3_l, q_l;
Start Sidebar

Test of the operands for 0. If one of the operands is zero, then there does not exist an inverse, but there does exist a greatest common divisor (cf. page 166). The result variable i_l is then undefined, which is indicated by being set to zero.

End Sidebar
  if (EQZ_L (a_l))
    {
      if (EQZ_L (n_l))
         {
           SETZERO_L (g_l);
           SETZERO_L (i_l);
           return;
         }
      else
         {
           cpy_l (g_l, n_l);
           SETZERO_L (i_l);
           return;
         }
    }
  else
    {
      if (EQZ_L (n_l))
         {
           cpy_l (g_l, a_l);
           SETZERO_L (i_l);
           return;
         }
    }
Start Sidebar

Step 1: Initialization of the variables.

End Sidebar
  cpy_l (g_l, a_l);
  cpy_l (v3_l, n_l);
  SETZERO_L (v1_l);
  SETONE_L (t1_l);
  do
    {
Start Sidebar

Step 2: With the test in GTZ_L (t3_l) after the division an unnecessary call to mmul_l() and msub_l() is avoided in the last run through the loop. The assignment to the result variable i_l is not carried out until the end.

End Sidebar
      div_l (g_l, v3_l, q_l, t3_l);

      if (GTZ_L (t3_l))
         {
           mmul_l (v1_l, q_l, q_l, n_l);
           msub_l (t1_l, q_l, q_l, n_l);
           cpy_l (t1_l, v1_l);
           cpy_l (v1_l, q_l);
           cpy_l (g_l, v3_l);
           cpy_l (v3_l, t3_l);
         }
    }
  while (GTZ_L (t3_l));
Start Sidebar

Step 3: As the last requisite assignment we take the greatest common divisor from the variable v3_l, and if the greatest common divisor is equal to 1, we take the inverse to a_l from the variable v1_l.

End Sidebar
 cpy_l (g_l, v3_l);
 if (EQONE_L (g_l))
   {
     cpy_l (i_l, v1_l);
   }
 else
   {
     SETZERO_L (i_l);
   }
}

[1]In this case [image] is in fact a field, since both ( [image] , +) and ( [image] , ·) = ( [image] \ {0}, ·) are abelian groups (see [Nive], Section 2.11). Finite fields have application, for example, to coding theory, and they play an important role in modern cryptography.

[2]The little Fermat theorem states that for a prime number p and for any integer a one has ap ≡ a mod p. If p is not a divisor of a, then ap−1 ≡ 1 mod p (see [Bund], Chapter 2, §3.3). The little theorem of Fermat and its generalization by Euler are among the most important theorems of number theory.


10.3 Roots and Logarithms

In this section we shall develop functions for calculating the integer part of square roots and logarithms to base 2 of CLINT objects. To this end we first consider the latter of these two functions, since we will need it for the first of them: For a natural number a we are seeking a number e for which 2e ≤ a < 2e+1. The number e = ⌊log2 a⌋ is the integer part of the logarithm of a to the base 2 and is easily obtained from the number of relevant bits of a, as determined from the following function ld_l(), reduced by 1. The function ld_l(), which is used in many other functions of the FLINT/C package, disregards leading zeros and counts only the relevant binary digits of a CLINT object.

Start Sidebar
Function: | number of relevant binary digits of a CLINT object
Syntax: | unsigned int ld_l (CLINT a_l);
Input: | a_l (operand)
Return: | number of relevant binary digits of a_l.
End Sidebar
unsigned int
ld_l (CLINT n_l)
  {
    unsigned int l;
    USHORT test;
Start Sidebar

Step 1: Determine the number of relevant digits to the base B.

End Sidebar
  l = (unsigned int) DIGITS_L (n_l);
  while (n_l[l] == 0 && l > 0)
    {
      --l;
    }
  if (l == 0)
    {
      return 0;
    }
Start Sidebar

Step 2: Determine the number of relevant bits of the most-significant digit. The macro BASEDIV2 defines the value of a digit that has a 1 in the most-significant bit and otherwise contains 0 (that is, 2BITPERDGT−1).

End Sidebar
  test = n_l[l];
  l <<= LDBITPERDGT;
  while ((test & BASEDIV2) == 0)
    {
      test <<= 1;
      --l;
    }
  return l;
}

We then calculate the integer part of the square root of a natural number based on the classical method of Newton (also known as the Newton-Raphson method), which is used for determining the zeros of a function by successive approximation: We assume that a function f (x) is twice continuously differentiable on an interval [a, b], that the first derivative f′(x) is positive on [a, b], and that we have

Then if xn Î [a, b] is an approximation for a number r with f(r) = 0, then xn+1 := xn − f (xn) / f′ (xn) is a better approximation of r. The sequence defined in this way converges to the zero r of f (cf. [Endl], Section 7.3).

If we set f (x) := x2 − c with c > 0, then f(x) for x > 0 satisfies the above conditions for the convergence of the Newton method, and with

Click To expand

we obtain a sequence that converges to [image] . Due to its favorable convergence behavior Newton's method is an efficient procedure for approximating square roots of rational numbers.

Since for our purposes we are interested in only the integer part r of [image] , for which r2 ≤ c < (r + 1)2 holds, where c itself is assumed to be a natural number, we can limit ourselves to computing the integer parts of the elements of the sequence of approximations. We begin with a number x1 > [image] and continue until we obtain a value greater than or equal to its predecessor, at which point the predecessor is the desired value. It is naturally a good idea to begin with a number that is as close to [image] as possible. For a CLINT object with value c and e := ⌊log2c⌋ we have that ⌊2(e+2)/2⌋ is always greater than [image] , and furthermore, we can easily calculate it with the function ld_l(). The algorithm goes as follows.

Algorithm for determining the integer part r of the square root of a natural number c > 0

  1. Set x ← ⌊2(e+2)/2⌋ with e := ⌊log2 c⌋.

  2. Set y ← ⌊(x + c/x) /2⌋.

  3. If y < x, set x ← y and go to step 2. Otherwise, output x and terminate the algorithm.

The proof of the correctness of this algorithm is not particularly difficult. The value of x decreases monotonically, and it is an integer and always positive, so that the algorithm certainly terminates. When this occurs, the condition y = ⌊(x + c/x) /2⌋ ≥ x holds, and we assume that x ≥ r + 1. From x ≤ r + 1 > [image] it follows that x2 > c, or c − x2 < 0.

However,

Click To expand

in contradiction to the condition for terminating the process. Our assertion is therefore false, and we must have x = r. The following function uses integer division with remainder for the operation y ← ⌊(x + c/x) /2⌋ without putting the validity of the procedure at risk.

Start Sidebar
Function: | integer part of the square root of a CLINT object
Syntax: | void iroot_l(CLINT n_l, CLINT floor_l);
Input: | n_l (operand > 0)
Output: | floor_l (integer square root of n_l)
End Sidebar
void
iroot_l (CLINT n_l, CLINT floor_l)
{
  CLINT x_l, y_l, r_l;
  unsigned l;
Start Sidebar

With the function ld_l() and a shift operation l is set to the value ⌊(⌊log2 (n_l)⌋ + 2) /2⌋, and y_l is set to 2l with the help of setbit_l().

End Sidebar
  l = (ld_l (n_l) + 1) >> 1;
  SETZERO_L (y_l);
  setbit_l (y_l, l);
  do
    {
      cpy_l (x_l, y_l);
Start Sidebar

Steps 2 and 3. Newton approximation and checking for termination.

End Sidebar
      div_l (n_l, x_l, y_l, r_l);
      add_l (y_l, x_l, y_l);
      shr_l (y_l);
    }
  while (LT_L (y_l, x_l));
  cpy_l (floor_l, x_l);
}

To determine whether a number n is the square of another number it suffices to square with sqr_l() the value output by iroot_l() and compare the result to n. If the values are unequal, then n is apparently not a square. One must, however, admit that this is not the most efficient method. There are criteria that in many cases can recognize such numbers that are not squares without the explicit calculation of root and square. Such an algorithm is given in [Cohe]. It uses four tables, q11, q63, q64, and q65, in which the quadratic residues modulo 11, 63, 64, and 65 are labeled with a "1" and the quadratic nonresidues with a "0":

Click To expand

From the representation of the residue class ring as the absolute smallest residue system (cf. page 68) one sees that we obtain all squares in this way.

Algorithm for identifying an integer n > 0 as a square. In this case the square root of n is output (from [Cohe], Algorithm 1.7.3)

  1. Set t ← n mod 64. If q64[t] = 0, then n is not a square and we are done. Otherwise, set r ← n mod (11 · 63 · 65).

  2. If q63[r mod 63] = 0, then n is not a square, and we are done.

  3. If q65[r mod 65] = 0, then n is not a square, and we are done.

  4. If q11[r mod 11] = 0, then n is not a square, and we are done.

  5. Compute q ← ⌊ [image] ⌋ using the function iroot_l(). If q2 ≠ n, then n is not a square and we are done. Otherwise, n is a square, and the square root q is output.

This algorithm appears rather strange due to the particular constants that appear. But this can be explained: A square n has the property for any integer k that if it is a square in the integers, then it is a square modulo k. We have used the contrapositive: If n is not a square modulo k, then it is not a square in the integers. By applying steps 1 through 4 above we are checking whether n is a square modulo 64, 63, 65, or 11. There are 12 squares modulo 64, 16 squares modulo 63, 21 squares modulo 65, and 6 squares modulo 11, so that the probability that we are in the case that a number that is not a square has not been identified by these four steps is

Click To expand

It is only for these relatively rare cases that the test in step 5 is carried out. If this test is positive, then n is revealed to be a square, and the square root of n is determined. The order of the tests in steps 1 through 4 is determined by the individual probabilities. We have anticipated the following function in Section 6.5 to exclude squares as candidates for primitive roots modulo p.

Start Sidebar
Function: | determining whether a CLINT number n_l is a square
Syntax: | unsigned int issqr_l(CLINT n_l, CLINT r_l);
Input: | n_l (operand)
Output: | r_l (square root of n_l, or 0 if n_l is not a square)
Return: | 1 if n_l is a square 0 otherwise
End Sidebar
static const UCHAR q11[11]=
  {1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0};

static const UCHAR q63[63]=
  {1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1,
    0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0,
    1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0};

static const UCHAR q64[64]=
  {1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
    0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
    0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0};

static const UCHAR q65[65]=
  {1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0,
    0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1};

unsigned int
issqr_l (CLINT n_l, CLINT r_l)
{
  CLINT q_l;
  USHORT r;

  if (EQZ_L (n_l))
    {
      SETZERO_L (r_l);
      return 1;
    }

  if (1 == q64[*LSDPTR_L (n_l) & 63])    /* q64[n_l mod 64]*/
    {
      r = umod_l (n_l, 45045);    /* n_l mod (11·63·65) */
      if ((1 == q63[r % 63]) && (1 == q65[r % 65]) && (1 == q11[r % 11]))
  /* Evaluation takes place from left to right; cf. [Harb], Section 7.7 */

         {
           iroot_l (n_l, r_l);
           sqr_l (r_l, q_l);
           if (equ_l (n_l, q_l))
              {
                return 1;
           }
         }
    }

  SETZERO_L (r_l);
  return 0;
}


10.4.2 Square Roots Modulo pk

 Team-Fly

10.4 Square Roots in Residue Class Rings

Now that we have calculated square roots of whole numbers, or their integer parts, we turn our attention once again to residue classes, where we shall do the same thing, that is, calculate square roots. Under certain assumptions there exist square roots in residue class rings, though in general they are not uniquely determined (that is, an element may have more than one square root). Put algebraically, the question is to determine whether for an element ā Î [image] there exist roots [image] for which [image] = ā. In number-theoretic notation (see Chapter 5) this would appear in congruence notation, and we would ask whether the quadratic congruence

has any solutions, and if it does, what they are.

If gcd(a, m) = 1 and there exists a solution b with b2 ≡ a mod m, then a is called a quadratic residue modulo m. If there is no solution to the congruence, then a is called a quadratic nonresidue modulo m. If b is a solution to the congruence, then so is b + m, and therefore we can restrict our attention to those residues that differ modulo m.

Let us clarify the situation with the help of an example: 2 is a quadratic residue modulo 7, since 32 ≡ 9 ≡ 2 (mod 7), while 3 is a quadratic nonresidue modulo 5.

In the case that m is a prime number, then the determination of square roots modulo m is easy, and in the next chapter we shall present the functions required for this purpose. However, the calculation of square roots modulo a composite number depends on whether the prime factorization of m is known. If it is not, then the determination of square roots for large m is a mathematically difficult problem in the complexity class NP (see page 100), and it is this level of complexity that ensures the security of modern cryptographic systems.[3] We shall return to more elucidative examples in Section 10.4.4

The determination of whether a number has the property of being a quadratic residue and the calculation of square roots are two different computational problems each with its own particular algorithm, and in the following sections we will provide explanations and implementations. We first consider procedures for determining whether a number is a quadratic residue modulo a given number. Then we shall calculate square roots modulo prime numbers, and in a later section we shall give approaches to calculating square roots of composite numbers.

10.4.1 The Jacobi Symbol

We plunge right into this section with a definition: Let p ≠ 2 be a prime number and a an integer. The Legendre symbol [image] (say "a over p") is defined to be 1 if a is a quadratic residue modulo p and to be −1 if a is a quadratic nonresidue modulo p. If p is a divisor of a, then [image] := 0. As a definition the Legendre then symbol does not seem to help us much, since in order to know its value we have to know whether a is a quadratic residue modulo p. However, the Legendre symbol has properties that will allow us to do calculations with it and above all to determine its value. Without going too far afield we cannot go into the theoretical background. For that the reader is referred to, for example, [Bund], Section 3.2. Nonetheless, we would like to cite some of these properties to give the reader an idea of the basis for calculation with the Legendre symbol:

  1. The number of solutions of the congruence x2 ≡ a (mod p) is 1 + [image] .

  2. There are as many quadratic residues as nonresidues modulo p, namely (p − 1)/2.

  3. a ≡ b (mod p) ⇒ [image] = [image] .

  4. The Legendre symbol is multiplicative: .

  5. = 0

  6. a(p−1)/2 ≡ [image] (mod p) (Euler criterion).

  7. For an odd prime q, q ≠ p, we have [image] = (−1)(p−1)(q−1)/4 [image] (law of quadratic reciprocity of Gauss).

  8. = (−1)(p−1)/2, [image] , [image] = 1.

The proofs of these properties of the Legendre symbol can be found in the standard literature on number theory, for example [Bund] or [Rose].

Two ideas for calculating the Legendre symbol come at once to mind: We can use the Euler criterion (vi) and compute a(p−1)/2 (mod p). This requires a modular exponentiation (an operation of complexity O (log3 p)). Using the reciprocity law, however, we can employ the following recursive procedure, which is based on properties (iii), (iv), (vii), and (viii).

Recursive algorithm for calculating the Legendre symbol [image] of an integer a and an odd prime p

  1. If a = 1, then [image] = 1 (property (viii)).

  2. If a is even, then [image] (properties (iv), (viii)).

  3. If a ≠ 1 and a = q1 ... qk is the product of odd primes q1,..., qk, then

    For each i we compute

    Click To expand

    by means of steps 1 through 3 (properties (iii), (iv), and (vii)).

Before we examine the programming techniques required for computing the Legendre symbol we consider a generalization that can be carried out without the prime decomposition, such as would be required by the direct application of the reciprocity law in the version above (vii), which for large numbers takes an enormous amount of time (for the factoring problem see page 199). At that point we will be able to fall back on a nonrecursive procedure: For an integer a and an integer b = p1 = p2...pk with not necessarily distinct prime factors pi the Jacobi symbol (or Jacobi-Kronecker, Kronecker-Jacobi, or Kronecker symbol) [image] is defined as the product of the Legendre symbols [image] :

where

For the sake of completeness we set [image] := 1 for a Î [image] , [image] := 1 if a = ±1, and [image] := 0 otherwise.

If b is itself an odd prime (that is, k = 1), then the values of the Jacobi and Legendre symbols are the same. In this case the Jacobi (Legendre) symbol specifies whether a is a quadratic residue modulo b, that is, whether there is a number c with c2 ≡ a mod b, in which case [image] = 1. Otherwise, [image] = −1 (or [image] = 0 if a ≡ 0 mod b). If b is not a prime (that is, k > 1), then we have that a is a quadratic residue modulo b if and only if gcd(a, b) = 1 and a is a quadratic residue modulo all primes that divide b, that is, if all Legendre symbols [image] , i = 1,..., k, have the value 1. This is clearly not equivalent to the Jacobi symbol [image] having the value 1: Since x2 ≡ 2 mod 3 has no solution, we have [image] = −1. However, by definition, [image] = [image] = 1, although for x2 ≡ 2 mod 9 there is likewise no solution. On the other hand, if [image] = −1, then a is in every case a quadratic nonresidue modulo b. The relation [image] = 0 is equivalent to gcd(a, b) ≠ 1.

From the properties of the Legendre symbol we can conclude the following about the Jacobi symbol:

  1. = [image] , and if b · c ≠ 0, then [image] = [image] .

  2. a ≡ c mod b ⇒ [image] = [image] .

  3. For odd b > 0 we have [image] = (−1)(b−1)/2, [image] = (−1)(b2−1)/8, and [image] = 1 (see (viii) above).

  4. For odd a and b with b > 0 we have the reciprocity law (see (viii) above) [image] = (−1)(a−1)(b−1)/4 [image] .

From these properties (see the above references for the proofs) of the Jacobi symbol we have the following algorithm of Kronecker, taken from [Cohe], Section 1.4, that calculates the Jacobi symbol (or, depending on the conditions, the Legendre symbol) of two integers in a nonrecursive way. The algorithm deals with a possible sign of b, and for this we set [image] := 1 for a ≥ 0 and [image] := −1 for a < 0.

Algorithm for calculating the Jacobi symbol [image] of integers a and b

  1. If b = 0, output 1 if the absolute value |a| of a is equal to 1; otherwise, output 0 and terminate the algorithm.

  2. If a and b are both even, output 0 and terminate the algorithm. Otherwise, set v ← 0, and as long as b is even, set v ← v + 1 and b ← b/2. If now v is even, set k ← 1; otherwise, set k ← [image] . If b < 0, then set b ← −b. If a < 0, set k ← −k (cf. (iii)).

  3. If a = 0, output 0 if b > 1, otherwise k, and terminate the algorithm. Otherwise, set v ← 0, and as long as a is even, set v ← v + 1 and a ← a/2. If now v is odd, set k ← [image] · k (cf. (iii)).

  4. Set k ← (−1)(a−1)(b−1)/4 · k, r ← |a|, a ← b mod r, b ← r, and go to step 3 (cf. (ii) and (iv)).

The run time of this procedure is O (log2 N), where N ≥ a, b represents an upper bound for a and b. This is a significant improvement over what we achieved with the Euler criterion. The following tips for the implementation of the algorithm are given in Section 1.4 of [Cohe].

  • The values [image] and [image] in steps 2 and 3 are best computed with the aid of a prepared table.

  • The value (−1)(a−1)(b−1)/4 · k in step 4 can be efficiently determined with the C expression if(a&b&2) k = −k, where & is bitwise AND.

In both cases the explicit computation of a power can be avoided, which of course has a positive effect on the total run time.

We would like to clarify the first tip with the help of the following considerations: If k in step 2 is set to the value [image] , then a is odd. The same holds for b in step 3. For odd a we have

or

so that 8 is a divisor of (a − 1)(a + 1) = a2 − 1. Thus [image] is an integer. Furthermore, we have [image] (this can be seen by placing the representation a = k · 8 + r in the exponent). The exponent must therefore be determined only for the four values a mod 8 = ±1 and ±3, for which the results are 1, −1, −1, and 1. These are placed in a vector {0, 1, 0, −1, 0, −1, 0, 1}, so that by knowing a mod 8 one can access the value of [image] . Observing that a mod 8 can be represented by the expression a & 7, where again & is binary AND, then the calculation of the power is reduced to a few fast CPU operations. For an understanding of the second tip we note that (a & b & 2) ≠ 0 if and only if (a − 1)/2 and (b − 1)/2, and hence (a − 1)(b − 1)/4 , are odd.

Finally, we use the auxiliary function twofact_l(), which we briefly introduce here, for determining v and b in step 2, for the case that b is even, as well as in the analogous case for the values v and a in step 3. The function twofact_l() decomposes a CLINT value into a product consisting of a power of two and an odd factor.

Start Sidebar
Function: | decompose a CLINT object a = 2ku with odd u
Syntax: | int twofact_l (CLINT a_l, CLINT b_l);
Input: | a_l (operand)
Output: | b_l (odd part of a_l)
Return: | k (logarithm to base 2 of the two-part of a_l)
End Sidebar
int
twofact_l (CLINT a_l, CLINT b_l)
{
  int k = 0;

  if (EQZ_L (a_l))
    {
      SETZERO_L (b_l);
      return 0;
    }
  cpy_l (b_l, a_l);
  while (ISEVEN_L (b_l))
    {
      shr_l (b_l);
      ++k;
    }
  return k;
}

Thus equipped we can now create an efficient function jacobi_l() for calculating the Jacobi symbol.

Start Sidebar
Function: | calculate the Jacobi symbol of two CLINT objects
Syntax: | int jacobi_l (CLINT aa_l, CLINT bb_l);
Input: | aa_l, bb_l (operands)
Return: | ±1 (value of the Jacobi symbol of aa_l over bb_l)
End Sidebar
static int tab2[] = 0, 1, 0, −1, 0, −1, 0, 1;

int
jacobi_l (CLINT aa_l, CLINT bb_l)
{
  CLINT a_l, b_l, tmp_l;
  long int k, v;
Start Sidebar

Step 1: The case bb_l = 0.

End Sidebar
  if (EQZ_L (bb_l))
    {
      if (equ_l (aa_l, one_l))
         {
           return 1;
         }
      else
         {
           return 0;
         }
    }
Start Sidebar

Step 2: Remove the even part of bb_l.

End Sidebar
  if (ISEVEN_L (aa_l) && ISEVEN_L (bb_l))
    {
      return 0;
    }
  cpy_l (a_l, aa_l);
  cpy_l (b_l, bb_l);
  v = twofact_l (b_l, b_l);
  if ((v & 1) == 0)    /* v even? */
    {
      k = 1;
    }
  else
    {
      k = tab2[*LSDPTR_L (a_l) & 7];  /* *LSDPTR_L (a_l) & 7 == a_l % 8 */
    }
Start Sidebar

Step 3: If a_l = 0, then we are done. Otherwise, the even part of a_l is removed.

End Sidebar
  while (GTZ_L (a_l))
    {
      v = twofact_l (a_l, a_l);
      if ((v & 1) != 0)
         {
           k = tab2[*LSDPTR_L (b_l) & 7];
         }
Start Sidebar

Step 4: Application of the quadratic reciprocity law.

End Sidebar
      if (*LSDPTR_L (a_l) & *LSDPTR_L (b_l) & 2)
         {
           k = -k;
         }
      cpy_l (tmp_l, a_l);
      mod_l (b_l, tmp_l, a_l);
      cpy_l (b_l, tmp_l);
    }
  if (GT_L (b_l, one_l))
    {
      k = 0;
    }
  return (int) k;
}

10.4.2 Square Roots Modulo pk

We now have an idea of the property possessed by an integer of being or not being a quadratic residue modulo another integer, and we also have at our disposal an efficient program to determine which case holds. But even if we know whether an integer a is a quadratic residue modulo an integer n, we still cannot compute the square root of a, especially not in the case where n is large. Since we are modest, we will first attempt this feat for those n that are prime. Our task, then, is to solve the quadratic congruence

(10.11)

where we assume that p is an odd prime and a is a quadratic residue modulo p, which guarantees that the congruence has a solution. We shall distinguish the two cases p ≡ 3 mod 4 and p ≡ 1 mod 4. In the former, simpler, case, x := a(p+1)/4 mod p solves the congruence, since

(10.12) Click To expand

where for a(p−1)/2 ≡ [image] ≡ 1 mod p we have used property (vi) of the Legendre symbol, the Euler criterion, cited above.

The following considerations, taken from [Heid], lead to a general procedure for solving quadratic congruences, and in particular for solving congruences of the second case, p ≡ 1 mod 4: We write p − 1 = 2kq, with k ≥ 1 and q odd, and we look for an arbitrary quadratic nonresidue n mod p by choosing a random number n with 1 ≤ n < p and calculating the Legendre symbol [image] . This has value −1 with probability [image] , so that we should find such an n relatively quickly. We set

(10.13)

Since by Fermat's little theorem we have a(p−1)/2 ≡ x2(p−1)/2 ≡ xp−1 ≡ 1 mod p for a and for a solution x of (10.11), and since additionally, for quadratic nonresidues n we have n(p−1)/2 ≡ −1 mod p (cf. (vi), page 218), we have

(10.14)

If z 0 ≡ 1 mod p, then x 0 is already a solution of the congruence (10.11). Otherwise, we recursively define numbers xi, yi, zi, ri such that

(10.15)

and ri > ri−1. After at most k steps we must have zi ≡ 1 mod p and that xi is a solution to (10.11). To this end we choose m 0 as the smallest natural number such that [image] ≡ 1 mod p, whereby m 0 ≤ r 0 − 1. We set

(10.16)

with ri+1 := mi := min { m ≥ 1 | [image] ≡ 1 mod p }. Then

(10.17) Click To expand

since [image] ≡ [image] ≡ 1 mod p, and therefore by the minimality of mi only [image] ≡ −1 mod p is possible.

We have thus proved a solution procedure for quadratic congruence, on which the following algorithm of D. Shanks is based (presented here as in [Cohe], Algorithm 1.5.1).

Algorithm for calculating square roots of an integer a modulo an odd prime p

  1. Write p − 1 = 2kq, q odd. Choose random numbers n until [image] = −1.

  2. Set x ← a(q−1)/2 mod p, y ← nq mod p, z ← a · x2 mod p, x ← a · x mod p, and r ← k.

  3. If z ≡ 1 mod p, output x and terminate the algorithm. Otherwise, find the smallest m for which [image] ≡ 1 mod p. If m = r, then output the information that a is not a quadratic residue p and terminate the algorithm.

  4. Set t ← [image] mod p, y ← t2 mod p, r ← m mod p, x ← x · t mod p, z ← z · y mod p, and go to step 3.

It is clear that if x is a solution of the quadratic congruence, then so is −x mod p, since (−x)2 ≡ x2 mod p.

Out of practical considerations in the following implementation of the search for a quadratic nonresidue modulo p we shall begin with 2 and run through all the natural numbers testing the Legendre symbol in the hope of finding a nonresidue in polynomial time. In fact, this hope would be a certainty if we knew that the still unproved extended Riemann hypothesis were true (see, for example, [Bund], Section 7.3, Theorem 12, or [Kobl], Section 5.1, or [Kran], Section 2.10). To the extent that we doubt the truth of the extended Riemann hypothesis the algorithm of Shanks is probabilistic.

For the practical application in constructing the following function proot_l() we ignore these considerations and simply expect that the calculational time is polynomial. For further details see [Cohe], pages 33 f.

Start Sidebar
Function: | compute the square root of a modulo p
Syntax: | int proot_l (CLINT a_l, CLINT p_l, CLINT x_l);
Input: | a_l, p_l (operands, p_l > 2 a prime)
Output: | x_l (square root of a_l modulo p_l)
Return: | 0 if a_l is a quadratic residue modulo p_l −1 otherwise
End Sidebar
int
proot_l (CLINT a_l, CLINT p_l, CLINT x_l)
{
  CLINT b_l, q_l, t_l, y_l, z_l;
  int r, m;

  if (EQZ_L (p_l) || ISEVEN_L (p_l))
    {
      return −1;
    }
Start Sidebar

If a_l == 0, the result is 0.

End Sidebar
  if (EQZ_L (a_l))
    {
      SETZERO_L (x_l);
      return 0;
    }
Start Sidebar

Step 1: Find a quadratic nonresidue.

End Sidebar
  cpy_l (q_l, p_l);
  dec_l (q_l);
  r = twofact_l (q_l, q_l);
  cpy_l (z_l, two_l);
  while (jacobi_l (z_l, p_l) == 1)
    {
      inc_l (z_l);
    }

  mexp_l (z_l, q_l, z_l, p_l);
Start Sidebar

Step 2: Initialization of the recursion.

End Sidebar
  cpy_l (y_l, z_l);
  dec_l (q_l);
  shr_l (q_l);
  mexp_l (a_l, q_l, x_l, p_l);
  msqr_l (x_l, b_l, p_l);
  mmul_l (b_l, a_l, b_l, p_l);
  mmul_l (x_l, a_l, x_l, p_l);
Start Sidebar

Step 3: End of the procedure; otherwise, find the smallest m such that [image] ≡ 1 mod p.

End Sidebar
  mod _l (b_l, p_l, q_l);
  while (!equ_l (q_l, one_l))
    {
      m = 0;
      do
         {
           ++m;
           msqr_l (q_l, q_l, p_l);
         }
      while (!equ_l (q_l, one_l));

      if (m == r)
         {
           return −1;
         }
Start Sidebar

Step 4: Recursion step for x, y, z, and r.

End Sidebar
      mexp2_l (y_l, (ULONG)(r − m − 1), t_l, p_l);
      msqr_l (t_l, y_l, p_l);
      mmul_l (x_l, t_l, x_l, p_l);
      mmul_l (b_l, y_l, b_l, p_l);
      cpy_l (q_l, b_l);
      r = m;
      }
    return 0;
  }

The calculation of roots modulo prime powers pk can now be accomplished on the basis of our results modulo p. To this end we first consider the congruence

(10.18)

based on the following approach: Given a solution x1 of the above congruence x2 ≡ a mod p we set x := x1 + p · x2, from which follows

Click To expand

From this we deduce that for solving (10.18) we will be helped by a solution x2 of the linear congruence

Proceeding recursively one obtains in a number of steps a solution of the congruence x2 ≡ a mod pk for any k Î [image] .

10.4.3 Square Roots Modulo n

The ability to calculate square roots modulo a prime power is a step in the right direction for what we really want, namely, the solution of the more general problem x2 ≡ a mod n for a composite number n. However, we should say at once that the solution of such a quadratic congruence is in general a difficult problem. In principle, it is solvable, but it requires a great deal of computation, which grows exponentially with the size of n: The solution of the congruence is as difficult (in the sense of complexity theory) as factorization of the number n. Both problems lie in the complexity class NP (cf. page 100). The calculation of square roots modulo composite numbers is therefore related to a problem for whose solution there has still not been discovered a polynomial-time algorithm. Therefore, for large n we cannot expect to find a fast solution to this general case.

Nonetheless, it is possible to piece together solutions of quadratic congruences y2 ≡ a mod r and z2 ≡ a mod s with relatively prime numbers r and s to obtain a solution of the congruence x2 ≡ a mod rs. Here we will be assisted by the Chinese remainder theorem:

  • Given congruences x ≡ ai mod mi with natural numbers m1,..., mr that are pairwise relatively prime (that is, gcd (mi, mj) = 1 for i ≠ j)and integers a1,..., ar, there exists a common solution to the set of congruences, and furthermore, such a solution is unique modulo the product m1 · m2 ··· mr.

We would like to spend a bit of time in consideration of the proof of this theorem, for it contains hidden within itself in a remarkable way the promised solution: We set m := m1 · m2 ··· mr and [image] := m/mj. Then [image] is an integer and gcd ( [image] ,mj) = 1. From Section 10.2 we know that that there exist integers uj and vj with 1 = [image] uj + mjvj, that is, [image] uj ≡ 1 mod mj, for j = 1,...,r and how to calculate them.

We then form the sum

and since [image] uj ≡ 0 mod mi for i ≠ j, we obtain

(10.19) Click To expand

and in this way have constructed a solution to the problem. For two solutions x 0 ≡ ai mod mi and x1 ≡ ai mod mi we have x 0 ≡ x1 mod mi. This is equivalent to the difference x 0 − x1 being simultaneously divisible by all mi, that is, by the least common multiple of the mi. Due to the pairwise relative primality of the mi, we have that the least common multiple is, in fact, the product of the mi, so that finally, we have that x 0 ≡ x1 mod m holds.

We now apply the Chinese remainder theorem to obtain a solution of x2 ≡ a mod rs with gcd(r, s) = 1, where r and s are distinct odd primes and neither r nor s is a divisor of a, on the assumption that we have already obtained roots of y2 ≡ a mod r and z2 ≡ a mod s. We now construct as above a common solution to the congruences

by

where 1 = ur + vs is the representation of the greatest common divisor of r and s. We thus have [image] ≡ a mod r and [image] ≡ a mod s, and since gcd(r, s) = 1, we also have [image] ≡ a mod rs, and so we have found a solution of the above quadratic congruence. Since as shown above every quadratic congruence modulo r and modulo s possesses two solutions, namely ±y and ±z, the congruence modulo rs has four solutions, obtained by substituting in ±y and ±z above:

(10.20)
(10.21) Click To expand
(10.22)
(10.23) Click To expand

We have thus found in principle a way to reduce the solution of quadratic congruences

with n odd to the case x2 ≡ a mod p for primes p. For this we determine the prime factorization n = [image] and then calculate the roots modulo the pi, which by the recursion in Section 10.4.2 can be used to obtain solutions of the congruences x2 ≡ a mod [image] . As the crowning glory of all this we then assemble these solutions with the help of the Chinese remainder theorem into a solution of x2 ≡ a mod n. The function that we give takes this path to solving a congruence x2 ≡ a mod n. However, it assumes the restricted hypothesis that n = p · q is the product of two odd primes p and q, and first calculates solutions x1 and x2 of the congruences

From x1 and x2 we assemble according to the method just discussed the solutions to the congruence

and the output is the smallest square root of a modulo pq.

Start Sidebar

Function:

calculate the square root of a modulo p · q for odd primes p, q

Syntax:

int root_l (CLINT a_l, CLINT p_l, CLINT q_l,
                CLINT x_l);

Input:

a_l, p_l, q_l (operands, primes p_l, q_l > 2)

Output:

x_l (square root of a_l modulo p_l)

Return:

0 if a_l is a quadratic residue modulo p_l * q_l −1 otherwise
End Sidebar
int
root_l (CLINT a_l, CLINT p_l, CLINT q_l, CLINT x_l)
 {
  CLINT x0_l, x1_l, x2_l, x3_l, xp_l, xq_l, n_l;
  CLINTD u_l, v_l;
  clint *xptr_l;
  int sign_u, sign_v;
Start Sidebar

Calculate the roots modulo p_l and q_l with the function proot_l(). If a_l == 0, the result is 0.

End Sidebar
if (0 != proot_l (a_l, p_l, xp_l) || 0 != proot_l (a_l, q_l, xq_l))
  {
    return −1;
  }

if (EQZ_L (a_l))
  {
    SETZERO_L (x_l);
    return 0;
  }
Start Sidebar

For the application of the Chinese remainder theorem we must take into account the signs of the factors u_l and v_l, represented by the auxiliary variables sign_u and sign_v, which assume the values calculated by the function xgcd_l(). The result of this step is the root x 0.

End Sidebar
mul_l (p_l, q_l, n_l);
xgcd_l (p_l, q_l, x0_l, u_l, &sign_u, v_l, &sign_v);
mul_l (u_l, p_l, u_l);
mul_l (u_l, xq_l, u_l);
mul_l (v_l, q_l, v_l);
mul_l (v_l, xp_l, v_l);
sign_u = sadd (u_l, sign_u, v_l, sign_v, x0_l);
smod (x0_l, sign_u, n_l, x0_l);
Start Sidebar

Now we calculate the roots x1, x2, and x3.

End Sidebar
sub_l (n_l, x0_l, x1_l);
msub_l (u_l, v_l, x2_l, n_l);
sub_l (n_l, x2_l, x3_l);
Start Sidebar

The smallest root is returned as result.

End Sidebar
xptr_l = MIN_L (x0_l, x1_l);
xptr_l = MIN_L (xptr_l, x2_l);
xptr_l = MIN_L (xptr_l, x3_l);
  cpy_l (x_l, xptr_l);
  return 0;
}

From this we can now easily deduce an implementation of the Chinese remainder theorem by taking the code sequence from the above function and extending it by the number of congruences that are to be simultaneously solved. Such a procedure is described in the following algorithm, due to Garner (see [MOV], page 612), which has an advantage with respect to the application of the Chinese remainder theorem in the above form in that reduction must take place only modulo the mi, and not modulo m = m1m2 ··· mr. This results in a significant savings in computing time.

Algorithm 1 for a simultaneous solution of a system of linear congruences

x ≡ ai mod mi, 1 ≤ i ≤ r, with gcd (mi, mj) = 1 for i ≠ j

  1. Set u ← a1, x ← u, and i ← 2.

  2. Set Ci ← 1, j ← 1.

  3. Set u ← [image] mod mi (computed by means of the extended Euclidean algorithm; see page 179) and Ci ← uCi mod mi.

  4. Set j ← j + 1; if j ≤ i − 1, go to step 3.

  5. Set u ← (ai − x)Ci mod mi, and x ← x + u [image] mj.

  6. Set i ← i + 1; if i ≤ r, go to step 2. Otherwise, output x.

It is not obvious that the algorithm does what it is supposed to, but this can be shown by an inductive argument. To this end let r = 2. In step 5 we then have

It is seen at once that x ≡ a1 mod m1. However, we also have

Click To expand

To finish the induction by passing from r to r + 1 we assume that the algorithm returns the desired result xr for some r ≥ 2, and we append a further congruence x ≡ ar+1 mod mr+1. Then by step 5 we have

Click To expand

Here we have x ≡ xr ≡ ai mod mi for i = 1,...,r according to our assumption. But we also have

Click To expand

which completes the proof.

For the application of the Chinese remainder theorem in programs one function would be particularly useful, one that is not dependent on a predetermined number of congruences, but rather allows the number of congruences to be specified at run time. This method is supported by an adaptation of the above construction procedure, which does not, alas, have the advantage that reduction need take place only modulo mi, but it does make it possible to process the parameters ai and mi of a system of congruences with i = 1, ..., r with variable r with a constant memory expenditure. Such a solution is contained in the following algorithm from [Cohe], Section 1.3.3.

Algorithm 2 for calculating a simultaneous solution of a system of linear congruences x ≡ ai mod mi, 1 ≤ i ≤ r, with gcd (mi, mj) = 1 for i ≠ j

  1. Set i ← 1, m ← m1, and x ← a1.

  2. If i = r, output x and terminate the algorithm. Otherwise, increase i ← i + 1 and calculate u and v with 1 = um + vmi using the extended Euclidean algorithm (cf. page 177).

  3. Set x ← umai + vmix, m ← mmi, x ← x mod m and go to step 2.

The algorithm immediately becomes understandable if we carry out the computational steps for three equations x = ai mod mi, i = 1, 2, 3: For i = 2 we have in step 2

and in step 3

In the next pass through the loop with i = 3 the parameters a3 and m3 are processed. In step 2 we then have

and in step 3

Click To expand

The summands u2m1m2a3and v2m3u1m1a2 disappear in forming the residue x2 modulo m1; furthermore, v2m3 ≡ v1m2 ≡ 1 mod m1 by construction, and thus x2 ≡ a1 mod m1 solves the first congruence. Analogous considerations lead us to see that x2 also solves the remaining congruences.

We shall implement this inductive variant of the construction principle according to the Chinese remainder theorem in the following function chinrem_l(), whose interface enables the passing of coefficients of a variable number of congruences. For this a vector with an even number of pointers to CLINT objects is passed, which in the order a1, m1, a2, m2, a3, m3,... are processed as coefficients of congruences x ≡ ai mod mi. Since the number of digits of the solution of a congruence system x ≡ ai mod mi is of order Σi log (mi), the procedure is subject to overflow in its dependence on the number of congruences and the size of the parameters. Therefore, such errors will be noted and indicated in the return value of the function.

Start Sidebar

Function:

solution of linear congruences with the

Chinese remainder theorem

Syntax:

int chinrem_l (int noofeq, clint **coeff_l,
                CLINT x_l);

Input:

noofeq (number of congruences)

coeff_l (vector of pointers to CLINT coefficients

ai, mi of congruences x ≡ ai mod mi, i = 1,...,noofeq)

Output:

x_l (solution of the system of congruences)

Return:

E_CLINT_OK if all is ok

E_CLINT_OFL if overflow

1 if noofeq is 0

2 if mi are not pairwise relatively prime
End Sidebar
int
chinrem_l (unsigned int noofeq, clint** coeff_l, CLINT x_l)
{
  clint *ai_l, *mi_l;
  CLINT g_l, u_l, v_l, m_l;
  unsigned int i;
  int sign_u, sign_v, sign_x, err, error = E_CLINT_OK;
  if (0 == noofeq)
    {
      return 1;
    }
Start Sidebar

Initialization: The coefficients of the first congruence are taken up.

End Sidebar
cpy_l (x_l, *(coeff_l++));
cpy_l (m_l, *(coeff_l++));
Start Sidebar

If there are additional congruences, that is, if no_of_eq > 1, the parameters of the remaining congruences are processed. If one of the mi_l is not relatively prime to the previous moduli occurring in the product m_l, then the function is terminated and 2 is returned as error code.

End Sidebar
  for (i = 1; i < noofeq; i++)
    {
      ai_l = *(coeff_l++);
      mi_l = *(coeff_l++);

      xgcd_l (m_l, mi_l, g_l, u_l, &sign_u, v_l, &sign_v);

      if (!EQONE_L (g_l))
         {
           return 2;
         }
Start Sidebar

In the following an overflow error is recorded. At the end of the function the status is indicated in the return of the error code stored in error.

End Sidebar
      err = mul_l (u_l, m_l, u_l);
      if (E_CLINT_OK == error)
         {
           error = err;
         }

      err = mul_l (u_l, ai_l, u_l);
      if (E_CLINT_OK == error)
         {
           error = err;
         }

      err = mul_l (v_l, mi_l, v_l);
      if (E_CLINT_OK == error)
         {
           error = err;
         }
      err = mul_l (v_l, x_l, v_l);
      if (E_CLINT_OK == error)
         {
           error = err;
         }
Start Sidebar

We again use our auxiliary functions sadd() and smod(), which take care of the signs sign_u and sign_v (or sign_x) of the variables u_l and v_l (or x_l).

End Sidebar
      sign_x = sadd (u_l, sign_u, v_l, sign_v, x_l);
      err = mul_l (m_l, mi_l, m_l);

      if (E_CLINT_OK == error)
         {
           error = err;
         }

      smod (x_l, sign_x, m_l, x_l);
    }

  return error;
}

10.4.4 Cryptography with Quadratic Residues

We come now to the promised (on page 188) examples for the cryptographic application of quadratic residues and their roots. To this end we consider first the encryption procedure of Rabin and then the identification schema of Fiat and Shamir.[4]

The encryption procedure published in 1979 by Michael Rabin (see [Rabi]) depends on the difficulty of calculating square roots in [image] . Its most important property is the provable equivalence of this calculation to the factorization problem (see also [Kran], Section 5.6). Since for encryption the procedure requires only a squaring modulo n, it is simple to implement, as the following demonstrates.

Rabin key generation

  1. Ms. A generates two large primes p ≈ q and computes n = p · q.

  2. Ms. A publishes n as a public key and uses the pair <p, q> as private key.

With the public key nA a correspondent Mr. B can encode a message M Î [image] in the following way and send it to Ms. A.

Rabin encryption

  1. Mr. B computes C := M2 mod nA with the function msqr_l() on page 75 and sends the encrypted text C to A.

  2. To decode the message, A computes from C the four square roots Mi modulo nA, i = 1,...,4, with the aid of the function root_l() (cf. page 201), which here is modified so that not only the smallest but all four square roots are output.[5] One of these roots is the plain text M.

Ms. A now has the problem of deciding which of the four roots Mi, represents the original plain text M. If prior to encoding the message B adds some redundant material, say a repetition of the last r bits, and informs A of this, then A will have no trouble in choosing the right text, since the probability that one of the other texts will have the same identifier is very slight.

Furthermore, redundancy prevents the following attack strategy against the Rabin procedure: If an attacker X chooses at random a number R Î [image] and is able to obtain from A one of the roots Ri of X := R2 mod nA (no matter how he or she may motivate A to cooperate), then Ri ≢ ±R mod nA will hold with probability [image] .

From nA = p · q | ( [image] − R2) = (Ri − R) (Ri + R) ≠ 0, however, one would have 1 ≠ gcd (R − Ri, nA) Î { p, q }, and X would have broken the code with the factorization of nA (cf. [Bres], Section 5.2). On the other hand, if the plain text is provided with redundancy, then A can always recognize which root represents a valid plain text. Then A would at most reveal R (on the assumption that R had the right format), which for Mr. or Ms. X, however, would be useless.

The avoidance of deliberate or accidental access to the roots of a pretended cipher text is a necessary condition for the use of the procedure in the real world.

The following example of the cryptographic application of quadratic residues deals with an identification schema published in 1986 by Amos Fiat and Adi Shamir. The procedure, conceived especially for use in connection with smart cards, uses the following aid: Let I be a sequence of characters with information for identifying an individual A, let m be the product of two large primes p and q, and let f (Z, n) → [image] be a random function that maps arbitrary finite sequences of characters Z and natural numbers n in some unpredictable fashion to elements of the residue class ring [image] . The prime factors p and q of the modulus m are known to a central authority, but are otherwise kept secret. For the identity represented by I and a yet to be determined k Î [image] the central authority has now the task of producing key components as follows.

Algorithm for key generation in the Fiat-Shamir procedure

  1. Compute numbers vi = f(I, i) Î [image] for some i ≥ k Î [image] .

  2. Choose k different quadratic residues [image] from among the vi and compute the smallest square roots [image] of [image] in [image] .

  3. Store the values I and [image] securely against unauthorized access (such as in a smart card).

For generating keys [image] we can use our functions jacobi_l() and root_l(); the function f can be constructed from one of the hash functions from Chapter 16, such as RIPEMD-160. As Adi Shamir once said at a conference, "Any crazy function will do."

With the help of the information stored by the central authority on the smart card Ms. A can now authenticate herself to a communication partner Mr. B.

Protocol for authentication à la Fiat-Shamir

  1. A sends I and the numbers ij, j = 1,..., k, to B.

  2. B generates [image] = f (I, ij) Î [image] for j = 1,..., k. The following steps 3–6 are repeated for τ = 1,...,t (for a value t Î [image] yet to be determined):

  3. A chooses a random number rτ Î [image] and sends xτ = [image] to B.

  4. B sends a binary vector [image] to A.

  5. A sends numbers yτ := rτ [image] si Î [image] to B.

  6. B verifies that xτ = [image] vi.

If A truly possesses the values [image] , then in step 6

Click To expand

holds (all calculations are in [image] ), and A thereby has proved her identity to B. An attacker who wishes to assume the identity of A can with probability 2−kt guess the vectors [image] that B will send in step 4, and as a precaution in step 3 send the values xτ = [image] vi to B; for k = t = 1, for example, this would give the attacker an average hit number of [image] . Thus the values of k and t should be chosen such that an attacker has no realistic probability of success and such that furthermore—depending on the application—suitable values result for

  • the size of the secret key;

  • the set of data to be exchanged between A and B;

  • the required computer time, measured as the number of multiplications.

Such parameters are given in [Fiat] for various values of k and t with kt = 72.

All in all, the security of the procedure depends on the secure storage of the values [image] , on the choice of k and t, and on the factorization problem: Anyone who can factor the modulus m into the factors p and q can compute the secret key components [image] , and the procedure has been broken. It is a matter, then, of choosing the modulus in such a way that it is not easily factorable. In this regard the reader is again referred to Chapter 16, where we discuss the generation of RSA moduli, subject to the same requirements.

A further security property of the procedure of Fiat and Shamir is that A can repeat the process of authentication as often as she wishes without thereby giving away any information about the secret key values. Algorithms with such properties are called zero knowledge processes (see, for example, [Schn], Section 32.11).

[3]The analogy between mathematical and cryptographic complexity should be approached with caution: In [Rein] we are informed that the question of whether P ≠ NP has little relevance in cryptographic practice. A polynomial algorithm for factorization with running time O (n20) would still be insuperable for even relatively small values of n, while an exponential algorithm with running time [image] would conquer even relatively large moduli. The security of cryptographic procedures in practice is really not dependent on whether P and NP are the same, despite the fact that one often sees precisely this formulation.

[4]For the fundamental concepts necessary for an understanding of asymmetric cryptography the reader is again referred to Chapter 16.

[5]We may assume that gcd (M, nA) = 1 and that therefore there really exist four distinct roots of C. Otherwise, the sender B could factor the modulus nA of the receiver A by calculating gcd (M, nA). This, of course, is not the way a public key system should operate.


10.5 A Primality Test

Not to stretch out the suspense, the largest known Mersenne prime, M11213, and, I believe, the largest prime known at present, has 3375 digits and is therefore just about T-281 [image] .[6]

—Isaac Asimov, Adding a Dimension, 1964

26972593 − 1 is prime!!!

— http://www.utm.edu/research/primes/largest.html (May 2000)

The study of prime numbers and their properties is one of the oldest branches of number theory and one of fundamental significance for cryptography. From the seemingly harmless definition of a prime number as a natural number greater than 1 that has no divisors other than itself and 1 there arises a host of questions and problems with which mathematicians have been grappling for centuries and many of which remain unanswered and unsolved to this day. Examples of such questions are, "Are there infinitely many primes?" "How are the primes distributed among the natural numbers?" "How can one tell whether a number is prime?" "How can one identify a natural number that is not prime, that is, a number that is composite?" "How does one find all the prime factors of a composite number?"

That there are infinitely many primes was proven already by Euclid about 2300 years ago (see, for example, [Bund], page 5, and especially the amusing proof variant and the serious proof variant on pages 39 and 40). Another important fact, which up to now we have tacitly assumed, will be mentioned here explicitly: The fundamental theorem of arithmetic states that every natural number greater than 1 has a unique decomposition as a product of finitely many prime numbers, where the uniqueness is up to the order of the factors. Thus prime numbers are truly the building blocks of the natural numbers.

As long as we stick close to home in the natural numbers and do not stray among numbers that are too big for us to deal with easily we can approach a number of questions empirically and carry out concrete calculations. Take note, however, that the degree to which results are achievable depends in large measure on the capacities of available computers and the efficiency of the algorithms used.

A list of the largest numbers identified as prime, published on the Internet, demonstrates the impressive size of the most recent discoveries (see Table 10.1).

Table 10.1: The largest known primes (as of August 2000)
Prime | Digits | Discoverer |
texttttextbfYear |  |  |
26972593 − 1 | 2098960 | Hajratwala, Woltman, Kurowski, GIMPS | 1999
23021377 − 1 | 909526 | Clarkson, Woltman, Kurowski, GIMPS | 1998
22976221 − 1 | 895932 | Spence, Woltman, GIMPS | 1997
21398269 − 1 | 420921 | Armengaud, Woltman, GIMPS | 1996
21257787 − 1 | 378632 | Slowinski, Gage | 1996
4859465536 + 1 | 307140 | Scott, Gallot | 2000
2859433 − 1 | 258716 | Slowinski, Gage | 1994
2756839 − 1 | 227832 | Slowinski, Gage | 1992
16717632768 + 1 | 171153 | Yves Gallot | 2000
169719·2557557 + 1 | 167847 | Scott, Gallot | 2000

The largest known prime numbers are of the form 2p − 1. Primes that can be represented in this way are called Mersenne primes, named for Marin Mersenne (1588–1648), who discovered this particular structure of prime numbers in his search for perfect numbers. (A natural number is said to be perfect if it equals the sum of its proper divisors. Thus, for example, 496 is a perfect number, since 496 = 1 + 2 + 4 + 8 + 16 + 31 + 62 + 124 + 248.)

For every divisor t of p we have that 2t − 1 is a divisor of 2p − 1, since if p = ab, then

Click To expand

Therefore, we see that 2p − 1 can be prime only if p is itself prime. Mersenne himself announced in 1644, without being in possession of a complete proof, that for p ≤ 257 the only primes of the form 2p − 1 were those for the primes p Î {2,3,5,7,13,17,19,31,67,127,257}. With the exception of p = 67 and p = 257, for which 2p − 1 is not prime, Mersenne's conjecture has been verified, and analogous results for many additional exponents have been established as well (see [Knut], Section 4.5.4, and [Bund], Section 3.2.12).

On the basis of the discoveries thus far of Mersenne primes one may conjecture that there exist Mersenne primes for infinitely many prime numbers p. However, there is as yet no proof of this conjecture (see [Rose], Section 1.2).

An interesting overview of additional unsolved problems in the realm of prime numbers can be found in [Rose], Chapter 12.

Because of their importance in cryptographic public key algorithms prime numbers and their properties have come increasingly to public attention, and it is a pleasure to see how algorithmic number theory in regard to this and other topics has become popular as never before. The problems of identifying a number as prime and the decomposition of a number into its prime factors are the problems that have attracted the greatest interest. The cryptographic invulnerability of many public key algorithms (foremost among them the famous RSA procedure) is based on the fact that factorization is a difficult problem (in the sense of complexity theory), which at least at present is unsolvable in polynomial time.[7]

The same holds true, in a by now weakened form, for the identification of a number as prime, if one is looking for a definitive proof that a number is prime. On the other hand, there are tests that determine, up to a small degree of uncertainty, whether a number is prime; furthermore, if the test determines that the number is composite, then that determination is definitive. Such probabilistic tests are, in compensation for the element of doubt, executable in polynomial time, and the probability of a "false positive" can be brought below any given positive bound, as we shall see, by repeating the test a sufficient number of times.

A venerable, but nonetheless still useful, method of determining all primes up to a given natural number N was developed by the Greek philosopher and astronomer Eratosthenes (276-195 B.C.E.; see also [Saga]), and in his honor it is known as the sieve of Eratosthenes. Beginning with a list of all natural numbers greater than 1 and less than or equal to N, we take the first prime number, namely 2, and strike from the list all multiples of 2 greater than 2 itself. The first remaining number above the prime number just used (2 in this case) is then identified as a prime p, whose multiples p(p + 2i), i = 0,1,...,are likewise struck from the list. This process is continued until a prime number greater than [image] is found, at which point the procedure is terminated. The numbers in the list that have not been struck are the primes less than or equal to N. They have been "caught" in the sieve.

We would like to elucidate briefly why it is that the sieve of Eratosthenes works as advertised: First, an induction argument makes it immediately plain that the next unstruck number above a prime is itself prime, since otherwise, the number would have a smaller prime divisor and thus would already have been struck from the list as a multiple of this prime factor. Since only composite numbers are struck, no prime numbers can be lost in the process.

Furthermore, it suffices to strike out only multiples of primes p for which p ≤ [image] , since if T is the smallest proper divisor of N, then T ≤ [image] . Thus if a composite number n ≤ N were to remain unstruck, then this number would have a smallest prime divisor p ≤ [image] ≤ [image] , and n would have been struck as a multiple of p, in contradiction to our assumption. Now we would like to consider how this sieve might be implemented, and as preparation we shall develop a programmable algorithm, for which we take the following viewpoint: Since except for 2 there are no even prime numbers, we shall consider only odd numbers as candidates for primality. Instead of making a list of odd numbers, we form the list fi, 1 ≤ i ≤ ⌊(N − 1)/2⌋, which represents the primality property of the numbers 2i + 1. Further, we use a variable p that will contain the current value 2i + 1 of our (imagined) list of odd numbers, as well as a variable s for which the relation 2s + 1 = p2 = (2i + 1)2, that is, s = 2i2 + 2i, always holds. We may now formulate the following algorithm (cf. [Knut], Section 4.5.4, Exercise 8).

Sieve of Eratosthenes, algorithm for calculating all prime numbers less than or equal to a natural number N

  1. Set L ← ⌊(N − 1)/2⌋ and B ← ⌈ [image] /2⌉. Set fi ← 1 for 1 ≤ i ≤ L. Set i ← 1, p ← 3, and s ← 4.

  2. If fi = 0, go to step 4. Otherwise, output p as a prime and set k ← s.

  3. If k ≤ L, set fk → 0, k ← k + p, and repeat step 3.

  4. If i ≤ B, then set i ← i + 1, s ← s + 2p, p ← p + 2, and go to step 2. Otherwise, terminate the algorithm.

The algorithm leads us to the following program, which as result returns a pointer to a list of ULONG values that contains, in ascending order, all primes below the input value. The first number in the list is the number of prime numbers found.

Start Sidebar
Function: | prime number generator (sieve of Eratosthenes)
Syntax: | ULONG * genprimes (ULONG N;)
Input: | N (upper bound for the prime search)
Return: | a pointer to vector of ULONG values with primes less than or equal to N. (At position 0 the vector contains the number of primes found.) NULL, if an error with malloc().
End Sidebar
ULONG *
genprimes (ULONG N)
{
  ULONG i, k, p, s, B, L, count;
  char *f;
  ULONG *primes;
Start Sidebar

Step 1: Initialization of the variables. The auxiliary function ul_iroot() computes the integer part of the square root of a ULONG variable. For this it uses the procedure elucidated in Section 10.3. Then comes the allocation of the vector f for marking the composite numbers.

End Sidebar
  B = (1 + ul_iroot (N)) >> 1;
  L = N >> 1;
  if (((N & 1) == 0) && (N > 0))
    {
      −−L;
    }

  if ((f = (char *) malloc ((size_t) L+1)) == NULL)
    {
      return (ULONG *) NULL;
    }

  for (i = 1; i <= L; i++)
    {
      f[i] = 1;
    }
  p = 3;
  s = 4;
Start Sidebar

Steps 2, 3, and 4 constitute the actual sieve. The variable i represents the numerical value 2i + 1.

End Sidebar
  for (i = 1; i <= B; i++)
    {
      if (f[i])
         {
           for (k = s; k <= L; k += p)
              {
                f[k] = 0;
              }
           }

    s += p + p + 2;
    p += 2;
  }
Start Sidebar

Now the number of primes found is reported, and a field of ULONG variables of commensurate size is allocated.

End Sidebar
  for (count = i = 1; i <= L; i++)
    {
      count += f[i];
    }
  if ((primes = (ULONG*)malloc ((size_t)(count+1) * sizeof (ULONG))) == NULL)
    {
      return (ULONG*)NULL;
    }
Start Sidebar

The field f[] is evaluated, and all the numbers 2i + 1 marked as primes are stored in the field primes. If N ≥ 2, then the number 2 is counted as well.

End Sidebar
  for (count = i = 1; i <= L; i++)
    {
      if (f[i])
         {
           primes[++count] = (i << 1) + 1;
         }
    }

  if (N < 2)
    {
      primes[0] = 0;
    }
  else
    {
      primes[0] = count;
      primes[1] = 2;
    }

    free (f);
    return primes;
  }

To determine whether a number n is composite it is sufficient, according to what we have said above, to institute a division test that divides n by all prime numbers less than or equal to [image] . If one fails to find a divisor, then n is itself prime; the prime test divisors are given to us by the sieve of Eratosthenes. However, this method is not practicable, since the number of primes that have to be tested becomes rapidly too large. In particular, we have the prime number theorem, formulated as a conjecture by A. M. Legendre, that the number π (x) of primes p, 2 ≤ p ≤ x, approaches x/ ln x asymptotically as x goes to infinity (see, for example, [Rose], Chapter 12).[8] A few values of the number of primes less than a given x will help to make clear the size of numbers we are dealing with. Table 10.2 gives the values of both π (x), the actual number of primes less than or equal to x, and the asymptotic approximation x/ ln x. The question mark in the last cell indicates a number to be filled in by the reader. ;-)

Table 10.2: The number of primes up to various limits x
x | 102 | 104 | 108 | 1016 | 1018 | 10100
x/ ln x | 22 | 1,086 | 5,428,681 | 271,434,051,189,532 | 24,127,471,216,847,323 | 4 × 1097
π (x) | 25 | 1,229 | 5,761,455 | 279,238,341,033,925 | 24,739,954,287,740,860 | ?

The number of necessary calculations for the division test of x grows almost with the number of digits of x in the exponent. Therefore, the division test alone is not a practicable method for determining the primality of large numbers. We shall see, in fact, that the division test is an important aid in connection with other tests, but in principle we would be content to have a test that gave information about the primality of a number without revealing anything about its factorization. An improvement in the situation is offered by the little theorem of Fermat, which tells us that for a prime p and all numbers a that are not multiples of p the congruence ap−1 ≡ 1 mod p holds (see page 175).

From this fact we can derive a primality test, called the Fermat test: If for some number a we have gcd(a, n) ≠ 1 or gcd(a, n) = 1 and 1 ≢ an−1 mod n, then n is composite. An exponentiation an−1 ≡ 1 mod n requires O (log3 n) CPU operations, and experience indicates that in only a few tries a composite number will reveal its lack of primality. However, there are exceptions, and these limit the utility of the Fermat test. Therefore, we shall have to have a closer look at them.

We must face the fact that the converse of Fermat's little theorem does not hold: Not every number n with gcd(a, n) = 1 and an−1 ≡ 1 mod n for 1 ≤ a ≤ n − 1 is prime. There exist composite numbers n that pass the Fermat test as long as a and n are relatively prime. Such numbers are called Carmichael numbers, named for their discoverer, Robert Daniel Carmichael (1879–1967). The smallest of these curious objects are

Click To expand

All Carmichael numbers have in common the property that each of them possesses at least three different prime factors (see [Kobl], Chapter 5). It was only in the early 1990s that it was proven that there are infinitely many Carmichael numbers (see [Bund], Section 2.3).

The relative frequency of numbers less than n that are relatively prime to n is

(10.24)

(for the Euler φ function see page 175), so that the proportion of numbers that are not relatively prime to n is close to 0 for large n. Therefore, in most cases one must run through the Fermat test very often to determine that a Carmichael number is composite. Letting a run through the range 2 ≤ a ≤ n − 1, eventually, one encounters the smallest prime divisor of n, and it is only when a assumes this value that n is exposed as composite.

In addition to the Carmichael numbers there are further odd composite numbers n for which there exist natural numbers a with gcd(a, n) = 1 and an−1 ≡ 1 mod n. Such numbers are known as pseudoprimes to the base a. To be sure, one can make the observation that there are only a few pseudoprimes to the bases 2 and 3, or that, for example, up to 25 × 109 there are only 1770 integers that are simultaneously pseudoprimes to the bases 2, 3, 5, and 7 (see [Rose], Section 3.4), yet the sad fact remains that there is no general estimate of the number of solutions of the Fermat congruence for composite numbers. Thus the problem with the Fermat test is that the uncertainty as to whether the method of random tests will reveal a composite number as such cannot be correlated with the number of tests.

However, such a connection is offered on the basis of the Euler criterion (see Section 10.4.1): For an odd prime p and for all integers a that are not multiples of p, we have

(10.25)

where [image] ≡ ±1 mod p denotes the Legendre-Jacobi symbol. In analogy to Fermat's little theorem we obtain an exclusionary criterion by taking the contrapositive of the following statement:

  • If for a natural number n there exists an integer a with gcd(a, n) = 1 and a(n−1)/2 ≡ [image] mod n, then n cannot be a prime number.

The required computational expenditure for establishing this criterion is the same as that for the Fermat test, namely O (log3 n).

As with the Fermat test, where there is the problem of pseudoprimes, there exist integers n that for certain a satisfy the Euler criterion although they are composite. Such n are called Euler pseudoprimes to the base a. An example is n = 91 = 7·13 to the bases 9 and 10, for which we have 945 ≡ [image] ≡ 1 mod 91 and 1045 ≡ [image] ≡ −1 = mod 91.[9]

An Euler pseudoprime to a base a is always a pseudoprime to the base a (see page 218), since by squaring a(n−1)/2 ≡ [image] mod n it follows that an−1 ≡ 1 mod n.

There is, however, no counterpart to the Carmichael numbers for the Euler criterion, and based on the following observations of R. Solovay and V. Strassen we can see that the risk of a false test result for Euler pseudoprimes is favorably bounded from above.

  1. For a composite number n the number of integers a relatively prime to n for which a(n−1)/2 ≡ [image] mod n is at most [image] ;φ(n) (see [Kobl], Section 2.2, Exercise 21). From this we have the following proposition.

  2. The probability that for a composite number n and k randomly selected numbers a1,...,ak relatively prime to n one has [image] mod n, for 1 ≤ r ≤ k, is at most 2−k.

These results make it possible to implement the Euler criterion as a probabilistic primality test, where "probabilistic" means that if the test returns that result "n is not prime," then this result is definitive, but it is only with a certain probability of error that we may infer that n is in fact prime.

Algorithm: Probabilistic primality test à la Solvay-Strassen for testing a natural number n for compositeness

  1. Choose a random number a ≤ n − 1 with gcd(a, n) = 1.

  2. If has a(n−1)/2 ≡ [image] mod n, then output "n is a probable prime." Otherwise, output "n is composite."

This test requires computation time O (log3 n) for the calculation of the exponent and the Jacobi symbol. By repeated application of this test we can reduce the probability of error in step (ii). For example, for k = 60 we obtain a vanishingly small probability of error less than 2−60 ≈ 10−18, and D. Knuth has indicated that this value is less than that of a transient hardware error, caused, for example, by an alpha particle that has found its way into the CPU or memory of a computer and thereby switched the value of a bit.

We might be satisfied with this test, since we have control over the probability of error and we have efficient algorithms for all the required computations. However, there are results that lead to a more efficient algorithm. For this we would like to introduce a few considerations that will improve our understanding of the most widely used probabilistic primality tests

Let us make the hypothesis that n is prime. Then by Fermat's little theorem we have an − 1 ≡ 1 mod n for integers a that are not multiples of n. The square root of an−1 mod n can assume only the value 1 or −1, since these are the only solutions of the congruence x2 ≡ 1 mod n (see Section 10.4.1). If we also compute from an−1 mod n the successive square roots

Click To expand

one after another until (n − 1)/2t is odd, and if in the process we arrive at a residue not equal to 1, then this residue must have the value −1, for otherwise, n cannot be prime, which we have hypothesized. For the case that the first square root different from 1 has the value −1, we stick by our hypothesis that n is prime. If n is nevertheless composite, then we shall call n on the basis of this special property a strong pseudoprime to the base a. Strong pseudoprimes to a base a are always Euler pseudoprimes to the base a (see [Kobl], Chapter 5).

We assemble all of this into the following probabilistic primality test, though for the sake of efficiency we shall first compute the power Click To expand mod n with odd (n − 1)/2t, and if this is not equal to 1, we continue to square b until we obtain a value of ±1 or have reached a(n−1)/2 mod n. In the last case we must have either b = −1 or that n is composite. The idea of shortening the algorithm so that the last square does not have to be calculated has been taken from [Cohe], Section 8.2.

Probabilistic primality test à la Miller-Rabin for odd integers n > 1

  1. Determine q and t with n − 1 = 2tq, with q odd.

  2. Choose a random integer a, 1 < a < n. Set e ← 0, b ← aq mod n. If b = 1, output "n is probably prime" and terminate the algorithm.

  3. As long as we have b ≢ ±1 mod n and e < t − 1, set b ← b2 mod n and e ← e + 1. If now b ≠ n − 1, then output "n is composite." Otherwise, output "n is probably prime."

With a running time of O (log3 n) for the exponentiations, the Miller-Rabin test (MR test for short) has complexity the same order of magnitude as the Solovay-Strassen test.

The existence of strong pseudoprimes means that the Miller-Rabin primality test offers us certainty only about the compositeness of numbers. The number 91, which we trotted out above as an example of an Euler pseudoprime (to base 9) is also—again to base 9—a strong pseudoprime. Further examples of strong pseudoprimes are

Click To expand

and

Click To expand

These two numbers are the only pseudoprimes below 1013 to the prime bases 2, 3, 5, 7, and 11 (see [Rose], Section 3.4).

Fortunately, the number of bases of strong pseudoprimes is again diminished by these numbers themselves. M. Rabin has proved that for a composite number n there are fewer than n/4 bases a, 2 ≤ a ≤ n − 1, to which n is a strong pseudoprime (see [Knut], Section 4.5.4, Exercise 22, and [Kobl], Chapter 5). From this we obtain with k-fold repetition of the test with k randomly chosen bases a1,...,ak a probability smaller than 4−k that a strong pseudoprime has been falsely accepted as a prime. Therefore, for the same amount of work, the Miller-Rabin test is always superior to the Solovay-Strassen, which with k repetitions has probability of error 2−k.

In practice, the Miller-Rabin test does much better than advertised, since the actual probability of error is in most cases much smaller than that guaranteed by the theorem of Rabin (see [Schn], Section 11.5).

Before we get down to implementing the Miller-Rabin test, we look at two approaches to improving efficiency.

By beginning the Miller-Rabin test with a division sieve that divides the prime candidates by small primes, we obtain an advantage: If a factor is found in the process, then the candidate can be eliminated from consideration without the expense of a Miller-Rabin test. The question at once presents itself as to how many prime numbers would be optimal to divide by before undertaking the MR test. We give a recommendation due to A. K. Lenstra: The greatest efficiency is achieved if one divides by the 303 prime numbers less than 2000 (see [Schn], Section 11.5). The reason for this arises from the observation that the relative frequency of odd numbers that are not multiples of a prime less than n is about 1.12/ ln n. Dividing by prime numbers under 2000 eliminates about 99 percent of all composite numbers without using the MR test, which is then used only on the remaining small percentage of candidates.

Each division by a small divisor requires computation time of order only O (ln n). We make use of an efficient division routine especially for small divisors and use it in instituting the division sieve.

The division sieve is implemented in the following function sieve_l(). It, in turn, uses the primes less than 65536 stored in the field smallprimes-[NOOFSMALLPRIMES]. The primes are stored as differences, where for each prime only a byte of storage is required. The diminished access to these primes is not a serious problem, since we are using them in their natural order. The case that the candidate itself is a small prime and is contained in the prime number table must be specially indicated.

Finally, we profit from the exponentiation function for small bases (see Chapter 6) by applying the MR test with small primes 2,3,5,7,11,... < B instead of randomly selected bases. According to experience this in no way impairs the results of the test.

We now introduce the division sieve. The function uses the division routine for short divisors that we developed for the function div_l().

Start Sidebar
Function: | division sieve
Syntax: | USHORT sieve_l (CLINT a_l, unsigned no_of_smallprimes);
Input: | a_l (candidate for primality search) no_of_smallprimes (number of primes to serve as divisors, without 2)
Return: | prime factor, if one is found 1, if the candidate itself is prime 0, if no factor is found
End Sidebar
  USHORT
  sieve_l (CLINT a_l, unsigned int no_of_smallprimes)
  {
    clint *aptr_l;
    USHORT bv, rv, qv;
    ULONG rhat;
    unsigned int i = 1;
Start Sidebar

For the sake of completeness we first test whether a_1 is a multiple of 2. If, in fact, a_1 has the value 2, then 1 is returned, while if a_1 is greater than 2 and is even, then 2 is returned as a factor.

End Sidebar
  if (ISEVEN_L (a_l))
    {
      if (equ_l (a_l, two_l))
         {
           return 1;
         }
      else
         {
           return 2;
         }
    }
  bv = 2;
  do
    {
Start Sidebar

The prime numbers are computed by successive addition of the numbers stored in smallprimes[] and stored in bv. The first prime that serves as a divisor is 3. We use the code of the fast routine for division by a USHORT (see Section 4.3).

End Sidebar
    rv = 0;
    bv += smallprimes[i];
    for (aptr_l = MSDPTR_L (a_l); aptr_l >= LSDPTR_L (a_l); aptr_l−−)
       {
         qv = (USHORT)((rhat = ((((ULONG)rv) << BITPERDGT) + (ULONG)*aptr_l)) / bv);
         rv = (USHORT)(rhat − (ULONG)bv * (ULONG)qv);
       }
  }
while (rv != 0 && ++i <= no_of_smallprimes);
Start Sidebar

If an actual divisor was found (rv == 0 and bv ≠ a_l; otherwise, a_l itself is prime!), this is returned. If a_l is itself a small prime, then 1 is returned. Otherwise, 0 is returned.

End Sidebar
  if (0 == rv)
    }
       if (DIGITS_L (a_l) == 1 && *LSDPTR_L (a_l) == bv)
         }
            bv = 1;
         }
       /* else: result in bv is a prime factor of a_l */
    }
  else /* no factor of a_l was found */
    }
       bv = 0;
    }
  return bv;
}

The function sieve_l() can be used for splitting off prime factors under 65536 from CLINT objects. To enable this, the macro SFACTOR_L(n_l) is defined in flint.h, which uses the call sieve_l(n_l, NOOFSMALLPRIMES) to test divide n_l by the primes stored in smallprimes[]; SFACTOR_L() returns the same value as sieve_l(). By repeated calls to SFACTOR_L() with subsequent division by the factors found, integers below 232, that is, integers that can be represented by the standard integer types, can be completely factored. If no factor is found, then we are dealing with a prime number.

The full-blown test function prime_l() integrates the division sieve and the Miller-Rabin test. To retain maximum flexibility the function is constituted in such a way that the number of divisions in the pretest and the number of passes through the Miller-Rabin test can be passed as parameters. To simplify the situation in applications, the macro ISPRIME_L(CLINT n_l) can be used, which in turn calls the function prime_l() with preset parameters. We use the exponentiation function wmexpm_l(), which combines Montgomery reduction with the advantages that accrue from exponentiation of small bases (see Chapter 6).

Start Sidebar

Function:

probabilistic primality test à la Miller-Rabin with division sieve

Syntax:

int prime_l (CLINT n_l, unsigned no_of_smallprimes,
                unsigned iterations);

Input:

n_l (candidate for primality)

no_of_smallprimes (number of primes for the division sieve)

iterations (number of test iterations)

Return:

1 if the candidate is "probably" prime

0 if the candidate is composite or equal to 1
End Sidebar
  int
  prime_l (CLINT n_l, unsigned int no_of_smallprimes, unsigned int iterations)
  {
    CLINT d_l, x_l, q_l;
    USHORT i, j, k, p;
    int isprime = 1;

    if (EQONE_L (n_l))
      {
         return 0;
      }
Start Sidebar

Now the division test is executed. If a factor is found, then the function is terminated with 0 returned. If 1 is returned by sieve_l(), indicating that n_l is itself prime, then the function is terminated with return value 1. Otherwise, the Miller-Rabin test is carried out.

End Sidebar
    k = sieve_l (n_l, no_of_smallprimes);

    if (1 == k)
      {
         return 1;
      }

    if (1 < k)
      {
         return 0;
      }
    else
      {
Start Sidebar

Step 1. The decomposition of n − 1 as n − 1 = 2k q with odd q is carried out by the function twofact_l(). The value n − 1 is retained in d_l.

End Sidebar
        cpy_l (d_l, n_l);
        dec_l (d_l);
        k = (USHORT)twofact_l (d_l, q_l);
        p = 2;
        i = 1;
        isprime = 1;
        do
          {
Start Sidebar

Step 2. The bases p are formed from the differences stored in the field smallprimes[]. For the exponentiation we use the Montgomery function wmexpm_l, since the base is always of type USHORT and, after the pretest with the division sieve of the prime candidate n_l, always odd. If afterwards the power in x_l is equal to 1, then the next test iteration begins.

End Sidebar
          p += smallprimes[i++];
          wmexpm_l (p, q_l, x_l, n_l);
          if (!EQONE_L (x_l))
             {
               j = 0;
Start Sidebar

Step 3. Squaring, as long as x_l is different from ±1 and k − 1 iterations have not yet been executed.

End Sidebar
              while (!EQONE_L (x_l) && !equ_l (x_l, d_l) && ++j < k)
                {
                    msqr_l (x_l, x_l, n_l);
                  }

              if (!equ_l (x_l, d_l))
                {
                  isprime = 0;
                }
         }
    }
Start Sidebar

Loop over the number iterations of test iterations.

End Sidebar
    while ((--iterations > 0) && isprime);
    return isprime;
  }
}

As for the open question as to how many iterations of the Miller-Rabin test should be executed to achieve reliable results, the literature provides various suggestions: [Gord] and [Schn] recommend five iterations (for cryptographic applications), while the algorithm in [Cohe] suggests 20 iterations. Knuth [Knut] informs us that with 25 test iterations the number of errors in a set of a billion prime candidates accepted as prime would be less than 10−6, though he does not actually recommend the number 25 and instead asks the philosophical question, "Do we really need to have a rigorous proof of primality?"[10]

For the cases in which this question must be answered in the affirmative, the APRCL test, published in 1981 by its developers L. Adleman, C. Pomerance, R. Rumely, H. Cohen, and A. K. Lenstra, shows the state of affairs at that time. H. Riesel praised this test as a breakthrough, proving that fast, generally applicable, definitive primality tests were possible (see [Ries], page 131). The test determines the primality property of an integer n in time of order O ((ln n)C ln ln ln n) for a suitable constant C. Since the exponent ln ln ln n behaves like a constant for all practical purposes, it can be considered a polynomial-time procedure, and integers with several hundreds of decimal digits can have their primality or lack thereof determined definitively in times that are otherwise achieved only by probabilistic tests.[11] The algorithm, which uses analogues of Fermat's little theorem for higher algebraic structures, is theoretically complicated and difficult to implement. For further information see [Cohe], Chapter 9, or the original article cited therein, as well as the extensive explication in [Ries].

One might also ask whether one would obtain a definitive proof of primality by testing sufficiently many bases with the Miller-Rabin test. In fact, G. Miller has proved, on the assumption of the extended Riemann hypothesis (see page 196) that an odd natural number n is prime if and only if for all bases a, 1 ≤ a ≤ C · ln2 n, the Miller-Rabin test indicates the primality of n (the constant C is specified in [Kobl], Section 5.2, as 2). Used in this way the Miller-Rabin test is a deterministic polynomial-time primality test that uses about 2.5 × 105 iterations, for primes of about 512 binary digits, to produce a definitive answer. If we suppose 10−1 seconds for each iteration (this is the order of magnitude for the time required for an exponentiation on a fast PC; cf. Appendix D), then a definitive test would take about seven hours. Considering that there is an unproven hypothesis to be reckoned with as well as a long time for the calculation, this theoretical result will satisfy neither the mathematical purists nor the computational pragmatists interested in fast procedures.

However, Henri Cohen seems to be answering the above-quoted question of Knuth when he categorically asserts ([Cohe], Section 8.2), "Primality testing, however, requires rigorous mathematical proofs."

[6]T is for trillion, whereby Asimov denotes the order of magnitude 1012. Thus T-281 [image] stands for 1012·281.25 = 103375 ≈ 211211.5.

[7]For a discussion of the complexity-theoretic aspects of cryptography one might have a look at [HKW], Chapter 6, or [Schn], Sections 19.3 and 20.8, and the many further references therein. One should also read the footnote in the present book on page 188.

[8]The prime number theorem was proved independently in 1896 by Jacques Hadamard and Charles-Jacques de la Vallée Poussin (see [Bund], Section 7.3).

[9]We have 93 ≡ 1 mod 91, since 3 is the order of 9 and 6 is the order of 10 in [image] . Therefore, 945 ≡ 93·15 ≡ 1 mod 91 and 1045 ≡ 106·7+3 ≡ 103 ≡ −1 mod 91.

[10]In the article "The Generation of Random Numbers That Are Probably Prime," by P. Beau-chemin, G. Brassard, C. Crépeau, C. Goutier, and C. Pomerance, Journal of Cryptology 1, 1, 1988, it is suggested that Knuth's assertion holds only because the probability of error for most composite numbers is significantly below [image] . Otherwise, the quoted error considered by Knuth would be significantly greater than the given value of 10−6.

[11]Cohen suggests in this connection that the practicably implementable variant of the APRCL algorithm is again probabilistic, but that nonetheless a less practical, but deterministic, version exists (see [Cohe], Chapter 9).


Chapter 11: Large Random Numbers

Mathematics is full of pseudorandomness—plenty enough to supply all would-be creators for all time.

—D. R. Hofstadter, Gödel, Escher, Bach

SEQUENCES OF "RANDOM" NUMERICAL VALUES are used in many statistical procedures, in numerical mathematics, in physics, and also in number-theoretic applications to replace statistical observations or to automate the input of variable quantities. Random numbers are used:

  • to select random samples from a larger set,

  • in cryptography to generate keys and in running security protocols,

  • as initial values in procedures to generate prime numbers,

  • to test computer programs (a topic to which we shall return),

  • for fun,

as well as in many additional applications. In computer simulations of natural phenomena random numbers can be used to represent measured values, thereby representing a natural process (Monte Carlo methods). Random numbers are also useful when arbitrary randomly selected numbers are required. Before we set out in this chapter to produce some functions for the generation of large random numbers, which will be required, in particular, for cryptographic applications, we should take care of some methodological preparations.

There are many sources of random numbers, but we should be sure to differentiate between genuine random numbers, which arise as the result of random experiments, and pseudorandom numbers, which are generated algorithmically. Genuine random numbers arise from such processes as the tossing of coins or dice, spinning a (fair) roulette wheel, observing processes of radioactive decay with the aid of suitable measuring equipment, and evaluating the output of electronic components. In contrast to these, pseudorandom numbers are computed by algorithms, generated with the aid of pseudorandom number generators, which are deterministic and therefore both predictable and reproducible. Pseudorandom numbers thus do not arise randomly in the strict sense of the word. The reason that this situation can frequently be ignored is that we are in possession of algorithms that are able to produce pseudorandom numbers of "high quality," where we shall have to explain what we mean by this term.

The first thing that we establish is that in fact, it makes no sense to talk about a single number being "random," but that mathematical requirements for randomness are always satisfied by sequences of numbers. Knuth speaks of a sequence of independent random numbers with a particular distribution, in which every number is produced randomly and independently of all other numbers of the sequence, and every number assumes a value within a certain range of values with a certain probability (see [Knut], Section 3.1). We use the terms "random" and "independent" here to mean that the events leading to the selection of concrete numbers are too complex in their formation and interaction to be detected by statistical or other tests.

This ideal is theoretically unachievable by generating numbers using deterministic procedures. Yet the goal of many different algorithmic techniques is to approach this ideal as closely as possible. In parallel to this has been the development of theoretical and empirical tests to detect structural properties of sequences of pseudorandom numbers and thereby measure the quality of the algorithms for generating such sequences. We shall not go into this more deeply. The theory in this area in deep and complex. Instead, we shall refer the reader to the literature, a good overview of which can be found in [Knut], while a comprehensive presentation of particularly the theoretical evaluation of random number generators is to be found in [Nied]. Some pragmatic suggestions for testing sequences of random numbers can be found in [FIPS].

For our purposes we first select from the many possibilities at hand a proven and frequently used procedure for generating pseudorandom numbers (for the sake of brevity we shall frequently drop the "pseudo" and speak simply of random numbers, random sequences, and random number generators) and spend some time with the method of linear congruences. Beginning with an initial value X0 the elements of a sequence are generated by the linear recursion

(11.1)

This procedure was developed in 1951 by D. Lehmer, and it has enjoyed considerable popularity since that time. Despite their simplicity, linear congruences can produce excellent random sequences, where their quality, as one might expect, depends on the choice of parameters a, b, and m. In [Knut] it is shown that linear congruences with carefully chosen parameters can pass through the hoops of statistical tests with flying colors, but that on the other hand, a random selection of parameters almost always leads to a poor generator. The moral is this: Be careful in your choice of parameters!

The choice of m as a power of two has at once the advantage that forming the residue modulo m can be accomplished with a mathematical AND. An accompanying disadvantage is that the least-significant binary digits of numbers thus generated demonstrate less random behavior than the most-significant digits, and thus one must be careful in working with such numbers. In general, one must look out for poor random properties of such numbers formed from sequential values of a linear congruence generator modulo a prime divisor of the modulus m, so that the choice of m as a prime number should also be considered, since in this case individual binary digits are no worse than any others.

The choice of a and m has influence on the periodic behavior of the sequence: Since only finitely many, namely at most m distinct, sequence values can appear, the sequence begins to repeat at the latest with the generation of the (m + 1)st number. That is, the sequence is periodic. (One says also that the sequence enters a period or a cycle.) The entry point into a cycle need not be the initial value X0, but can be some later value Xμ. The numbers X0, X1, X2, ..., Xμ−1 are called the nonrecurring elements. We may thus indicate the periodic behavior of the sequence as shown in Figure 11.1

Figure 11.1: Periodic behavior of a pseudorandom sequence

Since the regular repetition of numbers in short cycles represents poor random behavior according to all reasonable criteria, we must strive to maximize the length of the cycles or indeed to find generators that possess only cycles of maximum length. We can establish criteria by which a linear congruence sequence with parameters a, b, and m possesses exactly the maximal period length. Namely, the following conditions should be fulfilled:

  1. gcd(b, m) = 1.

  2. For all primes p one has p | m ⇒ p | (a − 1).

  3. 4 | m ⇒ 4 | (a − 1).

For a proof and additional details see [Knut], Section 3.2.1.2.

As an example of parameters that fulfill these criteria let us consider the linear congruence that the ISO-C standard recommends as exemplary for the function rand():

Click To expand

where m = 2k, with k determined by 2k − 1 being the largest number representable by the type unsigned int. The number Xi+1 is not returned as the value of rand(), but rather Xi+1/216 mod (RAND_MAX + 1), so that the function rand() generates all values between 0 and RAND_MAX. The macro RAND_MAX is defined in stdio.h and should have a value of at least 32267 (see [Pla1], p. 337). Here the recommendation of Knuth to do without the least-significant binary digits in the case of power-of-two moduli has apparently been taken into account. We easily determine that the above requirements (i)–(iii) are satisfied and that therefore a sequence produced by this generator possesses the maximum period length 2k.

Whether this happens to be the case for a particular implementation of the C library, whose source code is usually unavailable,[1] can be tested under favorable circumstances with the aid of the following algorithm by R. P. Brent. The Brent algorithm determines the period length λ of a sequence that is computed by the recursion Xi+1 = F (Xi) on a set of values D using the generating function F : D → D and an initial value X0 Î D. One needs at most 2 · max {μ, λ} calculations of the function F (cf.[HKW], 4.2).

Algorithm of Brent for determining the period length λ of a sequence generated by X0, Xi+1 = F (Xi)

  1. Set y ← X0, r ← 1, and k ← 0.

  2. Set x ← y, j ← k, and r ← r + r.

  3. Set k ← k + 1 and y ← F(y); repeat this step until x = y or k ≥ r.

  4. If x ≠ y, go to step 2. Otherwise, output λ = k − j.

This process is successful only if in step 3 one actually sees the actual sequence values F (y) and not, as in the above ISO recommendation, only their most-significant parts.

This cycle test can be extended with the chi-squared test (also written " χ2 test"), which tests how well an empirically obtained probability distribution corresponds to a theoretically expected distribution. The chi-squared test computes the statistic

(11.2)

where for t distinct events Xi we designate H (Xi) the observed frequency of the event Xi, pr (Xi) the probability for the occurrence of Xi, and n the number of observations. For the case to which these distributions correspond, the statistic χ2, viewed as a random variable, has the expected value E (χ2) = t − 1. The threshold values that lead to the rejection of the test hypothesis of equality of the distributions for given error probabilities can be read from tables of the chi-squared distribution for t − 1 degrees of freedom (cf. [Bos1], Section 4.1).

The chi-squared test is employed in connection with many empirical tests to measure their results for correspondence with the theoretically calculated test distributions. The test is particularly simple to apply for sequences of uniformly distributed (which is our test hypothesis!) random numbers Xi in a range of values W = { 0, ..., w − 1 }: We assume that each of the numbers in W is taken with the same probability p = 1/w and thus expect that among n random numbers Xi each number from W appears approximately n/w times (where we assume n > w). However, this should not be exactly the case, because the probability Pk that among n random numbers Xi a specific value w Î W appears k times is given by

(11.3) Click To expand

This binomial distribution indeed has the largest values for k ≈ n/w, but the probabilities P0 = (1 − p)n and Pn = pn are not equal to zero. Under the assumption of random behavior we therefore expect to observe in the sequence of the Xi frequencies hw of individual values w Î W according to the binomial distribution. Whether this actually occurs is established by the chi-squared test in calculating

(11.4) Click To expand

The test is repeated for several random samples (partial sequences of Xi). A rough approximation to the chi-squared distribution allows us to deduce that in most cases the test result χ2 must lie in the interval [w − 2 [image] , w + 2 [image] ]. Otherwise, the given sequence would attest to a lack of randomness. Based on this, the probability of an error, namely, that an actually "good" random sequence is declared "bad" based on the result of the chi-squared test, is about two percent. Take note that the test is valid only for a sufficiently large number of samples: This number must be at least n = 5w (see [Bos2], Section 6.1), with an even larger number to be preferred.

The linear congruence generator in the ISO-C standard that we considered above passes this simple test, as do the pseudorandom number generators that we shall implement below for the FLINT/C package.

After this brief excursion into statistics we should indicate that random sequences must meet other criteria in addition to the statistical requirements, depending on the area of application: Random sequences that are to be used for cryptographic purposes may not in any way be predictable or reproducible from a few elements without the knowledge of additional, secret, information, in order that attackers be denied the possibility of reconstructing cryptographic keys or sequences of keys generated by such a sequence. A random number generator that has been well examined with respect to such properties is the BBS bit generator of L. Blum, M. Blum, and M. Shub, which is based on results of complexity theory. We shall now describe this procedure and then, further below, implement it, though without going into the theoretical details, for which see [Blum] or [HKW], Chapter 4 and Section 6.5.

We shall need two primes p, q ≡ 3 mod 4, which we multiply by a modulus n as well as by a number X that is relatively prime to n. From X0 := X2 mod n we obtain the initial value X0 for a sequence of numbers that we calculate by repeated squaring modulo n:

As our random number we take the least-significant bit of each Xi.

A random sequence constructed from binary digits thus obtained is secure in the cryptographic sense: The predictability of further binary digits from those that have already been computed is possible only if the factors p and q of the modulus are known. If these are kept secret, then the modulus n must be factored in order to predict future bits with a probability of success greater than [image] or to reconstruct unknown parts of the sequence. The security of the BBS generator rests on the same principle as that of the RSA procedure. The price of the trust that one can derive in the qualities of the BBS generator is the difficulty in calculating the random bits: For each bit one must execute a squaring modulo a large integer, which is clearly reflected in a large time requirement for generating long random sequences. In the development of shorter sequences of random numbers, such as for generating individual cryptographic keys, this is of little consequence. Here the only issue is security, though in judging the degree of security the process for obtaining initial values must also be considered. Since the BBS generator is a deterministic process, "pure chance" can be obtained only if initial values are chosen by a suitable method. One can use dates and times, system statistics such as the number of clock ticks for given processes, or measurements of external events such as the time between keyboard events or mouse clicks executed by the user, as well as other methods that are best used in combination (for further tips on obtaining initial values, see [East] and [Matt].[2]

We turn our attention now to the actual topic of this chapter and from the resources available compose two methods for generating random numbers in CLINT format. As starting point for the generation of prime numbers, for example, we would like to be able to generate large numbers with a predetermined number of binary digits. For this the most-significant bit should be set to 1, and all remaining bits should be generated randomly.

First we construct a linear congruence generator from whose sequential values we will take the digits of a CLINT random number. The parameters a = 6364136223846793005 and m = 264 for our generator are taken from the table with results of the spectral test in Knuth ([Knut], pages 102–104). The sequence Xi+1 (Xi · a + 1) mod m thus generated possesses a maximal period length λ = m as well as good statistical properties, as we conclude from the test results presented in the table. The generator is implemented in the following function rand64_l(). On each call to rand64_l() the next number in the sequence is generated and then stored in the global CLINT object SEED64, declared as static. The parameter a is stored in the global variable A64. The function returns a pointer to SEED64.

Start Sidebar
Function: | linear congruence generator with period length 264
Syntax: | clint * rand64_l (void);
Return: | pointer to SEED64 with calculated random number
End Sidebar
clint *
rand64_l (void)
{
  mul_l (SEED64, A64, SEED64);
  inc_l (SEED64);
Start Sidebar

The reduction modulo 264 proceeds simply by setting the length field of SEED64 and costs almost no computational time.

End Sidebar
  SETDIGITS_L (SEED64, MIN (DIGITS_L (SEED64), 4));
  return ((clint *)SEED64);
}

Next, we require a function for setting the initial values for rand64_l(). This function is called seed64_l(), and it accepts a CLINT object as input, from which it takes at most four of the most-significant digits as initial values in SEED64. The previous value of SEED64 is copied into the static CLINT object BUFF64, and a pointer to BUFF64 is returned.

Start Sidebar
Function: | set an initial value for rand64_l()
Syntax: | clint * seed64_l (CLINT seed_l);
Input: | seed_l (initial value)
Return: | pointer to BUFF64 with previous value of SEED64
End Sidebar
clint *
seed64_l (CLINT seed_l)
{
  int i;

  cpy_l (BUFF64, SEED64);
  for (i = 0; i <= MIN (DIGITS_L (seed_l), 4); i++)
    {
      SEED64[i] = seed_l[i];
    }
  return BUFF64;
}

The following variant of the function seed64_l() accepts initial values of type ULONG.

Start Sidebar
Function: | Set an initial value for rand64_l()
Syntax: | clint * ulseed64_l (ULONG seed);
Input: | seed (initial value)
Return: | pointer to BUFF64 with previous value of SEED64
End Sidebar
clint *
ulseed64_l (ULONG seed)
{
  cpy_l (BUFF64, SEED64);
  ul2clint_l (SEED64, seed);
  return BUFF64;
}

The next function returns random numbers of type ULONG. All numbers are generated with a call to rand64_l(), where the most-significant digits of SEED64 are used to build a number of the requested type.

Start Sidebar
Function: | generation of a random number of type unsigned long
Syntax: | unsigned long ulrand64_l (void);
Return: | random number of type unsigned long
End Sidebar
ULONG
ulrand64_l (void)
{
  ULONG val;
  USHORT l;
  rand64_l();
  l = DIGITS_L (SEED64);
  switch (l)
    {
    case 4:
    case 3:
    case 2:
      val = (ULONG)SEED64[l-1];
      val += ((ULONG)SEED64[l] << BITPERDGT);
      break;
    case 1:
      val = (ULONG)SEED64[l];
      break;
    default:
      val = 0;
    }

  return val;
}

The FLINT/C package contains the additional functions ucrand64_l(void) and usrand64_l(void), which generate random numbers of types UCHAR and USHORT, respectively. However, we shall not discuss them here. We now present the function rand_l(), which generates large random numbers of CLINT type, with the number of binary digits to be specified.

Start Sidebar
Function: | generation of a random number of type CLINT
Syntax: | void rand_l (CLINT r_l, int l);
Input: | l (number of binary digits of the number to be generated)
Output: | r_l (random number in the interval 2l−1 ≤ r_l ≤ 2l − 1)
End Sidebar
void
rand_l (CLINT r_l, int l)
{
  USHORT i, j, ls, lr;
Start Sidebar

The requested number of binary digits l is first bounded by the maximum permitted value for CLINT objects. Then the number ls of required USHORT digits and the position lr of the most-significant binary digit of the most-significant USHORT are determined.

End Sidebar
 l = MIN (l, CLINTMAXBIT);
 ls = (USHORT)l >> LDBITPERDGT;
 lr = (USHORT)l & (BITPERDGT - 1UL);
Start Sidebar

Now the digits of r_l are generated by successive calls to the function usrand64_l(). The least-significant binary digits of SEED64 are therefore not used for the construction of CLINT digits.

End Sidebar
 for (i = 1; i <= ls; i++)
   {
     r_l[i] = usrand64_l ();
   }
Start Sidebar

Now follows the precise manufacture of r_l by setting the most-significant bit in position lr − 1 of the (ls + 1)st USHORT digit to 1 and the most-significant bits to 0. If lr == 0, then the most-significant bit of the USHORT digit ls is set to 1.

End Sidebar
  if (lr > 0)
    {
      r_l[++ls] = usrand64_l ();
      j = 1U << (lr - 1); /* j <- 2 ^ (lr - 1) */
      r_l[ls] = (r_l[ls] | j) & ((j << 1) - 1);
    }
  else
    {
      r_l[ls] |= BASEDIV2;
    }
  SETDIGITS_L (r_l, ls);
}

To close this chapter we shall now implement the BBS generator. To this end we determine, with the help of the function prime_l(), primes p ≡ q ≡ 3 mod 4, both with approximately the same number of binary digits (this leads to the result that the modulus is as difficult as possible to factor, upon which the cryptographic security of the BBS generator depends; see page 328), and we form from them a modulus n = pq. Such a modulus with 2048 binary digits is contained in the FLINT/C package, though without the associated factors (these are known only to the author ;-)).

The static variables XBBS and MODBBS hold the members of the sequence Xi and the modulus n. The function randbit() uses these to calculate the next random bit.

Start Sidebar
Function: | pseudorandom number generator à la Blum-Blum-Shub
Syntax: | int randbit_l (void);
Return: | value from {0, 1}
End Sidebar
static CLINT XBBS, MODBBS;
static const char *MODBBSSTR = "81aa5c..."; /* modulus as a character string */

int
randbit_l (void)
{
  msqr_l (XBBS, XBBS, MODBBS);
Start Sidebar

Output the least-significant bit of XBBS.

End Sidebar
  return (*LSDPTR_L (XBBS) & 1);
}

The initialization of the BBS generator is accomplished with the help of the function seedBBS_l().

Start Sidebar
Function: | set initial values for randbit_l() and randBBS_l()
Syntax: | int seedBBS_l (CLINT seed_l);
Input: | seed_l (initial value)
End Sidebar
int
seedBBS_l (CLINT seed_l)
{
  CLINT gcd_l;
  str2clint_l (MODBBS, (char *)MODBBSSTR, 16);
  gcd_l (seed_l, MODBBS, gcd_l);
  if (!EQONE_L (gcd_l))
    {
      return -1;
    }

  msqr_l (seed_l, XBBS, MODBBS);
  return 0;
}

The function ulrandBBS_l() below, which also generates random numbers of type ULONG, is the analogue of the function ulrand64_l().

Start Sidebar
Function: | generation of a random number of type unsigned long
Syntax: | unsigned long ulrandBBS_l (void);
Return: | random number of type unsigned long
End Sidebar
ULONG
ulrandBBS_l (void)
{
  ULONG i, r = 0;

  for (i = 0; i < (sizeof(ULONG) << 3); i++)
    {
      r = (r << 1) + randbit_l();
    }

  return r;
}

We still lack the function randBBS_l(CLINT r_l, int l), which generates random numbers r_l with exactly l binary digits r_l in the interval 2l−1 ≤ r_l ≤ 2l − 1. Since this corresponds to a great extent to the function rand_l(), we shall omit its description. Of course, the function is contained in the FLINT/C package.

[1]The GNU-C library, of the Free Software Foundation, and the EMX-C library, by Eberhard Mattes, are excellent exceptions. The rand() function of the EMX library uses the parameters a = 69069, b = 5, and m = 232. The multiplier a = 69069 suggested by G. Marsaglia produces, together with the modulus m = 232, good statistical results and a maximal period length (see [Knut], pp. 102–104).

[2]For highly sensitive applications the generation of initial values or entire random sequences made up of truly random numbers should always be done with suitable hardware components.


Chapter 12: Strategies for Testing LINT

Overview

Don't blame the Compiler.

—David A. Spuler: C++ and C Debugging, Testing, and Code Reliability

IN THE PREVIOUS CHAPTERS WE have encountered here and there hints for testing individual functions. Without meaningful tests to ensure the quality of our package, all of our work would be for naught, for on what else are we to base our confidence in the reliability of our functions? Therefore, we are now going to give our full attention to this important topic, and to this end we ask two questions that every software developer should ask:

  1. How can we be certain that our software functions behave according to their specifications, which in our case means first of all that they are mathematically correct?

  2. How can we achieve stability and reliability in the functioning of our software?

Although these two questions are closely related, they are actually concerned with two different problem areas. A function can be mathematically incorrect, for example if the underlying algorithm has been incorrectly implemented, yet it can reliably and stably reproduce this error and consistently give the same false output for a given input. On the other hand, functions that apparently return correct results can be plagued by other sorts of errors, for example an overflow of the length of a vector or the use of incorrectly initialized variables, leading to undefined behavior that remains undetected due to favorable (or should we rather say unfavorable?) test conditions.

We thus must be concerned with both of these aspects and institute development and test methods that can provide us sufficient trust in both the correctness and reliability of our programs. There are numerous publications that discuss the significance and consequences of these wide-ranging requirements for the entire software development process and delve deeply into the issues of software quality. Considered attention to this topic has found expression not least in the international trend to institute the ISO 9000 standard in software production. In this regard one no longer speaks merely of "testing" or "quality assurance," but instead one hears talk of "quality management" or "total quality management," which in part are simply the result of effective marketing, but which nonetheless cast the issue in the proper light, namely, to consider the process of software creation in its multifaceted entirety and thereby improve it. The frequently employed expression "software engineering" cannot blind us to the fact that this process, as it relates to predictability and precision, as a rule can scarcely compete with the classical discipline of engineering.

The comparison may be characterized aptly by the following joke: A mechanical engineer, an electrical engineer, and a software engineer have decided to take an automobile trip together. They seat themselves in the car, but it refuses to start. The mechanical engineer says at once, "The problem is with the motor. The injection nozzle is clogged." "Nonsense," retorts the electrical engineer. "The electronics are to blame. The ignition system has certainly failed." Whereupon the software engineer makes the following suggestion: "Let's all get out of the car and climb back in. Perhaps then it will start."

Without pursuing the further conversations and adventures of the three intrepid engineers, let us proceed to consider some of the options that were implemented in the creation and testing of the FLINT/C package. Above all, the following references were consulted, which do not exhaust the reader with abstract considerations and guidelines but get down to concrete assistance in solving concrete problems, without in the process losing sight of the big picture.[1] Each of these books contains numerous references to further important literature on this topic:

  • [Dene] is a standard work that deals with the entire process of software development. The book contains many methodological pointers based on the practical experience of the author as well as many clear and useful examples. The theme of testing is attacked again and again in connection with the various phases of programming and system integration, where the conceptual and methodological fundamentals are discussed together with the practical point of view, all in conjunction with a thoroughly worked out example project.

  • [Harb] contains a complete description of the programming language C and the C standard library, and it gives many valuable pointers and comments on the prescriptions of the ISO standard. This is an indispensable reference work to be consulted at every turn.

  • [Hatt] goes into great detail on the creation of security-critical software systems in C. Typical experience and sources of error are demonstrated by means of concrete examples and statistics—and C certainly offers many opportunities for error. There is also comprehensive methodological advice, which if heeded would lead to increased trust in software products.

  • [Lind] is an excellent and humorous book, which reveals a deep understanding of the C programming language. Moreover, the author knows how to transmit this understanding to the reader. Many of the topics considered could be supplied the subtitle, "Did you know that ... ?" and only a very few readers could honestly—hand on heart—reply in the affirmative.

  • [Magu] deals with the design of subsystems and is therefore of particular interest to us. Here are discussed the interpretation of interfaces and the principles of dealing with functions with input parameters. The differences between risky and defensive programming are elucidated as well. The effective use of assertions (see page 151) as testing aids and for the avoidance of undefined program states is a further strong point of this book.

  • [Murp] contains a host of testing tools that can be put to use in testing programs with little effort and that yield immediate useful results. Among its other features the book offers libraries on an accompanying diskette for the implementation of assertions, testing the processing of dynamic memory objects, and reporting the degree of coverage of tests, which were also used for testing the FLINT/C functions.

  • [Spul] offers a broad view of methods and tools for testing programs in the C and C++ languages and gives numerous pointers for their effective use. The book contains an extensive overview of programming errors typical in C and C++ and discusses techniques for recognizing and eliminating them.

[1]The titles named here represent the author's personal, subjective selection. There are many other books and publications that could as well have been listed here but that have been omitted for lack of space and time.


12.1 Static Analysis

The methodological approaches to testing can be divided into two categories: static testing and dynamic testing. In the first category are to be found code inspection, whereby the source code is carefully examined and inspected line by line for such problems as deviations from specifications (in our case these are the selected algorithms), errors in reasoning, inaccuracies with respect to the arrangement of code lines or the style guide, doubtful constructions, and the presence of unnecessary code sequences.

Code inspection is supported by the use of analytic tools, such as the well-known Unix lint tools, that largely automate this laborious task. Originally, one of the main applications of lint was to compensate for earlier existing deficits in C in consistency checking of parameters that were passed to functions in separately compiled modules. Meanwhile, there have appeared more convenient products than the classical lint, products that are capable of discovering an enormous bandwidth of potential problems in program code, represented only in small part by syntax errors that definitively prevent a compiler from effecting a translation of the code. A few examples of the problem domains that can be uncovered by static analysis are as follows:

  • syntax errors,

  • missing or inconsistent function prototypes,

  • inconsistencies in the passing of parameters to functions,

  • references to or joining of incompatible types,

  • use of uninitialized variables,

  • nonportable constructs,

  • unusual or implausible use of particular language constructs,

  • unreachable code sequences,

An imperative condition for stringent type-checking by automated tools is the use of function prototypes. With the help of prototypes an ISO-conforming C compiler is capable of checking, across all modules, the types of arguments passed to functions and detecting inconsistencies. Many compilers can also be set to analyze the source code, as they recognize many problems when the appropriate warning levels are turned on. The C/C++ compiler gcc of the GNU project of the Free Software Foundation, for example, possesses above-average analysis functions, which can be activated with the options -Wall -ansi and -pedantic.[2]

For static testing in setting up the FLINT/C functions, in addition to tests being performed on a number of different compilers (see page 7), above all were employed the product PC-lint from Gimpel Software (version 7.5; see [Gimp]) and LCLint from MIT (version 2.4; see [Evan]).[3]

PC-lint has proved itself to be a very useful tool for testing both C and C++ programs. It knows about approximately two thousand separate problems and uses mechanisms that in a limited way derive from the code the values loaded into automatic variables at run time and include this in the diagnosis. In this way many problems, such as exceeding the limits of vectors, that are usually, if at all, detected only at run time (which is to say during testing, it is to be hoped, and not afterwards) can be uncovered already during static analysis.

In addition to these tools, the freely obtainable LCLint has been adapted to run under Linux. LCLint distinguishes four modes (weak, standard, check, strict), each connected with certain presets, and which carry out tests of varying degrees of rigorousness. In addition to the typical lint functions, LCLint offers possibilities to test programs for the presence of particular specifications, which are inserted as specially formatted comments in the source code. In this way boundary conditions for the implementation of functions and their invocation can be formulated and their conformity to specifications checked, and there are additional possible semantic controls.

For programs not equipped with supplementary specifications the mode set with the option -weak is recommended as standard. However, according to the manual a special reward will be presented to the first person who can produce a "real program" that produces no errors with LCLint in -strict mode. As a precondition for using these two tools in a reasonable manner it proved to be useful in testing the FLINT/C functions to test precisely which options are used and to create corresponding profile files so as to configure the tools for individual use.

After extensive revisions of the FLINT/C code, at the end of the test phase neither of the two products produced any warnings that on close examination could be considered serious. With this goes the hope that we have come a long way in fulfilling the conditions set above for the quality of the FLINT/C functions.

[2]The compiler is included in the various Linux distributions and can also be obtained from http://www.leo.org.

[3]LCLint can be downloaded from the Internet. The home page of LCLint is to be found at http://www.sds.lcs.mit.edu/lclint/. By anonymous ftp one can download LCLint for Linux and Windows 9x/NT from ftp://sds.lcs.mit.edu/pub/lclint/.


12.2 Run-Time Tests

The goal of run-time tests is to prove that a building block of a piece of software fulfills its specifications. To give the tests sufficient expressive power so as to justify the expense of time and money that goes into them, we must make of them the same demands that we do of scientific experiments: They must be completely documented, and their results must be reproducible and able to be checked by outsiders. It is useful to distinguish between testing individual modules and integrated system tests, though here the boundaries are fluid (see [Dene], Section 16.1).

To achieve this goal in testing modules the test cases must be so constructed that functions can be exhaustively tested to the extent possible, or in other words, that as large a coverage as possible be achieved of the functions being tested. To establish test coverage various metrics can be employed. For example, for C0 coverage what is measured is the portion of instructions of a function or module that are actually run through, or, concretely, which instructions are not run through. There are more powerful measurements than C0 coverage, which take note of the portion of branches that are taken (C1 coverage), or even the portion of paths of a function that are run through. The last of these is a considerably more complex measure then the first two.

In each case the goal must be to achieve as great a coverage as possible with test cases that completely check the behavior of the interface of the software. This covers two aspects that are only loosely connected one with the other: A test driver that runs through all the branches of a function can still leave errors undetected. On the other hand, one can construct cases in which all the properties of a function are tested, even though some branches of the function are not considered. The quality of a test can thus be measured in at least two dimensions.

If to achieve a high degree of test coverage it does not suffice to establish the test cases based simply on knowledge of the specification, which leads to so-called black box tests, it is necessary to take into consideration the details of the implementation in the construction of test cases, a modus operandi that leads to so-called white box tests. An example where we have created test cases for a special branch of a function based only on the specification is the division algorithm on page 51: To test step 5 the special test data on page 62 were specified, which have the effect that the associated code is executed. On the other hand, the necessity of special test data for division by smaller divisors becomes clear only when one considers that this process is passed off to a special part of the function div_l(). What is involved here is an implementation detail that cannot be derived from the algorithm.

In practice, one usually ends up with a mixture of black box and white box methods, which in [Dene] are aptly called gray box tests. However, it can never be expected that one hundred percent coverage is to be achieved, as the following considerations demonstrate: Let us assume that we are generating prime numbers with the Miller-Rabin test with a large number of iterations (50, say) and a corresponding low probability of error [image] ≈ 10−30 (cf. Section 10.5) and then testing the prime numbers that are found with a further, definitive, primality test. Since the flow of control leads to one or the other branch of the program, depending on the outcome of this second test, we have no practically relevant chance to reach the branch that is followed exclusively after a negative test outcome. However, the probability that the doubtful branch will be executed when the program is actually used is just as irrelevant, so that possibly one can more easily live with doing without this aspect of the test than to alter the code semantically in order to create the test possibility artificially. In practice, there are thus always situations to be expected that require the abandonment of the goal of one hundred percent test coverage, however that is measured.

The testing of the arithmetic functions in the FLINT/C package, which is carried out primarily from a mathematical viewpoint, is quite a challenge. How can we establish whether addition, multiplication, division, or even exponentiation of large numbers produces the correct results? Pocket calculators can generally compute only on an order of magnitude equivalent to that of the standard arithmetic functions of the C compiler, and so both of these are of limited value in testing.

To be sure, one has the option of employing as a test vehicle another arithmetic software package by creating the necessary interface and transformations of the number formats and letting the functions compete against each other. However, there are two strikes against such an approach: First, this is not sporting, and second, one must ask oneself why one should have faith in someone else's implementation, about which one knows considerably less than about one's own product. We shall therefore seek other possibilities for testing and to this end employ mathematical structures and laws that embody sufficient redundancy to be able to recognize computational errors in the software. Discovered errors can then be attacked with the aid of additional test output and modern symbolic debuggers.

We shall therefore follow selectively a black box approach, and in the rest of this chapter we shall hope to work out a serviceable test plan for the run-time tests that follows essentially the actual course of testing that was used on the FLINT/C functions. In this process we had the goal of achieving high C1 coverage, although no measurements in this regard were employed.

The list of properties of the FLINT/C functions to be tested is not especially long, but it is not without substance. In particular, we must convince ourselves of the following:

  • All calculational results are generated correctly over the entire range of definition of all functions.

  • In particular, all input values for which special code sequences are supplied within a function are correctly processed.

  • Overflow and underflow are correctly handled. That is, all arithmetic operations are carried out modulo Nmax + 1.

  • Leading zeros are accepted without influencing the result.

  • Function calls in accumulator mode with identical memory objects as arguments, such as, for example, add_l(n_l, n_l, n_l), return correct results.

  • All divisions by zero are recognized and generate the appropriate error message.

There are many individual test functions necessary for the processing of this list, functions that call the FLINT/C operations to be tested and check their results. The test functions are collected in test modules and themselves individually tested before they are set loose on the FLINT/C functions. For testing the test functions the same criteria and the same means for static analysis are employed as for the FLINT/C functions, and furthermore, the test functions should be run through at least on a spot-check basis with the help of a symbolic debugger in single-step mode in order to check whether they test the right thing. In order to determine whether the test functions truly respond properly to errors, it is helpful deliberately to build errors into the arithmetic functions that lead to false results (and then after the test phase to remove these errors without a trace!).

Since we cannot test every value in the range of definition for CLINT objects, we need, in addition to fixed preset test values, randomly generated input values that are uniformly distributed across the range of definition [0, Nmax]. To this end we use our function rand_l(r_l, bitlen), where we select the number of binary digits to be set in the variable bitlen with the help of the function usrand64_l() modulo (MAX2 + 1) randomly from the interval [0, MAX2]. The first pass at testing must be the functions for generating pseudorandom numbers, which were discussed in Chapter 11, where among other things we employ the chi-squared test described there for testing the statistical quality of the functions usrand64_l() and usrandBBS_l(). Additionally, we must convince ourselves that the functions rand_l() and randBBS_l() properly generate the CLINT number format and return numbers of precisely the predetermined length. This test is also required for all other functions that output CLINT objects. For recognizing erroneous formats of CLINT arguments we have the function vcheck_l(), which is therefore to be placed at the beginning of the sequence of tests.

A further condition for most of the tests is the possibility of determining the equality or inequality and size comparison of integers represented by CLINT objects. We must also test the functions ld_l(), equ_l(), mequ_l(), and cmp_l(). This can be accomplished with the use of both predefined and random numbers, where all cases—equality as well as inequality with the corresponding size relations—are to be tested.

The input of predefined values proceeds optimally, depending on the purpose, by means of the function str2clint_l() or as an unsigned type with the conversion function u2clint_l() or ul2clint_l(). The function xclint2str_l(), complementary to str2clint_l(), is used for the generation of test output. These functions are therefore the next to appear on our list of functions to be tested. For the testing of string functions we exploit their complementarity and check whether executing one function after the other produces the original character string or, for the other order, the output value in CLINT format. We shall return to this principle repeatedly below.

All that now remains to test are the dynamic registers and their control mechanisms from Chapter 9, which in general we would like to include in the test functions. The use of registers as dynamically allocated memory supports us in our efforts to test the FLINT/C functions, where we additionally implement a debug library for the malloc() functions for allocation of memory. A typical function in such a package, of which there are to be found both public-domain and commercial products (cf. [Spul], Chapter 11), is checking for maintenance of the bounds of dynamically allocated memory. With access to the CLINT registers we can keep close tabs on our FLINT/C functions: Every penetration of the border into foreign memory territory will be reported.

A typical mechanism that enables this redirects calls to malloc() to a special test function that receives the memory requests, in turn calls malloc(), and thereby allocates a somewhat greater amount of memory than is actually requested. The block of memory is registered in an internal data structure, and a frame of a few bytes is constructed "right" and "left" of the memory originally requested, which is filled with a redundant pattern such as alternating binary zeros and ones. Then a pointer is returned to the free memory within the frame. A call to free() now in turn goes first to the debug shell of this function. Before the allocated block is released a check is made as to whether the frame has been left unharmed or whether the pattern has been destroyed by overwriting, in which case an appropriate message is generated and the memory is stricken from the registration list. Only then is the function free() actually called. At the end of the application one can check using the internal registration list whether, or which, areas of memory were not released. The orchestrating of the code for rerouting the calls to malloc() and free() to their debug shells is accomplished with macros that are usually defined in #include files.

For the test of the FLINT/C functions the ResTrack package from [Murp] is employed. This enables the detection, in certain cases, of subtle instances of exceeding the vector bounds of CLINT variables, which otherwise might have remained undetected during testing.

We have now completed the basic preparations and consider next the functions for basic calculation (cf. Chapter 4)

    add_l(), sub_l(), mul_l(), sqr_l(), div_l(), mod_l(), inc_l(),
    dec_l(), shl_l(), shr_l(), shift_l(),

including the kernel functions

    add(), sub(), mult(), umul(), sqr(),

the mixed arithmetic functions with a USHORT argument

    uadd_l(), usub_l(), umul_l(), udiv_l(), umod_l(), mod2_l(),

and finally the functions for modular arithmetic (cf. Chapters 5 and 6)

    madd_l(), msub_l(), mmul_l(), msqr_l(),

and the exponentiation function

    *mexp*_l().

The calculational rules that we shall employ in testing these functions arise from the group laws of the integers, which have been introduced already in Chapter 5 for the residue class rings [image] . The applicable rules for the natural numbers are again collected here, where we find an opportunity for testing wherever an equal sign stands between two expressions (see Table 12.1).

Table 12.1: Group law for the integers to help in testing
 | Addition | Multiplication
Identity | a + 0 = a | a · 1 = a
Commutative law | a + b = b + a | a · b = b · a
Associative law | (a + b) + c = a + (b + c) | (a · b) · c = a · (b · c)

Addition and multiplication can be tested one against the other by making use of the definition

at least for small values of k. Further relations amenable to testing are the distributive law and the first binomial formula:

  • Distributive law :

    Click To expand

    Binomial formula :

    Click To expand

The cancellation laws for addition and multiplication provide the following test possibilities for addition and subtraction, as well as for multiplication and division:

and

Division with remainder can be tested against multiplication and addition by using the division function to compute, for a dividend a and divisor b, first the quotient q and remainder r. Then multiplication and addition are brought into play to test whether

For testing modular exponentiation against multiplication for small k we fall back on the definition:

From here we can move on to the exponentiation laws (cf. Chapter 1)

which are likewise a basis for testing exponentiation in relation to multiplication and addition.

In addition to these and other tests based on the rules of arithmetic calculation we make use of special test routines that check the remaining points of our above list, in particular the behavior of the functions on the boundaries of the intervals of definition of CLINT objects or in other special situations, which for certain functions are particularly critical. Some of these tests are contained in the FLINT/C test suite, which is included on the accompanying CD-ROM. The test suite contains the modules listed in Table 12.2.

Table 12.2: FLINT/C test functions
Module name | Content of test
testrand.c | linear congruences, pseudorandom number generator
testbbs.c | Blum-Blum-Shub pseudorandom number generator
testreg.c | register management
testbas.c | basic functions cpy_l(), ld_l(), equ_l(), mequ_l(), cmp_l(), u2clint_l(), ul2clint_l(), str2clint_l(), xclint2str_l()
testadd.c | addition, including inc_l()
testsub.c | subtraction, including dec_l()
testmul.c | multiplication
testkar.c | Karatsuba multiplication
testsqr.c | squaring
testdiv.c | division with remainder
testmadd.c | modular addition
testmsub.c | modular subtraction
testmmul.c | modular multiplication
testmsqr.c | modular squaring
testmexp.c | modular exponentiation
testset.c | bit access functions
testshft.c | shift operations
testbool.c | Boolean operations
testiroo.c | integer square root
testgcd.c | greatest common divisor and least common multiple

We shall return to the tests of our number-theoretic functions at the end of Part 2, where they are presented as exercises for the especially interested reader (see Chapter 17).


Part II: Arithmetic in C++ with the Class LINT

Chapter List

Chapter 13: Let C++ Simplify Your Life
Chapter 14: The LINT Public Interface: Members and Friends
Chapter 15: Error Handling
Chapter 16: An Application Example: The RSA Cryptosystem
Chapter 17: Do It Yourself: Text LINT
Chapter 18: Approaches for Further Extensions
Chapter 19: Rijndael: A Successor to the Data Encryption Standard

Part Overview

The use of anatomic finds as ornamentation in the construction of objects is widespread in different geographical areas and in different ethno-anthropological groups. The human find, usually the bone, becomes a functional part in the construction of objects. The bone seems to lose, at least in part, its actual anatomic identity, in that it is worked and manipulated so that it becomes an integral part of an object thus acquiring a symbolic meaning which goes beyond its bodily essence.

— Sign at an exhibit at the National Museum of Anthropology and Ethnology, Florence, Italy


Chapter 13: Let C++ Simplify Your Life

Overview

Our life is frittered away by detail.... Simplify, simplify.

—H. D. Thoreau, Walden

THE PROGRAMMING LANGUAGE C++, UNDER development since 1979 by Bjarne Stroustrup[1] at Bell Laboratories, is an extension of C that promises to dominate the field of software development. C++ supports the principles of object-oriented programming, which is based on the tenet that programs, or, better, processes, comprise a set of objects that interact exclusively through their interfaces. That is, they exchange information or accept certain external commands and process them as a task. In this the methods by which an object carries out a task are an internal affair "decided upon" autonomously by the object alone. The data structures and functions that represent the internal state of an object and effect transitions between states are the private affair of the object and should not be detectable from the outside. This principle, known as information hiding, assists software developers in concentrating on the tasks that an object has to fulfill within the framework of a program without having to worry about implementation details. (Another way of saying this is that the focus is on "what," not on "how.")

The structural designs for what goes on in the "internal affairs" of objects, containing complete information on the organization of data structures and functions, are the classes. With these the external interface of an object is established, and this is decisive for the suite of behaviors that an object can perform. Since all objects of a class reflect the same structural design, they also possess the same interface. But once they have been created (computer scientists say that classes are instantiated by objects), they lead independent lives. Their internal states are changed independently of one another and they execute different tasks corresponding to their respective roles in the program.

Object-oriented programming propagates the use of classes as the building blocks of larger structures, which can again be classes or groups of classes, into complete programs, just as houses or automobiles are constructed of prefabricated modules. In the ideal case programs can be cobbled together from libraries of preexisting classes without the necessity for the creation of a significant amount of new code, at least not on the order of magnitude as is typical in conventional program development. As a result it is easier to orient program development to reflect the actual situation, to model directly the actual processes, and thereby to achieve successive refinement until the result is a collection of objects of particular classes and their interrelations, in which the underlying real-world model can still be recognized.

Such a way of proceeding is well known to us from many aspects of our lives, for we do not generally operate directly with raw materials if we wish to build something, but we use, rather, completed modules about whose construction or inner workings we have no detailed knowledge, nor the necessity of such knowledge. By standing on the shoulders of those who built before us, it becomes possible for us to create more and more complex structures with a manageable amount of effort. In the creation of software this natural state of affairs has not previously found its true expression, as software developers turn again and again to the raw materials themselves: Programs are constructed out of atomic elements of a programming language (this constructive process is commonly called coding). The use of run-time libraries such as the C standard library does not improve this situation to any great degree, since the functions contained in such libraries are too primitive to permit a direct connection to a more complex application.

Every programmer knows that data structures and functions that provide acceptable solutions for particular problems only seldom can be used for similar but different tasks without modification. The result is a reduction in the advantage of being able to rely on fully tested and trusted components, since any alteration contains the risk of new errors—as much in the design as in programming. (One is reminded of the notification in manuals that accompany various consumer products: "Any alteration by other than an authorized service provider voids the warranty.")

In order that the goal of reusability of software in the form of prefabricated building blocks not founder on the rocks of insufficient flexibility, the concept of inheritance, among a number of other concepts, has been developed. This makes it possible to modify classes to meet new requirements without actually altering them. Instead, the necessary changes are packaged in an extension layer. The objects that thus arise take on, in addition to their new properties, all the properties of the old objects. One might say that they inherit these properties. The principle of information hiding remains intact. The chances of error are greatly reduced, and productivity is increased. It is like a dream come true.

As an object-oriented programming language C++ possesses the requisite mechanisms for the support of these principles of abstraction.[2] These, however, represent only a potential, but not a guarantee, of being used in the sense of object-oriented programming. To the contrary, the switch from conventional to object-oriented software development requires a considerable intellectual retooling. This is particularly apparent in two respects: On the one hand, the developer who has hitherto achieved good results is forced to devote considerably more attention to the modeling and design phases than what was usually required in traditional methods of software development. On the other hand, in the development and testing of new classes the greatest care is required to obtain error-free building blocks, since they will go on to be used in a great variety of future applications. Information hiding can also mean bug hiding, since it defeats the purpose of the idea of object-oriented programming if the user of a class must become familiar with its inner workings in order to find a bug. The result is that errors contained in a class implementation are inherited together with the class, so that all subclasses will be infected with the same "hereditary disease." On the other hand, the analysis of errors that occur with the objects of a class can be restricted to the implementation of the class, which can greatly reduce the scope of the search for the error.

All in all, we must say that while there is a strong trend in the direction of using C++ as a programming language, nonetheless, the principles of object-oriented programming beyond an understanding of the essentially complex language elements of C++ are multifaceted, and it will be a long time before it is used as a standard method of software development.

Thus the title of this chapter refers not to object-oriented programming and the use of C++ in general, but to the mechanisms offered therein and their significance for our project. These enable the formulation of arithmetic operations with large numbers in a way that is so natural that it is as if they belonged to the standard operations of the programming language. In the following sections, therefore, we will not be presenting an introduction to C++, but a discussion of the development of classes that represent large natural numbers and that export functions to work with these numbers as abstract methods.[3] The (few) details of the data structures will be hidden both from the user and the client of the class, as will the implementation of the numerous arithmetic and number-theoretic functions. However, before we can use the classes they must be developed, and in this regard we shall have to get our hands dirty with the internal details. Nonetheless, it will surprise no one that we are not going to begin from scratch, but rather make use of the implementation work that we accomplished in the first part of the book and formulate the arithmetic class as an abstract layer, or shell, around our C library.

We shall give the name LINT (Large INTegers) to our class. It will contain data structures and functions as components with the attribute public, which determine the possibilities for external access. Access to the structures of the class declared as private, on the other hand, can be accomplished only with functions that have been declared either a member or friend of the class. Member functions of the class LINT can access the functions and data elements of LINT objects by name and are required for servicing the external interface, for processing instructions to the class, and serving as fundamental routines and auxiliary functions for managing and processing internal data structures. Member functions of the class LINT always possess a LINT object as implied left argument, which does not appear in its parameter list. Friend functions of the class do not belong to the class, but they can nonetheless access the internal structure of the class. Unlike the member functions, the friend functions do not possess an implicit argument.

Objects are generated as instances of a class by means of constructors, which complete the allocation of memory, the initialization of data, and other management tasks before an object is ready for action. We shall require several such constructors in order to generate our LINT objects from various contexts. Complementary to the constructors we have destructors, which serve the purpose of removing objects that are no longer needed and releasing the resources that have been bound to them.

The elements of C++ that we shall particularly use for our class development are the following:

  • the overloading of operators and functions;

  • the improved possibilities, vis à vis C, for input and output.

The following sections are devoted to the application of these two principles in the framework of our LINT class. To give the reader an idea of the form that the LINT class will assume, we show a small segment of its declaration:

class LINT
{
  public:
    LINT (void); // constructor
    ~LINT (); // destructor
    const LINT& operator= (const LINT&);
    const LINT& operator+= (const LINT&);
    const LINT& operator-= (const LINT&);
    const LINT& operator*= (const LINT&);
    const LINT& operator/= (const LINT&);
    const LINT& operator
    const LINT gcd (const LINT&);
    const LINT lcm (const LINT&);
    const int jacobi (const LINT&);

    friend const LINT operator + (const LINT&, const LINT&);
    friend const LINT operator - (const LINT&, const LINT&);
    friend const LINT operator * (const LINT&, const LINT&);
    friend const LINT operator / (const LINT&, const LINT&);
    friend const LINT operator
    friend const LINT mexp (const LINT&, const LINT&, const LINT&);
    friend const LINT mexp (const USHORT, const LINT&, const LINT&);
    friend const LINT mexp (const LINT&, const USHORT, const LINT&);
    friend const LINT gcd (const LINT&, const LINT&);
    friend const LINT lcm (const LINT&, const LINT&);
    friend const int jacobi (const LINT&, const LINT&);

  private:
    clint *n_l;
    int maxlen;
    int init;
    int status;
};

One may recognize the typical subdivision into two blocks: First the public block is declared with a constructor, a destructor, arithmetic operators, and member functions as well as the friend functions of the class. A short block of private data elements is joined to the public interface, identified by the label private. It is an aid to clarity and is considered good style to place the public interface before the private block and to use the labels "public" and "private" only once each within a class declaration.

The list of operators appearing in the section of the class declaration shown here is by no means complete. It is missing some arithmetic functions that cannot be represented as operators as well as most of the number-theoretic functions, which we know already as C functions. Furthermore, the announced constructors are as little represented as the functions for input and output of LINT objects.

In the following parameter lists of the operators and functions the address operator & appears, which has the effect that objects of the class LINT are passed not by value, but by reference, that is, as pointers to the object. The same holds for the return value of LINT objects. This use of & is unknown in C. On close inspection, however, one recognizes that only certain of the member functions return a pointer to a LINT object, while most of the others return their results by value. The basic rule that determines which of these two methods is followed is this: Functions that alter one or more of the arguments passed to them can return this result as a reference, while other functions, those that do not alter their arguments, return their results by value. As we proceed we shall see which method goes with which of the LINT functions.

Classes in C++ are an extension of the complex data type struct in C, and access to an element x of a class is accomplished syntactically in the same way as access to an element of a structure, that is, by A.x, where A denotes an object and x an element of the class.

One should note that in the parameter list of a member function an argument is less completely named than in a like-named friend function, as the following example illustrates:

  friend LINT gcd (const LINT&, const LINT&);

versus

  LINT LINT::gcd (const LINT&);

Since the function gcd() as a member function of the class LINT belongs to an object A of type LINT, a call to gcd() must be in the form A.gcd(b) without A appearing in the parameter list of gcd(). In contrast, the friend function gcd() belongs to no object and thus possesses no implicit argument.

We shall fill in the above sketch of our LINT class in the following chapters and work out many of the details, so that eventually we shall have a complete implementation of the LINT class. The reader who is also interested in a general discussion of C++ is referred to the standard references [Deit], [ElSt], [Lipp], and especially [Mey1] and [Mey2].

[1]The following, from Bjarne Stroustrup's Internet home page (http://www.research.att.com/~bs/), may help to answer the question, How do you pronounce "Bjarne Stroustrup"?: "It can be difficult for non-Scandinavians. The best suggestion I have heard yet was 'start by saying it a few times in Norwegian, then stuff a potato down your throat and do it again' :-). Both of my names are pronounced with two syllables: Bjar-ne Strou-strup. Neither the B nor the J in my first name are stressed and the NE is rather weak so maybe Be-ar-neh or By-ar-ne would give an idea. The first U in my second name really should have been a V making the first syllable end far down the throat: Strov-strup. The second U is a bit like the OO in OOP, but still short; maybe Strov-stroop will give an idea."

[2]C++ is not the only object-oriented language. Others are Simula (the precursor of all object-oriented languages), Smalltalk, Eiffel, Oberon, and Java.

[3]The reader is referred to several works in the standard literature for an introduction to C++ and discussions about it, namely [ElSt], [Str1], [Str2], [Deit], [Lipp], just to name a few of the more important titles. In particular, [ElSt] was taken as the basis for the standardization efforts of the ISO, which meanwhile has become standard.


13.1 Not a Public Affair: The Representation of Numbers in LINT

And if my ways are not as theirs

Let them mind their own affairs.

—A. E. Housman, Last Poems IX

The representation of large numbers that has been chosen for our class is an extension of the representation presented in Part I for the C language. We take from there the arrangement of the digits of a natural number as a vector of clint values, where more-significant digits occupy the places of higher index (cf. Chapter 2). The memory required for this is automatically allocated when an object is generated. This is carried out by the constructors, which are invoked either explicitly by the program or implicitly by the compiler using the allocation function new(). In the class declaration we therefore require a variable of type clint *n_l, to which is associated within one of the constructor functions a pointer to the memory allocated there. As the second element of our numerical representation we define the variable maxlen, which holds the amount of memory that was allocated by a constructor to a particular object. The variable maxlen specifies the maximum number of clint digits an object can accept. Furthermore, we would like to determine whether a LINT object has been initialized, that is, whether any numerical value at all has been assigned to it, before it is used in a numerical expression on the right side of the equal sign. To this end we introduce the integer variable init, which is initialized to zero and is then set to 1 the first time an object is assigned a numerical value. We shall organize our LINT functions and operators in such a way that an error message results if the value of a LINT object, and consequently the value of an expression, is undefined.

The variable status is, strictly speaking, not an element of our numerical representation. It is used to indicate an overflow or underflow (cf. page 18) if such an event occurs as a result of operations on LINT objects. The types and mechanisms of error messages and error handling are discussed in detail in Chapter 15.

The class LINT thus defines altogether the following elements for the representation of integers and for storing the states of objects:

  clint* n_l;
  int init;
  int maxlen;
  int status;

Since we are dealing here with private elements, access to these class elements is possible only by means of member or friend functions or operators. In particular, there is no possibility of direct access to the individual digits of a number represented by a LINT object.


13.2 Constructors

Constructors are functions for the generation of objects of a particular class. For the LINT class this can occur with or without initialization, where in the latter case an object is created and the required memory for the storage of the number is allocated, but no value is assigned to the object. The constructor required for this takes no argument and thus takes on the role of the default constructor of the class LINT (cf. [Str1], Section 10.4.2). The following default constructor LINT(void) in flintpp.cpp creates a LINT object without assigning it a value:

LINT::LINT (void)
  {
    n_l = new CLINT;
    if (NULL == n_l)
      {
         panic (E_LINT_NHP, "constructor 1", 0, __LINE__);
      }
    maxlen = CLINTMAXDIGIT;
    init = 0;
    status = E_LINT_OK;
  }

If a newly generated object is also to be initialized with a numerical value, then a suitable constructor must be invoked to generate a LINT object and then assign to it a predefined argument as the value. Depending on the type of argument various overloaded constructors must be provided. The class LINT contains the constructor functions as shown in Table 13.1.

Table 13.1: LINT constructors
Constructor | Semantics: generation of a LINT object
LINT (void); | without initialization (default constructor)
LINT (const char* const, const unsigned char); | from a character string, with the basis of the numerical representation given in the second argument
LINT (const UCHAR* const, const int) | from a byte vector with the length given in the second argument
LINT (const char* const); | from a character string, optionally with prefix 0X for hex numbers or 0B for binary digits
LINT (const LINT&); | from another LINT object (copy constructor)
LINT (const int); | from a value of type char, short, or integer
LINT (const long int); | from a value of type long integer
LINT (const UCHAR); | from a value of type UCHAR
LINT (const USHORT); | from a value of type USHORT
LINT (const unsigned int); | from a value of type unsigned integer
LINT (const ULONG); | from a value of type ULONG
LINT (const CLINT); | from a CLINT object

We would now like to consider a further example for the LINT construction of the function LINT (const char* const), which generates a LINT object and associates to it a value taken from a character string with ASCII digits. A prefix can be given to the digits contained in the string that contains information about the base of the numerical representation. If a character string is prefixed with 0x or 0X, then hexadecimal digits from the domains {0,1,...,9} and {a,b,...,f}, respectively {A,B,...,F}, are expected. If the prefix is 0b or 0B, then binary digits from the set { 0,1 } are expected. If there is no prefix at all, then the digits are interpreted as decimal digits. The constructor employs the function str2clint_l() to transform the character string into an object of type CLINT, from which then in the second step a LINT object is created:

LINT:: LINT (const char* const str)
  n_l = new CLINT;
  if (NULL == n_l) // error with new?
    {
      panic (E_LINT _NHP, "constructor 4", 0, __LINE__);
    }

  if (strncmp (str, "0x", 2) == 0 || strncmp (str, "0X", 2) == 0)
    {

      int error = str2clint_l (n_l, (char*)str+2, 16);
    }
  else
    {
      if (strncmp (str, "0b", 2) == 0 || strncmp (str, "0B", 2) == 0)
         {
           error = str2clint_l (n_l, (char*)str+2, 2);
         }
      else
         {
           error = str2clint_l (n_l, (char*)str, 10);
         }
    }
  switch (error) // evaluation error code
    {
      case 0:
         maxlen = CLINTMAXDIGIT;
         init = 1;
         status = E_LINT_OK;
         break;
      case E_CLINT_BOR:
         panic (E_LINT_BOR, "constructor 4", 1, __LINE__);
         break;
      case E_CLINT_OFL:
         panic (E_LINT_OFL, "constructor 4", 1, __LINE__);
         break;
      case E_CLINT_NPT:
         panic (E_LINT_NPT, "constructor 4", 1, __LINE__);
         break;
      default:
         panic (E_LINT_ERR, "constructor 4", error, __LINE__);
    }
}

Constructors make possible the initialization of LINT objects among themselves as well as LINT objects with standard types, constants, and character strings, as the following examples demonstrate:

  LINT a;
  LINT one (1);
  int i = 2147483647;
  LINT b (i);
  LINT c (one);
  LINT d ("0x123456789abcdef0");

The constructor functions are called explicitly to generate objects of type LINT from the specified arguments. The LINT constructor, which, for example, changes unsigned long values into LINT objects, is embodied in the following function:

LINT::LINT (const USHORT ul)
{
  n_l = new CLINT;
  if (NULL == n_l)
    {
      panic (E_LINT_NHP, "constructor 11", 0, __LINE__);
    }
  ul2clint_l (n_l, ul);
  maxlen = CLINTMAXDIGIT;
  init = 1;
  status = E_LINT_OK;
}

Now we must provide a destructor function to go with the constructors of the class LINT, which enable the release of objects and, in particular, the memory bound to them. To be sure, the compiler would gladly make a default destructor available to us, but this would release only the memory that the elements of a LINT object possess. The additional memory allocated by the constructors would not be released, and memory leakage would result. The following short destructor fulfills the important tasks of releasing memory occupied by LINT objects:

~LINT()
 {
   delete [] n_l;
 }


13.3 Overloaded Operators

The overloading of operators represents a powerful mechanism that makes it possible to define functions with the same name but with different parameter lists, functions that can then carry out differing operations. The compiler uses the specified parameter list to determine which function is actually meant. To make this possible C++ employs strong type-checking, which tolerates no ambiguity or inconsistency.

The overloading of operator functions makes it possible to use the "normal" way of expressing a sum c = a + b with LINT objects a, b, and c instead of having to invoke a function like, for example, add_l(a_l, b_l, c_l). This enables the seamless integration of our class into the programming language and significantly improves the readability of programs. For this example it is necessary to overload both the operator "+" and the assignment "=".

There are only a few operators in C++ that cannot be overloaded. Even the operator "[]", which is used for access to vectors, can be overloaded, for example by a function that simultaneously checks whether the access to a vector oversteps the vector's bounds. However, please note that the overloading of operators opens the door to all possible mischief. To be sure, the effect of the operators of C++ on the standard data types cannot be altered; nor can the predefined precedence order of the operators (cf. [Str1], Section 6.2) be changed or new operators "created." But for individual classes it is fully possible to define operator functions that have nothing in common with what one traditionally has associated with the operator as it is normally employed. In the interest of maintainability of programs one is well advised to stick close to the meaning of the standard operators in C++ when overloading operators if one is to avoid unnecessary confusion.

One should note in the above outline of the LINT class that certain operators have been implemented as friend functions and others as member functions. The reason for this is that we would like, for example, to use "+" or "*" as two-position operators that can not only process two equivalent LINT objects but accept alternatively one LINT object and one of the built-in C++ integer types, and moreover, accept the arguments in either order, since addition is commutative. To this end we require the above-described constructors, which create LINT objects of out integer types. Mixed expressions such as in

  LINT a, b, c;
  int number;

  // Initialize a, b, and number and calculate something or other
  // ...

  c = number * (a + b / 2)

are thus possible. The compiler takes care of calling the appropriate constructor functions automatically and sees to it that the transformation of the integer type number and the constant 2 into LINT objects takes place at run time, before the operators + and * are invoked. We thereby obtain the greatest possible flexibility in the application of the operators, with the restriction that expressions containing objects of type LINT are themselves of type LINT and can thereafter be assigned only to objects of type LINT.

Before we get ourselves involved in the details of the individual operators, we would like to give an overview of the operators defined by the class LINT, for which the reader is referred to Tables 13.2 through 13.5,

Table 13.2: LINT arithmetic operators
+ | addition
++ | increment (prefix and postfix operators)
− | subtraction
-- | decrement (prefix and postfix operators)
* | multiplication
/ | division (quotient)
% | remainder

We now would like to deal with the implementation of the operator functions "*", "=", "*=", and "==", which may serve as examples of the implementation

Table 13.3: LINT bitwise operators
& | bitwise AND
| | bitwise OR
^ | bitwise exclusive OR (XOR)
<< | shift left
>> | shift right
Table 13.4: LINT logical operators
== | equality
!= | inequality
<, <= | less than, less than or equal to
>, >= | greater than, greater than or equal to
Table 13.5: LINT assignment operators
= | simple assignment
+= | assignment after addition
−= | assignment after subtraction
*= | assignment after multiplication
/= | assignment after division
%= | assignment after remainder
&= | assignment after bitwise AND
|= | assignment after bitwise OR
^= | assignment after bitwise XOR
<<= | assignment after left shift
>>= | assignment after right shift

of the LINT operators. First, with the help of the operator "*=" we see how multiplication of LINT objects is carried out by the C function mul_l(). The operator is implemented as a friend function, to which both factors associated with the operation are passed as references. Since the operator functions do not change their arguments, the references are declared as const:

const LINT operator* (const LINT& lm, const LINT& ln)
{
  LINT prd;
  int error;
Start Sidebar

The first step is to query the operator function as to whether the arguments lm and ln passed by reference have been initialized. If this is not the case for both arguments, then error handling goes into effect, and the member function panic(), declared as static, is called (cf. Chapter 15).

End Sidebar
  if (!lm.init) LINT::panic (E_LINT_VAL, "*", 1, __LINE__);
  if (!ln.init) LINT::panic (E_LINT_VAL, "*", 2, __LINE__);
Start Sidebar

The C function mul_l() is called, to which are passed as arguments the vectors lm.n_l, ln.n_l as factors, as well as prd.n_l for storing the product.

End Sidebar
  error = mul_l (lm.n_l, ln.n_l, prd.n_l);
Start Sidebar

The evaluation of the error code stored in error distinguishes three cases: If error == 0, then all is right with the world, and the object prd can be marked as initialized. This takes place by setting the variables prd.init to 1. The status variable prd.status was already set by the constructor to the value E_LINT_OK. If an over-flow occurred with mul_l(), then error contains the value E_CLINT_OFL. Since the vector prd.n_l contains in this case a valid CLINT number, prd.init is set to 1, while the status variable prd.status contains the value E_LINT_OFL. If error has neither of these two values after the call to mul_l(), then something has gone awry in these functions without our being able to identify more precisely what error has occurred. In this case the function panic() is called for further error handling.

End Sidebar
  switch (error)
    {
      case 0:
         prd.init = 1;
         break;
      case E_CLINT_OFL:
         prd.status = E_LINT_OFL;
         prd.init = 1;
         break;
      default:
         lint::panic (E_LINT_ERR, "*", error, __LINE__);
    }
Start Sidebar

If the error cannot be repaired by panic(), there would be no point in returning to this location. The mechanism for error recognition leads here to a defined termination, which in principle is better than continuing the program in an undefined state. As a final step we have the elementwise return of the product prd.

End Sidebar
    return prd;
  }

Since the object prd exists only within the context of the function, the compiler makes sure that a temporary object is created automatically, which represents the value of prd outside the function. This temporary object is generated with the aid of the copy constructor LINT(const LINT&) (cf. page 265) and exists until the expression within which the operator was used has been processed, that is, until the closing semicolon has been reached. Due to the declaration of the function value as const such nonsensical constructs as (a * b) = c; will not get past the compiler. The goal is to treat LINT objects in exactly the same way as the built-in integer types.

We can extend the operator functions by the following detail: If the factors to be multiplied are equal, then the multiplication can be replaced by squaring, so that the advantage in efficiency associated with this changeover can be utilized automatically (cf. Section 4.2.2). However, since in general it costs an elementwise comparison of the arguments to determine whether they are equal, which is too expensive for us, we shall be satisfied with a compromise: Squaring will be brought into play only if both factors refer to one and the same object. Thus we test whether ln and lm point to the same object and in this case execute the squaring function instead of multiplication. Here is the relevant code:

  if (&lm == &ln)
    {
      error = sqr_l (lm.n_l, prd.n_l);
    }
  else
    {
      error = mul_l (lm.n_l, ln.n_l, prd.n_l);
    }

This falling back on the functions implemented in C from Part I is a model for all of the remaining functions of the class LINT, which is formed like a shell around the kernel of C functions and protects it from the user of the class.

Before we turn our attention to the more complex assignment operator "*=", it seems a good idea to take a closer look at the simple assignment operator "=". Already in Part I we established that assignment of objects requires particular attention (cf. Chapter 8). Therefore, just as in the C implementation we had to pay heed that in assigning one CLINT object to another the content and not the address of the object was assigned, we must likewise for our LINT class define a special version of the assignment operator "=" that does more than simply copy elements of the class: For the same reasons as were introduced in Chapter 8 we must therefore take care that it is not the address of the numerical vector n_l that is copied, but the digits of the numerical representation pointed to by n_l.

Once one has understood the fundamental necessity for proceeding thus, the implementation is no longer particularly complicated. The operator "=" is implemented as a member function, which returns as a result of the assignment a reference to the implicit left argument. Of course, we use internally the C function cpy_l() to move digits from one object into the other. For executing the assignment a = b the compiler calls the operator function "=" in the context of a, where a takes over the role of an implicit argument that is not given in the parameter list of the operator function. Within the member function reference to the elements of the implicit argument is made simply by naming them without context. Furthermore, a reference to the implicit object can be made via the special pointer this, as in the following implementation of the operator "=":

  const LINT& LINT::operator= (const LINT& ln)
  {
    if (!ln.init) panic (E_LINT_VAL, "=", 2, __LINE__);
    if (maxlen < DIGITS_L (ln.n_l))
      panic (E_LINT_OFL, "=" , 1, __LINE__);
Start Sidebar

First, a check is made as to whether the references to the right and left arguments are identical, since in this case copying is unnecessary. Otherwise, the digits of the numerical representation of ln are copied into those of the implied left argument *this, just as the values of init and status, and with *this the reference to the implicit argument is returned.

End Sidebar
    if (&ln != this)
      {
         cpy_l (n_l, ln.n_l);
         init = 1;
         status = ln.status;
      }
    return *this;
  }

One might ask whether the assignment operator must necessarily return any value at all, since after LINT::operator =(const LINT&) is called the intended assignment appears to have been accomplished. However, the answer to the question is clear if one recalls that expressions of the form

f (a = b);

are allowed. According to the semantics of C++, such an expression would result in a call to the function f with the result of the assignment a = b as argument. Thus is it imperative that the assignment operator return the assigned value as result, and for reasons of efficiency this is done by reference. A special case of such an expression is

a = b = c;

where the assignment operator is called two times, one after the other. At the second call the result of the first assignment b = c is assigned to a.

In contrast to the operator "*", the operator "*=" changes the leftmost of the two passed factors by overwriting it with the value of the product. The meaning of the expression a *= b as an abbreviated form of a = a * b should, of course, remain true for LINT objects. Therefore, the operator "*=" can, like the operator "=", be set up as a member function that for the reasons given above returns a reference to the result:

const LINT& LINT::operator*= (const LINT& ln)
{
    int error;

    if (!init) panic (E_LINT_VAL, "*=", 0, __LINE__);
    if (!ln.init) panic (E_LINT_VAL, "*=", 1, __LINE__);

    if (&ln == this)
      error = sqr_l (n_l, n_l);
    else
      error = mul_l (n_l, ln.n_l, n_l);

    switch (error)
      {
         case 0:
           status = E_LINT_OK;
           break;
         case E_CLINT_OFL:
           status = E_LINT_OFL;
           break;
         default:
           panic (E_LINT_ERR, "*=", error, __LINE__);
         }

  return *this;
  }

As our last example of a LINT operator we shall describe the function "==", which tests for the equality of two LINT objects: As result the value 1 is returned in the case of equality, and otherwise 0. The operator == also illustrates the implementation of other logical operators.

  const int operator == (const LINT& lm, const LINT& ln)
  {
    if (!lm.init) LINT::panic (E_LINT_VAL, "==", 1, __LINE__);
    if (!ln.init) LINT::panic (E_LINT_VAL, "==", 2, __LINE__);

    if (&ln == &lm)
      return 1;
    else
      return equ_l (lm.n_l, ln.n_l);
  }


Chapter 14: The LINT Public Interface: Members and Friends

Overview

Please accept my resignation. I don't want to belong to any club that will accept me as a member.

—Groucho Marx

Every time I paint a portrait I lose a friend

—John Singer Sargent

IN ADDITION TO THE CONSTRUCTOR functions and operators already discussed, there exist further LINT functions that make the C functions developed in Part I available to LINT objects. In the following discussion we make a rough separation of the functions into the categories "arithmetic" and "number-theoretic." The implementation of the functions will be discussed together with examples; otherwise, we shall restrict ourselves to a table of information needed for their proper use. We shall give more extensive treatment in the following sections to the functions for the formatted output of LINT objects, for which we shall make use of the properties of the stream classes contained in the C++ standard library. Possible applications, in particular for formatted output of objects of user-defined classes, are given rather short shrift in many C++ textbooks, and we are going to take the opportunity to explicate the construction of the functions needed to output our LINT objects.


14.1 Arithmetic

The following member functions implement the fundamental arithmetic operations as well as modular operations for calculation in residue class rings over the integers as accumulator operations: The object to which a called function belongs contains the function result as implicit argument after its termination. Accumulator functions are efficient, since they operate to the greatest extent without internal auxiliary objects and thus save unnecessary assignments and calls to constructors.

For the cases in which a free assignment of the results of calculations is unavoidable, or in which the automatic overwriting of the implicit argument of the member functions with the result is not desired, the member functions were extended by means of like-named analogous friend functions together with additional friend functions. These are not discussed further here, but are recorded in Appendix B. The treatment of possible error situations in LINT functions that can arise from the use of CLINT functions will be discussed in full in Chapter 15.

Before we list the public member functions, we consider first as an example of their implementation the functions

  LINT& LINT::mexp (const LINT& e, const LINT& m );

and

  LINT& LINT::mexp (const USHORT e, const LINT& m);

for exponentiation, an operation for which C++, alas, offers no operator. The functions mexp() were constructed in such a way that the functions used are, according to the type of the operands, the C functions mexpk_l(), mexpkm_l(), umexp_l(), and umexpm_l(), optimized for this purpose (with the corresponding arithmetic friend functions we are likewise dealing with the exponentiation functions wmexp_l() and wmexpm_l() with USHORT base).

Start Sidebar

Function:

Modular exponentiation with automatic use of Montgomery exponentiation if the modulus is odd.

Syntax:

const LINT&
LINT::mexp (const LINT& e, const LINT& m);

Input:

implicit argument (base)

e (exponent)

m (modulus)

Return:

pointer to the remainder

Example:

a.mexp (e, m);

End Sidebar
 const LINT& LINT::mexp (const LINT& e, const LINT& m)
 {
   int error;
   if (!init) panic (E_LINT_VAL, "mexp", 0, __LINE__);
   if (!e.init) panic (E_LINT_VAL, "mexp", 1, __LINE__);
   if (!m.init) panic (E_LINT_VAL, "mexp", 2, __LINE__);

   err = mexp_l (n_l, e.n_l, n_l, m.n_l);
   /* mexp_l() uses mexpk_l() or mexpkm_l() */

   switch (error)
     {
        case 0:
          status = E_LINT_OK;
          break;
        case E_CLINT_DBZ:
          panic (E_LINT_DBZ, "mexp", 2, __LINE__);
          break;
        default:
          panic (E_LINT_ERR, "mexp", error, __LINE__);
     }

   return *this;
 }
Start Sidebar

Function:

Modular exponentiation

Syntax:

const LINT&
LINT::mexp (const USHORT e, const LINT& m);

Example:

a.mexp (e, m);

End Sidebar
 const LINT& LINT::mexp (const USHORT e, const LINT& m)
 {
   int err;
   if (!init) panic (E_LINT_VAL, "mexp", 0, __LINE__);
   if (!m.init) panic (E_LINT_VAL, "mexp", 1, __LINE__);

   err = umexp_l (n_l, e, n_l, m.n_l);

   switch (err)
     {
        // Code as above with mexp (const LINT& e, const LINT& m)
     }
   return *this;
 }

We now present a collection of additional arithmetic and number-theoretic member functions.

Start Sidebar

Function:

addition

Syntax:

const LINT&
LINT::add(const LINT& s);

Input:

implicit argument (summand)

s (summand)

Return:

pointer to the sum

Example:

a.add (s); executes the operation a += s;

End Sidebar
Start Sidebar

Function:

subtraction

Syntax:

const LINT&
LINT::sub (const LINT& s);

Input:

implicit argument (minuend)

s (subtrahend)

Return:

pointer to the difference

Example:

a.sub (s); executes the operation a −= s;

End Sidebar
Start Sidebar

Function:

multiplication

Syntax:

const LINT&
LINT::mul (const LINT& s);

Input:

implicit argument (factor)

s (factor)

Return:

pointer to the product

Example:

a.mul (s); executes the operation a *= s;

End Sidebar
Start Sidebar

Function:

squaring

Syntax:

const LINT&
LINT::sqr (void);

Input:

implicit argument (factor)

Return:

pointer to the implicit argument, which contains the square

Example:

a.sqr (); executes the operation a *= a;

End Sidebar
Start Sidebar

Function:

division with remainder

Syntax:

const LINT&
LINT::divr(const LINT& d, LINT& r);

Input:

implicit argument (dividend)

d (divisor)

Output

r (remainder of the division modulo d)

Return:

pointer to the implicit argument, which contains the quotient

Example:

a.divr (d, r); executes the operation a /= d; r = a % d;

End Sidebar
Start Sidebar

Function:

residue

Syntax:

const LINT&
LINT::mod(const LINT& d);

Input:

implicit argument (dividend)

d (divisor)

Return:

pointer to the implicit argument, which contains the remainder of the division modulo d

Example:

a.mod (d); executes the operation a %= d;

End Sidebar
Start Sidebar

Function:

residue modulo a power of 2

Syntax:

const LINT&
LINT::mod2 (const USHORT e)

Input:

implicit argument (dividend)

e (exponent of the power of 2 divisor)

Return:

pointer to the implicit argument, which contains the remainder of the division modulo 2e

Example:

a.mod 2(e); executes the operation a %= d;, where d = 2e

Note:

mod2 cannot be created by overloading the previously presented function mod(), since mod() also accepts a USHORT argument, which is changed automatically into a LINT object by means of the appropriate constructor. Since it cannot be determined from the arguments which function is meant, mod2() is given its own name.
End Sidebar
Start Sidebar

Function:

test for equality modulo m

Syntax:

const int
LINT::mequ (const LINT& b, const LINT& m);

Input:

implicit argument a

second argument b

modulus m

Return:

1 if a ≡ b mod m, 0 otherwise

Example:

if (a.mequ (b, m)) // ...

End Sidebar
Start Sidebar

Function:

modular addition

Syntax:

const LINT&
LINT::madd(const LINT& s, const LINT& m);

Input:

implicit argument (summand)

s (summand)

m (modulus)

Return:

pointer to the implicit argument, which contains the sum modulo m

Example:

a.madd (s, m);

End Sidebar
Start Sidebar

Function:

modular subtraction

Syntax:

const LINT& LINT::msub(const LINT& s,
const LINT& m);

Input:

implicit argument (minuend)

s (subtrahend)

m (modulus)

Return:

pointer to the implicit argument, which contains the difference modulo m

Example:

a.msub (s, m);

End Sidebar
Start Sidebar

Function:

modular multiplication

Syntax:

const LINT& LINT::mmul (const LINT& s,
const LINT& m);

Input:

implicit argument (factor)

s (factor)

m (modulus)

Return:

pointer to the implicit argument, which contains the product modulo m

Example:

a.mmul (s, m);

End Sidebar
Start Sidebar

Function:

modular squaring

Syntax:

const LINT& LINT::msqr (const LINT& m);

Input:

implicit argument (factor)

m (modulus)

Return:

pointer to the implicit argument, which contains the square modulo m

Example:

a.msqr (m);

End Sidebar
Start Sidebar

Function:

modular exponentiation with exponent a power of 2

Syntax:

const LINT& LINT::mexp2 (const USHORT e,
const LINT& m);

Input:

implicit argument (base)

e (power to which 2 is to be raised)

m (modulus)

Return:

pointer to the implicit argument, which contains the power modulo m

Example:

a.mexp2 (e, m);

End Sidebar
Start Sidebar

Function:

modular exponentiation

(2k-ary method, Montgomery reduction)

Syntax:

const LINT& LINT::mexpkm (const LINT& e,
const LINT& m);

Input:

implicit argument (base)

e (exponent)

m (odd modulus)

Return:

pointer to the implicit argument, which contains the power modulo m

Example:

a.mexpkm (e, m);

End Sidebar
Start Sidebar

Function:

modular exponentiation

(25-ary method, Montgomery reduction)

Syntax:

const LINT& LINT::mexp5m(const LINT& e,
const LINT& m);

Input:

implicit argument (base)

e (exponent)

m (odd modulus)

Return:

pointer to the implicit argument, which contains the power modulo m

Example:

a.mexp5m (e, m);

End Sidebar
Start Sidebar

Function:

left/right shift

Syntax:

const LINT& LINT::shift (const int noofbits);

Input:

implicit argument (multiplicand/dividend) (+/−) noofbits (number of bit positions to be shifted)

Return:

pointer to the implicit argument, which contains the result of the shift operation

Example:

a.shift (512); executes the operation a <<= 512;

End Sidebar
Start Sidebar

Function:

test for divisibility by 2 of a LINT object

Syntax:

const int
LINT::iseven (void);

Input:

test candidate a as implicit argument

Return:

1 if a is odd, 0 otherwise

Example:

if(a.iseven ()) // ...

End Sidebar
Start Sidebar

Function:

set a binary digit of a LINT object to 1

Syntax:

const LINT&
LINT::setbit (const unsigned int pos);

Input:

implicit argument a

position pos of the bit to be set (counted from 0)

Return:

pointer to a with the set bit at position pos

Example:

a.setbit (512);

End Sidebar
Start Sidebar

Function:

test a binary digit of a LINT object

Syntax:

const int
LINT::testbit (const unsigned int pos);

Input:

implicit argument a

position pos of the bit to be tested (counted from 0)

Return:

1 if the bit at position pos is set, 0 otherwise

Example:

if(a.testbit (512)) // ...

End Sidebar
Start Sidebar

Function:

set a binary digit of a LINT object to 0

Syntax:

const LINT&
LINT::clearbit (const unsigned int pos);

Input:

implicit argument a

position pos of the bit to be cleared (counted from 0)

Return:

pointer to a with the cleared bit at position pos

Example:

a.clearbit (512);

End Sidebar
Start Sidebar

Function:

exchange values of two LINT objects

Syntax:

const LINT&
LINT::fswap (LINT& b);

Input:

implicit argument a

position b (the value to be swapped for a)

Return:

pointer to the implicit argument with the value b

Example:

a.fswap (b); exchanges the values a and b

End Sidebar


14.2 Number Theory

In contrast to the arithmetic functions, the following number-theoretic member functions do not overwrite the implicit first argument with the result. The reason for this is that with more complex functions it has been shown in practice not to be practical to overwrite, as is the case with simple arithmetic functions. The results of the following functions are thus returned as values rather than as pointers.

Start Sidebar

Function:

calculate the greatest integer less than or equal to the base-2 logarithm of a LINT object

Syntax:

const unsigned int
LINT::ld (void);

Input:

implicit argument a

Return:

integer part of the base-2 logarithm of a

Example:

i = a.ld ();

End Sidebar
Start Sidebar

Function:

calculate the greatest common divisor of two LINT objects

Syntax:

const LINT
LINT::gcd (const LINT& b);

Input:

implicit argument a

second argument b

Return:

gcd (a, b) of the input values

Example:

c = a.gcd (b);

End Sidebar
Start Sidebar

Function:

calculate the multiplicative inverse modulo n

Syntax:

const LINT
LINT::inv (const LINT& n);

Input:

implicit argument a

modulus n

Return:

multiplicative inverse of a modulo n (if the result is equal to zero, then gcd(a, n) > 1 and the inverse does not exist)

Example:

c = a.inv (n);

End Sidebar
Start Sidebar

Function:

calculate the greatest common divisor of a and b as well as its representation g = ua+vb as a linear combination of a and b

Syntax:

const LINT
LINT::xgcd(const LINT& b,
               LINT& u, int& sign_u,
               LINT& v, int& sign_v);

Input:

implicit argument a, second argument b

Output:

Factor u of the representation of gcd (a, b) sign of u in sign_u

factor v of the representation of gcd (a, b) sign of v in sign_v

Return:

gcd(a, b) of the input values

Example:

g = a.xgcd (b, u, sign_u, v, sign_v);

End Sidebar
Start Sidebar

Function:

calculate the least common multiple (lcm) of two LINT objects

Syntax:

const LINT
LINT::lcm(const LINT& b);

Input:

implicit argument a

factor b

Return:

lcm(a, b) of the input values

Example:

c = a.lcm (b);

End Sidebar
Start Sidebar

Function:

solution of a system of linear congruences x ≡ a mod m, x ≡ b mod n,

Syntax:

const LINT
LINT::chinrem(const LINT& m, const LINT& b,
               const LINT& n);

Input:

implicit argument a, modulus m, argument b, modulus n

Return:

solution x of the congruence system if all is ok (Get_Warning_Status() == E_LINT_ERR indicates that an overflow has occurred or that the congruences have no common solution)

Example:

x = a.chinrem (m, b, n);

End Sidebar

The friend function chinrem(const int noofeq, LINT** coeff) accepts a vector coeff of pointers to LINT objects, which are passed as coefficients a1, m1, a2, m2, a3, m3,... of a system of linear congruences with "arbitrarily" many equations x ≡ ai mod mi, i = 1,..., noofeq (see Appendix B).

Start Sidebar

Function:

calculation of the Jacobi symbol of two LINT objects

Syntax:

const int
LINT::jacobi (const LINT& b);

Input:

implicit argument a, second argument b

Return:

Jacobi symbol of the input values

Example:

i = a.jacobi (b);

End Sidebar
Start Sidebar

Function:

calculation of the integer part of the square root of a LINT object

Syntax:

const LINT
LINT::root (void);

Input:

implicit argument a

Return:

integer part of the square root of the input value

Example:

c = a.root ();

End Sidebar
Start Sidebar

Function:

calculation of the square root modulo a prime p of a LINT object

Syntax:

const LINT
LINT::root (const LINT& p);

Input:

implicit argument a, prime modulus p > 2

Return:

square root of a if a is a quadratic residue modulo p otherwise 0 (Get_Warning_Status() == E_LINT_ERR indicates that a is not a quadratic residue modulo p)

Example:

c = a.root (p);

End Sidebar
Start Sidebar

Function:

calculation of the square root of a LINT object modulo a prime product p·q

Syntax:

const LINT
LINT::root (const LINT& p, const LINT& q);

Input:

implicit argument a

prime modulus p > 2, prime modulus q > 2

Return:

square root of a if a is a quadratic residue modulo pq otherwise 0 (Get_Warning_Status() == E_LINT_ERR indicates that a is not a quadratic residue modulo p*q)

Example:

c = a.root (p, q);

End Sidebar
Start Sidebar

Function:

test of whether a LINT object is a square

Syntax:

const int
LINT::issqr(void);,

Input:

test candidate a as implicit argument

Return:

square root of a if a is a square otherwise 0 if a == 0 or a not a square

Example:

if(0 == (r = a.issqr ())) // ...

End Sidebar
Start Sidebar

Function:

probabilistic primality test of a LINT object

Syntax:

const int
LINT::isprime (void);

Input:

test candidate a as implicit argument

Return:

1 if a is a "probable" prime

0 otherwise

Example:

if(a.isprime ()) // ...

End Sidebar
Start Sidebar

Function:

calculate the two-part of a LINT object

Syntax:

const int
LINT::twofact (LINT& b);

Input:

implicit argument a

Output:

b (odd part of a)

Return:

exponent of the odd part of a

Example:

e = a.twofact (b);

End Sidebar


14.3 Stream I/O of LINT Objects

The classes contained in the C++ standard library such as istream and ostream are abstractions of input and output devices derived from the base class ios. The class iostream is in turn derived from istream and ostream, and it enables both writing and reading of its objects.[1] Input and output take place with the help of the insert and extract operators "<<" and ">>" (cf. [Teal], Chapter 8). These arise through overloading the shift operators, for example in the form

  ostream& ostream::operator<< (int i);
  istream& istream::operator>> (int& i);

in which they enable output, respectively input, of integer values through expressions of the form

  cout << i;
  cin >> i;

As special objects of the classes ostream and istream, cout and cin represent the same abstract files as the objects stdout and stdin of the standard C library.

The use of the stream operators "<<" and ">>" for input and output makes it unnecessary to consider particular properties of the hardware in use. In and of itself this is nothing new, for the C function printf() behaves the same way: A printf() instruction should always, regardless of platform, lead to the same result. However, above and beyond the altered syntax, which is oriented to the metaphorical image of the insertion of objects into a stream, the advantages of the C++ implementation of streams lie in the strict type checking, which in the case of printf() is possible only to a limited degree, and in its extensibility. In particular, we make use of the latter property by overloading the insert and extract operators so that they support input and output of LINT objects. To this end the class LINT defines the following stream operators:

  friend ostream& operator<< (ostream& s, const LINT& ln);
  friend fstream& operator<< (fstream& s, const LINT& ln);
  friend ofstream& operator<< (ofstream& s, const LINT& ln);
  friend fstream& operator>> (fstream& s, LINT& ln);
  friend ifstream& operator>> (ifstream& s, LINT& ln);

A simple formulation of the overloaded insert operators for the output of LINT objects might look something like the following:

  #include <iostream.h>

  ostream& operator<< (ostream& s, const LINT& ln)
  {
    if (!ln.init)
      LINT::panic (E_LINT_VAL, "ostream operator <<", 0, __LINE__);

    s << xclint2str (ln.n_l, 16, 0) << endl;
    s << ld (ln) << " bit" << endl;
    return s;
  }

The operator << thus defined outputs the digits of a LINT object as hexadecimal values and adds the binary length of the number in a separate line. In the next section we shall consider the possibilities of improving the appearance of the output of LINT objects with the aid of formatting functions, and we shall also use manipulators to make the output customizable.

14.3.1 Formatted Output of LINT Objects

In this section we shall make use of the base class ios of the C++ standard library and of its member functions to define our own LINT-specific formatting functions for the purpose of controlling the output format of LINT objects. Furthermore, we shall create manipulators that will make the customization of the output format for LINT objects as simple as it is for the standard types defined in C++.

The crucial point in the creation of formatted output of LINT objects is the possibility of setting formatting specifications that will be handled by the insert operator. To this end we shall consider the mechanism provided for the class ios (for details see [Teal], Chapter 6, and [Pla2], Chapter 6), whose member function xalloc() in the objects of the classes derived from ios allocates a status variable of type long and returns an index to this status variable also of type long. We store this index in the long variable flagsindex. By means of this index the member function ios::iword() can be used to access reading and writing to the allocated status variable (cf. [Pla2], page 125).

To ensure that this takes place before a LINT object is output, we define, in the file flintpp.h, the class LintInit as follows:

class LintInit
  {
    public:
      LintInit (void);
  };

  LintInit::LintInit (void)
  {
    // get index to long status variable in class ios
    LINT::flagsindex = ios::xalloc();
    // set the default status in cout and in cerr
    cout.iword (LINT::flagsindex) =
    cerr.iword (LINT::flagsindex) =
      LINT::lintshowlength|LINT::linthex|LINT::lintshowbase;
  }

The class LintInit has as its only element the constructor LintInit::LintInit(). Furthermore, in the class LINT we define a member datum setup of type LintInit, which is initialized via the constructor LintInit::LintInit(). The call to xalloc() takes place within this initialization, and the status variable thereby allocated is given the established standard output format for LINT objects. In the following we shall show a section of the LINT class declaration, which contains the declaration of LintInit() as a friend of LINT, the declaration of the variables flagsindex and setup, and various status values as enum types:

class LINT
  {
    public:
      // ...
      enum {
         lintdec = 0x10,
         lintoct = 0x20,
         linthex = 0x40,
         lintshowbase = 0x80,
         lintuppercase = 0x100,
         lintbin = 0x200,
         lintshowlength = 0x400
      };

      // ...
      friend LintInit::LintInit (void);
      // ...

    private:
      // ...
      static long flagsindex;
      static LintInit setup;
      // ...
  };

Setting the variable setup as static has the effect that this variable exists only once for all LINT objects and thus the associated constructor LintInit() is called only once.

We would like now to pause for a moment and consider what all this effort nets us. Setting the output format could just as well be managed via a status variable, which as a member of LINT would be much simpler to deal with. The decisive advantage of the method that we have chosen is that the output format can be set for each output stream separately and independently of the others (cf. [Pla2], page 125), which could not be accomplished with an internal LINT status variable. This is done through the power of the class ios, whose mechanisms we employ for such purposes.

Now that the necessary preliminaries have been taken care of, we can define the status functions as member functions of LINT. These are displayed in Table 14.1.

We shall consider as an example of the implementation of the status functions the function LINT::setf(), which returns the current value of the status variable as a long with reference to an output stream:

Table 14.1: LINT status functions and their effects

Status function

Explanation

static long
LINT::flags
(void);

read the status variable with reference to cout

static long
LINT::flags
(ostream&);

read the status variable with reference to an arbitrary output stream

static long
LINT::setf
(long);

set individual bits of the status variable with reference to cout and return the previous value

static long
LINT::setf
(ostream&, long);

set individual bits of the status variable with reference to an arbitrary output stream and return the previous value

static long
LINT::unsetf
(long);

restore individual bits of the status variable with reference with reference to cout and return the previous value

static long
LINT::unsetf
(ostream&, long);

restore individual bits of the status variable with reference to an arbitrary output stream and return the previous value

static long
LINT::restoref
(long);

set the status variable with reference to cout with a value and return the previous value

static long
LINT::restoref
(ostream&, long);

set the status variable with reference to an arbitrary output stream with a value and return the previous value

long LINT::setf (ostream& s, long flag)
  {
  long t = s.iword (flagsindex);
  // the flags for the basis of the numerical representation
  //are mutually exclusive
  if (flag & LINT::lintdec)
    {
    s.iword (flagsindex) = (t & ~LINT::linthex & ~LINT::lintoct
                  & ~LINT::lintbin) | LINT::lintdec;
    flag ^= LINT::lintdec;
    }

  if (flag & LINT::linthex)
    {
    s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::lintoct
                  & ~LINT::lintbin) | LINT::linthex;
    flag ^= LINT::linthex;
    }

  if (flag & LINT::lintoct)
    {
    s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::linthex
                  & ~LINT::lintbin) | LINT::lintoct;
    flag ^= LINT::lintoct;
    }

  if (flag & LINT::lintbin)
    {
    s.iword (flagsindex) = (t & ~LINT::lintdec & ~LINT::lintoct
                  & ~LINT::linthex) | LINT::lintbin;
    flag ^= LINT::lintbin;
    }

  // all remaining flags are mutually compatible
  s.iword (flagsindex) |= flag;

  return t;
  }

With the help of these and the remaining functions of Table 14.1 we can determine the output formats in the following. First, the standard output format represents the value of a LINT object as a hexadecimal number in a character string, where the output fills as many lines on the screen as required by the number of digits of the LINT object. In an additional line the number of digits of the LINT object is displayed flush left. The following additional modes for output of a LINT object have been implemented:

  1. Base for the representation of digits

    The standard base for the representation of digits of LINT objects is 16, and for the representation of the length it is 10. This default for LINT objects can be set for the standard output stream cout to a specified base by a call to

      LINT::setf (LINT::base);

    and to

      LINT::setf (ostream, LINT::base);

    for an arbitrary output stream. Here base can assume any one of the values

      linthex, lintdec, lintoct, lintbin,

    which denote the corresponding output format. A call to LINT::setf(lintdec), for example sets the output format to decimal digits. The base for the representation of the length can be set with the function

      ios::setf (ios::iosbase);

    with iosbase = hex, dec, oct.

  2. Display of the prefix for the numerical representation

    The default is for a LINT object to be displayed with a prefix indicating how it is represented. A call to

      LINT::unsetf (LINT::lintshowbase);
  LINT::unsetf (ostream, LINT::lintshowbase);

    changes this setting.

  3. Display of hexadecimal digits in uppercase letters

    The default is the display of hexadecimal digits and the display of the prefix 0x for a hexadecimal representation in lowercase letters abcdef. However, a call to

      LINT::setf (LINT::lintuppercase);
  LINT::setf (ostream, LINT::lintuppercase);

    changes this, so that the prefix 0X and uppercase letters ABCDEF are displayed.

  4. Display of the length of a LINT object

    The default is the display of the binary length of a LINT objects. This can be changed by a call to

      LINT::unsetf (LINT::lintshowlength);
  LINT::unsetf (ostream, LINT::lintshowlength);

    so that the length is not displayed.

  5. Restoring the status variable for the numerical representation

    The status variable for the formatting of a LINT object can be restored to a previous value oldflags by a call to the two functions

      LINT::unsetf (ostream, LINT::flags(ostream));
  LINT::setf (ostream, oldflags);

    Calls to these two functions are collected in the overloaded function restoref():

      LINT::restoref (flag);
  LINT::restoref (ostream, flag);

Flags can be combined, as in the call

 LINT::setf (LINT::bin | LINT::showbase);

This, however, is permitted only for flags that are not mutually exclusive.

The output function that finally generates the requested representational format for LINT objects is an extension of the operator ostream& operator <<(ostream& s, LINT ln) already sketched above, which evaluates the status variables of the output stream and generates the appropriate output. For this the operator uses the auxiliary function lint2str() contained in flintpp.cpp, which in turn calls the function xclint2str_l() to represent the numerical value of a LINT object in a character string:

ostream& operator << (ostream& s, const LINT& ln)
  {
    USHORT base = 16;
    long flags = LINT::flags (s);
    char* formatted_lint;

    if (!ln.init) LINT::panic (E_LINT_VAL, "ostream operator<<", 0, __LINE__);

    if (flags & LINT::linthex)
      {
         base = 16;
      }
    else
      {
         if (flags & LINT::lintdec)
           {
              base = 10;
           }
         else
           {
              if (flags & LINT::lintoct)
                {
                  base = 8;
                }
              else
                {
                  if (flags & LINT::lintbin)
                    {
                       base = 2;
                    }
                }
           }
      }

    if (flags & LINT::lintshowbase)
      {
         formatted_lint = lint2str (ln, base, 1);
      }
    else
      {
         formatted_lint = lint2str (ln, base, 0);
      }

    if (flags & LINT::lintuppercase)
      {
         strupr_l (formatted_lint);
      }

    s << formatted_lint << flush;

    if (flags & LINT::lintshowlength)
      {
         long _flags = s.flags (); // get current status
         s.setf (ios::dec);// set flag for decimal display
         s << endl << ld (ln) << " bit" << endl;
         s.setf (_flags); // restore previous status
      }

      return s;
  }

14.3.2 Manipulators

Building on the previous mechanisms, we would like in this section to obtain more convenient possibilities for controlling the output format for LINT objects. To this end we use manipulators, which are placed directly into the output stream and thus display the same effects as occur in calling the above status functions. Manipulators are addresses of functions for which there exist special insert operators that on their part accept a pointer to a function as argument. As an example we consider the following function:

  ostream& LintHex (ostream& s)
  {
    LINT::setf (s, LINT::linthex);
    return s;
  }

This function calls the status function setf(s, LINT::linthex) in the context of the specified output stream ostream& s and thereby effects the output of LINT objects as hexadecimal numbers. The name LintHex of the function without parentheses is viewed as a pointer to the function (cf. [Lipp], page 202) and can be set in an output stream as a manipulator with the help of the insert operator

  ostream& ostream::operator<< (ostream& (*pf)(ostream&))
  {
    return (*pf)(*this);
  }

defined in the class ostream:

  LINT a ("0x123456789abcdef0");
  cout << LintHex << a;

  ostream s;
  s << LintDec << a;

The LINT manipulator functions operate according to the same pattern as the standard manipulators in the C++ library, for example dec, hex, oct, flush, and endl: The insert operator << simply calls the manipulator function LintHex() or LintDec() at the appropriate place. The manipulators ensure that the status flags belonging to the output streams cout, respectively s, are set. The overloaded operator<< for the output of LINT objects takes over the representation of the LINT object a in the requested form.

The format settings for the output of LINT objects can all be carried out with the help of the manipulators presented in Table 14.2.

Table 14.2: LINT manipulators and their effects
Manipulator | Effect: form of the output of LINT values
LintBin | as binary numbers
LintDec | as decimal numbers
LintHex | as hexadecimal numbers
LintOct | as octal numbers
LintLwr | with lowercase letters a,b,c,d,e,f for hexadecimal representation
LintUpr | with uppercase letters A,B,C,D,E,F for hexadecimal representation
LintShowbase | with prefix for the numerical representation (0x or 0X for hexadecimal, 0b for binary)
LintNobase | without prefix for numerical representation
LintShowlength | indicating the number of digits
LintNolength | without indicating the number of digits

In addition to the manipulators of Table 14.2, which require no argument, the manipulators

  LINT_omanip<int> SetLintFlags (int flags)

and

  LINT_omanip<int> ResetLintFlags (int flags)

are available, which can be used as alternatives to the status functions LINT::setf() and LINT::unsetf():

  cout << SetLintFlags (LINT::flag) << ...; // turn on
  cout << ResetLintFlags (LINT::flag) << ...; // turn off

For the implementation of these manipulators the reader is referred to the sources (flintpp.h and flintpp.cpp) in connection with the explanation of the template class omanip<T> in [Pla2], Chapter 10. The LINT flags are shown once again in Table 14.3.

Table 14.3: LINT flags for output formatting and their effects
Flag | Value
lintdec | 0x010
lintoct | 0x020
linthex | 0x040
lintshowbase | 0x080
lintuppercase | 0x100
lintbin | 0x200
lintshowlength | 0x400

We shall now clarify the use of the format functions and manipulators by means of the following example:

 #include "flintpp.h"
 #include <iostream.h>
 #include <iomanip.h>

 main()
 {
   LINT n ("0x0123456789abcdef"); // LINT number with base 16
   long deflags = LINT::flags(); // store flags

   cout << "Default representation: " << n << endl;

   LINT::setf (LINT::linthex | LINT::lintuppercase);
   cout << "hex representation with uppercase letters: " << n << endl;
   cout << LintLwr << "hex representation with lowercase letters: " << n << endl;
   cout << LintDec << "decimal representation: " << n << endl;
   cout << LintBin << "binary representation: " << n << endl;
   cout << LintNobase << LintHex;
   cout << "representation without prefix: " << n << endl;
   cerr << "Default representation Stream cerr: " << n << endl;

   LINT::restoref (deflags);
   cout << "default representation: " << n << endl;

   return;
 }

14.3.3 File I/O for LINT Objects

Functions for the output of LINT objects to files and functions for reading them are unavoidable for practical applications. The input and output classes of the C++ standard library contain member functions that permit the setting of objects into an input or output stream for file operations, so we are fortunate in that we can use the same syntax as we used above. The operators needed for output to files are similar to those of the last section, where, however, we can do without the formatting.

We define the two operators

  friend ofstream& operator<< (ofstream& s, const LINT& ln);

  friend fstream& operator<< (fstream& s, const LINT& ln);

for output streams of the class ofstream and for streams of the class fstream, which supports both directions, that is, both input and output. Since the class ofstream is derived from the class ostream, we can use its member function ostream::write() to write unformatted data to a file. Since only the digits of a LINT object that are actually used are stored, we can deal sparingly with the storage space of the data medium. Here the USHORT digits of the LINT object are actually written as a sequence of UCHAR values. To ensure that this always occurs in the correct order, independent of the numerical representation scheme of a particular platform, an auxiliary function is defined that writes a USHORT value as a sequence of two UCHAR types. This function neutralizes the platform-specific ordering of the digits to base 256 in memory and thereby allows data that were written on one computer type to be read on another that possibly orders the digits of a number differently or perhaps interprets them differently when they are read from mass storage. Relevant examples in this connection are the little-endian and big-endian architectures of various processors, which in the former case order consecutive increasing memory addresses in increasing order, and in the latter case do so in decreasing order.[2]

template <class T>
  int write_ind_ushort (T& s, clint src)
  {
    UCHAR buff[sizeof(clint)];
    unsigned i, j;

    for (i = 0, j = 0; i < sizeof(clint); i++, j = i << 3)
      {
         buff[i] = (UCHAR)((src & (0xff << j)) >> j);
      }

    s.write (buff, sizeof(clint));

    if (!s)
      {
         return -1;
      }
    else
      {
         return 0;
      }
  }

The function write_ind_ushort() returns in the case of error the value −1, while it returns 0 if the operation is successful. It is implemented as a template, so that it can be used with both ofstream objects and fstream objects. The function read_ind_ushort() is created as its counterpart:

  template <class T>
  int read_ind_ushort (T& s, clint *dest)
  {
    UCHAR buff[sizeof(clint)];
    unsigned i;

    s.read (buff, sizeof(clint));

    if (!s)
      {
         return -1;
      }
    else
      {
        *dest = 0;
        for (i = 0; i < sizeof(clint); i++)
          {
             *dest |= ((clint)buff[i]) << (i << 3);
          }
        return 0;
     }
  }

The output operators now use this neutral format to write from a LINT object to a file. To elucidate the situation we shall present the implementation of the operator for the class ofstream.

ofstream& operator<< (ofstream& s, const LINT& ln)
  {
    if (!ln.init)
      {
         LINT::panic (E_LINT_VAL, "ofstream operator<<", 0, __LINE__);
      }
    for (int i = 0; i <= DIGITS_L (ln.n_l); i++)
      {
         if (write_ind_ushort (s, ln.n_l[i]))
           {
              LINT::panic (E_LINT_EOF, "ofstream operator<<", 0, __LINE__);
           }
      }

    return s;
  }

Before a LINT object is written to a file, the file must be opened for writing, for which one could use the constructor

  ofstream::ofstream (const char *, openmode)

or the member function

  ofstream::open (const char *, openmode)

In each case the ios flag ios::binary must be set, as in the following example:

  LINT r ("0x0123456789abcdef");
  // ...
  ofstream fout ("test.io", ios::out | ios::binary);
  fout << r << r*r;
  // ...
  fout.close();

The importation of a LINT object from a file is effected in a complementary way, with analogous operators, to that of output of a LINT object to a file.

  friend ifstream& operator >> (ifstream& s, LINT& ln);
  friend fstream& operator >> (fstream& s, LINT& ln);

Both operators first read a single value, which specifies the number of digits of the stored LINT object. Then the corresponding number of digits are read in. The USHORT values are read according to the above description under the action of the function read_ind_ushort():

  ifstream& operator>> (ifstream& s, LINT& ln)
  {
    if (read_ind_ushort (s, ln.n_l))
      {
         LINT::panic (E_LINT_EOF, "ifstream operator>>", 0, __LINE__);
      }
    if (DIGITS_L (ln.n_l) < CLINTMAXSHORT)
      {
         for (int i = 1; i <= DIGITS_L (ln.n_l); i++)
           {
              if (read_ind_ushort (s, &ln.n_l[i]))
                {
                  LINT::panic (E_LINT_EOF, "ifstream operator>>", 0, __LINE__);
                }
           }
      }

    // No paranoia! Check the imported value!
    if (vcheck_l (ln.n_l) == 0)
      {
         ln.init = 1;
      }
    else
      {
         ln.init = 0;
      }
  return s;
  }

To open a file from which the LINT object is to be read it is again necessary to set the ios flag ios::binary:

  LINT r, s;
  // ...
  ifstream fin;
  fin.open ("test.io", ios::in | ios::binary);
  fin >> r >> s;
  // ...
  fin.close();

In the importation of LINT objects the insert operator >> checks whether the values read represent the numerical representation of a valid LINT object. If this is not the case, the member datum init is set to 0, and the specified target object is thereby marked as "uninitialized." On the next operation on this object the LINT error handler is invoked, which is what we shall study in more detail in the next chapter.

[1]We use this name of the stream classes as a synonym for the terms now used in the C++ standard library, with which the class names known up to now are prefixed with basic_. The justification for this comes from the standard library itself, where the class names known hitherto are provided with corresponding typedefs (cf. [KSch], Chapter 12).

[2]Two bytes Bi and Bi+1 with addresses i and i + 1 are interpreted in the little-endian representation as USHORT value w = 28Bi+1 + Bi and in the big-endian representation as w = 28Bi + Bi+1. The analogous situation holds for the interpretation of ULONG values.


Chapter 15: Error Handling

O hateful error, melancholy's child!

—Shakespeare, Julius Caesar

15.1 (Don't) Panic ...

The C++ functions presented in the foregoing chapters embody mechanisms for analyzing whether during the execution of a called C function an error or other situation has occurred that requires a particular response or at least a warning. The functions test whether the passed variables have been initialized and evaluate the return value of the called C functions:

  LINT f (LINT arg1, LINT arg2)
  {
    LINT result;
    int err;

    if (!arg1.init) LINT::panic (E_LINT_VAL, "f", 1, __LINE__);
    if (!arg2.init) LINT::panic (E_LINT_VAL, "f", 2, __LINE__);

    // Call C function to execute operation; error code is stored in err
    err = f_l (arg1.n_l, arg2.n_l, result.n_l);

    switch (err)
      {
         case 0:
           result.init = 1;
           break;
         case E_CLINT_OFL:
           result.status = E_LINT_OFL;
           result.init = 1;
           break;
         case E_CLINT_UFL:
           result.status = E_LINT_UFL;
           result.init = 1;
           break;
         default:
           LINT::panic (E_LINT_ERR, "f", err, __LINE__);
      }

    return result;
  }

If the variable status contains the value E_LINT_OK, then this is the optimal case. In less happy situations, in which overflow or underflow has occurred in a C function, the variable status is set to the appropriate value E_LINT_OFL or E_LINT_UFL. Since our C functions already react to an overflow or underflow with a reduction modulo Nmax + 1 (cf. page 18), in such cases the functions terminate normally. The value of the variable status can then be queried with the member function

  LINT_ERRORS LINT::Get_Warning_Status (void);

Furthermore, we have seen that the LINT functions always call a function with the well-chosen name panic() when the situation gets too hot to handle. The task of this member function is first of all to output error messages, so that the user of the program is made aware that something has gone awry, and secondly to ensure a controlled termination of the program. The LINT error messages are output via the stream cerr, and they contain information about the nature of the error that has occurred, about the function that has detected the error, and about the arguments that have triggered the error. In order that panic() be able to output all of this information, such information arriving from the calling function must be delivered, as in the following example:

  LINT::panic (E_LINT_DBZ, "%", 2, __LINE__);

Here it is announced that a division by zero in the operator "%" has appeared in the line specified by the ANSI macro __LINE__, caused by the operator's argument number 2. The arguments are indicated as follows: 0 always denotes the implicit argument of a member function, and all other arguments are numbered from left to right, beginning with 1. The LINT error routine panic() outputs error messages of the following type:

Example 1: Use of an uninitialized LINT object as argument.
Start example
     critical run-time error detected by class LINT:
     Argument 0 in Operator *= uninitialized, line 1997
     ABNORMAL TERMINATION
End example
Example 2: Division by a LINT object with the value 0.
Start example
     critical run-time error detected by class LINT:
     Division by zero, operator/function/, line 2000
     ABNORMAL TERMINATION
End example

The functions and operators of the LINT class recognize the situations listed in Table 15.1.

Table 15.1: LINT function error codes
Code | Value | Explanation
E_LINT_OK | 0x0000 | everything ok
E_LINT_EOF | 0x0010 | file I/O error in the stream operator << or >>
E_LINT_DBZ | 0x0020 | division by zero
E_LINT_NHP | 0x0040 | Heap error: new returns the NULL pointer
E_LINT_OFL | 0x0080 | overflow in function or operator
E_LINT_UFL | 0x0100 | underflow in function or operator
E_LINT_VAL | 0x0200 | an argument of a function is uninitialized or has an illegal value
E_LINT_BOR | 0x0400 | incorrect base passed as argument to a constructor
E_LINT_MOD | 0x0800 | even modulus in mexpkm()
E_LINT_NPT | 0x1000 | NULL pointer passed as argument


15.2 User-Defined Error Handling

As a rule, it is necessary to adapt error handling to particular requirements. The LINT class offers support in this regard in that the LINT error function panic() can be replaced by user-defined functions. Additionally, the following function is called, which takes as argument a pointer to a function:

  void
  LINT::Set_LINT_Error_Handler (void (*Error_Handler)
        (LINT_ERRORS, const char* const, const int, const int))
  {
    LINT_User_Error_Handler = Error_Handler;
  }

The variable LINT_User_Error_Handler is defined and initialized in flintpp.cpp as

  static void (*LINT_User_Error_Handler)
         (LINT_ERRORS, const char*, const int, const int) = NULL;

If this pointer has a value other than NULL, then the specified function is called instead of panic(), and it contains the same information as panic() would have. With respect to the implementation of a user-defined error-handling routine one has a great deal of freedom. But one must realize that the errors reported by the class LINT usually signal program errors, which are irreparable at run time. It would make no sense to return to the program segment in which such an error has occurred, and in general, in such cases the only reasonable course of action is to terminate the program.

The return to the LINT error routine panic() is effected by a call to

  LINT::Set_LINT_Error_Handler(NULL);

The following example demonstrates the integration of a user-defined function for error handling:

#include "flintpp.h"

void my_error_handler (LINT_ERRORS err, const char* str,
                                   const int arg, const int line)
{
  //... Code
}

main()
{
  // activation of the user-defined error handler:
  LINT::Set_LINT_Error_Handler (my_error_handler);

  // ... Code

  // reactivate the LINT error handler:
  LINT::Set_LINT_Error_Handler (NULL);

  // ... Code
}


15.3 LINT Exceptions

The exception mechanism of C++ is an instrument that is easier to utilize and thereby more effective for error handling than the methods offered by C. The error routine LINT::panic() described previously is limited to the output of error messages and the controlled termination of a program. In general, we are less interested in the division function in which a division by zero has occurred than the function that has called the division and thereby precipitated the error, information that LINT::panic() does not contain and thus cannot pass along. In particular, it is impossible with LINT::panic() to return to this function in order to remove an error there or to react in a way specific to the function. Such possibilities, on the other hand, are offered by the exception mechanism of C++, and we would like here to create the conditions that will make this mechanism usable for the LINT class.

Exceptions in C++ are based principally on three types of constructs: the try block, the catch block, and the instruction throw, by means of which a function signals an error. The first, the catch block, has the function of a local error-handling routine for the try block: Errors that occur within a try block and are announced by means of throw will be caught by the catch block, which follows the try block. Further instructions of the try block are then ignored. The type of error is indicated by the value of the throw instruction as parameter of the accompanying expression.

The connection between try and catch blocks can be sketched as follows:

  try
    {
       ... // If an error is signaled within an operation with
       ... // throw, then it can be
       ... // caught by the following catch block.
    }
   ...

  catch (argument)
    {
       ... // here follows the error handling routine.
    }

If an error does not occur directly within a try block but in a function that is called from there, then this function is terminated, and control is returned to the calling function until, by following the chain of calls in reverse order, a function within a try block is reached. From there control is passed to the appropriate catch block. If no try block is found, then the generic error routine appended by the compiler is called, which then terminates the program, usually with some nonspecific output.

It is clear what the errors are in the LINT class, and it would be a simple possibility to call throw with the error codes, which are provided to the panic() routine by the LINT functions and operators. However, the following solution offers a bit more comfort: We define an abstract base class

  class LINT_Error
  {
    public:
      char* function;
      int argno, lineno;
      virtual void debug_print (void) const = 0; // pure virtual
      virtual LINT_Error() {};
  };

as well as classes of the following type that build on it:

  // division by zero
  class LINT_DivByZero : public LINT_Error
  {
    public:
      LINT_DivByZero (const char* const func, const int line);
      void debug_print (void) const;
  };

  LINT_DivByZero::LINT_DivByZero (const char* const func, const int line)
  {
    function = func;
    lineno = line;
    argno = 0;
  }

  void LINT_DivByZero::debug_print (void) const
  {
    cerr << "LINT-Exception:" << endl;
    cerr << "division by zero in function "
           << function << endl;
    cerr << "module: " << __FILE__ << ", line: "
           << lineno << endl;
  }

For every type of error there exists such a class that like the example shown here can be used with

  throw LINT_DivByZero(function, line);

to report this particular error. Altogether, the following subclasses of the base class LINT_Error are defined:

  class LINT_Base : public LINT_Error // invalid basis
  { ... };

  class LINT_DivByZero : public LINT_Error // division by zero
  { ... };

  class LINT_EMod : public LINT_Error // even modulus for mexpkm
  { ... };

  class LINT_File : public LINT_Error // error with file I/O
  { ... };

  class LINT_Heap : public LINT_Error // heap error with new
  { ... };

  class LINT_Init : public LINT_Error // function argument illegal or uninitialized
  { ... };

  class LINT_Nullptr : public LINT_Error // null pointer passed as argument
  { ... };

  class LINT_OFL : public LINT_Error // overflow in function
  { ... };

  class LINT_UFL : public LINT_Error // underflow in function
  { ... };

With this we are in a position, on the one hand, to catch LINT errors without distinguishing specifically which error has occurred by inserting a catch block

  catch (LINT_Error const &err) // notice: LINT_Error is abstract
    {
      // ...
      err.debug_print();
      // ...
    }

after a try block, while on the other hand we can carry on a goal-directed search for an individual error by specifying the appropriate error class as argument in the catch instruction.

One should note that as an abstract base class LINT_Error is not instantiatable as an object, for which reason the argument err can be passed only by reference and not by value. Although all the LINT functions have been equipped with the panic() instruction for error handling, the use of exceptions does not mean that we must alter all the functions. Rather, we integrate the appropriate throw instructions into the panic() routine, where they are called in conjunction with the error that has been reported. Control is then transferred to the catch block, which belongs to the try block of the calling function. The following code segment of the function panic() clarifies the modus operandi:

  void LINT::panic (LINT_ERRORS error, const char const * func,
              const int arg, const int line)
  {
    if (LINT_User_Error_Handler)
      {
         LINT_User_Error_Handler (error, func, arg, line);
      }
    else
      {
         cerr << "critical run-time error detected by the
                           class LINT:\n";
         switch (error)
           {
              case E_LINT_DBZ:
                cerr << "division by zero, operator/function "
                  << func << ", line " << line << endl;
  #ifdef LINT_EX
                throw LINT_DivByZero (func, line);
  #endif
                break;

                // ...
           }
      }
  }

The behavior that results in the case of an error can be completely controlled by user-defined routines for error handling without the necessity of intervention into the LINT implementation. Moreover, the exception handling can be completely turned off, which is necessary when this mechanism is not supported by a C++ compiler that is to be used. In the case of the present panic() function the exceptions must be turned on explicitly via the definition of the macro LINT_EX, such as with the compiler option -DLINT_EX. Some compilers require the specification of additional options for exception handling to be activated.

To close, we present a small demonstration of the LINT exceptions:

  #include "flintpp.h"
  main(void)
  {
    LINT a = 1, b = 0;
    try
      {
         b = a / b;// error: division by 0
      }

    catch (LINT_DivByZero error) // error handling for
                                   // division by 0
         error.debug_print ();
         cerr << "division by zero in the module" << __FILE__
              << ", line " << __LINE__;
      }
  }

Translated with GNU gcc by a call to

  gcc -fhandle-exceptions -DLINT_EX divex.cpp flintpp.cpp flint.c -lstdc++

the program produces, in addition to the error message of the function panic(), the following output:

  LINT-Exception:
  division by zero in operator/function /
  module: flintpp.cpp, line: 402
  division by zero in module divex.cpp, line 17

The significant difference between this and standard error handling without exceptions is that we discover by means of the catch routine where the error was actually caused, namely in line 17 of the module divex.cpp, even though it was discovered somewhere else entirely, namely in the module flintpp.cpp. For debugging large programs this is an extremely helpful source of information.


Chapter 16: An Application Example: The RSA Cryptosystem

Overview

The next question was the obvious one, "Can this be done with ordinary encipherment? Can we produce a secure encrypted message, readable by the authorised recipient without any prior secret exchange of the key etc?" . . . I published the existence theorem in 1970.

—J. H. Ellis, "The Story of Non-Secret Encryption"

AS WE APPROACH THE END of our story we would like to investigate the possibility of testing what we have labored over chapter by chapter against a realistic and current example, one that clearly demonstrates the connection between the theme of cryptographic application and the deployment of our programmed functions. We shall make a brief excursion into the principle of asymmetric cryptosystems and then turn our attention to the RSA algorithm as the classic example of such a system, which was published in 1978 by its inventors/discoverers, Ronald Rivest, Adi Shamir, and Leonard Adleman (see [Rive], [Elli]), and which by now has been implemented worldwide.[1] The RSA algorithm is patented in the United States of America, but the patent expired on 20 September 2000. Against the free use of the RSA algorithm stood the claims of RSA Security, who possessed rights to the trade name "RSA," which triggered vehement discussion in connection with work on the standard P1363 [IEEE], with in some cases rather grotesque results, for example, the suggestion of rechristening the RSA procedure "biprime cryptography." There have also appeared less serious suggestions, such as FRA (former RSA algorithm), RAL (Ron, Adi, Leonard), and QRZ (RSA − 1). Upon expiry of their patent RSA Security weighed in with its opinion:

Clearly, the terms "RSA algorithm," "RSA public-key algorithm," "RSA cryptosystem," and "RSA public-key cryptosystem" are well established in standards and open academic literature. RSA Security does not intend to prohibit the use of these terms by individuals or organizations that are implementing the RSA algorithm ("RSA-Security—Behind the Patent," September 2000).[2]

[1]According to http://www.rsasecurity.com by 1999 over three hundred million products containing RSA functions had been sold.

[2]http://www.rsasecurity.com/developers/total-solution/faq.htm (9/2000)


16.1 Asymmetric Cryptosystems

The fundamental idea behind asymmetric cryptosystems was published in 1976 by Whitfield Diffie and Martin Hellman in the groundbreaking article "New Directions in Cryptography" (see [Diff]). Asymmetric cryptosystems, in contrast to symmetric algorithms, do not use a secret key employed both for encryption and decryption of a message, but a pair of keys for each participant consisting of a public key E for encryption and a different, secret, key D for decryption. If the keys are applied to a message M one after another in sequence, then the following relation must hold:

(16.1)

One might picture this arrangement as a lock that can be closed with one key but for which one needs a second key to unlock it.

For the sake of security of such a procedure it is necessary that a secret key D not be able to be derived from the public key E, or that such a derivation be infeasible on the basis of time and cost constraints.

In contrast to symmetric systems, asymmetric systems enable certain simplifications in working with keys, since only the public key of a participant A need be transmitted to a communication partner B for the latter to be in a position to encrypt a message that only participant A, as possessor of the secret key, can decrypt. This principle contributes decisively to the openness of communication: For two partners to communicate securely it suffices to agree on an asymmetric encryption procedure and exchange public keys. No secret key information needs to be transmitted. However, before our euphoria gets out of hand we should note that in general, one cannot avoid some form of key management even for asymmetric cryptosystems. As a participant in a supposedly secure communication one would like to be certain that the public keys of other participants are authentic, so that an attacker, with the nefarious goal of intercepting secret information, cannot undetected interpose him- or herself and give out his or her key as the public key under the guise of its being that of the trusted partner. To ensure the authenticity of public keys there have appeared surprisingly complex procedures, and in fact, there are already laws on the books that govern such matters. We shall go into this in more detail below.

The principle of asymmetric cryptosystems has even more far-reaching consequences: It permits the generation of digital signatures in which the function of the key is turned on its head. To generate a digital signature a message is "encrypted" with a secret key, and the result of this operation is transmitted together with the message. Now anyone who knows the associated public key can "decrypt" the "encrypted" message and compare the result with the original message. Only the possessor of the secret key can generate a digital signature that can withstand such a comparison. We note that in the case of digital signatures the terms "encryption" and "decryption" are not quite the correct ones, so that we shall speak rather of "generation" and "verification" of a digital signature.

A requirement for the implementation of an asymmetric encryption system for the generation of digital signatures is that the association of D (M) and M can be reliably verified. The possibility of such a verification exists if the mathematical operations of encryption and decryption are commutative, that is, if their execution one after the other leads to the same, original, result regardless of the order in which they are applied:

(16.2)

By application of the public key E to D (M) it can be checked in this case whether D (M) is valid as a digital signature applied to the message M.

The principle of digital signatures has attained its present importance in two important directions:

  • The laws on digital, respectively electronic, signatures in Europe and the United States create a basis for the future use of digital signatures in legal transactions.

  • The increasing use of the Internet for electronic commerce has generated a strong demand for digital signatures for identification and authentication of those taking part in commercial transactions, for authenticating digital information, and for ensuring the security of financial transactions.

It is interesting to observe that the use of the terms "electronic signature" and "digital signature" bring into focus the two different approaches to signature laws: For an electronic signature all means of identification used by one party, such as electronic characters, letters, symbols, and images, are employed to authenticate a document. A digital signature, on the other hand, is realized as an electronic authentication procedure based on information-technological processes that is employed to verify the integrity and authenticity of a transmitted text. Confusion arises because these two terms are frequently used interchangeably, thus mixing up two different technical processes (see, for example, [Mied]).

While the laws on electronic signatures in general leave open just what algorithms will be used for the implementation of digital signatures, most protocols being discussed or already implemented for identification, authentication, and authorization in the area of electronic transactions over the Internet are based on the RSA algorithm, which suggests that it will continue to dominate the field. The generation of digital signatures by means of the RSA algorithm is thus a particularly current example of the application of our FLINT/C functions.

The author is aware that the following paragraphs represent a painfully brief introduction to an enormously significant cryptographic principle. Nevertheless, such brevity seems to be justified by the large number of extensive publications on this topic. The reader wishing to know more is referred to [Beut], [Fumy], [Salo], and [Stin] as introductory sources, to the more comprehensive works [MOV] and [Schn], and to the more mathematically oriented monographs [Kobl], [Kran], and [HKW].


16.2 The RSA Algorithm

All that is merely probable is probably false.

—René Descartes

We shall now present a brief outline of the mathematical properties of the RSA algorithm, and we shall see how the RSA procedure can be implemented both as an asymmetric cryptosystem and an asymmetric signature scheme. Following the mathematical principles of the RSA algorithm we shall develop C++ classes with RSA functions for encryption and decryption as well as for the generation and authentication of digital signatures. In this way we shall clarify the possibilities offered for the implementation of the methods of our LINT class.

The most important aspect of the RSA algorithm is the pair of keys, which have a particular mathematical form: An RSA key pair consists of three basic components: the modulus n, the public key component e (encryption), and the secret key component d (decryption). The pairs <e, n> and <d, n> then form a public and private key.

We first generate the modulus n as the product n = pq of two prime numbers p and q. If φ (n) = (p − 1)(q − 1) denotes the Euler phi function (cf. page 175), then for a given n the public key component e can be chosen such that e < φ(n) and gcd (e, φ(n)) = 1. The secret component d corresponding to n and e is obtained by calculating the inverse d = e− 1 mod φ(n) (cf. Section 10.2).

We illustrate this principle with the help of a small example: We choose p = 7 and q = 11. Then we have n = 77 and φ(n) = 22 · 3 · 5 = −60. Due to the condition gcd (e, φ(n)) = 1, the least possible value for the key component e is 7, from which we derive the value d = 43 for the key component d, since 7 · 43 ≡ 301 ≡ 1 mod 60. With these values we can apply the RSA algorithm to a toy example, in which we calculate, say, that the "message" 5 is encrypted to 57 mod 77 = 47 and the decryption 4743 mod 77 = 5 restores the original message.

Equipped now with such keys (we shall soon discuss what constitutes a realistic size for the various key components) and appropriate software, communication partners can securely exchange information with each other. To demonstrate the procedure we consider the process by which a participant Ms. A sends an RSA-encoded message to a communication partner Mr. B:

  1. B generates his RSA key with components nB, dB, and eB. He then gives the public key <eB, nB> to A.

  2. Now A would like to send an encrypted message M to B (0 ≤ M < nB). Since A has received from B his public key <eB, nB>, A calculates

    and sends the encrypted value C to B.

  3. After B has obtained the encrypted message C from A, then B decodes this message by calculating

    using his secret key <dB, nB>. Now B possesses the plain text M of the message.

It is not difficult to see why this works. Because d · e ≡ 1 mod φ(n), there exists an integer k such that d · e = 1 + k · φ(n). We thus obtain

(16.3) Click To expand

where we have made use of the theorem of Euler quoted on page 175, from which we deduce that Mφ(n) ≡ 1 mod n if gcd(M, n) = 1.

It is clear that the security of the RSA algorithm depends on the ease of factorability of n. If n can be factored into its components p and q, then the secret key d can be determined from the public key e. Conversely, the factorization of n can be easily accomplished if both key components d and e are known: If k := de − 1, then k is a multiple of φ(n), and therefore we have k = r · 2t with odd r and t ≥ 1. For every g Î [image] we have gk ≡ gde− 1 ≡ gg−1 ≡ 1 mod n, and therefore gk/2 is a square root of 1 modulo n, of which there are four: In addition to ±1 there are the roots ±x with x ≡ 1 mod p and x ≡ −1 mod q. Thus p | (x − 1) and q | (x + 1) (cf. Section 10.4.3). By calculating p = gcd(x − 1, n) one thus obtains the factorization of n (cf. page 208).

Possibilities for attacking the RSA algorithm other than factorization of the modulus are either as expensive as this or rely on the special weaknesses of individual protocols used in implementing the RSA cryptosystem, but do not rely on the RSA algorithm itself. Based on the current state of know ledge the following conditions lead to opportunities to attack the RSA algorithm:

  1. Common modulus

    The use of a common modulus for several participants leads to an obvious weakness: Based on what we have just said, each participant can use his or her own key components e and d to factorize the common modulus n = pq. From a knowledge of the factors p and q as well as the public key components of other participants with the same modulus their secret keys can be calculated.

  2. Small public exponents

    Since the computational time for the RSA algorithm for a given modulus depends directly on the size of the exponents e and d, it would appear attractive to choose these as small as possible. For example, 3, as the smallest possible exponent, requires only one squaring and one multiplication modulo n, so why not save valuable computing time in this way?

    Let us assume that an attacker was able to capture three encoded messages C1, C2 and C3, each of which encodes the same plain text M, encoded with the keys <3, ni> of three different recipients:

    Click To expand

    It is highly probable that gcd (ni, nj) = 1 for i ≠ j, so that the attacker can use the Chinese remainder theorem (cf. page 199) to find a value C for which

    Since it is also true that M3 < n1n2n3, we have that C is actually equal to M3, and the attacker can obtain M directly by calculating [image] . Such assaults, known as broadcast attacks, can always be carried out when the number of cryptograms Ci is greater than the public exponent, and this holds even if the plain texts to be encoded are not identical but are merely linearly dependent on one another, that is, if relations like Mi = a + b · Mj hold (cf. [Bone]). To avoid such an attack it is therefore necessary to choose the public exponents not too small (in no case less than 216 + 1 = 65537) and in addition to add random redundancy to broadcast messages before their encryption. This can take place by filling in the message in some appropriate way up to a suitable value less than the modulus. Such a process is called padding (cf. page 335 and [Schn], Section 9.1).

  3. Small secret exponents

    Even more problematic than small public exponents are small secret exponents: M. Wiener [Wien] showed already in 1990 how, given a key <e, n> with e < φ(n), the associated private key component d can be calculated if d is too small. Wiener's result was recently sharpened by D. Boneh and G. Durfee [Bone], who showed that d can be computed from <e, n> if d < n0.292. However, it is conjectured that the result holds as well for d < n0.5. We thus have the following lower bounds for d for typical sizes of RSA moduli:

    n

    d

    2768

    2384

    21024

    2512

    22048

    21024

    24096

    22048

  4. Weaknesses in implementation

    In addition to the weaknesses caused by the choice of parameters there is a host of potential implementation problems that can adversely affect the security of the RSA algorithm, as is the case with any encryption procedure. Certainly, the greatest care must be taken with implementations completely in software that are not protected from outside attack by measures implemented in hardware. Reading from memory contents, observation of bus activity or CPU states, can lead to the disclosure of secret key information. At the minimum, all data in main memory that in any way are correlated with the secret components of the RSA algorithm (or any other cryptosystem) should be erased immediately after use with active overwriting (for example, with the function purge_l(), which appears on page 162).

    The functions in the FLINT/C library are already equipped for this purpose. In secure mode local variables and allocated memory are overwritten with zeros before termination of the function and are thereby deleted. Here a certain degree of care is necessary, since the optimization capabilities of the compiler are so highly developed that a simple instruction at the end of a function that can be seen to have no effect on the function's termination might simply be ignored. And there is more: One must take into account that calls to the function memset() in the C standard library are ignored if the compiler cannot recognize any useful purpose in calling it.

    The following example illustrates these effects. The function f_l() uses two automatic variables: CLINT key_l and USHORT secret. At the end of the function, whose contents are of no further interest here, memory should be overwritten by assigning 0 to secret and, respectively, by a call to memset() in the case of key_l. The C code looks like this:

      int
  f_l (CLINT n_l)
  {
    CLINT key_l;
    USHORT secret;
     ...
     ...
     ...
    /* overwrite the variables */
    secret = 0;
    memset (key_l, 0, sizeof (key_l));
    return 0;
  }

    And what does the compiler make of this (Microsoft Visual C/C++ 6.0, compilation with cl -c -FAs -02)?

      PUBLIC   _f
  ;        COMDAT _f
  _TEXT    SEGMENT
  _ key _l$ = -516
  _ secret $ = -520
  _f       PROC NEAR                                ; COMDAT

  ; 5    :    CLINT key _l;
  ; 6    :    USHORT secret;
     ...
     ...
     ...
  ; 18   :    /* overwrite the variables */
  ; 19   :    secret = 0;
  ; 20   :    memset (key_l, 0, sizeof (key _l));
  ; 21   :
  ; 22   :    return 0;

           xor eax, eax

  ; 23 : }

           add esp, 532                             ; 00000214H
           ret 0
  _f       ENDP
  _TEXT    ENDS

    The assembler code generated by the compiler documents that the instructions to delete the variables key_l and secret are passed over without effect. From the point of view of optimization this is a desirable result. Even the inline version of the function memset() is simply optimized away. For security-critical applications, however, this strategy is simply too clever.

    The active deletion of security-critical variables by overwriting must therefore be implemented in such a way that it is actually carried out. One should note that in this case assertions can prevent the checking for effectiveness, since the presence of the assertions forces the compiler to execute the code. When the assertions are turned off, then optimization again goes into effect.

    For the FLINT/C package the following function is implemented, which accepts a variable number of arguments and treats them according to their size as standard integer types and sets them to 0, or for other data structures calls memset() and lets it do the overwriting:

      static void purgevars_l (int noofvars, ...)
  {
    va_list ap;
    size_t size;

    va_start (ap, noofvars);
    for (; noofvars > 0; -noofvars)
      {
         switch (size = va_arg (ap, size_t))
           {
              case 1:*va_arg (ap, char *) = 0;
                     break;
              case 2:*va_arg (ap, short *) = 0;
                     break;
              case 4:*va_arg (ap, long *) = 0;
                     break;
              default: Assert (size >= CLINTMAXBYTE);
                     memset (va_arg(ap, char *), 0, size);
           }
      }
    va_end (ap);
  }

    The function expects pairs of the form (byte length of the variable, pointer to the variable) as arguments, prefixed in noofvars by the number of such pairs.

    As an extension of this function the macro PURGEVARS_L() is defined by

      #ifdef FLINT_SECURE
  #define PURGEVARS_L(X) purgevars_l X
  #else
  #define PURGEVARS_L(X) (void)0
  #endif /* FLINT_SECURE */

    so that security mode can be turned on and off as required. Deletion of the variables in f() can take place as follows:

      /* overwrite the variables */
  PURGEVARS_L ((2, sizeof (secret), & secret,
                  sizeof (key_l), key_l));

    The compiler cannot ignore the call to this function on the principle of optimization strategy, which could be accomplished only by an extraordinarily effective global optimization. In any case, the effect of such security measures should be checked by means of code inspection:

      PUBLIC   _f
  EXTRN    _purgevars_l:NEAR
  ;        COMDAT _f
  _TEXT    SEGMENT
  _key_l$ = -516
  _secret$ = -520
  _f       PROC NEAR                                    ; COMDAT

  ; 9 : {

           sub  esp, 520                                ; 00000208H

  ; 10   :    CLINT key_l;
  ; 11   :    USHORT secret;
     ...
     ...
     ...
  ; 18   :        /* overwrite the variables */
  ; 19   :        PURGEVARS_L ((2, sizeof (secret), &secret,
                                  sizeof (key_l), key_l));

           lea  eax, DWORD PTR _key_l$[esp+532]
           push eax
           lea ecx, DWORD PTR _secret$[esp+536]
           push 514                                     ; 00000202H
           push ecx
           push 2
           push 2
           call _purgevars_l

  ; 20   :
  ; 21   :        return 0;

           xor  eax, eax

  ; 22   : }

           add  esp, 552                                ; 00000228H
           ret  0
  _f       ENDP
  _TEXT    ENDS

    As a further protective measure in connection with the implementation of security-critical applications we should mention a comprehensive mechanism for error handling that sees to it that even in the case of invalid arguments or other exceptional situations no sensitive information is divulged. Likewise, suitable measures should be considered for establishing the authenticity of the code of a cryptographic application, so that the insertion of Trojan horses is prevented or at least detected before the code can be executed. Taking a cue from the story of the Trojan War, Trojan horse is the name given to software that has been altered in such a way that it apparently functions correctly but has additional undesirable effects such as the transmittal of secret key information to an attacker via an Internet connection.

    To get a grip on such problems, frequently, in practice, for cryptographic operations, "security boxes," or "S-Boxes" for short, are implemented, whose hardware is protected against attack by encapsulation in conjunction with detectors or sensors.

If all these traps are avoided, then there remains only the risk that the modulus will be factored, and this risk can be effectively eliminated by choosing sufficiently large prime numbers. To be sure, it has not been proved that there is no easier method to break the RSA algorithm than factorization, and there is also no proof that factorization of large integers is truly as difficult a problem as it presently seems to be, but these issues have not adversely affected the practical application of the algorithm to date: The RSA algorithm is the most commonly implemented asymmetric cryptosystem worldwide, and its use over the Internet has steadily increased, particularly for authentication in the exchange of information.

Based on the current state of knowledge in reference to factorability, the important requirements in the generation of RSA keys are that the prime factors p and q have approximately the same number of digits, that they be sufficiently large, and that p and q be nonetheless different in a sufficient number of binary digits. This avoids the situation where the modulus n can be easily factored on account of p ≈ q ≈ [image] by being divided by natural numbers close to [image] .

There are many places in the literature where it is recommended that so-called strong primes p and q be used in order to protect the modulus against some of the simpler factorization methods. A prime p is called strong in this connection if

  1. p − 1 has a large prime divisor r,

  2. p + 1 has a large prime divisor s,

  3. r − 1 has a large prime divisor t.

There are varying opinions as to the importance of strong primes to the security of the RSA algorithm, and their necessity is not everywhere equally emphasized. Recently, the majority of opinions appear to state that while the use of strong prime numbers is not harmful, it also does not accomplish a great deal (cf. [MOV], Section 8.2.3, Note 8.8, as well as [RegT], Appendix 1.4) or even that they should not be used (see [Schn], Section 11.5). In the program example that follows we shall therefore do without the generation of strong primes. For those who are nonetheless interested, we sketch here a procedure for constructing such primes:

  1. The first step in the construction of a strong prime p with ℓp binary digits consists in the search for primes s and t satisfying log2(s) ≈ log2(t) ≈ [image] ℓp − log2ℓp. Then we search for a prime r for which r − 1 is divisible by t, by testing sequentially numbers of the form r = k · 2t + 1, k = 1, 2, . . . , for primality until we encounter a prime number. This almost always occurs in at most ⌊2 ln 2t⌋ steps (cf. [HKW], page 418).

  2. We now invoke the Chinese remainder theorem (see page 199) to calculate a solution of the simultaneous congruences x ≡ 1 mod r and x ≡ −1 mod s, by setting x0 := 1 − 2r−1 s mod rs, where r−1 is the multiplicative inverse of r modulo s.

  3. For our prime number search we use an odd initial value: We generate a random number z with a number of digits close to but less than (sometimes denoted by [image] ) the desired length of p, and set x0 ← x0 + z + rs − (z mod rs). If x0 is even, then we set x0 ← x0 + rs. With x0 in hand we begin our determination of p. We test the values p = x0 + k · 2rs, k = 0, 1, . . . , until the desired number of digits ℓp for p is reached and p is prime. If an RSA key is to contain a specified public exponent e, then it is worthwhile to ensure additionally that gcd(p − 1, e) = 1. The above conditions on p have now been completely fulfilled. For the prime number tests we use the Miller-Rabin test implemented in the function prime_l().

Whether or not strong primes are used for keys, in every case it is practical to have available a function that generates primes of a specified length or within a specified interval. A procedure for this that additionally ensures that a prime p thus generated satisfies the further condition gcd(p − 1, f) = 1 for a specified number f is given in [IEEE], page 73. Here is the algorithm in a slightly altered form.

Algorithm to generate a prime p such that pmin ≤ p ≤ pmax

  1. Generate a random number p, pmin ≤ p ≤ pmax.

  2. If p is even, set p ← p + 1.

  3. If p > pmax, set p ← pmin + p mod (pmax + 1) and go to step 2.

  4. Compute d := gcd(p − 1, f) (cf. Section 10.1). If d = 1, test p for primality (cf. Section 10.5). If p is prime, then output p and terminate the algorithm. Otherwise, set p ← p + 2 and go to step 3.

A realization of this procedure as a C++ function is contained in the FLINT/C package (source file flintpp.cpp).

Start Sidebar

Function:

generation of a prime number p within an interval [pmin, pmax] satisfying the additional condition gcd(p − 1, f) = 1, f an odd positive integer

Syntax:

const LINT
findprime(const LINT& pmin,
const LINT& pmax, const LINT& f);

Input:

pmin: smallest permissible value

pmax: largest permissible value

f: odd positive integer, which should be relatively prime to p − 1

Return:

LINT prime p determined by a probabilistic test (cf. Section 10.5) with gcd(p − 1, f)
End Sidebar
  const LINT findprime (const LINT& pmin, const LINT& pmax, const LINT& f)
  {
    if (!pmin.init) LINT::panic (E_LINT_VAL, "findprime", 1, __LINE__);
    if (!pmax.init) LINT::panic (E_LINT_VAL, "findprime", 2, __LINE__);
    if (pmin > pmax) LINT::panic (E_LINT_VAL, "findprime", 1, __LINE__);
    if (!f.init) LINT::panic (E_LINT_VAL, "findprime", 3, __LINE__);

    // 0 < f must be odd
    if (f.iseven()) LINT::panic (E_LINT_VAL, "findprime", 3, __LINE__);

    LINT p = randBBS (pmin, pmax);
    LINT t = pmax - pmin;

    if (p.iseven())
      {
         ++p;
      }

    if (p > pmax)
      {
         p = pmin + p % (t + 1);
      }

    while ((gcd (p - 1, f) != 1) || !p.isprime())
      {
         ++p;
         ++p;

         while (p > pmax)
           {
              p = pmin + p % (t + 1);

              if (p.iseven())
                {
                  ++p;
                }
           }
      }

    return p;
  }

Additionally, the function findprime() is overloaded so that instead of the interval boundaries pmin and pmax a binary length can be set.

Start Sidebar

Function:

generation of a prime number p within the interval [2ℓ−1, 2ℓ − 1] satisfying the additional condition gcd(p − 1, f) = 1, f an odd positive integer

Syntax:

const LINT
findprime(const USHORT l, const LINT& f);

Input:

l: desired binary length

f: odd positive integer, which should be relatively prime to p − 1

Return:

LINT prime p with gcd(p − 1, f)
End Sidebar

With regard to the key length to be chosen a look at the worldwide state of attempts at factorization is most informative: In April 1996 after months-long cooperative work at universities and research laboratories in the USA and Europe under the direction of A.K. Lenstra[3] the RSA modulus

Click To expand

with 130 decimal places was factored as

Click To expand

Then in February 1999 the modulus

Click To expand

was factored into the two 70-digit factors

Click To expand

This success was accomplished under the direction of Herman J. J. te Riele of CWI in the Netherlands with teams from the Netherlands, Australia, France, Great Britain, and the USA.[4] RSA-130 and RSA-140 came from a list of 42 RSA moduli published in 1991 by the firm RSA Data Security, Inc. as encouragement to the cryptographic research community.[5] The calculations that led to the factorization of RSA-130 and RSA-140 were divided among a large number of workstations and the results collated. The calculational effort to factor RSA-140 was estimated to be 2000 MIPS years[6] (for RSA-130 it was about 1000 MIPS years).

Only a short time later, namely at the end of August 1999, news of the factorization of RSA-155 flashed across the globe. At a cost of about 8000 MIPS years the next number in the list of RSA challenges had been laid to rest, again under the direction of Herman te Riele with international participation. With the factorization of

Click To expand

into the two 78-digit factors

Click To expand

the magical threshold of 512 was crossed, a length that for many years had been considered safe for key lengths.

Table 16.1: Recommended key lengths according to Lenstra and Verheul
Year | Key length (in bits)
2001 | 990
2005 | 1149
2010 | 1369
2015 | 1613
2020 | 1881
2025 | 2174

The question of what key length is to be considered adequate for the RSA algorithm is revised each time progress in factorization is made. A. K. Lenstra and Eric R. Verheul [LeVe] provide some concrete advice in this regard in their description of a model for the determination of recommended key lengths for many types of cryptosystems. Beginning with a set of well-founded and conservative assumptions, combined with current findings, they calculate some prognoses as to minimum key lengths to recommend in the future and display them in tabular form. The values shown in Table 16.1, which are valid for asymmetric procedures like RSA, El-Gamal, and Diffie-Hellman, are taken from their results.

We may conclude that an RSA key should possess no fewer than 1024 binary digits if it is to provide a comfortable margin of safety for critical applications. We may conclude as well, however, that successes in factorization are gradually approaching this value, and one must keep careful tabs on developments. It is therefore worthwhile to distinguish different application purposes and for particularly sensitive applications to consider using 2048 or more binary digits (cf. [Schn], Chapter 7, and [RegT], Appendix 1.4).[7] With the FLINT/C package we are well equipped to produce such key lengths. We need not be too concerned that the expense of factorization decreases in proportion to the speed of new hardware, since this same hardware allows us to create longer keys as required. The security of the RSA algorithm can thus always be ensured by keeping sufficiently ahead of the progress in factorization.

How many such keys are available? Are there enough so that every man, woman, and child on the planet (and perhaps even their pet cats and dogs) can be provided one or more RSA keys? To this the prime number theorem provides the answer, according to which the number of prime numbers less than an integer x is asymptotically approximated by x/ ln x (cf. page 216): Moduli of length 1024 bits are generated as products of two prime numbers each of length approximately 512 bits. There are about 2512/512 such primes, that is, about 10151, each pair of which forms a modulus. If we let N = 10151, then there are N(N − 1)/2 such pairs, which comes to about 10300 different moduli for which additionally that number again can be chosen for secret key components. This overwhelmingly large number is difficult to grasp, but consider, for example, that the entire visible universe contains "only" about 1080 elementary particles (cf. [Saga], Chapter 9). To put it another way, if every person on Earth were given ten new moduli every day, then the process could continue for 10287 years without a modulus being reused, and to date Earth has existed "only" a few billion years.

Finally, that an arbitrary text can be represented as a positive integer is obvious: By associating a unique integer to each letter of an alphabet texts can be interpreted in any number of ways as integers. A common example is the numerical representation of characters via ASCII (American Standard Code for Information Interchange) code. An ASCII-encoded text can be turned into a single integer by considering the code value of an individual character as a digit to base 256. The probability that such a process would result in an integer M for which gcd(M, n) > 1, that is, such that M contains as a factor one of the factors p, q, of an RSA key n, is vanishingly small. If M is too large for the modulus n of an RSA key, that is, larger than n − 1, then the text can be divided into blocks whose numerical representations M1, M2, M3, . . . all are less than n. These blocks must then be individually encrypted.

For texts of considerable length this becomes tiresome, and therefore one seldom uses the RSA algorithm for the encryption of long texts. For this purpose one may employ symmetric cryptosystems (such as Triple-DES, IDEA, or Rijndael; see Chapter 19 and [Schn], Chapters 12, 13, 14), with which the process goes much more quickly and with equivalent security. For an encrypted transmittal of the necessary key, which must be kept secret with symmetric procedures, the RSA algorithm is perfectly suited.

[3]Lenstra: Arjen K.: Factorization of RSA-130 using the Number Field Sieve, http://dbs.cwi.nl.herman.NFSrecords/RSA-130; see also [Cowi].

[4]E-mail from <Herman.te.Riele@cwi.nl> in Number Theory Network of 4 February 1999. See also http://www.rsasecurity.com/rsalabs/html/status.HTML.

[5]http://www.rsasecurity.com/rsalabs/html/factoring.html.

[6]MIPS = mega instructions per second measures the speed of a computer. A computer works at 1 MIPS if is can execute 700,000 additions and 300,000 multiplications per second.

[7]It is useful to choose RSA keys of binary length a multiple of 8, in conformity with the convention that such keys should end at the end of a byte.


16.3 Digital RSA Signatures

"Please, your Majesty," said the Knave, "I didn't write it, and they can't prove that I did: there's no name signed at the end."

—Lewis Carroll, Alice's Adventures in Wonderland

To clarify how the RSA algorithm is used for generating digital signatures we consider the following process, by which a participant A sends a message M with her digital signature to a participant B, upon which B checks the validity of the signature.

  1. A generates her RSA key with components nA, dA, and eA. She then transmits her public key <eA, nA> to B.

  2. A would now like to send a message M with her signature to B. To this end A generates the redundancy R = μ(M) with R < nA using a redundancy function μ (see below). Then A calculates the signature

    and sends (M, S) to B.

  3. B possesses the public key <eA, nA> of A. After B has received the message M and the signature S from A, then B calculates

    with the public key >eA, nA> of A.

  4. B now checks whether R′ = R. If this is the case, then B accepts the digital signature of A . Otherwise, B rejects it.

Digital signatures that must be checked by a separate transmittal of the signed message M are called digital signatures with appendix.

The signature procedures with appendix are used primarily for signing messages of variable lengths whose numerical representation exceeds the modulus, so that M ≥ n. In principle, one could, as we did above, divide the message into blocks M1, M2, M3, . . . of suitable lengths Mi < n and encrypt and sign each block separately. However, leaving aside the fact that in such a case a counterfeiting problem arises consisting in the possibility of mixing up the order of the blocks and the signatures belonging to the blocks, there are two further compelling reasons to employ, instead of construction of blocks, the function μ that we named the redundancy function in the paragraph above in which we discussed calculating a digital signature.

The first is that a redundancy function μ : maps arbitrary messages M from a message space [image] into the residue class ring [image] whereby messages typically are reduced by the application of hash functions (cf. page 337) to values z [image] 2160, which then are linked with predefined sequences of characters. Since the image of M under μ is signed in a single RSA step and hash functions can be calculated quickly by design, the use of such a procedure represents great savings in time over the number of RSA steps required for the individual blocks of M.

The second reason is that the RSA algorithm has an undesirable property for creating signatures: For two messages M1 and M2 the multiplicative relation

(16.4)

holds, which supports the counterfeiting of signatures if no measures are taken against it.

On account of this property, called homomorphism, of the RSA function it would be possible without the inclusion of redundancy R to have messages digitally signed with a "hidden" signature. To do this one could select a secret message M and a harmless message M1, from which a further message M2 := M M1 mod nA is formed. If one succeeded in getting a person or authority A to digitally sign the messages M1 and M2, then one would obtain the signatures S1 = [image] mod nA, and S2 = [image] mod nA, from which one can create the signature to M by calculating S2 [image] mod nA, which was probably not what A had in mind, though nonetheless A may not have noticed this when generating the signatures S1 and S2: The message M would in this case be said to have a hidden signature.

To be sure, one could counter that with high probability M2 does not represent a meaningful text and that anyway, A would not be well advised to sign M1 or M2 at a stranger's request and without examining the contents with care. Yet one should not rely on such assumptions of reasonableness when it comes to human behavior in order to justify the weaknesses of a cryptographic protocol, especially when such weaknesses can be eliminated, such as in this case by including redundancy. In order to achieve this redundancy, the redundancy function μ must satisfy the property

(16.5)

for all M1, M2 Î [image] and thus ensure that the signature function itself does not possess the undesirable homomorphism property.

Supplementary to the signature procedures with appendix there are additional methods known that make it possible to extract the signed message from the signature itself, the so-called digital signatures with message recovery (cf. [MOV], Chapter 11, [ISO2], and [ISO3]). Digital signatures with message recovery based on the RSA algorithm are particularly suited for short messages with a binary length less than one-half the binary length of the modulus.

However, in every case the security properties of redundancy functions should be carefully examined, such as is demonstrated by the procedure published in 1999 by Coron, Naccache, and Stern for attacking such schemes. The procedure is based on an attacker having access to a large number of RSA signatures attached to messages whose representation as an integer is divisible exclusively by small primes. Based on such a makeup of the messages it is possible under favorable conditions, without knowledge of the signature key, to construct additional signatures to additional messages, which would amount to counterfeiting these signatures (cf. [Coro]). The ISO has reacted to this development: In October 1999 the workgroup SC 27 removed the standard [ISO2] from circulation and published the following announcement:

Based on various attacks on RSA digital signature schemes . . . , it is the consensus of ISO/IEC JTC 1/SC 27 that IS 9796:1991 no longer provides sufficient security for application-independent digital signatures and is recommended to be withdrawn.[8]

The withdrawn standard refers to digital signatures for which the RSA function is applied directly to a short message. Signatures with appendix, which arise by way of a hash function, are not included.

A widely distributed redundancy scheme for which the attack of Coron, Naccache, and Stern has at best a theoretical significance and represents no real threat is set by the PKCS #1 format of RSA laboratories (cf. [RDS1], [Coro], pages 11–13, and [RDS2]). The PKCS #1 format specifies how a so-called encryption block EB should appear as input value to an encryption or signing operation:

Click To expand

At the head is a byte BT that describes the block type (01 for private key operations, that is, signatures; 02 for public key operations, that is, encryption) and then at least eight filler bytes PS1 . . . PSℓ, ℓ ≥ 8, with the value FF (hex) in the case of signing and nonzero random values in the case of encryption. There follows 00 as separator byte, and then come finally the data bytes D1 . . . Dn: the payload, so to speak. The number ℓ of filler bytes PSi depends on the size of the modulus m and the number n of data bytes: If k is defined by

(16.6)

then

(16.7)

The minimum number 8 ≤ ℓ of filler bytes is required for encryption for reasons of security. Even with a small message this prevents an attacker from encrypting all possible values and comparing the result with a given encrypted text to determine the plain text without knowing the associated secret key. However, it is important that the PSi be random numbers that are determined anew for each encryption operation. For consistency the minimum number of filler bytes is retained in signing operations, from which it follows that for the number n of data bytes we have

(16.8)

In the signing case the data bytes Di are typically constructed from an identifier for a hash function H and the value H(M) of this hash function (called the hash value), which represents the text M to be signed. The resulting data structure is called the DigestInfo. The number of data bytes depends in this case on the constant length of the hash value, independent of the length of the text. This is particularly advantageous when M is much longer than H(M). We shall not go into the precise process for the construction of the data structure DigestInfo, but simply assume that the data bytes correspond to the value H(M)(but see in this connection [RDS1]).

From the cryptographic point of view there are several fundamental requirements to place on hash functions so as not to diminish the security of a redundancy scheme based on such a function and thereby call the entire signing procedure into question. When we consider the use of hash and redundancy functions in connection with digital signatures and the possibilities for manipulating them that might arise, we observe the following:

In accordance with our considerations thus far, we start with the assumption that a digital signature with appendix relates to a redundancy R = μ(M) whose principal component is the hash value of the text to be signed. Two texts M and M′ for which H(M) = H(M′), and consequently μ(M) = μ(M′), possess the same signature S = Rd = μ(M)d = μ(M′)d mod n. The recipient of a signature to the text M could now conclude that the signature actually refers to the text M′, which in general would be contrary to the intent of the sender. Likewise, the sender could assume actually to have signed the text M′. The point here is that texts M ≠ M′ with H(M) = H(M′) always exist, due to the fact that infinitely many texts are mapped to finitely many hash values. This is the price to be paid for the convenience of hash values of fixed length.[9]

Since we must also assume the existence of texts that possess identical signatures in relation to a particular hash or redundancy function (where we assume that the same signature key has been used), then it is crucial that such texts not be easy to find or to construct.

In sum, a hash function should be easy to compute, but this should not be the case for the inverse mapping. That is, given a value H of a hash function it should not be easy to find a preimage that is mapped to H. Functions with this property are called one-way functions. Furthermore, a hash function must be collision-free, meaning that it must not be easy to find two different preimages of a given hash value. At present, these properties are satisfied by such strong hash functions as the widely used functions RIPEMD-160 (cf. [DoBP]) and the Secure Hash Algorithm SHA-1 (cf. [ISO1]).

We shall not go into further detail on this topic, which is very important to cryptography. The interested reader is referred to [Pren] or [MOV], Chapter 9, as well as the literature cited therein. Algorithms for transforming texts or hash values into natural numbers can be found in [IEEE], Chapter 12: "Encoding Methods" (we already have available the corresponding functions clint2byte_l() and byte2clint_l(); cf. page 150). An implementation of RIPEMD-160 can be found in ripemd.c on the accompanying CD-ROM.

In thinking about the signature protocol described above we are immediately confronted with the following question: How can B know whether he is in possession of the authentic public key of A? Without such certainty B cannot trust the signature, even if it can be verified as described above. This becomes critical when A and B do not know each other personally or when there has been no personal exchange of public keys, which is the normal case in communication over the Internet.

To make it possible for B nonetheless to trust in the authenticity of A's digital signature, A can present her communication partner a certificate from a certification authority that attests to the authenticity of A's public key. An informal "receipt," which one may believe or not, is, of course, inadequate. A certificate is rather a data set that has been formatted according to some standard[10] that among other things speaks to the identity of A as well as her public key and that itself has been digitally signed by the certification authority.

The genuineness of a participant's key can be verified with the help of the information contained in the certificate. Applications that support such verification in software already exist. The future multiplicity of such applications, whose technical and organizational basis will be based on so-called public key infrastructures (PKI), can today only be guessed at. Concrete uses are emerging in the digital signing of e-mail, the validation of commercial transactions, e-commerce and m-commerce, electronic banking, document management, and administrative procedures (see Figure 16.1).

Click To expand
Figure 16.1: Example of the construction of a certificate

On the assumption that B knows the public key of the certification authority, B can now verify the certificate presented by A and thereafter A's signature to become convinced of the authenticity of the information.

The example presented in Figure 16.2, which shows a client's digitally signed bank statement together with the certificate of the bank presented by a certification authority, demonstrates this process.

Such a bank statement has the advantage that it can reach the client over any electronic transmittal path, such as e-mail, in which it would be further encrypted to protect the confidentiality of the information.

Click To expand
Figure 16.2: Certification of a digital signature

However, the problem of trust has not been hereby miraculously cleared up, but merely shifted: Now B need no longer believe directly in the validity of A's key (in the above example that of the bank), but in exchange must check the genuineness of the certificate presented by A. For certainty to be attained, the validity of certificates must be verified anew for every occurrence, either from the source that issued the certificate or an authority that represents it. Such a procedure can succeed only if the following conditions are met:

  • the public key of the certification authority is known;

  • the certification authority takes the greatest care in the identification of the receivers of certificates and in the protection of their private certification keys.

To achieve the first of these desiderata the public key of the certification authority can be certified by an additional, higher, authority, and so forth, resulting in a hierarchy of certification authorities and certificates. However, verification along such a hierarchy assumes that the public key of the highest certification authority, the root certification authority, is known and can be accepted as authentic. Trust in this key must thus be established by other means through suitable technical or organizational measures.

The second condition holds, of course, for all authorities of a certification hierarchy. A certification authority, in the sense of granting legal force to a signature, must establish the organizational and technical processes for which detailed requirements have been set in law or associated implementing orders.

At the end of 1999 a European Union directive was adopted that will set a framework for the use of digital signatures in Europe (cf. [EU99]). There will also be an implementing order for the European Union that encompasses the various national approaches and creates a European standard, which of course must be incorporated into national law. A corresponding revision of the German signature law is supposed to take effect in 2001 (cf. [SigG]). In the United States the Electronic Signatures Act has been in effect since October 2000. These regulations lead one to believe that in the near future digital signatures that adhere to the statutes of the individual countries will be exchanged to set the seal on legal transactions throughout Europe and—one day perhaps—between Europe and the United States, and that those digital signatures will be valid within each of the different legal systems.

We now leave this interesting topic, which can pursued further in [Bies], [Glad], [Adam], [Mied], and [Fegh], and turn our attention, finally, to the implementation of C++ classes that provide for encryption and the generation of digital signatures.

[8]ISO/IEC JTC 1/SC27: Recommendation on the withdrawal of IS 9796:1991, 6 October 1991.

[9]In the language of mathematics we would say that hash functions H : → [image] that map texts of arbitrary length to values in [image] are not injective.

[10]Widely used is ISO 9594-8, which is equivalent to the ITU-T (formerly CCITT) recommendation X.509v3.


16.4 RSA Classes in C++

In this section we develop a C++ class RSAkey that contains the functions

  • RSAkey::RSAkey() for RSA key generation;

  • RSAkey::export() for exporting public keys;

  • RSAkey::decrypt() for decryption;

  • RSAkey::sign() for digital signing using the hash function RIPEMD-160;

as well as a class RSApub for the storage and application of public keys only with the functions

  • RSApub::RSApub() for importing a public key from an object of the class RSAkey;

  • RSApub::crypt() for encrypting a text;

  • RSApub::authenticate() for authenticating a digital signature.

The idea is not merely to look at cryptographic keys as numbers with particular cryptographic properties, but to consider them as objects that bring with themselves the methods for their application and make them available to the outside world but that nonetheless restrictively prevent unmediated access to private key data. Objects of the class RSAkey thus contain, after the generation of a key, a public and a private RSA key component as private elements, as well as the public functions for encryption and signing. Alternatively, the constructor functions enable the generation of keys

  • with fixed length and internal initialization of the BBS random number generator;

  • with adjustable length and internal initialization of the BBS random number generator;

  • with adjustable length and passing of a LINT initialization value for the BBS random number generator via the calling program.

Objects of the class RSApub contain only the public key, which they must import from an RSAkey object, and the public functions for encryption and verification of a signature. To generate an object of the RSApub class, then, an initialized RSAkey object must already exist. In contrast to objects of type RSAkey, RSApub objects are considered unreliable and can be more freely handled than RSAkey objects, which in serious applications may be transmitted or stored in data media only in encrypted form or protected by special hardware measures.

Before we realize these example classes we would like to set ourselves some boundary conditions that will limit the implementation cost at something appropriate for what is merely an example: For the sake of simplicity, input values for RSA encryption will be accepted only if they are smaller than the modulus; a subdivision of longer texts into blocks will not occur. Furthermore, we shall leave aside the more costly functional and security-related features that would be necessary in a full-fledged implementation of the RSA classes (see in this regard the pointer on page 323).

However, we do not wish to do without an effective possibility of speeding up the calculations for decryption or signing. By application of the Chinese remainder theorem (see page 199) the RSA operations with the secret key d can be made about four times as fast as with the usual method of calculating a single power: Given a secret key <d, n> with n = pq we form dp := d mod (p − 1) and dq := d mod (q − 1) and employ the extended Euclidean algorithm to compute the representation 1 = rp + sq, from which we extract the value r as the multiplicative inverse of p modulo q (cf. Section 10.2). We then employ the components p, q, dp, dq, r to calculate c = md mod n as follows:

  1. Calculate a1 ← [image] mod p and a2 ← [image] mod q.

  2. Calculate c ← a1 + p ((a2 − a1) r mod q).

After step 1 we have a1 ≡ [image] ≡ md mod p and a2 ≡ [image] ≡ md mod q. To see this, just use the little theorem of Fermat (cf. page 175), according to which mp−1 ≡ 1 mod p, respectively mq−1 ≡ 1 mod q. From d = ℓ (p − 1) + dp with integral ℓ it follows that

(16.9) Click To expand

and analogously, we have the same for md mod q. An application of the Garner algorithm (see page 203) with m1 := p, m2 := q, and r := 2 shows us at once that c in step 2 represents the desired solution. Rapid decryption is implemented in the auxiliary function RSAkey::fastdecrypt(). All exponents modulo p, q, or n are calculated via Montgomery exponentiation with the LINT function LINT::mexpkm () (cf. page 282).

  // Selection from the include file rsakey.h
   ...
  #include "flintpp.h"
  #include "ripemd.h"

  #define BLOCKTYPE_SIGN 01
  #define BLOCKTYPE_ENCR 02

  // The RSA key structure with all key components
  typedef struct
  {
      LINT kpub, kpriv, mod, p, q, ep, eq, r;
      USHORT bitlen_mod;// binary length of modulus
      USHORT bytelen_mod; // length of modulus in bytes
  } KEYSTRUCT;

  // the structure with the public key components
  typedef struct
  {
      LINT kpub, mod;
      USHORT bitlen_mod;// binary length of the modulus
      USHORT bytelen_mod; // length of modulus in bytes
  } PKEYSTRUCT;

  class RSAkey
  {
    public:
      inline RSAkey (void) {};
      RSAkey (const int);
      RSAkey (const int, const LINT&);
      PKEYSTRUCT export_public (void) const;
      UCHAR* decrypt (const LINT&, int*);
      LINT sign (const UCHAR* const, const int);

    private:
      KEYSTRUCT key;

      // auxiliary functions
      int makekey (const int);
      int testkey (void);
      LINT fastdecrypt (const LINT&);
  };

  class RSApub
  {
    public:
      inline RSApub (void) {};
      RSApub (const RSAkey&);
      LINT crypt (const UCHAR* const, const int);
      int authenticate (const UCHAR* const, const int, const LINT&);

    private:
      PKEYSTRUCT pkey;
  };

  // selection from module rsakey.cpp
   ...
  #include "rsakey.h"

  ////////////////////////////////////////////////////////////////////////
  // member functions of the class RSAkey

  // constructor generates RSA keys of specified binary length
  RSAkey::RSAkey (const int bitlen)
  {
    int done;
    seedBBS ((unsigned long)time (NULL));
    do
      {
         done = RSAkey::makekey (bitlen);
      }
    while (!done);
  }
  // constructor generates RSA keys of specified binary length,
  // initialization of random number generator randBBS() with
  // specified LINT argument
  RSAkey::RSAkey (const int bitlen, const LINT& rand)
  {
    int done;
    seedBBS (rand);
    do
      {
         done = RSAkey::makekey (bitlen);
      }
    while (!done);
  }

  // export function for public key components
  PKEYSTRUCT RSAkey::export_public (void) const
  {
    PKEYSTRUCT pktmp;
    pktmp.kpub = key.kpub;
    pktmp.mod = key.mod;
    pktmp.bitlen_mod = key.bitlen_mod;
    pktmp.bytelen_mod = key.bytelen_mod;
    return pktmp;
  }

  // RSA decryption
  UCHAR* RSAkey::decrypt (const LINT& Ciph, int* LenMess)
  {
    // decryption and transformation of plain text into byte vector
    UCHAR* Mess = lint2byte (fastdecrypt (Ciph), LenMess);
    // take decrypted data from PKCS #1 Encryption Block
    return parse_pkcs1 (Mess, LenMess);
  }
  // RSA signing
  LINT RSAkey::sign (const UCHAR* const Mess, const int LenMess)
  {
    int LenEncryptionBlock = key.bytelen_mod - 1;
    UCHAR* EncryptionBlock = new UCHAR[LenEncryptionBlock];

    if (NULL == format_pkcs1 (EncryptionBlock,
                    LenEncryptionBLock,
                    BLOCKTYPE_SIGN,
                    ripemd160 ((UCHAR*)Mess, (ULONG)LenMess),
                    RMDVER >> 3))
      {
         delete [] EncryptionBlock;
         return LINT (0);// error in formatting: message too long
      }

    // change encryption block into LINT number (constructor 3)
    LINT m = LINT (EncryptionBlock, LenEncryptionBlock);
    delete [] EncryptionBlock;

    return fastdecrypt (m);
  }

  ////////////////////////////////////////////////////////////////////////
  // private auxiliary functions of the class RSAkey

  // ... among other things: RSA key generation according to IEEE P1363, Annex A
  int RSAkey::makekey (const int length)
  {
    // generate prime p with length 2 ^ (m - r - 1) <= p < 2 ^ (m - r), where
    // m = ⌊(length + 1)/2⌋ and r random in interval 2 <= r < 13
    USHORT m = (length + 1) >> 1 - 2 - usrandBBS_l () % 11;
    key.p = findprime (m, 1);

    // determine interval bounds qmin and qmax for prime q
    // set qmin = ⌊(2 ^ (length - 1))/p + 1⌋
    LINT qmin = LINT(0).setbit (length - 1)/key.p + 1;
    // set qmax = ⌊(2 ^ length)/p)⌋
    LINT qmax = LINT(0).setbit (length)/key.p;
    // generate prime q > p with length qmin <= q <= qmax
    key.q = findprime (qmin, qmax, 1);

    // generate modulus p*q with length 2 ^ (length - 1) <= p*q < 2 ^ length
    key.mod = key.p * key.q;

    // calculate Euler phi function
    LINT phi_n = key.mod - key.p - key.q + 1;
    ...
    // generate public key of length 64 bits
    key.kpub = randBBS (64) | 1;
    while (gcd (key.kpub, phi_n) != 1)
      {
         ++key.kpub;
         ++key.kpub;
      }
    // generate secret key
    key.kpriv = key.kpub.inv (phi_n);

    // generate secret components for rapid decryption
    key.ep = key.kpriv % (key.p - 1);
    key.eq = key.kpriv % (key.q - 1);
    key.r = inv (key.p, key.q);
    return testkey();
  }

  // test function for RSA-key
  int RSAkey::testkey (void)
  {
    LINT mess = randBBS (ld (key.mod) >> 1);
    return (mess == fastdecrypt (mexpkm (mess, key.kpub, key.mod)));
  }

  // rapid RSA decryption
  LINT RSAkey::fastdecrypt (const LINT& mess)
  {
    LINT m, w;
    m = mexpkm (mess, key.ep, key.p);
    w = mexpkm (mess, key.eq, key.q);
    w = w.msub (m, key.q);
    w = w.mmul (key.r, key.q) * key.p;
    return (w + m);
  }

  ////////////////////////////////////////////////////////////////////////
  // member functions of the class RSApub

  // constructor RSApub()
  RSApub::RSApub (const RSAkey& k)
  {
    // import public key from k
    pkey = k.export();
  }

  // RSA encryption
  LINT RSApub::crypt (const UCHAR* const Mess, const int LenMess)
  {
    int LenEncryptionBlock = key.bytelen_mod - 1;
    UCHAR* EncryptionBlock = new UCHAR[LenEncryptionBlock];
    // format encryption block according to PKCS #1
    if (NULL == format_pkcs1 (EncryptionBlock,
                    LenEncryptionBlock,
                    BLOCKTYPE_ENCR,
                    Mess,
                    (ULONG)LenMess))
      {
         delete [] EncryptionBlock;
         return LINT (0); // formatting error: message too long
      }
    // transform encryption block into LINT number (constructor 3)
    LINT m = LINT (EncryptionBlock, LenEncryptionBlock);
    delete [] EncryptionBlock;

    return (mexpkm (m, pkey.kpub, pkey.mod));
  }

  // authentication of RSA signature
  int RSApub::authenticate (const UCHAR* const Mess, const int LenMess, const LINT& Signature)
  {
    int l, verification = 0;
    UCHAR* m = lint2byte (mexpkm (Signature, pkey.kpub, pkey.mod), &l);
    UCHAR* h = ripemd160 ((UCHAR*)Mess, (ULONG)LenMess);

    // take data from decrypted PKCS #1 encryption block
    m = parse_pkcs1 (m, &l);

    // compare length and value of decrypted data with hash value
    if (l == (RMDVER >> 3))
      {
         verification = !memcmp ((char*)h, (char*)m, RMDVER >> 3);
      }

    return verification;
 }

The class implementations RSAkey and RSApub contain in addition the following operators, which are not discussed further here:

   RSAkey& operator= (const RSAkey&);

   friend int operator== (const RSAkey&, const RSAkey&);
   friend int operator!= (const RSAkey&, const RSAkey&);
   friend fstream& operator<< (fstream&, const RSAkey&);
   friend fstream& operator>> (fstream&, RSAkey&);

and

   RSApub& operator= (const RSApub&);

   friend int operator== (const RSApub&, const RSApub&);
   friend int operator!= (const RSApub&, const RSApub&);
   friend fstream& operator<< (fstream&, const RSApub&);
   friend fstream& operator>> (fstream&, RSApub&);

These are for elementwise allocation, tests for equality and inequality, as well as reading and writing of keys to and from mass storage. However, one must note that the private key is stored in plain text, just as the public key is. For a real application the secret keys must be stored in encrypted form.

There are also the member functions

     RSAkey::purge (void),
     RSApub::purge (void),

which overwrite keys and set their LINT components to 0. The formatting of message blocks for encryption or signing corresponding to the PKCS #1 specification is taken over by the function

    UCHAR* format_pkcs1 (const UCHAR* EB, const int LenEB,
    const UCHAR BlockType, const UCHAR* Data, const int LenData);

The analysis of decrypted message blocks for verifying the format and for the extraction of useful data is handled by the function

     UCHAR* parse_pkcs1 (const UCHAR* EB, int* LenData);

The classes RSAkey and RSApub are extendable in a number of ways. For example, one could imagine a constructor that accepts a public key as parameter and generates a suitable modulus and secret key. For a practical implementation the inclusion of additional hash functions may be necessary. Message blocking is also required. The list of worthwhile extensions is long, and a full discussion would break the bounds and the binding of this book.

An example test application of the classes RSAkey and RSApub is to be found in the module rsademo.cpp contained in the FLINT/C package. The program is translated with

   gcc -O2 -DFLINT_ASM -o rsademo rsademo.cpp rsakey.cpp
     flintpp.cpp flint.c ripemd.c -lflint -lstdc++

if one implements, for example, the GNU C/C++ compiler gcc under Linux and uses the assembler functions in libflint.a.


Chapter 17: Do It Yourself: Test LINT

90% of the time is spent in 10% of the code.

—Robert Sedgewick, Algorithms

WE HAVE ALREADY DISCUSSED THE topic of testing in Chapter 12, where we subjected the basic arithmetic functions of the first part of the book to extensive static and dynamic tests. We now require a similar treatment for the validation of the C++ class LINT, and furthermore, we still must provide tests of the number-theoretic C functions.

The approach of the static tests can be carried over directly to the LINT class, where the tool PC-lint (see [Gimp]) used for the static analysis of C functions will stand us here in good stead as well, since we can use it for checking the syntactic correctness and (within certain bounds) the semantic plausibility of the LINT class and its elements.

We are also interested in the functional aspects of our class implementation: We must demonstrate that the methods contained in LINT return correct results. The process that we used earlier, where the results of equivalent or mutually inverse operations were used to establish correctness, can also, of course, be used on C++ functions. In the following example this process is embodied in the function testdist(), which links addition and multiplication by way of the distributive law. Even here one can see how much less syntactic complication is needed in comparison with the test functions in C. The test function consists principally of two lines of code!

  #include <stdio.h>
  #include <stdlib.h>
  #include "flintpp.h"

  void report_error (LINT&, LINT&, LINT&, int);
  void testdist (int);

  #define MAXTESTLEN CLINTMAXBIT
  #define CLINTRNDLN (ulrand64_l()% (MAXTESTLEN + 1))
  main()
  {
    testdist (1000000);
  }

  void testdist (int nooftests)
  {
    LINT a;
    LINT b;
    LINT c;
    int i;

    for (i = 1; i < nooftests; i++)
      {
         a = randl (CLINTRNDLN);
         b = randl (CLINTRNDLN);
         c = randl (CLINTRNDLN);

         // test of + and * by application of the distributive law
         if ((a + b)*c != (a*c + b*c))
           report_error (a, b, c, __LINE__);
      }
  }

  void report_error (LINT& a, LINT& b, LINT& c, int line)
  {
    LINT d = (a + b) * c;
    LINT e = a * c + b * c;
    cerr << "error in distributive law before line " << line << endl;
    cerr << "a = " << a << endl;
    cerr << "b = " << b << endl;
    cerr << "(a + b) * c = " << d << endl;
    cerr << "a * c + b * c = " << e << endl;
    abort();
  }

We now leave it to the reader as an exercise to test all the LINT operators in this or a similar manner. For orientation, one may look at the test routines for the C functions. However, there are some new aspects to consider, such as the prefix and postfix operators ++ and −−, as well as the fact that == is also an operator that must be tested. Here are some additional program notes:

  • Tests of the error routine panic() with all defined errors, with and without exceptions;

  • Tests of the I/O functions, stream operators, and manipulators;

  • Tests of the arithmetic and number-theoretic functions.

The number-theoretic functions can be tested according to the same principles as the arithmetic functions. To examine the function to be tested one is well advised to use inverse functions, equivalent functions, or different implementations of the same function that are as independent as possible from one another. We have examples of each of these variants:

  • If the Jacobi symbol indicates that an element of a finite ring is a square, this can be verified by calculating the square root. Conversely, a calculated square root can be verified as such by a simple modular squaring.

  • The function inv() for calculating the multiplicative inverse i of an integer a modulo n can be tested with the condition ai ≡ 1 mod n.

  • For calculating the greatest common divisor of two integers one may make use of the two FLINT/C functions gcd_l() and xgcd_l(), where the latter returns the representation of the greatest common divisor as a linear combination of the arguments. The results can be compared one with the other, and the linear combination can be constructed, which in turn must agree with the greatest common divisor.

  • Redundance is also to be found in the relation between the greatest common divisor and the least common multiple: For integers a, b one has the relation

    a fruitful relation that can also easily be checked. Additional useful formulas that relate the greatest common divisor and least common multiple are presented in Section 10.1.

  • Finally, for testing the primality test the RSA procedure can be invoked: If p or q is not prime, then with high probability φ(n) ≠ (p − 1)(q − 1), so that the RSA algorithm will not function correctly. This can be recognized in that a key pair modulo pq does not make possible the requisite mutually inverse RSA operations.

There are thus sufficiently many and varied approaches to effective testing of the LINT functions. The reader is encouraged to develop and implement at least one such test for each LINT function. It is highly effective both as a test and as an exercise and will develop in the user of the LINT class familiarity with how the class works and the uses to which it might be put.


Chapter 18: Approaches for Further Extensions

ALTHOUGH WE NOW HAVE AT our disposal a software package with a well-founded and well-rounded suite of functions, we confront now the question of in what directions our work might be continued. There are possibilities for work in the areas of functionality and performance. With regard to functionality, one can imagine the application of the basic functions in FLINT/C to areas that have been only touched upon or not even mentioned at all, such as factorization or elliptic curves, which have properties that have led to increasing interest in them for application to cryptography. The interested reader can find detailed explications in [Bres], [Kobl], and [Mene], but also in the standard works [Cohe], [Schn], and [MOV], which we have cited frequently and which contain many references to the literature.

A second area for development is that of measures for improving throughput, first and foremost the increase in digit length from 16 to 32 bits (B = 232), as well as through the use of assembler functions and, for platforms that support it, the C/C++ implementation.

The work in development and testing for this last approach could be carried out independent of platform, such as with the help of the GNU compiler gcc, using the gcc type unsigned long long: The type CLINT would be defined by typedef ULONG CLINT[CLINTMAXLONG];. Furthermore, certain constants would have to be adjusted that relate to the base of the internal representation of integers.

In the functions of the FLINT/C package all explicit casts and other references to USHORT must be replaced by ULONG and those to ULONG by unsigned long (or after a suitable typedef by, say, ULLONG. A few functions that make assumptions about the length of a digit in the data type used must be ported. After an extensive test and debugging phase including static syntax checking (cf. Chapter 12) the FLINT/C package would then be ready for CPUs with 64-bit word length.

The inclusion of assembler functions also makes it possible to operate with digits of 32 bits and results of 64 bits, and to do so on processors that themselves have only a 32-bit word length but that nonetheless support a 64-bit result of an arithmetic operation.

Since with the use of assembler functions we are abandoning our previous strategy of independence of special platforms, it is useful to implement such functions in a narrowly targeted way. We must therefore identify those FLINT/C functions that would most profit in speedup from assembler support. It is not difficult to determine which are the functions in question. They are those arithmetic functions with quadratic run-time behavior: multiplication, squaring, and division. Since the basic operations occupy the principal portion of time taken up by most of the number-theoretic functions, time improvements in those functions would be linear, without directly changing the implementation of the algorithms. To benefit from such a potential, for the FLINT/C package the functions

  mult(),  umul(),  sqr(),  div_l(),

are implemented in 80x86 assembler. The functions mult(), umul(), and sqr() form the respective kernels of the functions mult_l(), umul_l(), and sqr_l() (see page 70). The functions support arguments up to length 4096 binary digits, which is 256 (= MAXB) digits of a CLINT number, and results of double that length. The assembler functions are, like the corresponding C functions, implemented according to the algorithms given in Chapter 4, where access to the CPU register allows processing of 32-bit arguments and 64-bit results with the arithmetic machine commands (cf. Chapter 2).

The assembler module mult.asm, umul.asm, sqr.asm, and div.asm are contained in the FLINT/C package as source code. They can be assembled using Microsoft MASM (call: ml /Cx /c /Gd <filename>) or Watcom WASM,[1] and these replace the corresponding C functions when the module flint.c is translated with -DFLINT_ASM.[2] The calculation times given again in Appendix D permit a direct comparison of some of the important functions with and without assembler support.

Montgomery exponentiation (see Chapter 6) offers additional savings potential, and also the two auxiliary functions mulmon_l() and sqrmon_l() (cf. page 108) can be implemented profitably as assembler functions with 32-bit digits. A starting point for this is offered by the modules mul.asm and sqr.asm. As the interested reader can see, there is a very large sandbox out there to play in.

For now, this is all we know.

—Jon Hiseman, Colosseum

[1]Depending on the compiler used the indicators mult, umul, sqr, and div_l of the assembler procedures are to be provided with underscores (_mult, _umul, _sqr, and _div_l), since WASM does not generate them.

[2]With the modules mult.asm, sqr.asm, umul.asm, and div.asm this functions on 80x86 compatible platforms. For other platforms one must carry out the corresponding implementations.


Chapter 19: Rijndael: A Successor to the Data Encryption Standard

Overview

I don't know if we have any real chance. He can multiply and all we can do is add. He represents progress and I just drag my feet.

—Sten Nadolny (trans. Breon Mitchell), God of Impertinence

THE AMERICAN NATIONAL INSTITUTE OF Standards and Technology (NIST) launched a competition in 1997 under the aegis of an Advanced Encryption Standard (AES) with the goal of creating a new national standard (federal information processing standard, or FIPS) for encryption with a symmetric algorithm. Although we have concentrated our attention in this book on asymmetric cryptography, this development is important enough that we should give it some attention, if only cursorily. The new standard should establish an encryption algorithm that satisfies all of today's security requirements and that in all of its design and implementation aspects will be freely available without cost throughout the world. Finally, it should replace the dated data encryption standard (DES), which, however, as triple DES will remain available for use in government agencies. In the future, however, the AES is to be the cryptographic basis of the American administration for the protection of sensitive data.

The AES competition received a great deal of attention abroad as well as in the USA, not only because whatever happens in the United States in the area of cryptography produces great effects worldwide, but because international participation was specifically encouraged in the development of the new block encryption procedure.

From an original field of fifteen candidates who entered the contest in 1998, by 1999 ten had been eliminated, a process with involvement of an international group of experts. There then remained in competition the algorithms MARS, of IBM; RC6, of RSA Laboratories; Rijndael, of Joan Daemen and Vincent Rijmen; Serpent, of Ross Anderson, Eli Biham, and Lars Knudson; and Twofish, of Bruce Schneier et al. Finally, in October 2000 the winner of the selection process was announced. The algorithm with the name "Rijndael," by Joan Daemen and Vincent Rijmen, of Belgium, was named as the future advanced encryption standard (cf. [NIST]).[1] Rijndael is a successor of the block cipher "Square," published earlier by the same authors (cf. [Squa]), which, however, had proved to be not as powerful. Rijndael was especially strengthened to attack the weaknesses of Square. The AES report of NIST gives the following basis for its decision.

  1. Security

    All candidates fulfill the requirements of the AES with respect to security against all known attacks. In comparison to the other candidates, the implementations of Serpent and Rijndael can at the least cost be protected against attacks that are based on measurements of the time behavior of the hardware (so-called timing attacks) or changes in electrical current use (so-called power or differential power analysis attacks).[2] The degradation in performance associated with such protective measures is least for Rijndael, Serpent, and Twofish, with a greater advantage to Rijndael.

  2. Speed

    Rijndael is among the candidates that can most rapidly encrypt and decrypt. Rijndael distinguishes itself by equally good performance across all platforms considered, such as 32-bit processors, 8-bit microcontrollers, smart cards, and implementations in hardware (see below). Of all the candidates Rijndael allows the most rapid calculation of round keys.

  3. Memory requirement

    Rijndael makes use of very limited resources of RAM and ROM memory and is thus an excellent candidate for use in restricted-resource environments. In particular, the algorithm offers the possibility to calculate round keys separately "on the fly" for each round. These properties have great significance for applications on microcontrollers such as used in smart cards. Due to the structure of the algorithm the requirements on ROM storage are least when only one direction, that is, either encryption or decryption, is realized, and they increase when both functions are needed. Nonetheless, with respect to resource requirements Rijndael is not beaten by any of the other four contestants.

  4. Implementation in hardware

    Rijndael and Serpent are the candidates with the best performance in hardware implementations, with a slight advantage going to Rijndael due to its better performance in output and cipher feedback modes.

The report offers further criteria that contributed to the decision in favor of Rijndael, which are collected into a closing summary (see [NIST], Section 7):

  • There are many unknowns regarding future computing platforms and the wide range of environments in which the AES will be implemented. However, when considered together, Rijndael's combination of security, performance, efficiency, implementability, and flexibility make it an appropriate selection for the AES for use in the technology of today and in the future.

Given the openness of the selection process and the politically interesting fact that with Rijndael an algorithm of European vintage was selected, one might expect future speculation about secret properties, hidden trap doors, and deliberately built-in weaknesses to be silenced, which never quite succeeded with DES.

Before we get involved with the functionality of Rijndael, we would like as preparation to go on a brief excursion into the arithmetic of polynomials over finite fields, which leans heavily on the presentation in [DaRi], Section 2.

[1]The name "Rijndael" is a portmanteau word derived from the names of the authors. Sources tell me that the correct pronunciation is somewhere between "rain doll" and "Rhine dahl." Perhaps NIST should include in the standard a pronunciation key in the international phonetic alphabet.

[2]Power analysis attacks (simple PA/differential PA) are based on correlations between individual bits or groups of bits of a secret cryptographic key and the average consumption of electricity for the execution of individual instructions or code sequences depending on the key (see, for example, [KoJJ], [CJRR], [GoPa]).


19.1 Arithmetic with Polynomials

We start by looking at arithmetic in the field [image] , the finite field with 2n elements, where an element of [image] is represented as a polynomial f(x) = an−1xn−1 + an−2xn−2 + ··· + a1x + a0 with coefficients ai in [image] (which is isomorphic to [image] ). Equivalently, an element of [image] can be represented simply as an n-tuple of polynomial coefficients, each representation offering its own advantages. The polynomial representation is well suited for manual calculation, while the representation as a tuple of coefficients corresponds well to a computer's binary representation of numbers. To demonstrate this, we notate [image] as a sequence of eight polynomials and again as eight 3-tuples with their associated numerical values (see Table 19.1).

Table 19.1: Elements of
Polynomials in | 3-Tuples in |  |  | Numerical value
0 | 0 | 0 | 0 | '00'
1 | 0 | 0 | 1 | '01'
x | 0 | 1 | 0 | '02'
x + 1 | 0 | 1 | 1 | '03'
x2 | 1 | 0 | 0 | '04'
x2 + 1 | 1 | 0 | 1 | '05'
x2 + x | 1 | 1 | 0 | '06'
x2 + x + 1 | 1 | 1 | 1 | '07'

Addition of polynomials proceeds by adding the coefficients in [image] : If f(x) := x2 + x and g(x) := x2 + x + 1, then f(x) + g(x) = 2x2 + 2x + 1 = 1, since 1 + 1 = 0 in [image] . We can carry out addition of 3-tuples in [image] column by column. We see, then, for example, that the sum of (1 1 0) and (1 1 1) is (0 0 1):

The addition of digits takes place in [image] and is not to be confused with binary addition, which can involve a carry. This process is reminiscent of our XOR function in Section 7.2, which executes the same operation in [image] for large n.

Multiplication in [image] is accomplished by multiplying each term of the first polynomial by each term of the second and then summing the partial products. The sum is then reduced by an irreducible polynomial of degree 3 (in our example modulo m(x) := x3 + x + 1):[3]

Click To expand

This corresponds to the product of 3-tuples (1 1 0) • (1 1 1) = (1 0 1), or, expressed in hexadecimal notation, '06' • '07' = '05'.

The abelian group laws hold in [image] with respect to addition and in [image] \ {0} with respect to multiplication (cf. Chapter 5). The distributive law holds as well.

The structure and arithmetic of [image] can be carried over directly to the field [image] , which is the field that is actually of interest in studying Rijndael. Addition and multiplication are carried out as in our above example, the only differences being that [image] has 256 elements and that an irreducible polynomial of degree 8 will be used for reduction. For Rijndael this polynomial is m(x) := x8 + x4 + x3 + x + 1, which in tuple representation is (1 0 0 0 1 1 0 1 1), corresponding to the hexadecimal number '011B'.

Multiplication of a polynomial

Click To expand

by x (corresponding to a multiplication • '02') is particularly simple:

Click To expand

where the reduction modulo m(x) is required only in the case a7 ≠ 0, and then it can be carried out by subtracting m(x), that is, by a simple XOR of the coefficients.

For programming one therefore regards the coefficients of a polynomial as binary digits of integers and executes a multiplication by x by a left shift of one bit, followed by, if a7 = 1, a reduction by an XOR operation with the eight least-significant digits '1B' of the number '011B' corresponding to m(x) (whereby a7 is simply "forgotten"). The operation a • '02' for a polynomial f, or its numerical value a, is denoted by Daemen and Rijmen by b = xtime(a). Multiplication by powers of x can be executed by successive applications of xtime.

For example, multiplication of f(x) by x + 1 (or '03') is carried out by shifting the binary digits of the numerical value a of f one place to the left and XOR-ing the result with a. Reduction modulo m(x) proceeds exactly as with xtime. Two lines of C code demonstrate the procedure:

  f ^= f << 1;    /* multiplication of f by (x + 1) */
  if (f & 0×100) f ^= 0×11B;    /* reduction modulo m(x) */

Multiplication of two polynomials f and h in [image] can be speeded up by using logarithms: Let g(x) be a generating polynomial[4] of [image] \ {0}. Then there exist m and n such that f ≡ gm and h ≡ gn. Thus f · h ≡ gm+n mod m(x).

From a programming point of view this can be transposed with the help of two tables, into one of which we place the 255 powers of the generator polynomial g(x) := x + 1 and into the other the logarithms to the base g(x) (see Tables 19.2 and 19.3). The product f · h is now determined by three accesses to these tables: From the logarithm table are taken values m and n for which gm = f and gn = h. From the table of powers the value g((n+m)mod255) is taken (note that gord(g) = 1).

With the help of this mechanism we can also carry out polynomial division in [image] . That is,

Click To expand
Table 19.2: Powers of g(x) = x + 1
1 | 3 | 5 | 15 | 17 | 51 | 85 | 255 | 26 | 46 | 114 | 150 | 161 | 248 | 19 | 53
95 | 225 | 56 | 72 | 216 | 115 | 149 | 164 | 247 | 2 | 6 | 10 | 30 | 34 | 102 | 170
229 | 52 | 92 | 228 | 55 | 89 | 235 | 38 | 106 | 190 | 217 | 112 | 144 | 171 | 230 | 49
83 | 245 | 4 | 12 | 20 | 60 | 68 | 204 | 79 | 209 | 104 | 184 | 211 | 110 | 178 | 205
76 | 212 | 103 | 169 | 224 | 59 | 77 | 215 | 98 | 166 | 241 | 8 | 24 | 40 | 120 | 136
131 | 158 | 185 | 208 | 107 | 189 | 220 | 127 | 129 | 152 | 179 | 206 | 73 | 219 | 118 | 154
181 | 196 | 87 | 249 | 16 | 48 | 80 | 240 | 11 | 29 | 39 | 105 | 187 | 214 | 97 | 163
254 | 25 | 43 | 125 | 135 | 146 | 173 | 236 | 47 | 113 | 147 | 174 | 233 | 32 | 96 | 160
251 | 22 | 58 | 78 | 210 | 109 | 183 | 194 | 93 | 231 | 50 | 86 | 250 | 21 | 63 | 65
195 | 94 | 226 | 61 | 71 | 201 | 64 | 192 | 91 | 237 | 44 | 116 | 156 | 191 | 218 | 117
159 | 186 | 213 | 100 | 172 | 239 | 42 | 126 | 130 | 157 | 188 | 223 | 122 | 142 | 137 | 128
155 | 182 | 193 | 88 | 232 | 35 | 101 | 175 | 234 | 37 | 111 | 177 | 200 | 67 | 197 | 84
252 | 31 | 33 | 99 | 165 | 244 | 7 | 9 | 27 | 45 | 119 | 153 | 176 | 203 | 70 | 202
69 | 207 | 74 | 222 | 121 | 139 | 134 | 145 | 168 | 227 | 62 | 66 | 198 | 81 | 243 | 14
18 | 54 | 90 | 238 | 41 | 123 | 141 | 140 | 143 | 138 | 133 | 148 | 167 | 242 | 13 | 23
57 | 75 | 221 | 124 | 132 | 151 | 162 | 253 | 28 | 36 | 108 | 180 | 199 | 82 | 246 | 1
Table 19.3: Logarithms to base g(x) = x + 1 (e.g., logg(x) 3 = 25, logg(x) 255 = 7)
 | 0 | 25 | 1 | 50 | 2 | 26 | 198 | 75 | 199 | 27 | 104 | 51 | 238 | 223 | 3
100 | 4 | 224 | 14 | 52 | 141 | 129 | 239 | 76 | 113 | 8 | 200 | 248 | 105 | 28 | 193
125 | 194 | 29 | 181 | 249 | 185 | 39 | 106 | 77 | 228 | 166 | 114 | 154 | 201 | 9 | 120
101 | 47 | 138 | 5 | 33 | 15 | 225 | 36 | 18 | 240 | 130 | 69 | 53 | 147 | 218 | 142
150 | 143 | 219 | 189 | 54 | 208 | 206 | 148 | 19 | 92 | 210 | 241 | 64 | 70 | 131 | 56
102 | 221 | 253 | 48 | 191 | 6 | 139 | 98 | 179 | 37 | 226 | 152 | 34 | 136 | 145 | 16
126 | 110 | 72 | 195 | 163 | 182 | 30 | 66 | 58 | 107 | 40 | 84 | 250 | 133 | 61 | 186
43 | 121 | 10 | 21 | 155 | 159 | 94 | 202 | 78 | 212 | 172 | 229 | 243 | 115 | 167 | 87
175 | 88 | 168 | 80 | 244 | 234 | 214 | 116 | 79 | 174 | 233 | 213 | 231 | 230 | 173 | 232
44 | 215 | 117 | 122 | 235 | 22 | 11 | 245 | 89 | 203 | 95 | 176 | 156 | 169 | 81 | 160
127 | 12 | 246 | 111 | 23 | 196 | 73 | 236 | 216 | 67 | 31 | 45 | 164 | 118 | 123 | 183
204 | 187 | 62 | 90 | 251 | 96 | 177 | 134 | 59 | 82 | 161 | 108 | 170 | 85 | 41 | 157
151 | 178 | 135 | 144 | 97 | 190 | 220 | 252 | 188 | 149 | 207 | 205 | 55 | 63 | 91 | 209
83 | 57 | 132 | 60 | 65 | 162 | 109 | 71 | 20 | 42 | 158 | 93 | 86 | 242 | 211 | 171
68 | 17 | 146 | 217 | 35 | 32 | 46 | 137 | 180 | 124 | 184 | 38 | 119 | 153 | 227 | 165
103 | 74 | 237 | 222 | 197 | 49 | 254 | 24 | 13 | 99 | 140 | 128 | 192 | 247 | 112 | 7

We now ratchet the complexity level up one notch and consider arithmetic with polynomials of the form f(x) = f3x3 + f2x2 + f1x + f0 with coefficients fi in [image] , that is, coefficients that are themselves polynomials. The coefficients of such polynomials can be represented as fields of four bytes each. Now things begin to get interesting: While addition of such polynomials f(x) and g(x) again takes place by means of a bitwise XOR of the coefficients, the product h(x) = f(x)g(x) is calculated to be

Click To expand

with coefficients hk := [image] fi • gj, where the summation sign indicates addition ⊕ in [image] .

After reduction of h(x) by a polynomial of degree 4, one again obtains a polynomial of degree 3 over [image] .

For this Rijndael uses the polynomial M(x) := x4 + 1. Usefully, xj mod M(x) = xjmod4, so that h(x) mod M(x) can be easily computed as

Click To expand

with

Click To expand

From this one concludes that the coefficients di can be computed by matrix multiplication over [image] :

It is precisely this operation with the constant, invertible modulo M(x), polynomial a(x) := a3x3 + a2x2 + a1x + a0 over [image] , with coefficients a0(x) = x, a1(x) = 1, a2(x) = 1, and a3(x) = x + 1, that is executed in the so-called MixColumn transformation, which constitutes a principal component of the round transformations of Rijndael.

[3]A polynomial is said to be irreducible if it divisible (without remainder) only by itself and 1.

[4]g generates [image] \ {0} if g has order 255. That is, is the powers of g run through all the elements of [image] \ {0}.


19.2 The Rijndael Algorithm

Rijndael is a symmetric block encryption algorithm with variable block and key lengths. It can process blocks of 128, 192, and 256 bits and keys of the same lengths, where all combinations of block and key lengths are possible. The accepted key lengths correspond to the guidelines for AES, though the "official" block length is only 128 bits. Each block of plain text is encrypted several times with a repeating sequence of various functions, in so-called rounds. The number of rounds is dependent on the block and key lengths (see Table 19.4).

Table 19.4: Number of Rijndael rounds as a function of block and key length
 | Block length (bits)
Key length (bits) | 128 | 192 | 256
128 | 10 | 12 | 14
192 | 12 | 12 | 14
256 | 14 | 14 | 14

Rijndael is not a Feistel algorithm, whose essential characteristic is that blocks are divided into left and right halves, the round transformations applied to one half, and the result XOR-ed with the other half, after which the two halves are exchanged. DES is the best-known block algorithm built along these lines. Rijndael, on the other hand, is built up of separate layers, which successively apply various effects to an entire block. For the encryption of a block the following transformations are sequentially applied:

  1. The first round key is XOR-ed with the block.

  2. Lr − 1 regular rounds are executed.

  3. A terminal round is executed, in which the MixColumn transformation of the regular rounds is omitted.

Each regular round of step 2 consists of four individual steps, which we shall now examine:

  1. Substitution: Each byte of a block is replaced by application of an S-box.

  2. Permutation: The bytes of the block are permuted in a ShiftRow transformation.

  3. Diffusion: The MixColumn transformation is executed.

  4. Round key addition: The current round key is XOR-ed with the block.

The layering of transformations within a round is shown schematically in Figure 19.1.

Click To expand
Figure 19.1: Layering of transformations in the Rijndael rounds

Each layer exercises a particular effect within a round and thus on each block of plain text:

  1. Influence of the key

    XOR-ing with the round key before the first round and as the last step within each round has an effect on every bit of the round result. In the course of encryption of a block there is no step whose result is not dependent in every bit on the key.

  2. Nonlinear layer

    The S-box substitution is a nonlinear operation. The construction of the S-box provides almost ideal protection against differential and linear cryptanalysis (see [BiSh] and [NIST]).

  3. Linear layer

    The ShiftRow and MixColumn transformations ensure an optimal mixing up of the bits of a block.

In the following description of the internal Rijndael functions Lb will denote the block length in 4-byte words, Lk the length of the user key in 4-byte words (that is, Lb, Lk Î {4, 6, 8}), and Lr the number of rounds as indicated in Table 19.4.

Plain text and encrypted text are input, respectively output, as fields of bytes. A block of plain text, passed as a field m0,..., [image] , will be regarded in the following as a two-dimensional structure [image] as depicted in Table 19.5,

Table 19.5: Representation of message blocks
b0,0 | b0,1 | b0,2 | b0,3 | b0,4 | ...
b1,0 | b1,1 | b1,2 | b1,3 | b1,4 | ...
b2,0 | b2,1 | b2,2 | b2,3 | b2,4 | ...
b3,0 | b3,1 | b3,2 | b3,3 | b3,4 | ...

where the bytes of plain text are sorted according to the following ordering:

Click To expand

with i = n mod 4 and j = ⌊n/4⌋.

Access to [image] within the Rijndael functions takes place in different ways according to the operation. The S-box transformation operates bytewise, ShiftRow operates on rows (bi,0, bi,1, bi,2,..., [image] ) of [image] , and the functions AddRoundKey and MixColumn operate on 4-byte words and access the values of [image] by columns (b0,j, b1,j, b2,j, b3,j).


19.3 Calculating the Round Key

Encryption and decryption each require the generation of Lr round keys, called collectively the key schedule. This occurs through expansion of the secret user key by attaching recursively derived 4-byte words ki = (k0,i, k1,i, k2,i, k3,i) to the user key.

The first Lk words k0,..., [image] of the key schedule are formed from the secret user key itself. For Lk Î {4,6} the next 4-byte word ki is determined by XOR-ing the preceding word ki−1 with [image] . If i ≡ 0 mod Lk, then a function [image] (k, i) is applied before the XOR operation, which is composed of a cyclic left shift (left rotation) r(k) of k bytes, a substitution S(r(k)) from the Rijndael S-box (we shall return to this later), and an XOR with a constant c (⌊i/Lk⌋), so that altogether the function F is given by [image] (k, i) := S(r(k)) ⊕ c (⌊i/Lk⌋).

The constants c(j) are defined by c(j) := (rc(j), 0, 0, 0), where rc(j) are recursively determined elements from [image] : rc(1) := 1, rc(j) := rc(j − 1). · x = xj−1. Expressed in numerical values, this is equivalent to rc(1) := '01', rc(j) := rc(j − 1) • '02'. From the standpoint of programming, rc(j) is computed by a (j − 1)-fold execution of the function xtime described above, beginning with the argument 1, or more rapidly by access to a table (Tables 19.6 and 19.7).

Table 19.6: rc(j) constants (hexadecimal)
'01' | '02' | '04' | '08' | '10' | '20' | '40' | '80' | '1B' | '36'
'6C' | 'D8' | 'AB' | '4D' | '9A' | '2F' | '5E' | 'BC' | '63' | 'C6'
'97' | '35' | '6A' | 'D4' | 'B3' | '7D' | 'FA' | 'EF' | 'C5' | '91'
Table 19.7: rc(j) constants (binary)
00000001 | 00000010 | 00000100 | 00001000 | 00010000
00100000 | 01000000 | 10000000 | 00011011 | 00110110
01101100 | 11011000 | 10101011 | 01001101 | 10011010
00101111 | 01011110 | 10111100 | 01100011 | 11000110
10010111 | 00110101 | 01101010 | 11010100 | 10110011
01111101 | 11111010 | 11101111 | 11000101 | 10010001

For keys of length 256 bits (that is, Lk = 8) an additional S-box operation is inserted: If i ≡ 4 mod Lk, then before the XOR operation ki−1 is replaced by S (ki−1).

Thus a key schedule is built up of Lb · (Lr + 1) 4-byte words, including the secret user key. At each round i = 0,..., Lr −1 the next Lb 4-byte words [image] through kLb·(i+1) are taken as round keys from the key schedule. The round keys are conceptualized, in analogy to the structuring of the message blocks, as a two-dimensional structure of the form depicted in Table 19.8.

Table 19.8: Representation of the round keys
k0,0 | k0,1 | k0,2 | k0,3 | k0,4 | ...
k1,0 | k1,1 | k1,2 | k1,3 | k1,4 | ...
k2,0 | k2,1 | k2,2 | k2,3 | k2,4 | ...
k3,0 | k3,1 | k3,2 | k3,3 | k3,4 | ...

For key lengths of 128 bits key generation can be understood from an examination of Figure 19.2

Click To expand
Figure 19.2: Diagram for round keys for Lk= 4

There are no weak keys known, those whose use would weaken the procedure.


19.4 The S-Box

The substitution box, or S-box, of the Rijndael algorithm specifies how in each round each byte of a block is to be replaced by another value. The S-box consists of a list of 256 bytes, which are constructed by first thinking of each nonzero byte as a representative of [image] and replacing it with its multiplicative inverse (zero remains unchanged). Then an affine transformation over [image] is calculated as a matrix multiplication and addition of (1 1 0 0 0 1 1 0):

Click To expand

In this representation x0 and y0 denote the least-significant, and x7 and y7 the most-significant, bits of a byte, where the 8-tuple (1 1 0 0 0 1 1 0) corresponds to the hexadecimal value '63'.

There are design criteria on which the construction of the S-box is based, according to which susceptibility to methods of linear and differential cryptanalysis as well as algebraic attacks are minimized. Successive application of the construction plan to the values 0 to 255 leads to Table 19.9 (read horizontally from left to right).

Table 19.9: The values of the S-box
99 | 124 | 119 | 123 | 242 | 107 | 111 | 197 | 48 | 1 | 103 | 43 | 254 | 215 | 171 | 118
202 | 130 | 201 | 125 | 250 | 89 | 71 | 240 | 173 | 212 | 162 | 175 | 156 | 164 | 114 | 192
183 | 253 | 147 | 38 | 54 | 63 | 247 | 204 | 52 | 165 | 229 | 241 | 113 | 216 | 49 | 21
4 | 199 | 35 | 195 | 24 | 150 | 5 | 154 | 7 | 18 | 128 | 226 | 235 | 39 | 178 | 117
9 | 131 | 44 | 26 | 27 | 110 | 90 | 160 | 82 | 59 | 214 | 179 | 41 | 227 | 47 | 132
83 | 209 | 0 | 237 | 32 | 252 | 177 | 91 | 106 | 203 | 190 | 57 | 74 | 76 | 88 | 207
208 | 239 | 170 | 251 | 67 | 77 | 51 | 133 | 69 | 249 | 2 | 127 | 80 | 60 | 159 | 168
81 | 163 | 64 | 143 | 146 | 157 | 56 | 245 | 188 | 182 | 218 | 33 | 16 | 255 | 243 | 210
205 | 12 | 19 | 236 | 95 | 151 | 68 | 23 | 196 | 167 | 126 | 61 | 100 | 93 | 25 | 115
96 | 129 | 79 | 220 | 34 | 42 | 144 | 136 | 70 | 238 | 184 | 20 | 222 | 94 | 11 | 219
224 | 50 | 58 | 10 | 73 | 6 | 36 | 92 | 194 | 211 | 172 | 98 | 145 | 149 | 228 | 121
231 | 200 | 55 | 109 | 141 | 213 | 78 | 169 | 108 | 86 | 244 | 234 | 101 | 122 | 174 | 8
186 | 120 | 37 | 46 | 28 | 166 | 180 | 198 | 232 | 221 | 116 | 31 | 75 | 189 | 139 | 138
112 | 62 | 181 | 102 | 72 | 3 | 246 | 14 | 97 | 53 | 87 | 185 | 134 | 193 | 29 | 158
225 | 248 | 152 | 17 | 105 | 217 | 142 | 148 | 155 | 30 | 135 | 233 | 206 | 85 | 40 | 223
140 | 161 | 137 | 13 | 191 | 230 | 66 | 104 | 65 | 153 | 45 | 15 | 176 | 84 | 187 | 22

For decryption the S-box must be used backwards: The affine inverse transformation is used, followed by multiplicative inversion in [image] . The inverted S-box appears in Table 19.10.

Table 19.10: The values of the inverted S-box
82 | 9 | 106 | 213 | 48 | 54 | 165 | 56 | 191 | 64 | 163 | 158 | 129 | 243 | 215 | 251
124 | 227 | 57 | 130 | 155 | 47 | 255 | 135 | 52 | 142 | 67 | 68 | 196 | 222 | 233 | 203
84 | 123 | 148 | 50 | 166 | 194 | 35 | 61 | 238 | 76 | 149 | 11 | 66 | 250 | 195 | 78
8 | 46 | 161 | 102 | 40 | 217 | 36 | 178 | 118 | 91 | 162 | 73 | 109 | 139 | 209 | 37
114 | 248 | 246 | 100 | 134 | 104 | 152 | 22 | 212 | 164 | 92 | 204 | 93 | 101 | 182 | 146
108 | 112 | 72 | 80 | 253 | 237 | 185 | 218 | 94 | 21 | 70 | 87 | 167 | 141 | 157 | 132
144 | 216 | 171 | 0 | 140 | 188 | 211 | 10 | 247 | 228 | 88 | 5 | 184 | 179 | 69 | 6
208 | 44 | 30 | 143 | 202 | 63 | 15 | 2 | 193 | 175 | 189 | 3 | 1 | 19 | 138 | 107
58 | 145 | 17 | 65 | 79 | 103 | 220 | 234 | 151 | 242 | 207 | 206 | 240 | 180 | 230 | 115
150 | 172 | 116 | 34 | 231 | 173 | 53 | 133 | 226 | 249 | 55 | 232 | 28 | 117 | 223 | 110
71 | 241 | 26 | 113 | 29 | 41 | 197 | 137 | 111 | 183 | 98 | 14 | 170 | 24 | 190 | 27
252 | 86 | 62 | 75 | 198 | 210 | 121 | 32 | 154 | 219 | 192 | 254 | 120 | 205 | 90 | 244
31 | 221 | 168 | 51 | 136 | 7 | 199 | 49 | 177 | 18 | 16 | 89 | 39 | 128 | 236 | 95
96 | 81 | 127 | 169 | 25 | 181 | 74 | 13 | 45 | 229 | 122 | 159 | 147 | 201 | 156 | 239
160 | 224 | 59 | 77 | 174 | 42 | 245 | 176 | 200 | 235 | 187 | 60 | 131 | 83 | 153 | 97
23 | 43 | 4 | 126 | 186 | 119 | 214 | 38 | 225 | 105 | 20 | 99 | 85 | 33 | 12 | 125


19.5 The ShiftRow Transformation

The next step in the cycle of a round consists in the permutation of a block at the byte level. To this end the bytes are exchanged within the individual lines (bi,0, bi,1, bi,2,..., [image] ) of a block according to the schemata depicted in Tables 19.11-19.13.

Table 19.11: ShiftRow for blocks of length 128 bits (Lb = 4)
Before ShiftRow |  |  |  | After ShiftRow
0 | 4 | 8 | 12 | 0 | 4 | 8 | 12
1 | 5 | 9 | 13 | 5 | 9 | 13 | 1
2 | 6 | 10 | 14 | 10 | 14 | 2 | 6
3 | 7 | 11 | 15 | 15 | 3 | 7 | 11
Table 19.12: ShiftRow for blocks of length 192 bits (Lb = 6)
Before ShiftRow |  |  |  |  |  | After ShiftRow
0 | 4 | 8 | 12 | 16 | 20 | 0 | 4 | 8 | 12 | 16 | 20
1 | 5 | 9 | 13 | 17 | 21 | 5 | 9 | 13 | 17 | 21 | 1
2 | 6 | 10 | 14 | 18 | 22 | 10 | 14 | 18 | 22 | 2 | 6
3 | 7 | 11 | 15 | 19 | 23 | 15 | 19 | 23 | 3 | 7 | 11
Table 19.13: ShiftRow for blocks of length 256 bits (Lb = 8)
Before ShiftRow |  |  |  |  |  |  |  | After ShiftRow
0 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 0 | 4 | 8 | 12 | 16 | 20 | 24 | 28
1 | 5 | 9 | 13 | 17 | 21 | 25 | 29 | 5 | 9 | 13 | 17 | 21 | 25 | 29 | 1
2 | 6 | 10 | 14 | 18 | 22 | 26 | 30 | 14 | 18 | 22 | 26 | 30 | 2 | 6 | 10
3 | 7 | 11 | 15 | 19 | 23 | 27 | 31 | 19 | 23 | 27 | 31 | 3 | 7 | 11 | 15

In each first row no exchange takes place. In lines i = 2, 3, 4 the bytes are rotated left by [image] positions, from position j to position j − [image] mod Lb, where [image] is taken from Table 19.14

Table 19.14: Distances of line rotations in ShiftRow
Lb |  |  |
4 | 1 | 2 | 3
6 | 1 | 2 | 3
8 | 1 | 3 | 4

For inverting this step positions j in rows i = 2, 3, 4 are shifted to positions j + [image] mod Lb.


19.6 The MixColumn Transformation

After the rowwise permutation in the last step, in this step each column (bi,j), i = 0,..., 3, j = 0,..., Lb of a block is taken to be a polynomial over [image] and multiplied by the constant polynomial a(x) := a3x3 + a2x2 + a1x + a0, with coefficients a0(x) = x, a1(x) = 1, a2(x) = 1, a3(x) = x + 1, and reduced modulo M(x) := x4 + 1. Each byte of a column thus interacts with every other byte of the column. The rowwise operating ShiftRow transformation has the effect that in each round, other bytes are mixed with one another, resulting in strong diffusion.

We have already seen (see page 363) how this step can be reduced to a matrix multiplication

Multiplication by '02' (respectively x) has already been reduced to the application of the function xtime; multiplication by '03' (respectively x + 1) was already handled similarly (cf. page 366).

For inverting the MixColumn transformation every column (bi,j) of a block is multiplied by the polynomial r(x) := r3x3 + r2x2 + r1x + r0 with coefficients r0(x) = x3 + x2 + x, r1(x) = x3 + 1, r2(x) = x3 + x2 + 1, and r3(x) = x3 + x + 1 and reduced modulo M(x) := x4 + 1. The corresponding matrix is


19.7 The AddRoundKey Step

The last step of a round carries out an XOR of the round key with the block:

Click To expand

for j = 0,..., Lb − 1.


19.8 Encryption of a Block as a Complete Process

Encryption with Rijndael is encapsulated in the following pseudocode according to [DaRi], Sections 4.2–4.4. The arguments are passed as pointers to fields of bytes or 4-byte words. The interpretation of the fields, variables, and functions employed is provided in Tables 19.15-19.17.

Table 19.15: Interpretation of variables
Variable | Interpretation
Nk | length Lk of the secret user key in 4-byte words
Nb | block length Lb in 4-byte words
Nr | round number Lr according to the table above
Table 19.16: Interpretation of fields
Variables | Size in bytes | Interpretation
CipherKey | 4*Nk | secret user key
ExpandedKey | 4*Nb * (Nr+1) | field of 4-byte words to hold the round key
Rcon | 4*Nb * (Nr+1)/Nk | field of 4-byte words as constant c(j) := (rc(j), 0, 0, 0)
State | 4*Nb | field for input and output of plain text and encrypted blocks
RoundKey | 4*Nb | round key, segment of ExpandedKey
Table 19.17: Interpretation of functions
Function | Interpretation
KeyExpansion | generation of round key
RotByte | left rotation of a 4-byte word by 1 byte: (abcd) → (bcda)
ByteSub | S-box substitution S of all bytes of the passed field
Round | regular round
FinalRound | last round without MixColumn
ShiftRow | ShiftRow transformation
MixColumn | MixColumn transformation
AddRoundKey | addition of a round key

Key generation for Lk < 8:

  KeyExpansion (byte CipherKey, word ExpandedKey)
  {
    for (i = 0; i < Nk; i++)
      ExpandedKey[i] = (CipherKey[4*i], CipherKey[4*i + 1],
         CipherKey[4*i + 2], CipherKey[4*i + 3]);
    for (i = Nk; i < Nb * (Nr + 1); i++)
    {
      temp = ExpandedKey[i − 1];
      if (i % Nk == 0)
         temp = ByteSub (RotByte (temp))^ Rcon[i/Nk];
      ExpandedKey[i] = ExpandedKey[i − Nk] ^ temp;
    }
  }

Key generation for Lk = 8:

  KeyExpansion (byte CipherKey, word ExpandedKey)
  {
    for (i = 0; i < Nk; i++)
      ExpandedKey[i] = (CipherKey[4*i], CipherKey[4*i + 1],
              CipherKey[4*i + 2], CipherKey[4*i + 3]);

    for (i = Nk; i < Nb * (Nr + 1); i++)
    {
      temp = ExpandedKey[i − 1];
      if (i % Nk == 0)
         temp = ByteSub (RotByte (temp)) ^ Rcon[i/Nk];
      else if (itemp = ByteSub (temp);
      ExpandedKey[i] = ExpandedKey[i − Nk] ^ temp;
    }
  }

Round functions:

  Round (word State, word RoundKey)
  {
    ByteSub (State);
    ShiftRow (State);
    MixColumn (State);
    AddRoundKey (State, RoundKey)
  }

  FinalRound (word State, word RoundKey)
  {
    ByteSub (State);
    ShiftRow (State);
    AddRoundKey (State, RoundKey)
  }

Entire operation for encrypting a block:

  Rijndael (byte State, byte CipherKey)
  {
    KeyExpansion (CipherKey, ExpandedKey);
    AddRoundKey (State, ExpandedKey);
    for (i = 1; i < Nr; i++)
      Round (State, ExpandedKey + Nb*i);
    FinalRound (State, ExpandedKey + Nb*Nr);
  }

There exists the possibility of preparing the round key outside of the function Rijndael and to pass the key schedule ExpandedKey instead of the user key CipherKey. This is advantageous when it is necessary in the encryption of texts that are longer than a block to make several calls to Rijndael with the same user key.

  Rijndael (byte State, byte ExpandedKey)
  {
    AddRoundKey (State, ExpandedKey);
    for (i = 1; i < Nr; i++)
      Round (State, ExpandedKey + Nb*i);
    FinalRound (State, ExpandedKey + Nb*Nr);
  }

Especially for 32-bit processors it is advantageous to precompute the round transformation and to store the results in tables. By replacing the permutation and matrix operations by accesses to tables, a great deal of CPU time is saved, yielding improved results for encryption, and, as we shall see, for decryption as well. With the help of four tables each of 256 4-byte words of the form

Click To expand

(for w = 0,..., 255, S(w) denotes, as above, the S-box replacement), the transformation of a block b = (b0,j, b1,j, b2,j, b3,j), j = 0,..., Lb − 1, can be determined quickly by the substitution

Click To expand

with d(i, j) := j − [image] mod Lb (cf. ShiftRow, Table 19.14) and kj = (k0,j, k1,j, k2,j, k3,j) as the jth column of the round key.

For the derivation of this result, see [DaRi], Section 5.2.1. In the last round the MixColumn transformation is omitted, and thus the result is determined by

Click To expand

Clearly, it is possible to use a table of 256 4-byte words, in which

Click To expand

with a left rotation r(a, b, c, d) = (b, c, d, a) by one byte. For environments with limited memory this can be a useful compromise, the price being only a slightly increased calculation time for the three rotations.


19.9 Decryption

For Rijndael decryption one runs the encryption process in reverse order with the inverse transformations. We have already considered the inverses of the transformations ByteSub, ShiftRow, and MixColumn, which in the following are represented in pseudocode by the functions InvByteSub, InvShiftRow, and InvMixColumn. The inverted S-box, the distances for inversion, the ShiftRow transformation, and the inverted matrix for the inversion of the MixColumn transformation are given on pages 369–370. The inverse round functions are the following:

  InvFinalRound (word State, word RoundKey)
  {
    AddRoundKey (State, RoundKey);
    InvShiftRow (State);
    InvByteSub (State);
  }

  InvRound (word State, word RoundKey)
  {
    AddRoundKey (State, RoundKey);
    InvMixColumn (State);
    InvShiftRow (State);
    InvByteSub (State);
  }

The entire operation for decryption of a block is as follows:

  InvRijndael (byte State, byte CipherKey)
  {
    KeyExpansion (CipherKey, ExpandedKey);
    InvFinalRound (State, ExpandedKey + Nb*Nr);
    for (i = Nr − 1; i > 0; i−−)
      InvRound (State, ExpandedKey + Nb*i);
    AddRoundKey (State, ExpandedKey);
  }

The algebraic structure of Rijndael makes it possible to arrange the transformations for encryption in such a way that here, too, tables can be employed. Here one must note that the substitution S and the ShiftRow transformation commute, so that within a round their order can be switched. Because of the homomorphism property f(x + y) = f(x) + f (y) of linear transformations the MixColumn transformation and addition of the round key can be exchanged when MixColumn was used previously on the round key. Within a round the following course is taken:

  InvFinalRound (word State, word RoundKey)
  {
    AddRoundKey (State, RoundKey);
    InvByteSub (State);
    InvShiftRow (State);
  }

  InvRound (word State, word RoundKey)
  {
    InvMixColumn (State);
    AddRoundKey (State, MixColumn (RoundKey));
    InvByteSub (State);
    InvShiftRow (State);
  }

Without changing the sequence of transformations over both functions ordered one after the other, they can be redefined as follows:

  AddRoundKey (State, RoundKey);

  InvRound (word State, word RoundKey)
  {
    InvByteSub (State);
    InvShiftRow (State);
    InvMixColumn (State);
    AddRoundKey (State, MixColumn (RoundKey));
  }

  InvFinalRound (word State, word RoundKey)
  {
    InvByteSub (State);
    InvShiftRow (State);
    AddRoundKey (State, RoundKey)
  }

With this is created the analogous structure to that for encryption. For reasons of efficiency the application of MixColumn to the round key in InvRound() is postponed until the key expansion, where the first and last round keys of MixColumn are left untouched. The "inverse" round keys are generated with

  InvKeyExpansion (byte CipherKey, word InvEpandedKey)
  {
    KeyExpansion (CipherKey, InvExpandedKey);
    for (i = 1; i < Nr; I++)
      InvMixColumn (InvExpandedKey + Nb*i);
  }

The entire decryption operation of a block is now as follows:

  InvRijndael (byte State, byte CipherKey)
  {
    InvKeyExpansion (CipherKey, InvExpandedKey);
    AddRoundKey (State, InvExpandedKey + Nb*Nr);
    for (i = Nr − 1; i > 0; i−−)
      InvRound (State, InvExpandedKey + Nb*i);
    InvFinalRound (State, InvExpandedKey);
  }

In analogy to encryption, tables can be precomputed for this form of decryption. With

Click To expand

(for w = 0,..., 255, S−1(w) denotes the inverse S-box replacement) the result of an inverse round operation on a block b = (b0,j, b1,j, b2,j, b3,j), j = 0,..., Lb − 1, can be determined by

Click To expand

for j = 0,..., Lb − 1 with d−1 (i, j) := j + [image] mod Lb (cf. page 369) and the jth column [image] the "inverse" round key.

Again in the last round the MixColumn transformation is omitted, and thus the result of the last round is given by

Click To expand

for j = 0,..., Lb − 1.

To save memory one can also make do in decryption with a table of only 256 4-byte words, in which

Click To expand

with a left rotation r(a, b, c, d) = (b, c, d, a) of one byte.

For further details, investigations into security and cryptanalysis, computational times, and current information on AES and Rijndael the reader is referred to the literature cited above as well as the Internet sites of NIST and Vincent Rijmen, which in turn contain many links to further sources of information:

  • http://csrc.nist.gov/encryption/aes/aes_home.htm

  • http://www.esat.kuleuven.ac.be/~rijmen/rijndael

On the accompanying CD-ROM are to be found three implementations of Rijndael, which are recommended for deepening the reader's understanding of the procedure and for further investigations. The programs also contain application segments with functions for Cipher Feedback Mode (CFB) and Output Feedback Mode (OFB). Once again, the author wishes to thank all the authors for permission to include their source code in this book. The source code is located in the following directories:

Directory | Implementation
rijndael\c_ref | C reference implementation with test suite by Vincent Rijmen and Paulo Barreto
rijndael\c_opt | optimized implementation in C by Vincent Rijmen, Antoon Bosselaers, and Paulo Barreto
rijndael\cpp_opt | optimized C++ implementation by Brian Gladman


Part Overview

 Team-Fly

Part III: Appendices

Appendix List

Appendix A: Directory of C Functions
Appendix B: Directory of C++ Functions
Appendix C: Macros
Appendix D: Calculation Times
Appendix E: Notation
Appendix F: Arithmetic and Number-Theoretic Packages

Part Overview

And all the little girls cried, "Boohoo, we want to have our appendix out too!"

—Ludwig Bemelmans, Madeline


Appendix A: Directory of C Functions

A.1 Input/Output, Assignment, Conversions, Comparisons

void
cpy_l (CLINT dest_l,
     CLINT src_l);

assignment of src_l to dest_l

void
fswap_l (CLINT a_l,
     CLINT b_l);

exchange of a_l and b_l

int
equ_l (CLINT a_l, CLINT b_l);

test of equality of a_l and b_l

int
cmp_l (CLINT a_l, CLINT b_l);

size comparison of a_l and b_l

void
u2clint_l (CLINT num_l,
     USHORT ul);

convert USHORT to CLINT

void
ul2clint_l (CLINT num_l,
     ULONG ul);

convert ULONG to CLINT

UCHAR*
clint2byte_l (CLINT n_l,
     int *len);

convert CLINT to a byte vector (according to IEEE, P1363, 5.5.1)

int
byte2clint_l (CLINT n_l,
     char *bytes, int len);

convert a byte vector to CLINT (according to IEEE, P1363, 5.5.1)

char*
xclint2str_l (CLINT n_l,
     USHORT base,
     int showbase);

convert from CLINT into a character string to the base base, with or without prefix

int
str2clint_l (CLINT n_l,
     char *N, USHORT b);

convert a character string to the base b to CLINT

unsigned int
vcheck_l (CLINT n_l);

CLINT format check

clint*
setmax_l (CLINT n_l);

set a CLINT integer to the maximal value 2CLINTMAXBIT − 1

char*
verstr_l ();

output the version of the FLINT/C library in a character string, with identifiers 'a' for assembler support and 's' for FLINT/C security mode


A.2 Basic Calculations

int
add_l (CLINT a_l, CLINT b_l,
     CLINT s_l)

addition: sum of a_l and b_l, output in s_l

int
uadd_l (CLINT a_l, USHORT b,
     CLINT s_l)

mixed addition: sum of a_l and b, output in s_l

int
inc_l (CLINT a_l)

increment a_l

int
sub_l (CLINT a_l, CLINT b_l,
     CLINT s_l)

subtraction: difference of a_l and b_l, output in s_l

int
usub_l (CLINT a_l, USHORT b,
     CLINT c_l)

mixed subtraction: difference of a_l and b, output in s_l

int
dec_l (CLINT a_l)

decrement a_l

int
mul_l (CLINT a_l, CLINT b_l,
     CLINT p_l)

multiplication: product of a_l and b_l, output in p_l

int
umul_l (CLINT a_l, USHORT b,
     CLINT p_l)

mixed multiplication: product of a_l and b, output in p_l

int
sqr_l (CLINT a_l, CLINT p_l)

square a_l, output in p_l

int
div_l (CLINT a_l,
CLINT b_l, CLINT q_l, CLINT
r_l)

division with remainder: division of a_l by b_l, quotient in q_l, remainder in r_l

int
udiv_l (CLINT a_l, USHORT b,
     CLINT q_l, CLINT r_l)

mixed division with remainder: division of a_l by b, quotient in q_l, remainder in r_l


A.3 Modular Arithmetic

int
mod_l (CLINT d_l, CLINT n_l,
    CLINT r_l);

residue of d_l mod n_l, output in r_l

USHORT
umod_l (CLINT d_l, USHORT n);

residue d_l mod n

int
mod2_l (CLINT d_l, ULONG k,
    CLINT r_l);

residue d_l mod 2k

int
mequ_l (CLINT a_l, CLINT b_l,
    CLINT m_l);

test for equality of a_l and b_l modulo m_l

int
madd_l (CLINT a_l, CLINT b_l,
    CLINT c_l, CLINT m_l);

modular addition: addition of a_l and b_l modulo m_l, output in c_l

int
umadd_l (CLINT a_l, USHORT b,
     CLINT c_l, CLINT m_l);

mixed modular addition: addition of a_l and b mod m_l, output in c_l

int
msub_l (CLINT a_l, CLINT b_l,
     CLINT c_l, CLINT m_l);

modular subtraction: subtraction of a_l and b_l mod m_l, output in c_l

int
umsub_l (CLINT a_l, USHORT b,
     CLINT c_l, CLINT m_l);

mixed modular subtraction: subtraction of a_l and b mod m_l, output in c_l

int
mmul_l (CLINT a_l, CLINT b_l,
     CLINT c_l, CLINT m_l);

modular multiplication: multiplication of a_l and b_l mod m_l, output in c_l

void
mulmon_l (CLINT a_l,
     CLINT b_l, CLINT n_l,
     USHORT nprime,
     USHORT log B_r,
     CLINT p_l);

modular multiplication of a_l and b_l mod n_l, product in p_l (Montgomery method, Blog B_r−1 ≤ n_l < Blog B_r)

void
sqrmon_l (CLINT a_l,
     CLINT n_l, USHORT nprime,
     USHORT logB_r,
     CLINT p_l);

modular squaring of a_l mod n_l, square in p_l (Montgomery method, Blog B_r−1 ≤ n_l < Blog B_r)

int
ummul_l (CLINT a_l, USHORT b,
     CLINT p_l, CLINT m_l);

mixed modular multiplication of a_l and b_l mod n_l, product in p_l

int
msqr_l (CLINT a_l, CLINT c_l,
     CLINT m_l);

modular squaring of a_l mod n_l, square in p_l

int
mexp5_l (CLINT bas_l,
     CLINT exp_l, CLINT p_l,
     CLINT m_l);

modular exponentiation, 25-ary method

int
mexpk_l (CLINT bas_l,
     CLINT exp_l,CLINT p_l,
     CLINT m_l);

modular exponentiation, 2k-ary method, dynamic memory with malloc()

int
mexp5m_l (CLINT bas_l,
     CLINT exp_l, CLINT p_l,
     CLINT m_l);

Montgomery exponentiation, 25-ary method, odd modulus

int
mexpkm_l (CLINT bas_l,
     CLINT exp_l, CLINT p_l,
     CLINT m_l);

Montgomery exponentiation, 25-ary method, odd modulus

int
umexp_l (CLINT bas_l,
     USHORT e, CLINT p_l,
     CLINT m_l);

modular exponentiation, USHORT exponent

int
umexpm_l (CLINT bas_l,
     USHORT e, CLINT p_l,
     CLINT m_l);

modular exponentiation, odd modulus, USHORT exponent

int
wmexp_l (USHORT bas,
     CLINT e_l, CLINT p_l,
     CLINT m_l);

modular exponentiation, USHORT base

int
wmexpm_l (USHORT bas,
     CLINT e_l, CLINT p_l,
     CLINT m_l);

Montgomery exponentiation, odd modulus, USHORT base

int
mexp2_l (CLINT bas_l,
     USHORT e, CLINT p_l,
     CLINT m_l);

modular exponentiation, exponent e a power of 2

int
mexp_l (CLINT bas_l,
     CLINT e_l, CLINT p_l,
     CLINT m_l);

modular exponentiation, automatic use of mexpkm_l() if modulus odd, otherwise mexpk_l()


A.4 Bitwise Operations

int
setbit_l (CLINT a_l,
     unsigned int pos)

test and set the bit of a_l in position pos

int
testbit_l (CLINT a_l,
     unsigned int pos)

test the bit of a_l in position pos

int
clearbit_l (CLINT a_l,
     unsigned int pos)

test and clear the bit of a_l in position pos

void
and_l (CLINT a_l, CLINT b_l,
     CLINT c_l)

bitwise AND of a_l and b_l, output in c_l

void
or_l (CLINT a_l, CLINT b_l,
     CLINT c_l)

bitwise OR of a_l and b_l, output in c_l

void
xor_l (CLINT a_l, CLINT b_l,
     CLINT c_l)

bitwise exclusive OR (XOR) of a_l and b_l, output in c_l

int
shr_l (CLINT a_l)

left shift of a_l by 1 bit

int
shl_l (CLINT a_l)

right shift of a_l by 1 bit

int
shift_l (CLINT a_l,
     long int noofbits)

left/right shift of a_l by noofbits bits


A.5 Number-Theoretic Functions

unsigned
issqr_l (CLINT a_l, CLINT b_l)

test for square property of a_l, if so, output of square root in b_l

unsigned
iroot_l (CLINT a_l, CLINT b_l)

integer part of square root of a_l, output in b_l

void
gcd_l (CLINT a_l, CLINT b_l,
     CLINT g_l)

greatest common divisor of a_l and b_l, output in g_l

void
xgcd_l (CLINT a_l, CLINT b_l,
     CLINT g_l,
     CLINT u_l, int *sign_u,
     CLINT v_l, int *sign_v)

greatest common divisor of a_l and b_l and representation of gcd in u_l and v_l with sign in sign_u and sign_v

void
inv_l (CLINT a_l, CLINT n_l,
     CLINT g_l, CLINT i_l)

gcd of a_l mod n_l and inverse of a_l mod n_l

void
lcm_l (CLINT a_l, CLINT b_l,
     CLINT v_l)

least common multiple of a_l and b_l, output in v_l

int
chinrem_l (unsigned noofeq,
     clint** coeff_l,
     CLINT x_l)

solution of simultaneous linear congruences, output in x_l

int
jacobi_l (CLINT a_l,
     CLINT b_l)

Legendre/Jacobi symbol, a_l over b_l

int
proot_l (CLINT a_l, CLINT p_l,
     CLINT x_l)

square root of a_l mod p_l, output in x_l

int
root_l (CLINT a_l, CLINT p_l,
     CLINT q_l, CLINT x_l)

square of a_l mod p_l*q_l, output in x_l

int
primroot_l (CLINT x_l,
     unsigned noofprimes,
     clint** primes_l)

determine a primitive root modulo n, output in x_l

USHORT
sieve_l (CLINT a_l,
     unsigned noofsmallprimes)

division sieve, division of a_l by small primes

int prime_l (CLINT n_l,
unsigned noofsmallprimes,
     unsigned iterations)

Miller-Rabin primality test of n_l with division sieve


A.6 Generation of Pseudorandom Numbers

clint* rand64_l (void) | 64-bit random number generator
void rand_l (CLINT r_l, int l) | CLINT random number with l binary digits, linear congruences
UCHAR ucrand64_l (void) | generator for random numbers of type UCHAR
USHORT usrand64_l (void) | generator for random numbers of type USHORT
ULONG ulrand64_l (void) | generator for random numbers of type ULONG
clint* seed64_l (CLINT seed_l) | initialization of rand64_l() with CLINT value
clint* ulseed64_l (ULONG seed) | initialization of rand64_l() with ULONG value
int randbit_l (void) | BBS bit generator
clint* randBBS_l (void) | CLINT random number with l binary digits using BBS bit generator
UCHAR ucrandBBS_l (void) | BBS generator for random number of type UCHAR
USHORT usrandBBS_l (void) | BBS generator for random number of type USHORT
ULONG ulrandBBS_l (void) | BBS generator for random number of type ULONG
void seedBBS_l (CLINT seed_l) | initialization of randbit_l() with CLINT value
void ulseedBBS_l (ULONG seed) | initialization of randbit_l() with ULONG value


A.7 Register Management

void
set_noofregs_l (unsigned int nregs)

set number of registers

int
create_reg_l (void)

generate the CLINT register bank

clint*
get_reg_l (unsigned int reg)

generate reference to register reg of the register bank

int
purge_reg_l (unsigned int reg)

clear a register of the register bank by overwriting

int
purgeall_reg_l (void)

clear all registers of the register bank by overwriting

void
free_reg_l (void)

clear all registers of the register bank by overwriting, then release memory

clint*
create_l (void)

generate a CLINT register

void
purge_l (CLINT n_l)

clear a CLINT object by overwriting

void
free_l (CLINT n_l);

clear a register by overwriting and release memory


Appendix B: Directory of C++ Functions

B.1 Input/Output, Conversion, Comparison: Member Functions

LINT (void);

Constructor 1:

an uninitialized LINT object is generated

LINT (const char* const str,
    const int base);

Constructor 2:

LINT is constructed from a string of digits to base base

LINT (const UCHAR* const byte,
    const int len);

Constructor 3:

LINT is constructed from a byte vector with digits to base 28 according to IEEE P1363, significance of bits grows from left to right

LINT (const char* const str);

Constructor 4:

LINT is constructed from an ASCII string with C-Syntax

LINT (const LINT&);

Constructor 5:

LINT is constructed from LINT (copy constructor)

LINT (const signed int);

Constructor 6:

LINT is constructed from an integer of type int

LINT (const signed long);

Constructor 7:

LINT is constructed from an integer of type long

LINT (const unsigned char);

Constructor 8:

LINT is constructed from an integer of type unsigned char

LINT (const USHORT);

Constructor 9:

LINT is constructed from an integer of type unsigned short

LINT (const unsigned int);

Constructor 10:

LINT is constructed from an integer of type unsigned int

LINT (const unsigned long);

Constructor 11:

LINT is constructed from an integer of type unsigned long

LINT (const CLINT);

Constructor 12:

LINT is constructed from an integer of type CLINT

const LINT&
operator = (const LINT& b);

assignment a ← b

inline void
disp (char* str);

display of a LINT integer, with previous output of str

inline char*
hexstr (void) const;

representation of a LINT integer as hexadecimal number

inline char*
decstr (void) const;

representation of a LINT integer as decimal number

inline char*
octstr (void) const;

representation of a LINT integer as octal number

inline char*
binstr (void) const;

representation of a LINT integer as binary number

char*
lint2str (const USHORT base,
    const int showbase = 0) const;

representation of a LINT integer as character string to base base, with prefix 0x, or 0b if showbase > 0

UCHAR*
lint2byte (int* len) const;

transformation of a LINT integer into a byte vector, output of length in len, according to IEEE P1363, significance of bytes increasing from left to right

LINT&
fswap (LINT& b);

exchange of implicit argument a with argument b

void
purge (void);

clear implicit argument a by overwriting

Static long
flags (ostream& s);

read static LINT status variable to ostream s

static long
flags (void);

read static LINT status variable to ostream cout

static long
setf (ostream& s,
    long intflags);

set status bits of value flags in LINT status variable referring to ostream s

static long
setf (long int flags);

set status bits in flags in LINT status variable referring to ostream cout

static long
unsetf (ostream& s,
     long int flags);

unset status bits in flags in LINT status variable referring to ostream s

static long
unsetf (long int flags);

unset status bits of flags in LINT status variable referring to ostream cout

static long
restoref (ostream& s,
     long int flags);

reset the LINT status variable referring to ostream s to the value in flags

static long
restoref (long int flags);

reset LINT status variable referring to ostream cout to the value in flags


B.2 Input/Output, Conversion, Comparison: Friend Functions

const int
operator == (const LINT& a,
     const LINT& b);

test a == b

const int
operator != (const LINT& a
     const LINT& b);

test a != b

const int
operator < (const LINT& a,
     const LINT& b);

comparison a < b

const int
operator > (const LINT& a,
     const LINT& b);

comparison a > b

const int
operator <= (const LINT& a,
     const LINT& b);

comparison a <= b

const int
operator >= (const LINT& a,
     const LINT& b);

comparison a >= b

void
fswap (LINT& a, LINT& b);

exchange of a and b

void
purge (LINT& a);

clear by overwriting

char*
lint2str (const LINT& a,
     const USHORT base,
     const int showbase);

representation of a as character string to base base, with prefix 0x, or 0b if showbase > 0

UCHAR*
lint2byte (const LINT& a,
     int* len);

transformation of a into a byte vector, output of length in len, according to IEEE P1363, significance of bytes increasing from left to right

ostream&
LintHex (ostream& s);

ostream manipulator for hex representation of LINT integers

ostream&
LintDec (ostream& s);

ostream manipulator for decimal representation of LINT integers

ostream&
LintShowbase (ostream& s);

ostream manipulator for the display of a prefix 0x (resp. 0b) in hex (resp. binary) representation of LINT integers

ostream&
LintNobase (ostream& s);

ostream manipulator for omission of a prefix 0x or 0b in hex or binary representation of LINT integers

ostream&
LintShowlength (ostream& s);

ostream manipulator for display of the binary length in the output of LINT integers

ostream&
LintNolength (ostream& s);

manipulator for the omission of binary length in the output LINT integers

LINT_omanip<int>
SetLintFlags (int flag);

manipulator for setting status bits of the value flag in the LINT status variable

LINT_omanip<int>
ResetLintFlags (int flag);

manipulator to unset status bits of value flag in the LINT status variable

ostream&
operator << (ostream& s,
     const LINT& ln);

overloaded insert operator for output of LINT integers, output stream of type ostream

ofstream&
operator << (ofstream& s,
     const LINT& ln);

overloaded insert operator for writing LINT integers to files, output stream of type ofstream

fstream&
operator << (fstream& s,
     const LINT& ln);

overloaded insert operator for writing LINT integers to files, output stream of type fstream

ifstream&
operator >> (ifstream& s,
    LINT& ln);

overloaded extract operator for reading LINT integers from files, input stream of type ifstream

fstream&
operator >> (fstream& s,
    LINT& ln);

overloaded extract operator for reading LINT integers from files, input/output stream of type fstream


B.3 Basic Operations: Member Functions

const LINT&
operator ++ (void);

increment operator (prefix) ++a;

const LINT
operator ++ (int);

increment operator (postfix) a++;

const LINT& operator --
(void);

decrement operator (prefix) −−a;

const LINT
operator -- (int);

decrement operator (postfix) a−−;

const LINT&
operator += (const LINT& b);

addition and assignment a += b;

const LINT&
operator -= (const LINT& b);

subtraction and assignment a −= b;

const LINT&
operator *= (const LINT& b);

multiplication and assignment a *= b;

const LINT&
operator /= (const LINT& b);

division and assignment a /= b;

const LINT&
operator %= (const LINT& b);

remainder and assignment a %= b;

const LINT&
add (const LINT& b);

addition c = a.add (b);

const LINT&
sub (const LINT& b);

subtraction c = a.sub (b);

const LINT&
mul (const LINT& b);

multiplication c = a.mul (b);

const LINT&
sqr (void);

squaring c = a.sqr (b);

const LINT&
divr (const LINT& d, LINT& r);

division with remainder quotient = dividend.div (divisor, remainder);


B.4 Basic Operations: Friend Functions

const LINT
operator + (const LINT& a,
     const LINT& b);

addition c = a + b;

const LINT
operator - (const LINT& a,
     const LINT& b);

subtraction c = a − b;

const LINT
operator * (const LINT& a,
     const LINT& b);

multiplication c = a * b;

const LINT
operator / (const LINT& a,
     const LINT& b);

division c = a / b;

const LINT
operator % (const LINT& a,
     const LINT& b);

remainder c = a % b;

const LINT
add (const LINT& a,
     const LINT& b);

addition c = add (a, b);

const LINT
sub (const LINT& a,
     const LINT& b);

subtraction c = sub (a, b);

const LINT
mul (const LINT& a,
     const LINT& b);

multiplication c = mul (a, b);

const LINT
sqr (const LINT& a);

squaring b = sqr (a);

const LINT
divr (const LINT& a,
    const LINT& b, LINT& r);

division with remainder quotient = div (dividend, divisor, remainder);


B.5 Modular Arithmetic: Member Functions

const LINT&
mod (const LINT& m);

remainder b = a.mod (m);

const LINT&
mod2 (const USHORT u);

remainder modulo power of two 2u, b = a.mod (u);

const int
mequ (const LINT& b,
     const LINT& m) const;

comparison of a and b modulo m if (a.mequ (b, m)) ...

const LINT&
madd (const LINT& b,
     const LINT& m);

modular addition, c = a.madd (b, m);

const LINT&
msub (const LINT& b,
     const LINT& m);

modular subtraction, c = a.msub(b, m);

const LINT&
mmul (const LINT& b,
     const LINT& m);

modular multiplication, c = a.mmul (b, m);

const LINT&
msqr (const LINT& m);

modular squaring, c = a.msqr (m);

const LINT&
mexp (const LINT& e,
     const LINT& m);

modular exponentiation with Montgomery reduction for odd modulus m,c = a.mexp (e, m);

const LINT&
mexp (const USHORT u,
     const LINT& m);

modular exponentiation with USHORT exponent, Montgomery reduction for odd modulus m, c = a.mexp (u, m);

const LINT&
mexp5m (const LINT& e,
     const LINT& m);

modular exponentiation with Montgomery reduction for odd modulus m,c = a.mexp5m (e, m);

const LINT&
mexpkm (const LINT& e,
     const LINT& m);

modular exponentiation with Montgomery reduction for odd modulus m, c = a.mexpkm (e, m);

const LINT&
mexp2 (const USHORT u,
    const LINT& m);

modular exponentiation with power of two exponent 2u, c = a.mexp2 (u, m);


B.6 Modular Arithmetic: Friend Functions

const LINT
mod (const LINT& a,
     const LINT& m);

remainder b = mod (a, m);

const LINT
mod2 (const LINT& a,
     const USHORT u);

remainder modulo power of two 2u, b = mod (a, u);

const int
mequ (const LINT& a,
     const LINT& b,
     const LINT& m);

comparison of a and b modulo m if (mequ (a, b, m)) ...

const LINT
madd (const LINT& a,
     const LINT& b,
     const LINT& m);

modular addition, c = madd (a, b, m);

const LINT
msub (const LINT& a,
     const LINT& b,
     const LINT& m);

modular subtraction, c = msub(a, b, m);

const LINT
mmul (const LINT& a,
     const LINT& b,
     const LINT& m);

modular multiplication, c = mmul (a, b, m);

const LINT
msqr (const LINT& a,
     const LINT& m);

modular squaring, c = msqr (a, m);

const LINT
mexp (const LINT& a,
     const LINT& e,
     const LINT& m);

modular exponentiation with Montgomery reduction for odd modulus m, c = mexp (a, e, m);

const LINT
mexp (const USHORT u,
     const LINT& e,
     const LINT& m);

modular exponentiation with USHORT base, Montgomery reduction for odd modulus m, c = mexp (u, e, m);

const LINT
mexp (const LINT& a,
     const USHORT u,
     const LINT& m);

modular exponentiation with USHORT exponent, Montgomery reduction for odd modulus m, c = mexp (a, u, m);

const LINT
mexp5m (const LINT& a,
     const LINT& e,
     const LINT& m);

modular exponentiation with Montgomery reduction, only for odd modulus m, c = mexp5m (a, e, m);

const LINT
mexpkm (const LINT& a,
     const LINT& b,
     const LINT& m);

modular exponentiation with Montgomery reduction, only for odd modulus m, c = mexpkm (a, e, m);

const LINT
mexp2 (const LINT& a,
     const USHORT u,
     const LINT& m);

modular exponentiation with power of two exponent 2u, c = mexp2 (a, u, m);


B.7 Bitwise Operations: Member Functions

const LINT&
operator ^= (const LINT& b);

XOR and assignment, a ^= b;

const LINT&
operator |= (const LINT& b);

OR and assignment, a |= b;

const LINT&
operator &= (const LINT& b);

AND and assignment, a &= b;

const LINT&
operator <<= (const int i);

left shift and assignment, a <<= i;

const LINT&
operator >>= (const int i);

right shift and assignment, a <<= i;

const LINT&
shift (const int i);

shift (left and right) by i bit positions, a.shift (i);

const LINT&
setbit (const unsigned int i);

set a bit at position i, a.setbit (i);

const LINT&
clearbit (const
     unsigned int i);

clear a bit at position i, a.clearbit (i);

const int
testbit (const
     unsigned int i) const;

test a bit at position i, a.testbit (i);


B.8 Bitwise Operations: Friend Functions

const LINT
operator ^ (const LINT& a,
     const LINT& b);

XOR, c = a ^ b;

const LINT
operator | (const LINT& a,
     const LINT& b);

OR, c = a | b;

const LINT
operator & (const LINT& a,
     const LINT& b);

AND, c = a & b;

const LINT
operator << (const LINT& a,
     const int i);

left shift, b = a << i;

const LINT
     const int i);

right shift, b = a >> i;

const LINT
shift (const LINT& a,
     const int i);

shift (left and right) by i bit positions, b = shift (a, i);


B.9 Number-Theoretic Member Functions

const unsigned int
ld (void) const;

return ⌊log2(a)⌋

const int
iseven (void) const;

test a for divisibility by 2: true if a even

const int
isodd (void) const;

test a for divisibility by 2: true if a odd

const LINT
issqr (void) const;

test a for being square

const int
isprime (void) const;

test a for primality

const LINT
gcd (const LINT& b);

return gcd of a and b

const LINT
xgcd (const LINT& b,
    LINT& u, int& sign_u,
    LINT& v, int& sign_v)
const;

extended Euclidean algorithm with return of gcd of a and b, u and v contain the absolute values of the factors of the linear combination g = sign_u*u*a + sign_v*v*b

const LINT
inv (const LINT& b) const;

return the multiplicative inverse of a mod b

const LINT
lcm (const LINT& b) const;

return the least common multiple of a and b

const int
jacobi (const LINT& b) const;

return the Jacobi symbol ( [image] )

const LINT
root (void) const;

return the integer part of the square root of a

const LINT
root (const LINT& p) const;

return the square root of a modulo an odd prime p

const LINT
root (const LINT& p,
    const LINT& q) const;

return the square root of a modulo p*q, where p and q are odd primes

const int
twofact (LINT& odd) const;

return the even part of a, odd contains the odd part of a

const LINT
chinrem (const LINT& m,
     const LINT& b,
     const LINT& n) const;

return a solution x of the system of simultaneous congruences x ≡ a mod m and x ≡ b mod n, if a solution exists


B.10 Number-Theoretic Friend Functions

const unsigned
ld (const LINT& a);

return ⌊log2(a)⌋

const int
iseven (const LINT& a);

test a for divisibility by 2: true if a even

const int
isodd (const LINT& a);

test a for divisibility by 2: true if a odd

const LINT
issqr (const LINT& a);

test a for being a square

const int
isprime (const LINT& a);

test a for primality

const LINT
gcd (const LINT& a,
     const LINT& b);

return gcd of a and b

const LINT
xgcd (const LINT& a,
     const LINT& b,
     LINT& u, int& sign_u,
     LINT& v, int& sign_v);

extended Euclidean algorithm with return of gcd of a and b, u and v contain the absolute values of the factors of the linear combination g = sign_u*u*a + sign_v*v*b

const LINT
inv (const LINT& a,
     const LINT& b);

return the multiplicative inverse of a mod b

const LINT
lcm (const LINT& a,
     const LINT& b);

return the least common multiple of a and b

const int
jacobi (const LINT& a,
     const LINT& b);

return the Jacobi symbol ( [image] )

const LINT
root (const LINT& a);

return the integer part of the square root of a

const LINT
root (const LINT& a,
     const LINT& p);

return the square root of a modulo an odd prime p

const LINT
root (const LINT& a,
     const LINT& p,
     const LINT& q);

return the square root of a modulo p*q for p and q odd primes

const LINT
chinrem (const unsigned
     noofeq, LINT** coeff);

return a solution of a system of simultaneous linear congruences. In coeff is passed a vector of pointers to LINT objects as coefficients a1, m1, a2, m2, a3, m3, ... of the congruence system with noofeq equations x ≡ ai mod mi

const LINT
primroot (const unsigned
     noofprimes,
     LINT** primes);

return a primitive root modulo p. In noofprimes is passed the number of distinct prime factors of the group order p − 1, in primes a vector of pointers to LINT objects, beginning with p − 1, then come the prime divisors p1,...,pk of the group order p − 1 = [image] with k = noofprimes

const int
twofact (const LINT& even,
    LINT& odd);

return the even part of a, odd contains the odd part of a

const LINT
findprime (const USHORT l);

return a prime p of length l bits, i.e., 2l−1 ≤ p < 2l

const LINT
findprime (const USHORT l,
     const LINT& f);

return a prime p of length l bits, i.e., 2l−1 ≤ p < 2l and gcd(p − 1, f) = 1, f odd

const LINT
findprime (const LINT& pmin,
     const LINT& pmax,
     const LINT& f);

return a prime p with pmin ≤ p ≤ pmax and gcd(p − 1, f) = 1, f odd

const LINT
nextprime (const LINT& a,
     const LINT& f);

return the smallest prime p above a with gcd(p − 1, f) = 1, f odd

const LINT
extendprime (const USHORT l,
     const LINT& a,
     const LINT& q,
     const LINT& f);
const LINT
extendprime (const LINT& pmin,
const LINT& pmax,
     const LINT& a,
     const LINT& q,
     const LINT& f);

return a prime p of length l bits, i.e., 2l−1 ≤ p < 2l, with p ≡ a mod q and gcd(p − 1, f) = 1, f odd

return a prime p with pmin ≤ p ≤ pmax, with p ≡ a mod q and gcd(p − 1, f) = 1, f odd

const LINT
strongprime (const USHORT l);

return a strong prime p of length l bits, i.e., 2l−1 ≤ p < 2l

const LINT
strongprime (const USHORT l,
     const LINT& f);

return a strong prime p of length l bits, i.e., 2l−1 ≤ p < 2l, with gcd(p − 1, f) = 1, f odd

const LINT
strongprime (const USHORT l,
     const USHORT lt,
     const USHORT lr,
     const USHORT ls,
     const LINT& f);

return a strong prime p of length l bits, i.e., 2l−1 ≤ p < 2l, with gcd(p − 1, f) = 1, f odd lt [image] , ls ≈ lr [image] of length of p

const LINT
strongprime (const LINT& pmin,
     const LINT& pmax,
     const LINT& f);

return a strong prime p with pmin ≤ p ≤ pmax, gcd(p − 1, f) = 1, f odd, default lengths lr, lt, ls of prime divisors r of p − 1, t of r−1, s of p+1:lt [image] , ls ≈ lr [image] of the binary length of pmin

const LINT
strongprime (const LINT& pmin,
     const LINT& pmax,
     const USHORT lt,
     const USHORT lr,
     const USHORT ls,
     const LINT& f);

return a strong prime p with pmin ≤ p ≤ pmax, gcd(p − 1, f) = 1, f odd, lengths lr, lt, ls of prime divisors r of p − 1, t of r − 1, s of p + 1


B.11 Generation of Pseudorandom Numbers

void
seedl (const LINT& seed);

initialization of the 64-bit random number generator based on linear congruences with initial value seed

LINT
randl (const int l);

return a LINT random number of length l bits

LINT
randl (const LINT& rmin,
     const LINT& rmax);

return a LINT random number r with rmin ≤ r ≤ rmax

int
seedBBS (const LINT& seed);

initialization of BBS random number generator with initial value seed

LINT
randBBS (const int l);

return a LINT random number of length l bits

LINT
randBBS (const LINT& rmin,
     const LINT& rmax);

return a LINT random number r with rmin ≤ r ≤ rmax


B.12 Miscellaneous Functions

LINT_ERRORS
Get_Warning_Status (void);

query error status of a LINT object

static void
Set_LINT_Error_Handler
     (void (*)(LINT_ERRORS
     err,
     const char* const,
     const int, const int));

activation of a user routine for handling errors with LINT operations. The registered routine replaces the LINT standard error handler panic(); deactivation of registration of user routine and simultaneous reactivation of use routine panic() managed by the call Set_LINT_Error_Handler (NULL);


Appendix C: Macros

C.1 Error Codes and Status Values

E_CLINT_DBZ | −1 | division by zero
E_CLINT_OFL | −2 | overflow
E_CLINT_UFL | −3 | underflow
E_CLINT_MAL | −4 | memory allocation error
E_CLINT_NOR | −5 | register not available
E_CLINT_BOR | −6 | invalid base in str2clint_l()
E_CLINT_MOD | −7 | even modulus in Montgomery reduction
E_CLINT_NPT | −8 | null pointer passed as argument
E_VCHECK_OFL | 1 | vcheck_l() warning: number too long
E_VCHECK_LDZ | 2 | vcheck_l() warning: leading zeros
E_VCHECK_MEM | −1 | vcheck_l() error: null pointer


C.2 Additional Constants

BASE

0×10000

base B = 216 of the CLINT number format

BASEMINONE

0×ffffU

B −1

DBASEMINONE

0×ffffffffUL

B2 −1

BASEDIV2

0×8000U

⌊B/2⌋

NOOFREGS

16U

standard number of registers in register bank

BITPERDGT

16UL

number of binary digits per CLINT digit

LDBITPERDGT

4U

logarithm of BITPERDGT to base 2

CLINTMAXDIGIT

256U

maximal number of digits to base B of a CLINT object

CLINTMAXSHORT

(CLINTMAXDIGIT + 1)

USHORTs to be allocated for a CLINT object

CLINTMAXBYTE

(CLINTMAXSHORT << 1)

number of allocated bytes for a CLINT object

CLINTMAXBIT

(CLINTMAXDIGIT << 4)

maximal number of binary digits of a CLINT object

r0_l, ... , r15_l

get_reg_l(0), ... ,
get_reg_l(15)

pointer to CLINT registers 0,...,15

FLINT_VERMAJ

higher version number of the FLINT/C library

FLINT_VERMIN

lower version number of the FLINT/C library

FLINT_VERSION

((FLINT_VERMAJ << 8)
+ FLINT_VERMIN)

version number of the FLINT/C library

FLINT_SECURE

0×73, 0

identifier 's' or''for the FLINT/C security mode


C.3 Macros with Parameters

clint2str_l
     (n_l, base)
CLINT2STR_L)
     (n_l, base)

xclint2str_l((n_l),(base),0)

representation of a CLINT object as character string without prefix

DISP_L (S, A)

printf("%s%s\n%u bit\n\n",
(S), HEXSTR_L(A), ld_l(A))

standard output of a CLINT object

HEXSTR_L (n_l)

xclint2str_l((n_l), 16, 0)

conversion of a CLINT object into hex representation

DECSTR_L (n)

xclint2str_l((n), 10, 0)

conversion of a CLINT object into decimal representation

OCTSTR_L (n_l)

xclint2str_l((n_l), 8, 0)

conversion of a CLINT object into octal representation

BINSTR_L (n_l)

xclint2str_l((n_l), 2, 0)

conversion of a CLINT object into binary representation

SET_L (n_l, ul)

ul2clint_l((n_l), (ul))

assignment n_l ← ULONG ul

SETZERO_L (n_l)

(*(n_l) = 0)

set n_l to 0

SETONE_L (n_l)

u2clint_l((n_l), 1U)

set n_l to 1

SETTWO_L (n_l)

u2clint_l((n_l), 2U)

set n_l to 2

ASSIGN_L
     (a_l, b_l)

cpy_l((a_l), (b_l))

assignment a_l ← b_l

ANDMAX_L (a_l)

SETDIGITS_L((a_l),
(MIN(DIGITS_L(a_l),
(USHORT)CLINTMAXDIGIT));
RMLDZRS_L((a_l))

reduction modulo (Nmax + 1)

DIGITS_L (n_l)

(*(n_l))

read number of digits of n_l to base B

SETDIGITS_L
     (n_l, 1)

(*(n_l) = (USHORT)(l))

set number of digits n_l to l

INCDIGITS_L (n_l)

(++*(n_l))

increase number of digits by 1

DECDIGITS_L (n_l)

(−−*(n_l))

reduce number of digits by 1

LSDPTR_L (n_l)

((n_l) + 1)

pointer to least-significant digit of a CLINT object

MSDPTR_L (n_l)

((n_l) + DIGITS_L(n_l))

pointer to most-significant digit of a CLINT object

RMLDZRS_L (n_l)

while((DIGITS_L(n_l) >
0)&& (*MSDPTR_L(n_l) == 0))
{DECDIGITS_L(n_l);}

remove leading zeros from a CLINT object

SWAP (a, b)

((a)^=(b),(b)^=(a),(a)^=(b))

exchange

SWAP_L (a_l, b_l)

(xor_l((a_l),(b_l),(a_l)),
xor_l((b_l),(a_l),(b_l)),
xor_l((a_l),(b_l),(a_l)))

exchange two CLINT values

LT_L (a_l, b_l)

(cmp_l((a_l), (b_l)) == −1)

comparison a_l < b_l

LE_L (a_l, b_l)

(cmp_l((a_l), (b_l)) < 1)

comparison a_l ≤ b_l

GT_L (a_l, b_l)

(cmp_l((a_l), (b_l)) == 1)

comparison a_l > b_l

GE_L (a_l, b_l)

(cmp_l((a_l), (b_l)) > −1)

comparison a_l ≥ b_l

GTZ_L (a_l)

(cmp_l((a_l), nul_l) == 1)

comparison a_l > 0

EQZ_L (a_l)

(equ_l((a_l), nul_l) == 1)

comparison a_l == 0

EQONE_L (a_l)

(equ_l((a_l), one_l) == 1)

comparison a_l == 1

MIN_L (a_l, b_l)

(LT_L((a_l), (b_l))
? (a_l) : (b_l))

minimum of two CLINT values

MAX_L (a_l, b_l)

(GT_L((a_l), (b_l)) ? (a_l) :
(b_l))

maximum of two CLINT values

ISEVEN_L (n_l)

(DIGITS_L(n_l) == 0 ||
(DIGITS_L(n_l) > 0 &&
(*(LSDPTR_L(n_l)) & 1U) ==
0))

test whether n_l is odd

ISODD_L (n_l)

(DIGITS_L(n_l) > 0 &&
(*(LSDPTR_L(n_l)) & 1U) == 1)

test whether n_l is odd

MEXP_L (a_l, e_l,
    p_l, n_l)

mexpk_l((a_l), (e_l), (p_l),
(n_l))

exponentiation

MEXP_L (a_l, e_l,
     p_l, n_l)

mexp5_l((a_l), (e_l),
    (p_l), (n_l))
mexpkm_l((a_l), (e_l),
    (p_l), (n_l))
mexp5m_l((a_l), (e,_l),
    (p_l), (n_l))

exponentiation, alternative

INITRAND64_LT()

seed64_l((unsigned long)
time(NULL)

initialization of random number generator rand64_l() with system clock

INITRANDBBS_Lt()

seedBBS_l((unsigned long)
time(NULL)

initialization of the random bit generator randbit_l() by means of system clock

ISPRIME_L (n_l)

prime_l((n_l), 302, 5)

primality test with fixed parameters

ZEROCLINT_L (n_l)

memset((A), 0,
sizeof(A))


Appendix D: Calculation Times

CALCULATION TIMES FOR SEVERAL FLINT/C functions, calculated with a Pentium III processor running at 500 MHz and 64 Mbyte main memory, are given in Tables D.1 and D.2. The times for n operations were measured and then divided by n. Depending on the functions, n ranged between 100 and 5 million. An additional table shows, for comparison, calculation times that were measured for several functions in the GNU Multi Precision Arithmetic library (GMP, version 2.0.2); cf. page 420.

Table D.1: Calculation times for several C functions (without assembler support)
 | Binary digits of the arguments; time in seconds
 | 128 | 256 | 512 | 768 | 1024 | 2048
add_l | 3.8 × 10−7 | 5.5 × 10−7 | 8.8 × 10−7 | 1.2 × 10−6 | 1.5 × 10−6 | 2.2 × 10−6
mul_l | 1.9 × 10−6 | 5.1 × 10−6 | 1.6 × 10−5 | 3.3 × 10−5 | 5.6 × 10−5 | 2.1 × 10−4
sqr_l | 1.5 × 10−6 | 3.7 × 10−6 | 1.1 × 10−5 | 2.1 × 10−5 | 3.5 × 10−5 | 1.3 × 10−4
div_l[a] | 2.6 × 10−6 | 5.8 × 10−6 | 7.6 × 10−5 | 2.7 × 10−5 | 5.1 × 10−5 | 7.8 × 10−4
mmul_l | 7.5 × 10−6 | 2.1 × 10−5 | 6.6 × 10−5 | 1.4 × 10−4 | 2.3 × 10−4 | 8.9 × 10−4
msqr_l | 7.3 × 10−6 | 1.9 × 10−5 | 6.1 × 10−5 | 1.2 × 10−4 | 2.1 × 10−4 | 8.1 × 10−4
mexpk_l | 1.2 × 10−3 | 6.4 × 10−3 | 3.8 × 10−2 | 1.2 × 10−1 | 2.1 × 10−1 | 1.9
mexpkm_l | 5.4 × 10−4 | 2.9 × 10−3 | 1.7 × 10−2 | 5.2 × 10−2 | 1.2 × 10−1 | 8.6 × 10−1
[a]For the function div_l the number of digits refers to the dividend, while the divisor has half that number of digits.
Table D.2: Calculation times for several C functions (with 80×86 assembler support)
 | Binary digits of the arguments; time in seconds
 | 128 | 256 | 512 | 768 | 1024 | 2048
mul_l | 4.3 × 10−6 | 6.9 × 10−6 | 1.5 × 10−5 | 2.9 × 10−5 | 4.7 × 10−5 | 1.6 × 10−4
sqr_l | 2.5 × 10−6 | 4.6 × 10−6 | 1.0 × 10−5 | 1.8 × 10−5 | 2.9 × 10−5 | 9.5 × 10−5
div_l[a] | 2.7 × 10−6 | 4.6 × 10−6 | 1.0 × 10−5 | 1.8 × 10−5 | 2.9 × 10−5 | 9.5 × 10−5
mmul_l | 8.0 × 10−6 | 1.3 × 10−5 | 3.3 × 10−5 | 6.4 × 10−5 | 1.0 × 10−4 | 3.8 × 10−4
msqr_l | 7.2 × 10−6 | 1.1 × 10−5 | 2.9 × 10−5 | 5.6 × 10−5 | 9.0 × 10−5 | 3.1 × 10−4
mexpk_l | 1.3 × 10−3 | 4.3 × 10−3 | 1.9 × 10−2 | 5.3 × 10−2 | 1.1 × 10−1 | 8.7 × 10−1
mexpkm_l | 7.6 × 10−4 | 3.3 × 10−3 | 1.7 × 10−2 | 4.9 × 10−2 | 1.1 × 10−1 | 7.8 × 10−1
[a]For the function div_l the number of digits refers to the dividend, while the divisor has half that number of digits.

One can see clearly the savings that squaring achieves over multiplication. Even the advantage realized by Montgomery exponentiation in mexpkm_l() can been seen, which requires less than half the time for the exponentiation with mexpk_l(). An RSA step with a 2048-bit key can thereby, with application of the Chinese remainder theorem (cf. page 199), be computed in one-fourth of a second.

Table D.2 demonstrates the difference in time that results from the use of assembler routines. Assembler support results in a speed advantage of about 70% for the modular functions. The gap between multiplication and squaring remains stable at about 50%.

Since the two functions mulmon_l() and sqrmon_l() do not exist as assembler routines, in this comparison the exponentiation function mexpk_l() can catch up significantly to the Montgomery exponentiation mexpm_l(). Both functions are roughly equally fast. There exists here an interesting potential for further improvement in the performance (cf. Chapter 18) by suitable assembler extensions.

In the comparison between the FLINT/C and GMP functions (see Table D.3) one may see that the GMP multiplication and division are faster by 30% and 40% than the corresponding FLINT/C functions. However, with the routine of modular exponentiation, the functions of both libraries amount more or less to the same thing, which turns out to be the same for arguments with 4096 digits as well. Since the GMP is considered the fastest of the available libraries for large-integer arithmetic, we need not be dissatisfied with this result.

Table D.3: Calculation times for several GMP functions (with 80×86 assembler support)
 | Binary digits of the arguments; time in seconds
 | 128 | 256 | 512 | 768 | 1024 | 2048
mpz_add | 2.4 × 10−7 | 3.2 × 10−7 | 3.6 × 10−7 | 4.2 × 10−7 | 4.5 × 10−7 | 6.9 × 10−7
mpz_mul | 9.8 × 10−7 | 3.0 × 10−6 | 1.1 × 10−5 | 2.2 × 10−5 | 4.1 × 10−5 | 4.8 × 10−5
mpz_mod[a] | 5.2 × 10−7 | 1.8 × 10−6 | 5.0 × 10−6 | 6.4 × 10−6 | 1.6 × 10−5 | 4.0 × 10−5
mpz_powm | 4.5 × 10−4 | 2.6 × 10−3 | 1.7 × 10−2 | 5.2 × 10−2 | 1.7 × 10−1 | 7.8 × 10−1
[a]For the function mpz_mod the number of digits refers to the dividend, while the divisor has half that number of digits.


Appendix E: Notation

 | the set of nonnegative integers 0,1,2,3,...
 | the set of positive integers 1,2,3,...
 | the set of integers...,−2,−1,0,1,2,3,...
 | the residue class ring modulo n over the integers (Chapter 5)
ā | the residue class a + n [image] in
a ≈ b | a approximately equal to b
a [image] b | a less than and approximately equal to b
a ← b | assignment of b to a
|a| | absolute value of a
a | b | a divides b without remainder
a ∤ b | a does not divide b
a ≡ b mod n | a is congruent to b modulo n, that is, n | (a − b)
a ≢ b mod n | a is not congruent to b modulo n, that is, n ∤ (a − b)
gcd(a, b) | greatest common divisor of a and b (Section 10.1)
lcm(a, b) | least common multiple of a and b (Section 10.1)
φ(n) | Euler phi function (Section 10.2)
O( ) | "Big-Oh." For two real-valued functions f and g with g(x) ≥ 0 one writes f = O(g) and says "f is big-Oh of g" if there exists a constant C such that f (x) ≤ Cg(x) for all x sufficiently large.
 | Jacobi symbol (Section 10.4.1)
⌊x⌋ | greatest integer less than or equal to x
⌈x⌉ | least integer greater than or equal to x
P | the set of computational problems that can be solved in polynomial time
NP | the set of computational problems that can be solved nondeterministically in polynomial time
logbx | logarithm of x to the base b
B | B = 216, the base for the representation of objectsof type CLINT
MAXb | maximal number of digits for a CLINT object to base B
MAX2 | maximal number of digits for a CLINT object to base 2
Nmax | largest natural number that can be represented by a CLINT object


Appendix F: Arithmetic and Number-Theoretic Packages

IF THERE BE ANY LINGERING doubt in the mind of the reader as to the attractiveness and utility of algorithmic number theory, a glance at the large number of web sites that treat this topic should bring doubt to any such doubt at once, perhaps even by overwriting the reader's cerebral registers. Just punch the search string "number theory" into your favorite Internet search engine, and up pop thousands of entries, a few of which have already been cited in this book. Many of these web sites contain links to available software packages or enable such packages to be downloaded. Such offers encapsulate a large bandwidth of functions for large-integer arithmetic, algebra, group theory, and number theory, demonstrating the efforts of many able and enthusiastic developers.

An extensive list of sources for such software packages can be found on the Number Theory Web Page, managed by Keith Matthews (University of Queensland, Brisbane, Australia). The web site is located at

  • http://www.math.uq.edu.au/~krm/web.html,

with an American mirror site at

  • http://www.math.uga.edu/~ntheory/web.html

and a UK mirror site at

  • http://www.dpmms.cam.ac.uk/Number-Theory-Web/web.html.

The site also contains links to universities and research institutes as well as pointers to publications on relevant topics. In sum, this site is a veritable treasure trove. The following overview represents a small selection from the list of available software packages:

  • ARIBAS is an interpreter that executes arithmetic and number-theoretic functions for large integers. ARIBAS implements the algorithms from [Fors] in Pascal. ARIBAS can be obtained as a supplement to that book, by anonymous ftp, from the directory pub/forster/aribas under ftp.mathematik.uni-muenchen.de or from http://www.mathematik.uni-muenchen.de/ forster.

  • CALC, by Keith Matthews, is a calculation program for arbitrarily large integers that takes commands on a command line, executes them, and displays the results. CALC makes available about 60 number-theoretic functions. The package is implemented in ANSI C and uses the parser generator YACC or BISON for parsing the command line. CALC can be obtained from ftp://www.maths.uq.edu.au/pub/krm/calc/.

  • GNU MP, or GMP, from the GNU project, is a portable C library for arithmetic with arbitrarily large integers, as well as rational and real numbers. GMP achieves excellent performance due to the use of assembler code for an impressive array of CPUs. GMP can be obtained via ftp from www.gnu.org,prep.ai.mit.edu, as well as GNU mirror sites.

  • LiDIA is one of the software libraries developed at the Technical University Darmstadt for number-theoretic calculations. LiDIA contains an extensive collection of highly optimized functions for calculating in [image] , [image] , [image] , [image] , [image] , [image] , as well as for interval arithmetic. Current factorization algorithms are also implemented, such as for lattice base reduction, linear-algebraic algorithms, methods for calculating in number fields, and polynomials. LiDIA supports interfaces to other arithmetic packages, including the GMP package. LiDIA's own interpreted language LC facilitates, through its support of C++, the transition to translated programs. All platforms are supported that permit the use of long file names and for which a suitable C++ compiler is available, such as Linux 2.0.x, Windows NT 4.0, OS/2 Warp 4.0, HPUX-10.20, Sun Solaris 2.5.1/2.6. A port to the Apple Macintosh is also available. LiDIA can be obtained at http://www.informatik.tu-darmstadt.de/TI/LiDIA.

  • Numbers, by Ivo Düntsch, is a library of object files that provide basic number-theoretic functions for numbers with up to 150 decimal digits. The functions, written in Pascal, and the interpreter, contained in the package as well, were developed with the goal of providing students with nontrivial examples and experiments in calculation. The source for Numbers is http://archives.math.utk.edu/software/msdos/number.theory/num202d/.html.

  • PARI is a number-theoretic package by Henri Cohen et al. that implements the algorithms presented in [Cohe]. PARI can be used as an interpreter and as a function library that can be linked to programs. Through the use of assembler code for various platforms (UNIX, Macintosh, PC, and others) a high level of performance is achieved. PARI can be obtained at www.parigp-home.de.


References

[Adam] Adams, Carlisle, Steve Lloyd: Understanding Public Key Infrastructure Concepts, Standards & Deployment, Macmillan Technical Publishing, Indianapolis, 1999.

[BaSh] Bach, Eric, Jeffrey Shallit: Algorithmic Number Theory, Vol. 1, Efficient Algorithms, MIT Press, Cambridge (MA), London, 1996.

[Beut] Beutelspacher, Albrecht: Kryptologie, 2 .Auflage, Vieweg, 1991.

[Bies] Bieser, Wendelin, Heinrich Kersten: Elektronisch unterschreiben—die digitale Signatur in der Praxis, 2 .Auflage, Hüthig 1999.

[BiSh] Biham, Eli, Adi Shamir: Differential cryptanalysis of DES-like cryptosystems, Journal of Cryptology, Vol. 4, No. 1, 1991, pp. 3–72.

[Blum] Blum, L., M. Blum, M. Shub: A simple unpredictable pseudo-random number generator, SIAM Journal on Computing, Vol. 15, No. 2, 1986, pp. 364–383.

[BMBF] Bundesministerium für Bildung, Wissenschaft, Forschung und Technologie: IUKDG—Informations- und Kommunikationsdienste-Gesetz—Umsetzung und Evaluierung, Bonn, 1997.

[BMWT] Bundesministerium für Wirtschaft und Technologie: Entwurfeines Gesetzes über Rahmenbedingungen für elektronische Signaturen—Diskussionsentwurf zur Anhörung und Unterrichtung der beteiligten Fachkreise und Verbände, April 2000.

[Bone] Boneh, Dan: Twenty years of attacks on the RSA-cryptosystem Proc. ECC, 1998.

[Bos1] Bosch, Karl: Elementare Einführung in die Wahrscheinlichkeitsrechnung, Vieweg, 1984.

[Bos2] Bosch, Karl: Elementare Einführung in die angewandte Statistik, Vieweg, 1984.

[Boss] Bosselaers, Antoon, René Govaerts, Joos Vandewalle: Comparison of three modular reduction functions in Advances in Cryptology, CRYPTO 93, Lecture Notes in Computer Science No. 773, pp. 175–186, Springer-Verlag, New York, 1994.

[Bres] Bressoud, David M.: Factorization and Primality Testing, Springer-Verlag, New York, 1989.

[Bund] Bundschuh, Peter: Einführung in die Zahlentheorie, 3. Auflage, Springer-Verlag, Berlin, Heidelberg, 1996.

[CJRR] Chari, Suresh, Charanjit Jutla, Josyula R. Rao, Pankaj Rohatgi: A Cautionary Note Regarding Evaluation of AES Candidates on Smart Cards, 1999, http://csrc.nist.gov/encryption/aes/round1/conf2/papers/chari.pdf

[Cohe] Cohen, Henri: A Course in Computational Algebraic Number Theory, Springer-Verlag, Berlin, Heidelberg, 1993.

[Coro] Coron, Jean-Sebastien, David Naccache, Julien P. Stern: On the security of RSA padding, ed. M. Wiener, in Advances in Cryptology, CRYPTO '99, Lecture Notes in Computer Science No. 1666, pp. 1–17, Springer-Verlag, New York, 1999.

[Cowi] Cowie, James, Bruce Dodson, R.-Marije Elkenbracht-Huizing, Arjen K. Lenstra, Peter L. Montgomery, Joerg Zayer: A world wide number field sieve factoring record: on to 512 bits, ed. K. Kim and T. Matsumoto, in Advances in Cryptology, ASIACRYPT '96, Lecture Notes in Computer Science No. 1163, pp. 382–394, Springer-Verlag, Berlin 1996.

[DaRi] Daemen, Joan, Vincent Rijmen: AES-Proposal: Rijndael, Doc. Vers. 2.0, September 1999, http://www.nist.gov/encryption/aes

[Deit] Deitel, H. M., P. J. Deitel: C++: How To Program, Prentice Hall, 1994.

[Dene] Denert, Ernst: Software-Engineering, Springer-Verlag, Heidelberg, 1991.

[Diff] Diffie, Whitfield, Martin E. Hellman: New Directions in Cryptography, IEEE Trans. Information Theory, pp. 644–654, Vol. IT-22, 1976.

[DoBP] Dobbertin, Hans, Antoon Bosselaers, Bart Preneel: RIPEMD-160, a strengthened version of RIPEMD, ed. D. Gollman, in Fast Software Encryption, Third International Workshop, Lecture Notes in Computer Science No. 1039, pp. 71–82, Springer-Verlag, Berlin, Heidelberg, 1996.

[DuKa] Dussé, Stephen R., Burton. S. Kaliski: A cryptographic library for the Motorola DSP56000, in Advances in Cryptology, EUROCRYPT '90, Lecture Notes in Computer Science No. 473, pp. 230–244, Springer-Verlag, New York, 1990.

[Dunc] Duncan, Ray: Advanced OS/2-Programming: The Microsoft Guide to the OS/2-Kernel for Assembly Language and C Programmers, Microsoft Press, Redmond, Washington, 1981.

[East] Eastlake, D., S. Crocker, J. Schiller: Randomness Recommendations for Security, RFC1750, 1994.

[Elli] Ellis, J. H.: The Possibility of Non-Secret Encryption, 1970, http://www.cesg.gov.uk/about/nsecret/home.htm

[ElSt] Ellis, Margaret A., Bjarne Stroustrup: The Annotated C++ Reference Manual, Addison-Wesley, Reading, MA, 1990.

[Endl] Endl, Kurth, Wolfgang Luh: Analysis I, Akademische Verlagsgesellschaft Wiesbaden, 1977.

[Enge] Engel-Flechsig, Stefan, Alexander Roßnagel eds., Multimedia-Recht, C. H. Beck, Munich, 1998.

[EU99] Richtlinie 1999/93/EG des Europäischen Parlaments und des Rates vom 13. Dezember 1999 über gemeinschaftliche Rahmenbedingungen für elektronische Signaturen.

[Evan] Evans, David: LCLint Users Guide, Version 2.4, MIT Laboratory for Computer Science, April 1998.

[Fegh] Feghhi, Jalal, Jalil Feghhi, Peter Williams: Digital Certificates: Applied Internet Security, Addison-Wesley, Reading, MA, 1999.

[Fiat] Fiat, Amos, Adi Shamir: How to prove yourself: practical solutions to identification and signature problems, in Advances in Cryptology, CRYPTO '86, Lecture Notes in Computer Science No. 263, pp. 186–194, Springer-Verlag, New York, 1987.

[FIPS] Federal Information Processing Standard Publication 140 - 1: Security requirements for cryptographic modules, US Department of Commerce/ National Institute of Standards and Technology (NIST), 1992.

[Fisc] Fischer, Gerd, Reinhard Sacher: Einführung in die Algebra, Teubner, 1974.

[Fors] Forster, Otto: Algorithmische Zahlenthorie, Vieweg, Braunschweig, 1996.

[Fumy] Fumy, Walter, Hans Peter Rieß: Kryptographie, 2. Auflage, Oldenbourg, 1994.

[Gimp] Gimpel Software: PC-lint, A Diagnostic Facility for C and C++.

[Glad] Glade, Albert, Helmut Reimer, Bruno Struif, editors: Digitale Signatur & Sicherheitssensitive Anwendungen, DuD-Fachbeiträge, Vieweg, 1995.

[GoPa] Goubin, Louis, Jacques Patarin DES and differential power analysis, Proceedings of CHES'99, Lecture Notes in Computer Science, No. 1717, Springer-Verlag, 1999.

[Gord] Gordon, J. A.: Strong primes are easy to find, Advances in Cryptology, Proceedings of Eurocrypt '84, pp. 216–223, Springer-Verlag, Berlin, Heidelberg, 1985.

[Halm] Halmos, Paul, R.: Naive Set Theory, Springer-Verlag New York, 1987.

[Harb] Harbison, Samuel P, Guy L. Steele, Jr.: C: A Reference Manual, 4th Edition, Prentice Hall, Englewood Cliffs, 1995.

[Hatt] Hatton, Les: Safer C: Developing Software for High-Integrity and Safety-Critical Systems, McGraw-Hill, London, 1995.

[Heid] Heider, Franz-Peter: Quadratische Kongruenzen, unpublished manuscript, Cologne, 1997.

[Henr] Henricson, Mats, Erik Nyquist: Industrial Strength C++, Prentice Hall, New Jersey, 1997.

[HeQu] Heise, Werner, Pasquale Quattrocchi: Informations- und Codierungstheorie, Springer-Verlag, Berlin, Heidelberg, 1983.

[HKW] Heider, Franz-Peter, Detlef Kraus, Michael Welschenbach: Mathematische Methoden der Kryptoanalyse, DuD-Fachbeiträge, Vieweg, Braunschweig, 1985.

[Herk] Herkommer, Mark: Number Theory: A Programmer's Guide, McGraw-Hill, 1999.

[IEEE] IEEE P1363 / D13: Standard Specifications for Public Key Cryptography, Draft Version 13, November 1999.

[ISO1] ISO/IEC 10118-3: Information Technology—Security Techniques—Hash-Functions. Part 3: Dedicated Hash-Functions, CD, 1996.

[ISO2] ISO/IEC 9796: Information Technology—Security Techniques—Digital Signature Scheme giving Message Recovery, 1991.

[ISO3] ISO/IEC 9796-2: Information Technology—Security Techniques—Digital Signature Scheme Giving Message Recovery, Part 2: Mechanisms Using a Hash-Function, 1997.

[Knut] Knuth, Donald Ervin: The Art of Computer Programming, Vol. 2: Seminumerical Algorithms, 3rd Edition, Addison-Wesley, Reading, MA, 1998.

[Kobl] Koblitz, Neal: A Course in Number Theory and Cryptography, Springer-Verlag, New York, 2nd Edition 1994.

[Kob2] Koblitz, Neal: Algebraic Aspects of Cryptography, Springer-Verlag, Berlin, Heidelberg, 1998.

[KoJJ] Kocher, Paul, Joshua Jaffe, Benjamin Jun: Introduction to Differential Power Analysis and Related Attacks, 1998, http://www.cryptography.com/dpa/technical/

[Kran] Kranakis, Evangelos: Primality and Cryptography, Wiley-Teubner Series in Computer Science, 1986.

[KSch] Kuhlins, Stefan, Martin Schader: Die C++-Standardbibliothek, Springer-Verlag, 1999.

[LeVe] Lenstra, Arjen K., Eric R. Verheul: Selecting Cryptographic Key Sizes, 1999, http://www.cryptosavvy.com

[Lind] van der Linden, Peter: Expert C Programming, SunSoft/Prentice Hall, Mountain View, CA, 1994.

[Lipp] Lippman, Stanley, B.: C++ Primer, 2nd Edition, Addison-Wesley, Reading, MA, 1993.

[Magu] Maguire, Stephen A.: Writing Solid Code, Microsoft Press, Redmond, Washington, 1993.

[Matt] Matthews, Tim: Suggestions for Random Number Generation in Software, RSA Data Security Engineering Report, December 1995.

[Mene] Menezes, Alfred J.: Elliptic Curve Public Key Cryptosystems, Kluwer Academic Publishers, 1993.

[Mey1] Meyers, Scott D.: Effective C++, 2nd Edition, Addison-Wesley, Reading, Mass., 1998.

[Mey2] Meyers, Scott D.: More Effective C++, 2nd Edition, Addison-Wesley, Reading, Mass., 1998.

[Mied] Miedbrodt, Anja: Signaturregulierung im Rechtsvergleich, Der Elektronische Rechtsverkehr 1, Nomos Verlagsgesellschaft Baden-Baden, 2000.

[Mont] Montgomery, Peter L.: Modular multiplication without trial division, Mathematics of Computation, pp. 519–521, 44 (170), 1985.

[MOV] Menezes, Alfred J., Paul van Oorschot, Scott A. Vanstone, Handbook of Applied Cryptography, CRC Press, 1997.

[Murp] Murphy, Mark L.: C/C++ Software Quality Tools, Prentice Hall, New Jersey, 1996.

[Nied] Niederreiter, Harald: Random Number Generation and Quasi-Monte Carlo Methods, SIAM, Philadelphia, 1992.

[NIST] Nechvatal, James, Elaine Barker, Lawrence Bassham, William Burr, Morris Dworkin, James Foti, Edward Roback: Report on the Development of the Advanced Encryption Standard, National Institute of Standards and Technology, 2000.

[Nive] Niven, Ivan, Herbert S. Zuckerman: Einführung in die Zahlentheorie vols. I und II, Bibliographisches Institut, Mannheim, 1972.

[Odly] Odlyzko, Andrew: Discrete Logarithms: The Past and the Future, AT&T Labs Research, 1999.

[Petz] Petzold, Charles: Programming Windows: The Microsoft Guide to Writing Applications for Windows 3.1, Microsoft Press, Redmond, Washington, 1992.

[Pla1] Plauger, P. J.: The Standard C Library, Prentice-Hall, Englewood Cliffs, New Jersey, 1992.

[Pla2] Plauger, P. J.: The Draft Standard C++ Library, Prentice-Hall, Englewood Cliffs, New Jersey, 1995.

[Pren] Preneel, Bart: Analysis and Design of Cryptographic Hash Functions, Dissertation at the Katholieke Universiteit Leuven, 1993.

[Rabi] Rabin, Michael, O.: Digital Signatures and Public-Key Functions as Intractable as Factorization, MIT Laboratory for Computer Science, Technical Report, MIT/LCS/TR-212, 1979.

[RDS1] RSA Data Security, Inc.: Public Key Cryptography Standards, PKCS #1: RSA Encryption, RSA Laboratories Technical Note, Version 2.0, 1998.

[RDS2] RSA Security, Inc.: Recent Results on Signature Forgery, RSA Laboratories Bulletin, 1999, http://www.rsasecurity.com/rsalabs/html/sigforge.html

[RegT] Regulierungsbehörde für Telekommunikation und Post (RegTP): Bekanntmachung zur digitalen Signatur nach Signaturgesetz und Signaturverordnung, Bundesanzeiger Nr. 31, 14.02.1998.

[Rein] Reinhold, Arnold: P=?NP Doesn't Affect Cryptography, May 1996, http://world.std.com/_reinhold/p=np.txt

[Ries] Riesel, Hans: Prime Numbers and Computer Methods for Factorization, Birkhäuser, Boston, 1994.

[Rive] Rivest, Ronald, Adi Shamir, Leonard Adleman: A method for obtaining digital signatures, Communications of the ACM 21, pp. 120–126, 1978.

[Rose] Rose, H: E.: A Course in Number Theory, 2nd Edition, Oxford University Press, Oxford, 1994.

[Saga] Sagan, Carl: Cosmos, Random House, New York, 1980.

[Salo] Salomaa, Arto: Public-Key Cryptography, 2nd Edition, Springer-Verlag, Berlin, Heidelberg, 1996.

[Schn] Schneier, Bruce: Applied Cryptography, 2nd Edition, John Wiley & Sons, New York, 1996.

[Scho] Schönhage, Arnold: A lower bound on the length of addition chains, Theoretical Computer Science, pp. 229–242, Vol. 1, 1975.

[Schr] Schröder, Manfred R.: Number Theory in Science and Communications, 3rd edition, Springer-Verlag, Berlin, Heidelberg, 1997.

[SigG] Gesetz über Rahmenbedingungen für elektronische Signaturen (Signaturgesetz—SigG), at http://www.iid.de/iukdg

[Skal] Skaller, John Maxwell: Multiple precision arithmetic in C, edited by Dale Schumacher, in Software Solutions in C, Academic Press, pp. 343–454, 1994.

[Spul] Spuler, David A.: C++ and C Debugging, Testing and Reliability, Prentice Hall, New Jersey, 1994.

[Squa] Daemen, Joan, Lars Knudsen, Vincent Rijmen: The block cipher square, Fast Software Encryption, Lecture Notes in Computer Science No. 1267, pp. 149–165, Springer-Verlag, 1997.

[Stal] Stallings, William: Cryptography and Network Security, 2nd Edition, Prentice Hall, New Jersey, 1999.

[Stin] Stinson, Douglas R.: Cryptography—Theory and Practice, Prentice Hall, New Jersey, 1995.

[Stlm] Stallman, Richard M.: Using and Porting GNU CC, Free Software Foundation.

[Str1] Stroustrup, Bjarne: The C++ Programming Language, 3rd Edition, Addison-Wesley, Reading, MA, 1997.

[Str2] Stroustrup, Bjarne: The Design and Evolution of C++, Addison-Wesley, Reading, MA, 1994.

[Teal] Teale, Steve: C++ IOStreams Handbook, Addison-Wesley, Reading, MA, 1993.

[Wien] Wiener, Michael: Cryptanalysis of short RSA secret exponents, in IEEE Transactions on Information Theory, 36 (3): pp. 553–558, 1990.

[Yaco] Yacobi, Y.: Exponentiating faster with addition chains, Advances in Cryptology, EUROCRYPT '90, Lecture Notes in Computer Science No. 473, pp. 222–229, Springer-Verlag, New York, 1990.

[Zieg] Ziegler, Joachim: personal communication 1998, 1999.


 Team-Fly

Index

A

add, 70
add_l, 23
addition, 17, 22, 26
of residue classes, 66, 67
additive identity, 67
add_l, 70
Advanced Encryption Standard, 357
algorithm
addition, 22
binary Euclidean, 168
binary exponentiation modulo m, 80
Brent, 232
division with remainder, 51
Euclidean, 167
Garner, 203
identifying squares, 185
integer part of a square root, 183
inverse modulo 2n, 111
Jacobi symbol, 191
key generation à la Fiat-Shamir, 209
key generation à la Rabin, 207
Legendre symbol, 189
M-ary exponentiation modulo m, 85, 87
multiplication, 34
period length of a sequence, 232
prime factorization, 329
solution of linear congruences, 203, 204
square root modulo p, 196
squaring, 40
subtraction, 26
window method for exponentiation, 102
alphabet, 69
and_l, 131
ANDMAX_L, 412
assembler, 355, 356
ASSIGN_L, 411
associative law, 5
associativity, 67
authentication, 319
axiom of infinity, 4


 Team-Fly

Index

B

big-endian, 302
binomial distribution, 233
binomial formula, 252
BINSTR_L, 149, 411
biprime cryptography, 317
Brent algorithm, 232
byte2clint_l, 150


 Team-Fly

Index

C

Carmichael number, 217
carry, 23
catch block, 311
certificate, 339
certification authority, 339
chi-squared test, 232
Chinese remainder theorem, 199, 203, 205, 342
chinrem_l, 205
class
LINT, 260
RSAkey, 341
RSApub, 341
clearbit_l, 137
CLINT, 14
clint2byte_l, 150
clint2str_l, 149, 411
closure, 67
cmp_l, 139
commutative law, 5
commutativity, 67
congruence, linear, 230
construction, strong primes, 328
constructor, 260, 263, 266
copy constructor, 265, 271
cpy_l, 143
create_l, 161
create_reg_l, 157
cryptosystem, asymmetric, 318
curve, elliptic, 355
cycle of a pseudoprime sequence, 231


 Team-Fly

Index

D

data encryption standard, 357
dec_l, 31
DECDIGITS_L, 412
decrement, 31
DECSTR_L, 149, 411
Diffie-Hellman
key exchange, 116
problem, 116
DigestInfo, 337
digital signature, see signature
DIGITS_L, 13, 24, 412
distributive law, 5, 67
for gcd and lcm, 173
div_l, 52
division
short, 59
with remainder, 49, 66, 124, 126
divisor, greatest common, 167, 171, 353
dynamic link library (DLL), 9


 Team-Fly

Index

E

element, inverses, 67
encryption algorithm
RSA, 317
encryption block, 337
equ_l, 141
equivalence relation, 65
EQZ_L, 413
Eratosthenes (276–195 B.C.E.), 3
sieve of Eratosthenes, 213
error codes, 309
error handling, 310
Euclid (third century B.C.E.)
Euclidean algorithm, 167, 168
Euclid (third century B.C.E.), 3, 211
Euler, Leonhard (1707–1783), 4
Euler criterion, 189, 218
Euler phi function, 175, 320
Euler pseudoprime, 218
exception, 311
exponentiation
laws, 79, 252
modular, 217


 Team-Fly

Index

F

factorization, 199, 212, 321, 328, 331
Fermat, Pierre de (1601–1665), 4
Fermat test, 217
Fermat's little theorem, 176, 217
Fiat, Amos, 208
field, finite, 175
file, writing LINT objects, 302
findprime, 329, 331
free, 251
free_l, 162
friend function, 276
fswap_l, 145
function header, 17
function, mixed, 28


 Team-Fly

Index

G

Garner algorithm, 203
Gauss, Carl Friedrich (1777–1855), 4, 65, 189
gcc, 246
gcd, 166, 167, 171, 353
gcd_l, 168
GE_L, 413
genprimes, 214
get_reg_l, 159
GNU, 246
greatest common divisor, 166
group
abelian, 67
laws, 67
GT_L, 413
GTZ_L, 413


 Team-Fly

Index

H

hash function, 335
hash value, 337
HEXSTR_L, 149, 411


 Team-Fly

Index

I

identification, 319
à la Fiat-Shamir, 208
inc_l, 30
INCDIGITS_L, 412
increment, 30
induction, complete, 5
information hiding, 257
integers, set of, 68
Internet, 319
inv_l, 179
invmon_l, 112
IPSec, 115
IPv, 115
iroot_l, 184
ISEVEN_L, 413
ISODD_L, 413
ISPRIME_L, 224, 414


 Team-Fly

Index

J

Jacobi symbol, 189–191, 193, 219, 288, 353
jacobi_l, 193


 Team-Fly

Index

K

Karatsuba multiplication, 32, 44, 45, 47
kernel function, 45
key component
private, 320
public, 320
key length for RSA, 331
kmul_l, 45, 47
Kronecker symbol, 189


 Team-Fly

Index

L

LCLint, 246
lcm, 171, 172, 353
lcm_l, 172
ld_l, 181
LE_L, 412
Legendre, Adrien Marie (1752–1833), 4
Legendre symbol, 188, 190
limits.h, 12
LINT, 260
format flags, 301
manipulators, 300
operators, 268, 269
lint2str, 297
LINT::
add, 278
chinrem, 288
clearbit, 284
divr, 279
fswap, 285
Get_Warning_Status, 288, 308
gcd, 286
inv, 286
iseven, 283
isprime, 290
issqr, 289
jacobi, 288
lcm, 287
ld, 285
madd, 281
mequ, 280
mexp2, 282
mexp5m, 283
mexpkm, 282, 343
mexp, 276, 277
mmul, 281
mod, 279
mod2, 280
msqr, 282
msub, 281
mul, 278
restoref, 294
root, 288, 289
Set_LINT_Error_Handler, 309
setbit, 284
setf, 294
shift, 283
sqr, 279
sub, 278
testbit, 284
twofact, 290
unsetf, 296
vroot, 289
xgcd, 287
LINT_omanip<int>, 300
little-endian, 302
LSDPTR_L, 13, 24, 412
LT_L, 412


 Team-Fly

Index

M

madd_l, 72
malloc(), 251
man-in-the-middle attack, 117
manipulator, 299, 300
MAX_L, 413
member function, 275
mequ_l, 77
Mersenne prime, 211
mexp2_l, 97
mexp5m_l, 111
mexpk_l, 90
MEXP_L, 413
Miller-Rabin test, 220
MIN_L, 413
mmul_l, 75
mod2_l, 61
modulus, 320, 342
mod_l, 60
Monte Carlo method, 229
Montgomery exponentiation, 343
MSDPTR_L, 13, 24, 412
msqr_l, 75
msub_l, 73
mulmon_l, 108, 112
mult, 70
multiple, least common, 171, 172, 353
multiplication, 32, 124, 125
of residue classes, 66
mul_l, 35, 70, 71


 Team-Fly

Index

N

nonrecurring elements of a pseudorandom sequence, 231
nonresidue, quadratic, 187
NP, 188, 199
nul_l, 15
Number Theory Web Page, 419
number(s)
natural, 4
rational, 68


 Team-Fly

Index

O

object, 257
object-oriented programming, 259
OCTSTR_L, 149, 411
one_l, 15
or_l, 132
order, of digits of a number, 13
output of a function, 17
overflow, arithmetic, 18, 26, 30


 Team-Fly

Index

P

padding, 322
panic, 308, 310
partial product, 33, 41, 49
PC-lint, 246
Peano axioms, 5
period length, maximal, 231
period of a pseudoprime sequence, 231
PGP (pretty good privacy, 119
PKCS #1 format, 337
PKCS #1 specification, 349
PKI, see public key infrastructures
polynomial, 360
irreducible, 360
power, 6
predecessor, 4
primality test
deterministic, 227
Miller-Rabin, 220
Solvay-Strassen, 219
prime number(s), 210, 320
identification of, 212
stored in smallprimes, 221
strong, 328
theorem, 216
prime_l, 224
primitive root modulo p, 117
primroot_l, 118
product, 5
proot_l, 197
protocol
authentication à la Fiat-Shamir, 209
digital signature with RSA, 334
Rabin encryption, 208
pseudoprime, 218
strong, 220
pseudorandom numbers, 229
public key cryptosystem, 114
public key infrastructures, 339
purge_l, 162
purgeall_reg_l, 160
purge_reg_l, 159


 Team-Fly

Index

Q

quotient, 49, 50, 126


 Team-Fly

Index

R

Rabin encryption, 208
Rabin, Michael O., 207
rand64_l, 235
randbit_l, 239
rand_l, 238
reciprocity law, 189
redundancy, 322
function, 335
scheme, PKCS#1, 337
remainder, 49, 50, 66
representative of a residue class, 66
ResetLintFlags, 300
residue
class, 66
system
absolute smallest complete, 68
complete, 68
residue, quadratic, 187
ResTrack, 251
return value of a function, 17
Riemann hypothesis, 196
ring, commutative, 67
RMLDZRS_L, 412
root certification authority, 341
root_l, 201
RSA
algorithm, 317
attack, 321
class, 342
key length, 333
key pair, 320
procedure, 212
rapid decryption, 343


 Team-Fly

Index

S

S-box, 327
secure mode, 323
secure socket layer, 115
security mode, 9
seed64_l, 236
seedBBS_l, 240
semaphore, 158
semigroup, 68, 173
set, empty, 4
setbit_l, 135
SETDIGITS_L, 13
SETDIGITS_L, 24, 412
SetLintFlags, 300
setmax_l, 27
SETZERO_L, 411
SET_L, 411
set_noofregs_l, 158
SFACTOR_L, 223
Shamir, Adi, 208
shift_l, 127
shl_l, 125
shr_l, 126
sieve of Eratosthenes, 213, 214
sieve_1, 222
signature
digital, 318, 319
laws on, 319
hidden, 336
with appendix, 335
with message recovery, 336
smallprimes, 221
sqr, 70
sqrmon_l, 110, 112
sqr_l, 41, 70
squaring, 39–41, 43
SSL, see secure socket layer
stack, 8
str2clint_l, 145
Stroustrup, Bjarne, 257
sub, 70
sub_l, 27, 70
subsystem, 151
subtraction, 26, 49
of residue classes, 67
successor, 4
set, 4
sum, 5
SWAP_L, 412


 Team-Fly

Index

T

template, omanip<T>, 301
test plan, 249
test suite, 253
testbit_l, 137
throw, 311
Trojan horse, 327
try block, 311
twofact_l, 192
two-part of an integer, 192
two_l, 15


 Team-Fly

Index

U

uadd_l, 29
udiv_l, 60
ul2clint_l, 151
ulrand64_l, 237
ulrandBBS_l, 240
ulseed64_l, 236
umadd_l, 76
umexpm_l, 113
umexp_l, 81
umod_l, 62
umul, 70
umul_l, 38
underflow, arithmetic, 18, 26, 30, 124
unit, 67
usub_l, 29


 Team-Fly

Index

V

vcheck_l, 152


 Team-Fly

Index

W

see zeros, leading
wmexpm_l, 113
wmexp_l, 83


 Team-Fly

Index

X

xclint2str_l, 148
xgcd_l, 177
xor_l, 134


 Team-Fly

Index

Z

zero, 13
zero knowledge process, 210
ZEROCLINT_L, 414
zeros, leading 18


 Team-Fly

List of Figures

Chapter 4: The Fundamental Operations

Figure 4.1: Calculations for multiplication
Figure 4.2: Calculations for squaring
Figure 4.3: CPU time for Karatsuba multiplication
Figure 4.4: Calculational schema for division

Chapter 11: Large Random Numbers

Figure 11.1: Periodic behavior of a pseudorandom sequence

Chapter 16: An Application Example: The RSA Cryptosystem

Figure 16.1: Example of the construction of a certificate
Figure 16.2: Certification of a digital signature

Chapter 19: Rijndael: A Successor to the Data Encryption Standard

Figure 19.1: Layering of transformations in the Rijndael rounds
Figure 19.2: Diagram for round keys for Lk= 4


 Team-Fly

List of Tables

Chapter 1: Introduction

Table 1.1: Arithmetic and Number Theory in C in Directory flint/src
Table 1.2: Arithmetic and Number Theory in C++ in Directory flint/src
Table 1.3: Arithmetic Module in 80×86 Assembler (see Chapter 18) in Directory flint/src/asm
Table 1.4: Arithmetic Libraries in 80×86 Assembler (see Chapter 18) in Directory flint/lib
Table 1.5: Tests (see Section 12.2 and Chapter 17) in Directory flint/test
Table 1.6: RSA Implementation (see Chapter 16) in Directory flint/rsa

Chapter 3: Interface Semantics

Table 3.1: FLINT/C error codes

Chapter 5: Modular Arithmetic: Calculating with Residue Classes

Table 5.1: Composition table for addition modulo 5
Table 5.2: Composition table for multiplication modulo 5

Chapter 6: Where All Roads Meet: Modular Exponentiation

Table 6.1: Requirements for exponentiation
Table 6.2: Numbers of multiplications for typical sizes of exponents and various bases 2k
Table 6.3: Values for the factorization of the exponent digits into products of a power of 2 and an odd factor
Table 6.4: Numbers of multiplications for typical sizes of exponents and various bases 2k
Table 6.5: Exponentiation functions in FLINT/C

Chapter 7: Bitwise and Logical Functions

Table 7.1: Values of a Boolean function
Table 7.2: Values of the CLINT function and_l()
Table 7.3: Values of the CLINT function or_l()
Table 7.4: Values of the CLINT function xor_l()

Chapter 8: Input, Output, Assignment, Conversion

Table 8.1: Diagnostic values of the function vcheck_l()

Chapter 10: Basic Number-Theoretic Functions

Table 10.1: The largest known primes (as of August 2000)
Table 10.2: The number of primes up to various limits x

Chapter 12: Strategies for Testing LINT

Table 12.1: Group law for the integers to help in testing
Table 12.2: FLINT/C test functions

Chapter 13: Let C++ Simplify Your Life

Table 13.1: LINT constructors
Table 13.2: LINT arithmetic operators
Table 13.3: LINT bitwise operators
Table 13.4: LINT logical operators
Table 13.5: LINT assignment operators

Chapter 14: The LINT Public Interface: Members and Friends

Table 14.1: LINT status functions and their effects
Table 14.2: LINT manipulators and their effects
Table 14.3: LINT flags for output formatting and their effects

Chapter 15: Error Handling

Table 15.1: LINT function error codes

Chapter 16: An Application Example: The RSA Cryptosystem

Table 16.1: Recommended key lengths according to Lenstra and Verheul

Chapter 19: Rijndael: A Successor to the Data Encryption Standard

Table 19.1: Elements of
Table 19.2: Powers of g(x) = x + 1
Table 19.3: Logarithms to base g(x) = x + 1 (e.g., logg(x) 3 = 25, logg(x) 255 = 7)
Table 19.4: Number of Rijndael rounds as a function of block and key length
Table 19.5: Representation of message blocks
Table 19.6: rc(j) constants (hexadecimal)
Table 19.7: rc(j) constants (binary)
Table 19.8: Representation of the round keys
Table 19.9: The values of the S-box
Table 19.10: The values of the inverted S-box
Table 19.11: ShiftRow for blocks of length 128 bits (Lb = 4)
Table 19.12: ShiftRow for blocks of length 192 bits (Lb = 6)
Table 19.13: ShiftRow for blocks of length 256 bits (Lb = 8)
Table 19.14: Distances of line rotations in ShiftRow
Table 19.15: Interpretation of variables
Table 19.16: Interpretation of fields
Table 19.17: Interpretation of functions

Appendix D: Calculation Times

Table D.1: Calculation times for several C functions (without assembler support)
Table D.2: Calculation times for several C functions (with 80×86 assembler support)
Table D.3: Calculation times for several GMP functions (with 80×86 assembler support)


 Team-Fly
Previous Section

List of Listings

Chapter 15: Error Handling

Example 1: Use of an uninitialized LINT object as argument.
Example 2: Division by a LINT object with the value 0.
Team-Fly Previous Section